##// END OF EJS Templates
cleanup: prefer matchmod.{always,never}() over accessing matchers directly...
Martin von Zweigbergk -
r41822:1db5ae4b default
parent child Browse files
Show More
@@ -1,1418 +1,1418 b''
1 1 # changegroup.py - Mercurial changegroup manipulation functions
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import struct
12 12 import weakref
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullid,
18 18 nullrev,
19 19 short,
20 20 )
21 21
22 22 from . import (
23 23 error,
24 24 match as matchmod,
25 25 mdiff,
26 26 phases,
27 27 pycompat,
28 28 repository,
29 29 util,
30 30 )
31 31
32 32 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct("20s20s20s20s")
33 33 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct("20s20s20s20s20s")
34 34 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(">20s20s20s20s20sH")
35 35
36 36 LFS_REQUIREMENT = 'lfs'
37 37
38 38 readexactly = util.readexactly
39 39
40 40 def getchunk(stream):
41 41 """return the next chunk from stream as a string"""
42 42 d = readexactly(stream, 4)
43 43 l = struct.unpack(">l", d)[0]
44 44 if l <= 4:
45 45 if l:
46 46 raise error.Abort(_("invalid chunk length %d") % l)
47 47 return ""
48 48 return readexactly(stream, l - 4)
49 49
50 50 def chunkheader(length):
51 51 """return a changegroup chunk header (string)"""
52 52 return struct.pack(">l", length + 4)
53 53
54 54 def closechunk():
55 55 """return a changegroup chunk header (string) for a zero-length chunk"""
56 56 return struct.pack(">l", 0)
57 57
58 58 def _fileheader(path):
59 59 """Obtain a changegroup chunk header for a named path."""
60 60 return chunkheader(len(path)) + path
61 61
62 62 def writechunks(ui, chunks, filename, vfs=None):
63 63 """Write chunks to a file and return its filename.
64 64
65 65 The stream is assumed to be a bundle file.
66 66 Existing files will not be overwritten.
67 67 If no filename is specified, a temporary file is created.
68 68 """
69 69 fh = None
70 70 cleanup = None
71 71 try:
72 72 if filename:
73 73 if vfs:
74 74 fh = vfs.open(filename, "wb")
75 75 else:
76 76 # Increase default buffer size because default is usually
77 77 # small (4k is common on Linux).
78 78 fh = open(filename, "wb", 131072)
79 79 else:
80 80 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
81 81 fh = os.fdopen(fd, r"wb")
82 82 cleanup = filename
83 83 for c in chunks:
84 84 fh.write(c)
85 85 cleanup = None
86 86 return filename
87 87 finally:
88 88 if fh is not None:
89 89 fh.close()
90 90 if cleanup is not None:
91 91 if filename and vfs:
92 92 vfs.unlink(cleanup)
93 93 else:
94 94 os.unlink(cleanup)
95 95
96 96 class cg1unpacker(object):
97 97 """Unpacker for cg1 changegroup streams.
98 98
99 99 A changegroup unpacker handles the framing of the revision data in
100 100 the wire format. Most consumers will want to use the apply()
101 101 method to add the changes from the changegroup to a repository.
102 102
103 103 If you're forwarding a changegroup unmodified to another consumer,
104 104 use getchunks(), which returns an iterator of changegroup
105 105 chunks. This is mostly useful for cases where you need to know the
106 106 data stream has ended by observing the end of the changegroup.
107 107
108 108 deltachunk() is useful only if you're applying delta data. Most
109 109 consumers should prefer apply() instead.
110 110
111 111 A few other public methods exist. Those are used only for
112 112 bundlerepo and some debug commands - their use is discouraged.
113 113 """
114 114 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
115 115 deltaheadersize = deltaheader.size
116 116 version = '01'
117 117 _grouplistcount = 1 # One list of files after the manifests
118 118
119 119 def __init__(self, fh, alg, extras=None):
120 120 if alg is None:
121 121 alg = 'UN'
122 122 if alg not in util.compengines.supportedbundletypes:
123 123 raise error.Abort(_('unknown stream compression type: %s')
124 124 % alg)
125 125 if alg == 'BZ':
126 126 alg = '_truncatedBZ'
127 127
128 128 compengine = util.compengines.forbundletype(alg)
129 129 self._stream = compengine.decompressorreader(fh)
130 130 self._type = alg
131 131 self.extras = extras or {}
132 132 self.callback = None
133 133
134 134 # These methods (compressed, read, seek, tell) all appear to only
135 135 # be used by bundlerepo, but it's a little hard to tell.
136 136 def compressed(self):
137 137 return self._type is not None and self._type != 'UN'
138 138 def read(self, l):
139 139 return self._stream.read(l)
140 140 def seek(self, pos):
141 141 return self._stream.seek(pos)
142 142 def tell(self):
143 143 return self._stream.tell()
144 144 def close(self):
145 145 return self._stream.close()
146 146
147 147 def _chunklength(self):
148 148 d = readexactly(self._stream, 4)
149 149 l = struct.unpack(">l", d)[0]
150 150 if l <= 4:
151 151 if l:
152 152 raise error.Abort(_("invalid chunk length %d") % l)
153 153 return 0
154 154 if self.callback:
155 155 self.callback()
156 156 return l - 4
157 157
158 158 def changelogheader(self):
159 159 """v10 does not have a changelog header chunk"""
160 160 return {}
161 161
162 162 def manifestheader(self):
163 163 """v10 does not have a manifest header chunk"""
164 164 return {}
165 165
166 166 def filelogheader(self):
167 167 """return the header of the filelogs chunk, v10 only has the filename"""
168 168 l = self._chunklength()
169 169 if not l:
170 170 return {}
171 171 fname = readexactly(self._stream, l)
172 172 return {'filename': fname}
173 173
174 174 def _deltaheader(self, headertuple, prevnode):
175 175 node, p1, p2, cs = headertuple
176 176 if prevnode is None:
177 177 deltabase = p1
178 178 else:
179 179 deltabase = prevnode
180 180 flags = 0
181 181 return node, p1, p2, deltabase, cs, flags
182 182
183 183 def deltachunk(self, prevnode):
184 184 l = self._chunklength()
185 185 if not l:
186 186 return {}
187 187 headerdata = readexactly(self._stream, self.deltaheadersize)
188 188 header = self.deltaheader.unpack(headerdata)
189 189 delta = readexactly(self._stream, l - self.deltaheadersize)
190 190 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
191 191 return (node, p1, p2, cs, deltabase, delta, flags)
192 192
193 193 def getchunks(self):
194 194 """returns all the chunks contains in the bundle
195 195
196 196 Used when you need to forward the binary stream to a file or another
197 197 network API. To do so, it parse the changegroup data, otherwise it will
198 198 block in case of sshrepo because it don't know the end of the stream.
199 199 """
200 200 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
201 201 # and a list of filelogs. For changegroup 3, we expect 4 parts:
202 202 # changelog, manifestlog, a list of tree manifestlogs, and a list of
203 203 # filelogs.
204 204 #
205 205 # Changelog and manifestlog parts are terminated with empty chunks. The
206 206 # tree and file parts are a list of entry sections. Each entry section
207 207 # is a series of chunks terminating in an empty chunk. The list of these
208 208 # entry sections is terminated in yet another empty chunk, so we know
209 209 # we've reached the end of the tree/file list when we reach an empty
210 210 # chunk that was proceeded by no non-empty chunks.
211 211
212 212 parts = 0
213 213 while parts < 2 + self._grouplistcount:
214 214 noentries = True
215 215 while True:
216 216 chunk = getchunk(self)
217 217 if not chunk:
218 218 # The first two empty chunks represent the end of the
219 219 # changelog and the manifestlog portions. The remaining
220 220 # empty chunks represent either A) the end of individual
221 221 # tree or file entries in the file list, or B) the end of
222 222 # the entire list. It's the end of the entire list if there
223 223 # were no entries (i.e. noentries is True).
224 224 if parts < 2:
225 225 parts += 1
226 226 elif noentries:
227 227 parts += 1
228 228 break
229 229 noentries = False
230 230 yield chunkheader(len(chunk))
231 231 pos = 0
232 232 while pos < len(chunk):
233 233 next = pos + 2**20
234 234 yield chunk[pos:next]
235 235 pos = next
236 236 yield closechunk()
237 237
238 238 def _unpackmanifests(self, repo, revmap, trp, prog):
239 239 self.callback = prog.increment
240 240 # no need to check for empty manifest group here:
241 241 # if the result of the merge of 1 and 2 is the same in 3 and 4,
242 242 # no new manifest will be created and the manifest group will
243 243 # be empty during the pull
244 244 self.manifestheader()
245 245 deltas = self.deltaiter()
246 246 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
247 247 prog.complete()
248 248 self.callback = None
249 249
250 250 def apply(self, repo, tr, srctype, url, targetphase=phases.draft,
251 251 expectedtotal=None):
252 252 """Add the changegroup returned by source.read() to this repo.
253 253 srctype is a string like 'push', 'pull', or 'unbundle'. url is
254 254 the URL of the repo where this changegroup is coming from.
255 255
256 256 Return an integer summarizing the change to this repo:
257 257 - nothing changed or no source: 0
258 258 - more heads than before: 1+added heads (2..n)
259 259 - fewer heads than before: -1-removed heads (-2..-n)
260 260 - number of heads stays the same: 1
261 261 """
262 262 repo = repo.unfiltered()
263 263 def csmap(x):
264 264 repo.ui.debug("add changeset %s\n" % short(x))
265 265 return len(cl)
266 266
267 267 def revmap(x):
268 268 return cl.rev(x)
269 269
270 270 changesets = files = revisions = 0
271 271
272 272 try:
273 273 # The transaction may already carry source information. In this
274 274 # case we use the top level data. We overwrite the argument
275 275 # because we need to use the top level value (if they exist)
276 276 # in this function.
277 277 srctype = tr.hookargs.setdefault('source', srctype)
278 278 tr.hookargs.setdefault('url', url)
279 279 repo.hook('prechangegroup',
280 280 throw=True, **pycompat.strkwargs(tr.hookargs))
281 281
282 282 # write changelog data to temp files so concurrent readers
283 283 # will not see an inconsistent view
284 284 cl = repo.changelog
285 285 cl.delayupdate(tr)
286 286 oldheads = set(cl.heads())
287 287
288 288 trp = weakref.proxy(tr)
289 289 # pull off the changeset group
290 290 repo.ui.status(_("adding changesets\n"))
291 291 clstart = len(cl)
292 292 progress = repo.ui.makeprogress(_('changesets'), unit=_('chunks'),
293 293 total=expectedtotal)
294 294 self.callback = progress.increment
295 295
296 296 efiles = set()
297 297 def onchangelog(cl, node):
298 298 efiles.update(cl.readfiles(node))
299 299
300 300 self.changelogheader()
301 301 deltas = self.deltaiter()
302 302 cgnodes = cl.addgroup(deltas, csmap, trp, addrevisioncb=onchangelog)
303 303 efiles = len(efiles)
304 304
305 305 if not cgnodes:
306 306 repo.ui.develwarn('applied empty changelog from changegroup',
307 307 config='warn-empty-changegroup')
308 308 clend = len(cl)
309 309 changesets = clend - clstart
310 310 progress.complete()
311 311 self.callback = None
312 312
313 313 # pull off the manifest group
314 314 repo.ui.status(_("adding manifests\n"))
315 315 # We know that we'll never have more manifests than we had
316 316 # changesets.
317 317 progress = repo.ui.makeprogress(_('manifests'), unit=_('chunks'),
318 318 total=changesets)
319 319 self._unpackmanifests(repo, revmap, trp, progress)
320 320
321 321 needfiles = {}
322 322 if repo.ui.configbool('server', 'validate'):
323 323 cl = repo.changelog
324 324 ml = repo.manifestlog
325 325 # validate incoming csets have their manifests
326 326 for cset in pycompat.xrange(clstart, clend):
327 327 mfnode = cl.changelogrevision(cset).manifest
328 328 mfest = ml[mfnode].readdelta()
329 329 # store file cgnodes we must see
330 330 for f, n in mfest.iteritems():
331 331 needfiles.setdefault(f, set()).add(n)
332 332
333 333 # process the files
334 334 repo.ui.status(_("adding file changes\n"))
335 335 newrevs, newfiles = _addchangegroupfiles(
336 336 repo, self, revmap, trp, efiles, needfiles)
337 337 revisions += newrevs
338 338 files += newfiles
339 339
340 340 deltaheads = 0
341 341 if oldheads:
342 342 heads = cl.heads()
343 343 deltaheads = len(heads) - len(oldheads)
344 344 for h in heads:
345 345 if h not in oldheads and repo[h].closesbranch():
346 346 deltaheads -= 1
347 347 htext = ""
348 348 if deltaheads:
349 349 htext = _(" (%+d heads)") % deltaheads
350 350
351 351 repo.ui.status(_("added %d changesets"
352 352 " with %d changes to %d files%s\n")
353 353 % (changesets, revisions, files, htext))
354 354 repo.invalidatevolatilesets()
355 355
356 356 if changesets > 0:
357 357 if 'node' not in tr.hookargs:
358 358 tr.hookargs['node'] = hex(cl.node(clstart))
359 359 tr.hookargs['node_last'] = hex(cl.node(clend - 1))
360 360 hookargs = dict(tr.hookargs)
361 361 else:
362 362 hookargs = dict(tr.hookargs)
363 363 hookargs['node'] = hex(cl.node(clstart))
364 364 hookargs['node_last'] = hex(cl.node(clend - 1))
365 365 repo.hook('pretxnchangegroup',
366 366 throw=True, **pycompat.strkwargs(hookargs))
367 367
368 368 added = [cl.node(r) for r in pycompat.xrange(clstart, clend)]
369 369 phaseall = None
370 370 if srctype in ('push', 'serve'):
371 371 # Old servers can not push the boundary themselves.
372 372 # New servers won't push the boundary if changeset already
373 373 # exists locally as secret
374 374 #
375 375 # We should not use added here but the list of all change in
376 376 # the bundle
377 377 if repo.publishing():
378 378 targetphase = phaseall = phases.public
379 379 else:
380 380 # closer target phase computation
381 381
382 382 # Those changesets have been pushed from the
383 383 # outside, their phases are going to be pushed
384 384 # alongside. Therefor `targetphase` is
385 385 # ignored.
386 386 targetphase = phaseall = phases.draft
387 387 if added:
388 388 phases.registernew(repo, tr, targetphase, added)
389 389 if phaseall is not None:
390 390 phases.advanceboundary(repo, tr, phaseall, cgnodes)
391 391
392 392 if changesets > 0:
393 393
394 394 def runhooks():
395 395 # These hooks run when the lock releases, not when the
396 396 # transaction closes. So it's possible for the changelog
397 397 # to have changed since we last saw it.
398 398 if clstart >= len(repo):
399 399 return
400 400
401 401 repo.hook("changegroup", **pycompat.strkwargs(hookargs))
402 402
403 403 for n in added:
404 404 args = hookargs.copy()
405 405 args['node'] = hex(n)
406 406 del args['node_last']
407 407 repo.hook("incoming", **pycompat.strkwargs(args))
408 408
409 409 newheads = [h for h in repo.heads()
410 410 if h not in oldheads]
411 411 repo.ui.log("incoming",
412 412 "%d incoming changes - new heads: %s\n",
413 413 len(added),
414 414 ', '.join([hex(c[:6]) for c in newheads]))
415 415
416 416 tr.addpostclose('changegroup-runhooks-%020i' % clstart,
417 417 lambda tr: repo._afterlock(runhooks))
418 418 finally:
419 419 repo.ui.flush()
420 420 # never return 0 here:
421 421 if deltaheads < 0:
422 422 ret = deltaheads - 1
423 423 else:
424 424 ret = deltaheads + 1
425 425 return ret
426 426
427 427 def deltaiter(self):
428 428 """
429 429 returns an iterator of the deltas in this changegroup
430 430
431 431 Useful for passing to the underlying storage system to be stored.
432 432 """
433 433 chain = None
434 434 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
435 435 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
436 436 yield chunkdata
437 437 chain = chunkdata[0]
438 438
439 439 class cg2unpacker(cg1unpacker):
440 440 """Unpacker for cg2 streams.
441 441
442 442 cg2 streams add support for generaldelta, so the delta header
443 443 format is slightly different. All other features about the data
444 444 remain the same.
445 445 """
446 446 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
447 447 deltaheadersize = deltaheader.size
448 448 version = '02'
449 449
450 450 def _deltaheader(self, headertuple, prevnode):
451 451 node, p1, p2, deltabase, cs = headertuple
452 452 flags = 0
453 453 return node, p1, p2, deltabase, cs, flags
454 454
455 455 class cg3unpacker(cg2unpacker):
456 456 """Unpacker for cg3 streams.
457 457
458 458 cg3 streams add support for exchanging treemanifests and revlog
459 459 flags. It adds the revlog flags to the delta header and an empty chunk
460 460 separating manifests and files.
461 461 """
462 462 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
463 463 deltaheadersize = deltaheader.size
464 464 version = '03'
465 465 _grouplistcount = 2 # One list of manifests and one list of files
466 466
467 467 def _deltaheader(self, headertuple, prevnode):
468 468 node, p1, p2, deltabase, cs, flags = headertuple
469 469 return node, p1, p2, deltabase, cs, flags
470 470
471 471 def _unpackmanifests(self, repo, revmap, trp, prog):
472 472 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
473 473 for chunkdata in iter(self.filelogheader, {}):
474 474 # If we get here, there are directory manifests in the changegroup
475 475 d = chunkdata["filename"]
476 476 repo.ui.debug("adding %s revisions\n" % d)
477 477 deltas = self.deltaiter()
478 478 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
479 479 raise error.Abort(_("received dir revlog group is empty"))
480 480
481 481 class headerlessfixup(object):
482 482 def __init__(self, fh, h):
483 483 self._h = h
484 484 self._fh = fh
485 485 def read(self, n):
486 486 if self._h:
487 487 d, self._h = self._h[:n], self._h[n:]
488 488 if len(d) < n:
489 489 d += readexactly(self._fh, n - len(d))
490 490 return d
491 491 return readexactly(self._fh, n)
492 492
493 493 def _revisiondeltatochunks(delta, headerfn):
494 494 """Serialize a revisiondelta to changegroup chunks."""
495 495
496 496 # The captured revision delta may be encoded as a delta against
497 497 # a base revision or as a full revision. The changegroup format
498 498 # requires that everything on the wire be deltas. So for full
499 499 # revisions, we need to invent a header that says to rewrite
500 500 # data.
501 501
502 502 if delta.delta is not None:
503 503 prefix, data = b'', delta.delta
504 504 elif delta.basenode == nullid:
505 505 data = delta.revision
506 506 prefix = mdiff.trivialdiffheader(len(data))
507 507 else:
508 508 data = delta.revision
509 509 prefix = mdiff.replacediffheader(delta.baserevisionsize,
510 510 len(data))
511 511
512 512 meta = headerfn(delta)
513 513
514 514 yield chunkheader(len(meta) + len(prefix) + len(data))
515 515 yield meta
516 516 if prefix:
517 517 yield prefix
518 518 yield data
519 519
520 520 def _sortnodesellipsis(store, nodes, cl, lookup):
521 521 """Sort nodes for changegroup generation."""
522 522 # Ellipses serving mode.
523 523 #
524 524 # In a perfect world, we'd generate better ellipsis-ified graphs
525 525 # for non-changelog revlogs. In practice, we haven't started doing
526 526 # that yet, so the resulting DAGs for the manifestlog and filelogs
527 527 # are actually full of bogus parentage on all the ellipsis
528 528 # nodes. This has the side effect that, while the contents are
529 529 # correct, the individual DAGs might be completely out of whack in
530 530 # a case like 882681bc3166 and its ancestors (back about 10
531 531 # revisions or so) in the main hg repo.
532 532 #
533 533 # The one invariant we *know* holds is that the new (potentially
534 534 # bogus) DAG shape will be valid if we order the nodes in the
535 535 # order that they're introduced in dramatis personae by the
536 536 # changelog, so what we do is we sort the non-changelog histories
537 537 # by the order in which they are used by the changelog.
538 538 key = lambda n: cl.rev(lookup(n))
539 539 return sorted(nodes, key=key)
540 540
541 541 def _resolvenarrowrevisioninfo(cl, store, ischangelog, rev, linkrev,
542 542 linknode, clrevtolocalrev, fullclnodes,
543 543 precomputedellipsis):
544 544 linkparents = precomputedellipsis[linkrev]
545 545 def local(clrev):
546 546 """Turn a changelog revnum into a local revnum.
547 547
548 548 The ellipsis dag is stored as revnums on the changelog,
549 549 but when we're producing ellipsis entries for
550 550 non-changelog revlogs, we need to turn those numbers into
551 551 something local. This does that for us, and during the
552 552 changelog sending phase will also expand the stored
553 553 mappings as needed.
554 554 """
555 555 if clrev == nullrev:
556 556 return nullrev
557 557
558 558 if ischangelog:
559 559 return clrev
560 560
561 561 # Walk the ellipsis-ized changelog breadth-first looking for a
562 562 # change that has been linked from the current revlog.
563 563 #
564 564 # For a flat manifest revlog only a single step should be necessary
565 565 # as all relevant changelog entries are relevant to the flat
566 566 # manifest.
567 567 #
568 568 # For a filelog or tree manifest dirlog however not every changelog
569 569 # entry will have been relevant, so we need to skip some changelog
570 570 # nodes even after ellipsis-izing.
571 571 walk = [clrev]
572 572 while walk:
573 573 p = walk[0]
574 574 walk = walk[1:]
575 575 if p in clrevtolocalrev:
576 576 return clrevtolocalrev[p]
577 577 elif p in fullclnodes:
578 578 walk.extend([pp for pp in cl.parentrevs(p)
579 579 if pp != nullrev])
580 580 elif p in precomputedellipsis:
581 581 walk.extend([pp for pp in precomputedellipsis[p]
582 582 if pp != nullrev])
583 583 else:
584 584 # In this case, we've got an ellipsis with parents
585 585 # outside the current bundle (likely an
586 586 # incremental pull). We "know" that we can use the
587 587 # value of this same revlog at whatever revision
588 588 # is pointed to by linknode. "Know" is in scare
589 589 # quotes because I haven't done enough examination
590 590 # of edge cases to convince myself this is really
591 591 # a fact - it works for all the (admittedly
592 592 # thorough) cases in our testsuite, but I would be
593 593 # somewhat unsurprised to find a case in the wild
594 594 # where this breaks down a bit. That said, I don't
595 595 # know if it would hurt anything.
596 596 for i in pycompat.xrange(rev, 0, -1):
597 597 if store.linkrev(i) == clrev:
598 598 return i
599 599 # We failed to resolve a parent for this node, so
600 600 # we crash the changegroup construction.
601 601 raise error.Abort(
602 602 'unable to resolve parent while packing %r %r'
603 603 ' for changeset %r' % (store.indexfile, rev, clrev))
604 604
605 605 return nullrev
606 606
607 607 if not linkparents or (
608 608 store.parentrevs(rev) == (nullrev, nullrev)):
609 609 p1, p2 = nullrev, nullrev
610 610 elif len(linkparents) == 1:
611 611 p1, = sorted(local(p) for p in linkparents)
612 612 p2 = nullrev
613 613 else:
614 614 p1, p2 = sorted(local(p) for p in linkparents)
615 615
616 616 p1node, p2node = store.node(p1), store.node(p2)
617 617
618 618 return p1node, p2node, linknode
619 619
620 620 def deltagroup(repo, store, nodes, ischangelog, lookup, forcedeltaparentprev,
621 621 topic=None,
622 622 ellipses=False, clrevtolocalrev=None, fullclnodes=None,
623 623 precomputedellipsis=None):
624 624 """Calculate deltas for a set of revisions.
625 625
626 626 Is a generator of ``revisiondelta`` instances.
627 627
628 628 If topic is not None, progress detail will be generated using this
629 629 topic name (e.g. changesets, manifests, etc).
630 630 """
631 631 if not nodes:
632 632 return
633 633
634 634 cl = repo.changelog
635 635
636 636 if ischangelog:
637 637 # `hg log` shows changesets in storage order. To preserve order
638 638 # across clones, send out changesets in storage order.
639 639 nodesorder = 'storage'
640 640 elif ellipses:
641 641 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
642 642 nodesorder = 'nodes'
643 643 else:
644 644 nodesorder = None
645 645
646 646 # Perform ellipses filtering and revision massaging. We do this before
647 647 # emitrevisions() because a) filtering out revisions creates less work
648 648 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
649 649 # assumptions about delta choices and we would possibly send a delta
650 650 # referencing a missing base revision.
651 651 #
652 652 # Also, calling lookup() has side-effects with regards to populating
653 653 # data structures. If we don't call lookup() for each node or if we call
654 654 # lookup() after the first pass through each node, things can break -
655 655 # possibly intermittently depending on the python hash seed! For that
656 656 # reason, we store a mapping of all linknodes during the initial node
657 657 # pass rather than use lookup() on the output side.
658 658 if ellipses:
659 659 filtered = []
660 660 adjustedparents = {}
661 661 linknodes = {}
662 662
663 663 for node in nodes:
664 664 rev = store.rev(node)
665 665 linknode = lookup(node)
666 666 linkrev = cl.rev(linknode)
667 667 clrevtolocalrev[linkrev] = rev
668 668
669 669 # If linknode is in fullclnodes, it means the corresponding
670 670 # changeset was a full changeset and is being sent unaltered.
671 671 if linknode in fullclnodes:
672 672 linknodes[node] = linknode
673 673
674 674 # If the corresponding changeset wasn't in the set computed
675 675 # as relevant to us, it should be dropped outright.
676 676 elif linkrev not in precomputedellipsis:
677 677 continue
678 678
679 679 else:
680 680 # We could probably do this later and avoid the dict
681 681 # holding state. But it likely doesn't matter.
682 682 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
683 683 cl, store, ischangelog, rev, linkrev, linknode,
684 684 clrevtolocalrev, fullclnodes, precomputedellipsis)
685 685
686 686 adjustedparents[node] = (p1node, p2node)
687 687 linknodes[node] = linknode
688 688
689 689 filtered.append(node)
690 690
691 691 nodes = filtered
692 692
693 693 # We expect the first pass to be fast, so we only engage the progress
694 694 # meter for constructing the revision deltas.
695 695 progress = None
696 696 if topic is not None:
697 697 progress = repo.ui.makeprogress(topic, unit=_('chunks'),
698 698 total=len(nodes))
699 699
700 700 configtarget = repo.ui.config('devel', 'bundle.delta')
701 701 if configtarget not in ('', 'p1', 'full'):
702 702 msg = _("""config "devel.bundle.delta" as unknown value: %s""")
703 703 repo.ui.warn(msg % configtarget)
704 704
705 705 deltamode = repository.CG_DELTAMODE_STD
706 706 if forcedeltaparentprev:
707 707 deltamode = repository.CG_DELTAMODE_PREV
708 708 elif configtarget == 'p1':
709 709 deltamode = repository.CG_DELTAMODE_P1
710 710 elif configtarget == 'full':
711 711 deltamode = repository.CG_DELTAMODE_FULL
712 712
713 713 revisions = store.emitrevisions(
714 714 nodes,
715 715 nodesorder=nodesorder,
716 716 revisiondata=True,
717 717 assumehaveparentrevisions=not ellipses,
718 718 deltamode=deltamode)
719 719
720 720 for i, revision in enumerate(revisions):
721 721 if progress:
722 722 progress.update(i + 1)
723 723
724 724 if ellipses:
725 725 linknode = linknodes[revision.node]
726 726
727 727 if revision.node in adjustedparents:
728 728 p1node, p2node = adjustedparents[revision.node]
729 729 revision.p1node = p1node
730 730 revision.p2node = p2node
731 731 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
732 732
733 733 else:
734 734 linknode = lookup(revision.node)
735 735
736 736 revision.linknode = linknode
737 737 yield revision
738 738
739 739 if progress:
740 740 progress.complete()
741 741
742 742 class cgpacker(object):
743 743 def __init__(self, repo, oldmatcher, matcher, version,
744 744 builddeltaheader, manifestsend,
745 745 forcedeltaparentprev=False,
746 746 bundlecaps=None, ellipses=False,
747 747 shallow=False, ellipsisroots=None, fullnodes=None):
748 748 """Given a source repo, construct a bundler.
749 749
750 750 oldmatcher is a matcher that matches on files the client already has.
751 751 These will not be included in the changegroup.
752 752
753 753 matcher is a matcher that matches on files to include in the
754 754 changegroup. Used to facilitate sparse changegroups.
755 755
756 756 forcedeltaparentprev indicates whether delta parents must be against
757 757 the previous revision in a delta group. This should only be used for
758 758 compatibility with changegroup version 1.
759 759
760 760 builddeltaheader is a callable that constructs the header for a group
761 761 delta.
762 762
763 763 manifestsend is a chunk to send after manifests have been fully emitted.
764 764
765 765 ellipses indicates whether ellipsis serving mode is enabled.
766 766
767 767 bundlecaps is optional and can be used to specify the set of
768 768 capabilities which can be used to build the bundle. While bundlecaps is
769 769 unused in core Mercurial, extensions rely on this feature to communicate
770 770 capabilities to customize the changegroup packer.
771 771
772 772 shallow indicates whether shallow data might be sent. The packer may
773 773 need to pack file contents not introduced by the changes being packed.
774 774
775 775 fullnodes is the set of changelog nodes which should not be ellipsis
776 776 nodes. We store this rather than the set of nodes that should be
777 777 ellipsis because for very large histories we expect this to be
778 778 significantly smaller.
779 779 """
780 780 assert oldmatcher
781 781 assert matcher
782 782 self._oldmatcher = oldmatcher
783 783 self._matcher = matcher
784 784
785 785 self.version = version
786 786 self._forcedeltaparentprev = forcedeltaparentprev
787 787 self._builddeltaheader = builddeltaheader
788 788 self._manifestsend = manifestsend
789 789 self._ellipses = ellipses
790 790
791 791 # Set of capabilities we can use to build the bundle.
792 792 if bundlecaps is None:
793 793 bundlecaps = set()
794 794 self._bundlecaps = bundlecaps
795 795 self._isshallow = shallow
796 796 self._fullclnodes = fullnodes
797 797
798 798 # Maps ellipsis revs to their roots at the changelog level.
799 799 self._precomputedellipsis = ellipsisroots
800 800
801 801 self._repo = repo
802 802
803 803 if self._repo.ui.verbose and not self._repo.ui.debugflag:
804 804 self._verbosenote = self._repo.ui.note
805 805 else:
806 806 self._verbosenote = lambda s: None
807 807
808 808 def generate(self, commonrevs, clnodes, fastpathlinkrev, source,
809 809 changelog=True):
810 810 """Yield a sequence of changegroup byte chunks.
811 811 If changelog is False, changelog data won't be added to changegroup
812 812 """
813 813
814 814 repo = self._repo
815 815 cl = repo.changelog
816 816
817 817 self._verbosenote(_('uncompressed size of bundle content:\n'))
818 818 size = 0
819 819
820 820 clstate, deltas = self._generatechangelog(cl, clnodes,
821 821 generate=changelog)
822 822 for delta in deltas:
823 823 for chunk in _revisiondeltatochunks(delta,
824 824 self._builddeltaheader):
825 825 size += len(chunk)
826 826 yield chunk
827 827
828 828 close = closechunk()
829 829 size += len(close)
830 830 yield closechunk()
831 831
832 832 self._verbosenote(_('%8.i (changelog)\n') % size)
833 833
834 834 clrevorder = clstate['clrevorder']
835 835 manifests = clstate['manifests']
836 836 changedfiles = clstate['changedfiles']
837 837
838 838 # We need to make sure that the linkrev in the changegroup refers to
839 839 # the first changeset that introduced the manifest or file revision.
840 840 # The fastpath is usually safer than the slowpath, because the filelogs
841 841 # are walked in revlog order.
842 842 #
843 843 # When taking the slowpath when the manifest revlog uses generaldelta,
844 844 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
845 845 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
846 846 #
847 847 # When taking the fastpath, we are only vulnerable to reordering
848 848 # of the changelog itself. The changelog never uses generaldelta and is
849 849 # never reordered. To handle this case, we simply take the slowpath,
850 850 # which already has the 'clrevorder' logic. This was also fixed in
851 851 # cc0ff93d0c0c.
852 852
853 853 # Treemanifests don't work correctly with fastpathlinkrev
854 854 # either, because we don't discover which directory nodes to
855 855 # send along with files. This could probably be fixed.
856 856 fastpathlinkrev = fastpathlinkrev and (
857 857 'treemanifest' not in repo.requirements)
858 858
859 859 fnodes = {} # needed file nodes
860 860
861 861 size = 0
862 862 it = self.generatemanifests(
863 863 commonrevs, clrevorder, fastpathlinkrev, manifests, fnodes, source,
864 864 clstate['clrevtomanifestrev'])
865 865
866 866 for tree, deltas in it:
867 867 if tree:
868 868 assert self.version == b'03'
869 869 chunk = _fileheader(tree)
870 870 size += len(chunk)
871 871 yield chunk
872 872
873 873 for delta in deltas:
874 874 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
875 875 for chunk in chunks:
876 876 size += len(chunk)
877 877 yield chunk
878 878
879 879 close = closechunk()
880 880 size += len(close)
881 881 yield close
882 882
883 883 self._verbosenote(_('%8.i (manifests)\n') % size)
884 884 yield self._manifestsend
885 885
886 886 mfdicts = None
887 887 if self._ellipses and self._isshallow:
888 888 mfdicts = [(self._repo.manifestlog[n].read(), lr)
889 889 for (n, lr) in manifests.iteritems()]
890 890
891 891 manifests.clear()
892 892 clrevs = set(cl.rev(x) for x in clnodes)
893 893
894 894 it = self.generatefiles(changedfiles, commonrevs,
895 895 source, mfdicts, fastpathlinkrev,
896 896 fnodes, clrevs)
897 897
898 898 for path, deltas in it:
899 899 h = _fileheader(path)
900 900 size = len(h)
901 901 yield h
902 902
903 903 for delta in deltas:
904 904 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
905 905 for chunk in chunks:
906 906 size += len(chunk)
907 907 yield chunk
908 908
909 909 close = closechunk()
910 910 size += len(close)
911 911 yield close
912 912
913 913 self._verbosenote(_('%8.i %s\n') % (size, path))
914 914
915 915 yield closechunk()
916 916
917 917 if clnodes:
918 918 repo.hook('outgoing', node=hex(clnodes[0]), source=source)
919 919
920 920 def _generatechangelog(self, cl, nodes, generate=True):
921 921 """Generate data for changelog chunks.
922 922
923 923 Returns a 2-tuple of a dict containing state and an iterable of
924 924 byte chunks. The state will not be fully populated until the
925 925 chunk stream has been fully consumed.
926 926
927 927 if generate is False, the state will be fully populated and no chunk
928 928 stream will be yielded
929 929 """
930 930 clrevorder = {}
931 931 manifests = {}
932 932 mfl = self._repo.manifestlog
933 933 changedfiles = set()
934 934 clrevtomanifestrev = {}
935 935
936 936 state = {
937 937 'clrevorder': clrevorder,
938 938 'manifests': manifests,
939 939 'changedfiles': changedfiles,
940 940 'clrevtomanifestrev': clrevtomanifestrev,
941 941 }
942 942
943 943 if not (generate or self._ellipses):
944 944 # sort the nodes in storage order
945 945 nodes = sorted(nodes, key=cl.rev)
946 946 for node in nodes:
947 947 c = cl.changelogrevision(node)
948 948 clrevorder[node] = len(clrevorder)
949 949 # record the first changeset introducing this manifest version
950 950 manifests.setdefault(c.manifest, node)
951 951 # Record a complete list of potentially-changed files in
952 952 # this manifest.
953 953 changedfiles.update(c.files)
954 954
955 955 return state, ()
956 956
957 957 # Callback for the changelog, used to collect changed files and
958 958 # manifest nodes.
959 959 # Returns the linkrev node (identity in the changelog case).
960 960 def lookupcl(x):
961 961 c = cl.changelogrevision(x)
962 962 clrevorder[x] = len(clrevorder)
963 963
964 964 if self._ellipses:
965 965 # Only update manifests if x is going to be sent. Otherwise we
966 966 # end up with bogus linkrevs specified for manifests and
967 967 # we skip some manifest nodes that we should otherwise
968 968 # have sent.
969 969 if (x in self._fullclnodes
970 970 or cl.rev(x) in self._precomputedellipsis):
971 971
972 972 manifestnode = c.manifest
973 973 # Record the first changeset introducing this manifest
974 974 # version.
975 975 manifests.setdefault(manifestnode, x)
976 976 # Set this narrow-specific dict so we have the lowest
977 977 # manifest revnum to look up for this cl revnum. (Part of
978 978 # mapping changelog ellipsis parents to manifest ellipsis
979 979 # parents)
980 980 clrevtomanifestrev.setdefault(
981 981 cl.rev(x), mfl.rev(manifestnode))
982 982 # We can't trust the changed files list in the changeset if the
983 983 # client requested a shallow clone.
984 984 if self._isshallow:
985 985 changedfiles.update(mfl[c.manifest].read().keys())
986 986 else:
987 987 changedfiles.update(c.files)
988 988 else:
989 989 # record the first changeset introducing this manifest version
990 990 manifests.setdefault(c.manifest, x)
991 991 # Record a complete list of potentially-changed files in
992 992 # this manifest.
993 993 changedfiles.update(c.files)
994 994
995 995 return x
996 996
997 997 gen = deltagroup(
998 998 self._repo, cl, nodes, True, lookupcl,
999 999 self._forcedeltaparentprev,
1000 1000 ellipses=self._ellipses,
1001 1001 topic=_('changesets'),
1002 1002 clrevtolocalrev={},
1003 1003 fullclnodes=self._fullclnodes,
1004 1004 precomputedellipsis=self._precomputedellipsis)
1005 1005
1006 1006 return state, gen
1007 1007
1008 1008 def generatemanifests(self, commonrevs, clrevorder, fastpathlinkrev,
1009 1009 manifests, fnodes, source, clrevtolocalrev):
1010 1010 """Returns an iterator of changegroup chunks containing manifests.
1011 1011
1012 1012 `source` is unused here, but is used by extensions like remotefilelog to
1013 1013 change what is sent based in pulls vs pushes, etc.
1014 1014 """
1015 1015 repo = self._repo
1016 1016 mfl = repo.manifestlog
1017 1017 tmfnodes = {'': manifests}
1018 1018
1019 1019 # Callback for the manifest, used to collect linkrevs for filelog
1020 1020 # revisions.
1021 1021 # Returns the linkrev node (collected in lookupcl).
1022 1022 def makelookupmflinknode(tree, nodes):
1023 1023 if fastpathlinkrev:
1024 1024 assert not tree
1025 1025 return manifests.__getitem__
1026 1026
1027 1027 def lookupmflinknode(x):
1028 1028 """Callback for looking up the linknode for manifests.
1029 1029
1030 1030 Returns the linkrev node for the specified manifest.
1031 1031
1032 1032 SIDE EFFECT:
1033 1033
1034 1034 1) fclnodes gets populated with the list of relevant
1035 1035 file nodes if we're not using fastpathlinkrev
1036 1036 2) When treemanifests are in use, collects treemanifest nodes
1037 1037 to send
1038 1038
1039 1039 Note that this means manifests must be completely sent to
1040 1040 the client before you can trust the list of files and
1041 1041 treemanifests to send.
1042 1042 """
1043 1043 clnode = nodes[x]
1044 1044 mdata = mfl.get(tree, x).readfast(shallow=True)
1045 1045 for p, n, fl in mdata.iterentries():
1046 1046 if fl == 't': # subdirectory manifest
1047 1047 subtree = tree + p + '/'
1048 1048 tmfclnodes = tmfnodes.setdefault(subtree, {})
1049 1049 tmfclnode = tmfclnodes.setdefault(n, clnode)
1050 1050 if clrevorder[clnode] < clrevorder[tmfclnode]:
1051 1051 tmfclnodes[n] = clnode
1052 1052 else:
1053 1053 f = tree + p
1054 1054 fclnodes = fnodes.setdefault(f, {})
1055 1055 fclnode = fclnodes.setdefault(n, clnode)
1056 1056 if clrevorder[clnode] < clrevorder[fclnode]:
1057 1057 fclnodes[n] = clnode
1058 1058 return clnode
1059 1059 return lookupmflinknode
1060 1060
1061 1061 while tmfnodes:
1062 1062 tree, nodes = tmfnodes.popitem()
1063 1063
1064 1064 should_visit = self._matcher.visitdir(tree[:-1] or '.')
1065 1065 if tree and not should_visit:
1066 1066 continue
1067 1067
1068 1068 store = mfl.getstorage(tree)
1069 1069
1070 1070 if not should_visit:
1071 1071 # No nodes to send because this directory is out of
1072 1072 # the client's view of the repository (probably
1073 1073 # because of narrow clones). Do this even for the root
1074 1074 # directory (tree=='')
1075 1075 prunednodes = []
1076 1076 else:
1077 1077 # Avoid sending any manifest nodes we can prove the
1078 1078 # client already has by checking linkrevs. See the
1079 1079 # related comment in generatefiles().
1080 1080 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1081 1081
1082 1082 if tree and not prunednodes:
1083 1083 continue
1084 1084
1085 1085 lookupfn = makelookupmflinknode(tree, nodes)
1086 1086
1087 1087 deltas = deltagroup(
1088 1088 self._repo, store, prunednodes, False, lookupfn,
1089 1089 self._forcedeltaparentprev,
1090 1090 ellipses=self._ellipses,
1091 1091 topic=_('manifests'),
1092 1092 clrevtolocalrev=clrevtolocalrev,
1093 1093 fullclnodes=self._fullclnodes,
1094 1094 precomputedellipsis=self._precomputedellipsis)
1095 1095
1096 1096 if not self._oldmatcher.visitdir(store.tree[:-1] or '.'):
1097 1097 yield tree, deltas
1098 1098 else:
1099 1099 # 'deltas' is a generator and we need to consume it even if
1100 1100 # we are not going to send it because a side-effect is that
1101 1101 # it updates tmdnodes (via lookupfn)
1102 1102 for d in deltas:
1103 1103 pass
1104 1104 if not tree:
1105 1105 yield tree, []
1106 1106
1107 1107 def _prunemanifests(self, store, nodes, commonrevs):
1108 1108 # This is split out as a separate method to allow filtering
1109 1109 # commonrevs in extension code.
1110 1110 #
1111 1111 # TODO(augie): this shouldn't be required, instead we should
1112 1112 # make filtering of revisions to send delegated to the store
1113 1113 # layer.
1114 1114 frev, flr = store.rev, store.linkrev
1115 1115 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1116 1116
1117 1117 # The 'source' parameter is useful for extensions
1118 1118 def generatefiles(self, changedfiles, commonrevs, source,
1119 1119 mfdicts, fastpathlinkrev, fnodes, clrevs):
1120 1120 changedfiles = [f for f in changedfiles
1121 1121 if self._matcher(f) and not self._oldmatcher(f)]
1122 1122
1123 1123 if not fastpathlinkrev:
1124 1124 def normallinknodes(unused, fname):
1125 1125 return fnodes.get(fname, {})
1126 1126 else:
1127 1127 cln = self._repo.changelog.node
1128 1128
1129 1129 def normallinknodes(store, fname):
1130 1130 flinkrev = store.linkrev
1131 1131 fnode = store.node
1132 1132 revs = ((r, flinkrev(r)) for r in store)
1133 1133 return dict((fnode(r), cln(lr))
1134 1134 for r, lr in revs if lr in clrevs)
1135 1135
1136 1136 clrevtolocalrev = {}
1137 1137
1138 1138 if self._isshallow:
1139 1139 # In a shallow clone, the linknodes callback needs to also include
1140 1140 # those file nodes that are in the manifests we sent but weren't
1141 1141 # introduced by those manifests.
1142 1142 commonctxs = [self._repo[c] for c in commonrevs]
1143 1143 clrev = self._repo.changelog.rev
1144 1144
1145 1145 def linknodes(flog, fname):
1146 1146 for c in commonctxs:
1147 1147 try:
1148 1148 fnode = c.filenode(fname)
1149 1149 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1150 1150 except error.ManifestLookupError:
1151 1151 pass
1152 1152 links = normallinknodes(flog, fname)
1153 1153 if len(links) != len(mfdicts):
1154 1154 for mf, lr in mfdicts:
1155 1155 fnode = mf.get(fname, None)
1156 1156 if fnode in links:
1157 1157 links[fnode] = min(links[fnode], lr, key=clrev)
1158 1158 elif fnode:
1159 1159 links[fnode] = lr
1160 1160 return links
1161 1161 else:
1162 1162 linknodes = normallinknodes
1163 1163
1164 1164 repo = self._repo
1165 1165 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1166 1166 total=len(changedfiles))
1167 1167 for i, fname in enumerate(sorted(changedfiles)):
1168 1168 filerevlog = repo.file(fname)
1169 1169 if not filerevlog:
1170 1170 raise error.Abort(_("empty or missing file data for %s") %
1171 1171 fname)
1172 1172
1173 1173 clrevtolocalrev.clear()
1174 1174
1175 1175 linkrevnodes = linknodes(filerevlog, fname)
1176 1176 # Lookup for filenodes, we collected the linkrev nodes above in the
1177 1177 # fastpath case and with lookupmf in the slowpath case.
1178 1178 def lookupfilelog(x):
1179 1179 return linkrevnodes[x]
1180 1180
1181 1181 frev, flr = filerevlog.rev, filerevlog.linkrev
1182 1182 # Skip sending any filenode we know the client already
1183 1183 # has. This avoids over-sending files relatively
1184 1184 # inexpensively, so it's not a problem if we under-filter
1185 1185 # here.
1186 1186 filenodes = [n for n in linkrevnodes
1187 1187 if flr(frev(n)) not in commonrevs]
1188 1188
1189 1189 if not filenodes:
1190 1190 continue
1191 1191
1192 1192 progress.update(i + 1, item=fname)
1193 1193
1194 1194 deltas = deltagroup(
1195 1195 self._repo, filerevlog, filenodes, False, lookupfilelog,
1196 1196 self._forcedeltaparentprev,
1197 1197 ellipses=self._ellipses,
1198 1198 clrevtolocalrev=clrevtolocalrev,
1199 1199 fullclnodes=self._fullclnodes,
1200 1200 precomputedellipsis=self._precomputedellipsis)
1201 1201
1202 1202 yield fname, deltas
1203 1203
1204 1204 progress.complete()
1205 1205
1206 1206 def _makecg1packer(repo, oldmatcher, matcher, bundlecaps,
1207 1207 ellipses=False, shallow=False, ellipsisroots=None,
1208 1208 fullnodes=None):
1209 1209 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1210 1210 d.node, d.p1node, d.p2node, d.linknode)
1211 1211
1212 1212 return cgpacker(repo, oldmatcher, matcher, b'01',
1213 1213 builddeltaheader=builddeltaheader,
1214 1214 manifestsend=b'',
1215 1215 forcedeltaparentprev=True,
1216 1216 bundlecaps=bundlecaps,
1217 1217 ellipses=ellipses,
1218 1218 shallow=shallow,
1219 1219 ellipsisroots=ellipsisroots,
1220 1220 fullnodes=fullnodes)
1221 1221
1222 1222 def _makecg2packer(repo, oldmatcher, matcher, bundlecaps,
1223 1223 ellipses=False, shallow=False, ellipsisroots=None,
1224 1224 fullnodes=None):
1225 1225 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1226 1226 d.node, d.p1node, d.p2node, d.basenode, d.linknode)
1227 1227
1228 1228 return cgpacker(repo, oldmatcher, matcher, b'02',
1229 1229 builddeltaheader=builddeltaheader,
1230 1230 manifestsend=b'',
1231 1231 bundlecaps=bundlecaps,
1232 1232 ellipses=ellipses,
1233 1233 shallow=shallow,
1234 1234 ellipsisroots=ellipsisroots,
1235 1235 fullnodes=fullnodes)
1236 1236
1237 1237 def _makecg3packer(repo, oldmatcher, matcher, bundlecaps,
1238 1238 ellipses=False, shallow=False, ellipsisroots=None,
1239 1239 fullnodes=None):
1240 1240 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1241 1241 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags)
1242 1242
1243 1243 return cgpacker(repo, oldmatcher, matcher, b'03',
1244 1244 builddeltaheader=builddeltaheader,
1245 1245 manifestsend=closechunk(),
1246 1246 bundlecaps=bundlecaps,
1247 1247 ellipses=ellipses,
1248 1248 shallow=shallow,
1249 1249 ellipsisroots=ellipsisroots,
1250 1250 fullnodes=fullnodes)
1251 1251
1252 1252 _packermap = {'01': (_makecg1packer, cg1unpacker),
1253 1253 # cg2 adds support for exchanging generaldelta
1254 1254 '02': (_makecg2packer, cg2unpacker),
1255 1255 # cg3 adds support for exchanging revlog flags and treemanifests
1256 1256 '03': (_makecg3packer, cg3unpacker),
1257 1257 }
1258 1258
1259 1259 def allsupportedversions(repo):
1260 1260 versions = set(_packermap.keys())
1261 1261 if not (repo.ui.configbool('experimental', 'changegroup3') or
1262 1262 repo.ui.configbool('experimental', 'treemanifest') or
1263 1263 'treemanifest' in repo.requirements):
1264 1264 versions.discard('03')
1265 1265 return versions
1266 1266
1267 1267 # Changegroup versions that can be applied to the repo
1268 1268 def supportedincomingversions(repo):
1269 1269 return allsupportedversions(repo)
1270 1270
1271 1271 # Changegroup versions that can be created from the repo
1272 1272 def supportedoutgoingversions(repo):
1273 1273 versions = allsupportedversions(repo)
1274 1274 if 'treemanifest' in repo.requirements:
1275 1275 # Versions 01 and 02 support only flat manifests and it's just too
1276 1276 # expensive to convert between the flat manifest and tree manifest on
1277 1277 # the fly. Since tree manifests are hashed differently, all of history
1278 1278 # would have to be converted. Instead, we simply don't even pretend to
1279 1279 # support versions 01 and 02.
1280 1280 versions.discard('01')
1281 1281 versions.discard('02')
1282 1282 if repository.NARROW_REQUIREMENT in repo.requirements:
1283 1283 # Versions 01 and 02 don't support revlog flags, and we need to
1284 1284 # support that for stripping and unbundling to work.
1285 1285 versions.discard('01')
1286 1286 versions.discard('02')
1287 1287 if LFS_REQUIREMENT in repo.requirements:
1288 1288 # Versions 01 and 02 don't support revlog flags, and we need to
1289 1289 # mark LFS entries with REVIDX_EXTSTORED.
1290 1290 versions.discard('01')
1291 1291 versions.discard('02')
1292 1292
1293 1293 return versions
1294 1294
1295 1295 def localversion(repo):
1296 1296 # Finds the best version to use for bundles that are meant to be used
1297 1297 # locally, such as those from strip and shelve, and temporary bundles.
1298 1298 return max(supportedoutgoingversions(repo))
1299 1299
1300 1300 def safeversion(repo):
1301 1301 # Finds the smallest version that it's safe to assume clients of the repo
1302 1302 # will support. For example, all hg versions that support generaldelta also
1303 1303 # support changegroup 02.
1304 1304 versions = supportedoutgoingversions(repo)
1305 1305 if 'generaldelta' in repo.requirements:
1306 1306 versions.discard('01')
1307 1307 assert versions
1308 1308 return min(versions)
1309 1309
1310 1310 def getbundler(version, repo, bundlecaps=None, oldmatcher=None,
1311 1311 matcher=None, ellipses=False, shallow=False,
1312 1312 ellipsisroots=None, fullnodes=None):
1313 1313 assert version in supportedoutgoingversions(repo)
1314 1314
1315 1315 if matcher is None:
1316 matcher = matchmod.alwaysmatcher(repo.root, '')
1316 matcher = matchmod.always(repo.root, '')
1317 1317 if oldmatcher is None:
1318 oldmatcher = matchmod.nevermatcher(repo.root, '')
1318 oldmatcher = matchmod.never(repo.root, '')
1319 1319
1320 1320 if version == '01' and not matcher.always():
1321 1321 raise error.ProgrammingError('version 01 changegroups do not support '
1322 1322 'sparse file matchers')
1323 1323
1324 1324 if ellipses and version in (b'01', b'02'):
1325 1325 raise error.Abort(
1326 1326 _('ellipsis nodes require at least cg3 on client and server, '
1327 1327 'but negotiated version %s') % version)
1328 1328
1329 1329 # Requested files could include files not in the local store. So
1330 1330 # filter those out.
1331 1331 matcher = repo.narrowmatch(matcher)
1332 1332
1333 1333 fn = _packermap[version][0]
1334 1334 return fn(repo, oldmatcher, matcher, bundlecaps, ellipses=ellipses,
1335 1335 shallow=shallow, ellipsisroots=ellipsisroots,
1336 1336 fullnodes=fullnodes)
1337 1337
1338 1338 def getunbundler(version, fh, alg, extras=None):
1339 1339 return _packermap[version][1](fh, alg, extras=extras)
1340 1340
1341 1341 def _changegroupinfo(repo, nodes, source):
1342 1342 if repo.ui.verbose or source == 'bundle':
1343 1343 repo.ui.status(_("%d changesets found\n") % len(nodes))
1344 1344 if repo.ui.debugflag:
1345 1345 repo.ui.debug("list of changesets:\n")
1346 1346 for node in nodes:
1347 1347 repo.ui.debug("%s\n" % hex(node))
1348 1348
1349 1349 def makechangegroup(repo, outgoing, version, source, fastpath=False,
1350 1350 bundlecaps=None):
1351 1351 cgstream = makestream(repo, outgoing, version, source,
1352 1352 fastpath=fastpath, bundlecaps=bundlecaps)
1353 1353 return getunbundler(version, util.chunkbuffer(cgstream), None,
1354 1354 {'clcount': len(outgoing.missing) })
1355 1355
1356 1356 def makestream(repo, outgoing, version, source, fastpath=False,
1357 1357 bundlecaps=None, matcher=None):
1358 1358 bundler = getbundler(version, repo, bundlecaps=bundlecaps,
1359 1359 matcher=matcher)
1360 1360
1361 1361 repo = repo.unfiltered()
1362 1362 commonrevs = outgoing.common
1363 1363 csets = outgoing.missing
1364 1364 heads = outgoing.missingheads
1365 1365 # We go through the fast path if we get told to, or if all (unfiltered
1366 1366 # heads have been requested (since we then know there all linkrevs will
1367 1367 # be pulled by the client).
1368 1368 heads.sort()
1369 1369 fastpathlinkrev = fastpath or (
1370 1370 repo.filtername is None and heads == sorted(repo.heads()))
1371 1371
1372 1372 repo.hook('preoutgoing', throw=True, source=source)
1373 1373 _changegroupinfo(repo, csets, source)
1374 1374 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1375 1375
1376 1376 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1377 1377 revisions = 0
1378 1378 files = 0
1379 1379 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1380 1380 total=expectedfiles)
1381 1381 for chunkdata in iter(source.filelogheader, {}):
1382 1382 files += 1
1383 1383 f = chunkdata["filename"]
1384 1384 repo.ui.debug("adding %s revisions\n" % f)
1385 1385 progress.increment()
1386 1386 fl = repo.file(f)
1387 1387 o = len(fl)
1388 1388 try:
1389 1389 deltas = source.deltaiter()
1390 1390 if not fl.addgroup(deltas, revmap, trp):
1391 1391 raise error.Abort(_("received file revlog group is empty"))
1392 1392 except error.CensoredBaseError as e:
1393 1393 raise error.Abort(_("received delta base is censored: %s") % e)
1394 1394 revisions += len(fl) - o
1395 1395 if f in needfiles:
1396 1396 needs = needfiles[f]
1397 1397 for new in pycompat.xrange(o, len(fl)):
1398 1398 n = fl.node(new)
1399 1399 if n in needs:
1400 1400 needs.remove(n)
1401 1401 else:
1402 1402 raise error.Abort(
1403 1403 _("received spurious file revlog entry"))
1404 1404 if not needs:
1405 1405 del needfiles[f]
1406 1406 progress.complete()
1407 1407
1408 1408 for f, needs in needfiles.iteritems():
1409 1409 fl = repo.file(f)
1410 1410 for n in needs:
1411 1411 try:
1412 1412 fl.rev(n)
1413 1413 except error.LookupError:
1414 1414 raise error.Abort(
1415 1415 _('missing file data for %s:%s - run hg verify') %
1416 1416 (f, hex(n)))
1417 1417
1418 1418 return revisions, files
@@ -1,562 +1,561 b''
1 1 # fileset.py - file set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import re
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 filesetlang,
17 17 match as matchmod,
18 18 merge,
19 19 pycompat,
20 20 registrar,
21 21 scmutil,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 stringutil,
26 26 )
27 27
28 28 # common weight constants
29 29 _WEIGHT_CHECK_FILENAME = filesetlang.WEIGHT_CHECK_FILENAME
30 30 _WEIGHT_READ_CONTENTS = filesetlang.WEIGHT_READ_CONTENTS
31 31 _WEIGHT_STATUS = filesetlang.WEIGHT_STATUS
32 32 _WEIGHT_STATUS_THOROUGH = filesetlang.WEIGHT_STATUS_THOROUGH
33 33
34 34 # helpers for processing parsed tree
35 35 getsymbol = filesetlang.getsymbol
36 36 getstring = filesetlang.getstring
37 37 _getkindpat = filesetlang.getkindpat
38 38 getpattern = filesetlang.getpattern
39 39 getargs = filesetlang.getargs
40 40
41 41 def getmatch(mctx, x):
42 42 if not x:
43 43 raise error.ParseError(_("missing argument"))
44 44 return methods[x[0]](mctx, *x[1:])
45 45
46 46 def getmatchwithstatus(mctx, x, hint):
47 47 keys = set(getstring(hint, 'status hint must be a string').split())
48 48 return getmatch(mctx.withstatus(keys), x)
49 49
50 50 def stringmatch(mctx, x):
51 51 return mctx.matcher([x])
52 52
53 53 def kindpatmatch(mctx, x, y):
54 54 return stringmatch(mctx, _getkindpat(x, y, matchmod.allpatternkinds,
55 55 _("pattern must be a string")))
56 56
57 57 def patternsmatch(mctx, *xs):
58 58 allkinds = matchmod.allpatternkinds
59 59 patterns = [getpattern(x, allkinds, _("pattern must be a string"))
60 60 for x in xs]
61 61 return mctx.matcher(patterns)
62 62
63 63 def andmatch(mctx, x, y):
64 64 xm = getmatch(mctx, x)
65 65 ym = getmatch(mctx.narrowed(xm), y)
66 66 return matchmod.intersectmatchers(xm, ym)
67 67
68 68 def ormatch(mctx, *xs):
69 69 ms = [getmatch(mctx, x) for x in xs]
70 70 return matchmod.unionmatcher(ms)
71 71
72 72 def notmatch(mctx, x):
73 73 m = getmatch(mctx, x)
74 74 return mctx.predicate(lambda f: not m(f), predrepr=('<not %r>', m))
75 75
76 76 def minusmatch(mctx, x, y):
77 77 xm = getmatch(mctx, x)
78 78 ym = getmatch(mctx.narrowed(xm), y)
79 79 return matchmod.differencematcher(xm, ym)
80 80
81 81 def listmatch(mctx, *xs):
82 82 raise error.ParseError(_("can't use a list in this context"),
83 83 hint=_('see \'hg help "filesets.x or y"\''))
84 84
85 85 def func(mctx, a, b):
86 86 funcname = getsymbol(a)
87 87 if funcname in symbols:
88 88 return symbols[funcname](mctx, b)
89 89
90 90 keep = lambda fn: getattr(fn, '__doc__', None) is not None
91 91
92 92 syms = [s for (s, fn) in symbols.items() if keep(fn)]
93 93 raise error.UnknownIdentifier(funcname, syms)
94 94
95 95 # symbols are callable like:
96 96 # fun(mctx, x)
97 97 # with:
98 98 # mctx - current matchctx instance
99 99 # x - argument in tree form
100 100 symbols = filesetlang.symbols
101 101
102 102 predicate = registrar.filesetpredicate(symbols)
103 103
104 104 @predicate('modified()', callstatus=True, weight=_WEIGHT_STATUS)
105 105 def modified(mctx, x):
106 106 """File that is modified according to :hg:`status`.
107 107 """
108 108 # i18n: "modified" is a keyword
109 109 getargs(x, 0, 0, _("modified takes no arguments"))
110 110 s = set(mctx.status().modified)
111 111 return mctx.predicate(s.__contains__, predrepr='modified')
112 112
113 113 @predicate('added()', callstatus=True, weight=_WEIGHT_STATUS)
114 114 def added(mctx, x):
115 115 """File that is added according to :hg:`status`.
116 116 """
117 117 # i18n: "added" is a keyword
118 118 getargs(x, 0, 0, _("added takes no arguments"))
119 119 s = set(mctx.status().added)
120 120 return mctx.predicate(s.__contains__, predrepr='added')
121 121
122 122 @predicate('removed()', callstatus=True, weight=_WEIGHT_STATUS)
123 123 def removed(mctx, x):
124 124 """File that is removed according to :hg:`status`.
125 125 """
126 126 # i18n: "removed" is a keyword
127 127 getargs(x, 0, 0, _("removed takes no arguments"))
128 128 s = set(mctx.status().removed)
129 129 return mctx.predicate(s.__contains__, predrepr='removed')
130 130
131 131 @predicate('deleted()', callstatus=True, weight=_WEIGHT_STATUS)
132 132 def deleted(mctx, x):
133 133 """Alias for ``missing()``.
134 134 """
135 135 # i18n: "deleted" is a keyword
136 136 getargs(x, 0, 0, _("deleted takes no arguments"))
137 137 s = set(mctx.status().deleted)
138 138 return mctx.predicate(s.__contains__, predrepr='deleted')
139 139
140 140 @predicate('missing()', callstatus=True, weight=_WEIGHT_STATUS)
141 141 def missing(mctx, x):
142 142 """File that is missing according to :hg:`status`.
143 143 """
144 144 # i18n: "missing" is a keyword
145 145 getargs(x, 0, 0, _("missing takes no arguments"))
146 146 s = set(mctx.status().deleted)
147 147 return mctx.predicate(s.__contains__, predrepr='deleted')
148 148
149 149 @predicate('unknown()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
150 150 def unknown(mctx, x):
151 151 """File that is unknown according to :hg:`status`."""
152 152 # i18n: "unknown" is a keyword
153 153 getargs(x, 0, 0, _("unknown takes no arguments"))
154 154 s = set(mctx.status().unknown)
155 155 return mctx.predicate(s.__contains__, predrepr='unknown')
156 156
157 157 @predicate('ignored()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
158 158 def ignored(mctx, x):
159 159 """File that is ignored according to :hg:`status`."""
160 160 # i18n: "ignored" is a keyword
161 161 getargs(x, 0, 0, _("ignored takes no arguments"))
162 162 s = set(mctx.status().ignored)
163 163 return mctx.predicate(s.__contains__, predrepr='ignored')
164 164
165 165 @predicate('clean()', callstatus=True, weight=_WEIGHT_STATUS)
166 166 def clean(mctx, x):
167 167 """File that is clean according to :hg:`status`.
168 168 """
169 169 # i18n: "clean" is a keyword
170 170 getargs(x, 0, 0, _("clean takes no arguments"))
171 171 s = set(mctx.status().clean)
172 172 return mctx.predicate(s.__contains__, predrepr='clean')
173 173
174 174 @predicate('tracked()')
175 175 def tracked(mctx, x):
176 176 """File that is under Mercurial control."""
177 177 # i18n: "tracked" is a keyword
178 178 getargs(x, 0, 0, _("tracked takes no arguments"))
179 179 return mctx.predicate(mctx.ctx.__contains__, predrepr='tracked')
180 180
181 181 @predicate('binary()', weight=_WEIGHT_READ_CONTENTS)
182 182 def binary(mctx, x):
183 183 """File that appears to be binary (contains NUL bytes).
184 184 """
185 185 # i18n: "binary" is a keyword
186 186 getargs(x, 0, 0, _("binary takes no arguments"))
187 187 return mctx.fpredicate(lambda fctx: fctx.isbinary(),
188 188 predrepr='binary', cache=True)
189 189
190 190 @predicate('exec()')
191 191 def exec_(mctx, x):
192 192 """File that is marked as executable.
193 193 """
194 194 # i18n: "exec" is a keyword
195 195 getargs(x, 0, 0, _("exec takes no arguments"))
196 196 ctx = mctx.ctx
197 197 return mctx.predicate(lambda f: ctx.flags(f) == 'x', predrepr='exec')
198 198
199 199 @predicate('symlink()')
200 200 def symlink(mctx, x):
201 201 """File that is marked as a symlink.
202 202 """
203 203 # i18n: "symlink" is a keyword
204 204 getargs(x, 0, 0, _("symlink takes no arguments"))
205 205 ctx = mctx.ctx
206 206 return mctx.predicate(lambda f: ctx.flags(f) == 'l', predrepr='symlink')
207 207
208 208 @predicate('resolved()', weight=_WEIGHT_STATUS)
209 209 def resolved(mctx, x):
210 210 """File that is marked resolved according to :hg:`resolve -l`.
211 211 """
212 212 # i18n: "resolved" is a keyword
213 213 getargs(x, 0, 0, _("resolved takes no arguments"))
214 214 if mctx.ctx.rev() is not None:
215 215 return mctx.never()
216 216 ms = merge.mergestate.read(mctx.ctx.repo())
217 217 return mctx.predicate(lambda f: f in ms and ms[f] == 'r',
218 218 predrepr='resolved')
219 219
220 220 @predicate('unresolved()', weight=_WEIGHT_STATUS)
221 221 def unresolved(mctx, x):
222 222 """File that is marked unresolved according to :hg:`resolve -l`.
223 223 """
224 224 # i18n: "unresolved" is a keyword
225 225 getargs(x, 0, 0, _("unresolved takes no arguments"))
226 226 if mctx.ctx.rev() is not None:
227 227 return mctx.never()
228 228 ms = merge.mergestate.read(mctx.ctx.repo())
229 229 return mctx.predicate(lambda f: f in ms and ms[f] == 'u',
230 230 predrepr='unresolved')
231 231
232 232 @predicate('hgignore()', weight=_WEIGHT_STATUS)
233 233 def hgignore(mctx, x):
234 234 """File that matches the active .hgignore pattern.
235 235 """
236 236 # i18n: "hgignore" is a keyword
237 237 getargs(x, 0, 0, _("hgignore takes no arguments"))
238 238 return mctx.ctx.repo().dirstate._ignore
239 239
240 240 @predicate('portable()', weight=_WEIGHT_CHECK_FILENAME)
241 241 def portable(mctx, x):
242 242 """File that has a portable name. (This doesn't include filenames with case
243 243 collisions.)
244 244 """
245 245 # i18n: "portable" is a keyword
246 246 getargs(x, 0, 0, _("portable takes no arguments"))
247 247 return mctx.predicate(lambda f: util.checkwinfilename(f) is None,
248 248 predrepr='portable')
249 249
250 250 @predicate('grep(regex)', weight=_WEIGHT_READ_CONTENTS)
251 251 def grep(mctx, x):
252 252 """File contains the given regular expression.
253 253 """
254 254 try:
255 255 # i18n: "grep" is a keyword
256 256 r = re.compile(getstring(x, _("grep requires a pattern")))
257 257 except re.error as e:
258 258 raise error.ParseError(_('invalid match pattern: %s') %
259 259 stringutil.forcebytestr(e))
260 260 return mctx.fpredicate(lambda fctx: r.search(fctx.data()),
261 261 predrepr=('grep(%r)', r.pattern), cache=True)
262 262
263 263 def _sizetomax(s):
264 264 try:
265 265 s = s.strip().lower()
266 266 for k, v in util._sizeunits:
267 267 if s.endswith(k):
268 268 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
269 269 n = s[:-len(k)]
270 270 inc = 1.0
271 271 if "." in n:
272 272 inc /= 10 ** len(n.split(".")[1])
273 273 return int((float(n) + inc) * v) - 1
274 274 # no extension, this is a precise value
275 275 return int(s)
276 276 except ValueError:
277 277 raise error.ParseError(_("couldn't parse size: %s") % s)
278 278
279 279 def sizematcher(expr):
280 280 """Return a function(size) -> bool from the ``size()`` expression"""
281 281 expr = expr.strip()
282 282 if '-' in expr: # do we have a range?
283 283 a, b = expr.split('-', 1)
284 284 a = util.sizetoint(a)
285 285 b = util.sizetoint(b)
286 286 return lambda x: x >= a and x <= b
287 287 elif expr.startswith("<="):
288 288 a = util.sizetoint(expr[2:])
289 289 return lambda x: x <= a
290 290 elif expr.startswith("<"):
291 291 a = util.sizetoint(expr[1:])
292 292 return lambda x: x < a
293 293 elif expr.startswith(">="):
294 294 a = util.sizetoint(expr[2:])
295 295 return lambda x: x >= a
296 296 elif expr.startswith(">"):
297 297 a = util.sizetoint(expr[1:])
298 298 return lambda x: x > a
299 299 else:
300 300 a = util.sizetoint(expr)
301 301 b = _sizetomax(expr)
302 302 return lambda x: x >= a and x <= b
303 303
304 304 @predicate('size(expression)', weight=_WEIGHT_STATUS)
305 305 def size(mctx, x):
306 306 """File size matches the given expression. Examples:
307 307
308 308 - size('1k') - files from 1024 to 2047 bytes
309 309 - size('< 20k') - files less than 20480 bytes
310 310 - size('>= .5MB') - files at least 524288 bytes
311 311 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
312 312 """
313 313 # i18n: "size" is a keyword
314 314 expr = getstring(x, _("size requires an expression"))
315 315 m = sizematcher(expr)
316 316 return mctx.fpredicate(lambda fctx: m(fctx.size()),
317 317 predrepr=('size(%r)', expr), cache=True)
318 318
319 319 @predicate('encoding(name)', weight=_WEIGHT_READ_CONTENTS)
320 320 def encoding(mctx, x):
321 321 """File can be successfully decoded with the given character
322 322 encoding. May not be useful for encodings other than ASCII and
323 323 UTF-8.
324 324 """
325 325
326 326 # i18n: "encoding" is a keyword
327 327 enc = getstring(x, _("encoding requires an encoding name"))
328 328
329 329 def encp(fctx):
330 330 d = fctx.data()
331 331 try:
332 332 d.decode(pycompat.sysstr(enc))
333 333 return True
334 334 except LookupError:
335 335 raise error.Abort(_("unknown encoding '%s'") % enc)
336 336 except UnicodeDecodeError:
337 337 return False
338 338
339 339 return mctx.fpredicate(encp, predrepr=('encoding(%r)', enc), cache=True)
340 340
341 341 @predicate('eol(style)', weight=_WEIGHT_READ_CONTENTS)
342 342 def eol(mctx, x):
343 343 """File contains newlines of the given style (dos, unix, mac). Binary
344 344 files are excluded, files with mixed line endings match multiple
345 345 styles.
346 346 """
347 347
348 348 # i18n: "eol" is a keyword
349 349 enc = getstring(x, _("eol requires a style name"))
350 350
351 351 def eolp(fctx):
352 352 if fctx.isbinary():
353 353 return False
354 354 d = fctx.data()
355 355 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
356 356 return True
357 357 elif enc == 'unix' and re.search('(?<!\r)\n', d):
358 358 return True
359 359 elif enc == 'mac' and re.search('\r(?!\n)', d):
360 360 return True
361 361 return False
362 362 return mctx.fpredicate(eolp, predrepr=('eol(%r)', enc), cache=True)
363 363
364 364 @predicate('copied()')
365 365 def copied(mctx, x):
366 366 """File that is recorded as being copied.
367 367 """
368 368 # i18n: "copied" is a keyword
369 369 getargs(x, 0, 0, _("copied takes no arguments"))
370 370 def copiedp(fctx):
371 371 p = fctx.parents()
372 372 return p and p[0].path() != fctx.path()
373 373 return mctx.fpredicate(copiedp, predrepr='copied', cache=True)
374 374
375 375 @predicate('revs(revs, pattern)', weight=_WEIGHT_STATUS)
376 376 def revs(mctx, x):
377 377 """Evaluate set in the specified revisions. If the revset match multiple
378 378 revs, this will return file matching pattern in any of the revision.
379 379 """
380 380 # i18n: "revs" is a keyword
381 381 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
382 382 # i18n: "revs" is a keyword
383 383 revspec = getstring(r, _("first argument to revs must be a revision"))
384 384 repo = mctx.ctx.repo()
385 385 revs = scmutil.revrange(repo, [revspec])
386 386
387 387 matchers = []
388 388 for r in revs:
389 389 ctx = repo[r]
390 390 mc = mctx.switch(ctx.p1(), ctx)
391 391 matchers.append(getmatch(mc, x))
392 392 if not matchers:
393 393 return mctx.never()
394 394 if len(matchers) == 1:
395 395 return matchers[0]
396 396 return matchmod.unionmatcher(matchers)
397 397
398 398 @predicate('status(base, rev, pattern)', weight=_WEIGHT_STATUS)
399 399 def status(mctx, x):
400 400 """Evaluate predicate using status change between ``base`` and
401 401 ``rev``. Examples:
402 402
403 403 - ``status(3, 7, added())`` - matches files added from "3" to "7"
404 404 """
405 405 repo = mctx.ctx.repo()
406 406 # i18n: "status" is a keyword
407 407 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
408 408 # i18n: "status" is a keyword
409 409 baseerr = _("first argument to status must be a revision")
410 410 baserevspec = getstring(b, baseerr)
411 411 if not baserevspec:
412 412 raise error.ParseError(baseerr)
413 413 reverr = _("second argument to status must be a revision")
414 414 revspec = getstring(r, reverr)
415 415 if not revspec:
416 416 raise error.ParseError(reverr)
417 417 basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec])
418 418 mc = mctx.switch(basectx, ctx)
419 419 return getmatch(mc, x)
420 420
421 421 @predicate('subrepo([pattern])')
422 422 def subrepo(mctx, x):
423 423 """Subrepositories whose paths match the given pattern.
424 424 """
425 425 # i18n: "subrepo" is a keyword
426 426 getargs(x, 0, 1, _("subrepo takes at most one argument"))
427 427 ctx = mctx.ctx
428 428 sstate = ctx.substate
429 429 if x:
430 430 pat = getpattern(x, matchmod.allpatternkinds,
431 431 # i18n: "subrepo" is a keyword
432 432 _("subrepo requires a pattern or no arguments"))
433 433 fast = not matchmod.patkind(pat)
434 434 if fast:
435 435 def m(s):
436 436 return (s == pat)
437 437 else:
438 438 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
439 439 return mctx.predicate(lambda f: f in sstate and m(f),
440 440 predrepr=('subrepo(%r)', pat))
441 441 else:
442 442 return mctx.predicate(sstate.__contains__, predrepr='subrepo')
443 443
444 444 methods = {
445 445 'withstatus': getmatchwithstatus,
446 446 'string': stringmatch,
447 447 'symbol': stringmatch,
448 448 'kindpat': kindpatmatch,
449 449 'patterns': patternsmatch,
450 450 'and': andmatch,
451 451 'or': ormatch,
452 452 'minus': minusmatch,
453 453 'list': listmatch,
454 454 'not': notmatch,
455 455 'func': func,
456 456 }
457 457
458 458 class matchctx(object):
459 459 def __init__(self, basectx, ctx, badfn=None):
460 460 self._basectx = basectx
461 461 self.ctx = ctx
462 462 self._badfn = badfn
463 463 self._match = None
464 464 self._status = None
465 465
466 466 def narrowed(self, match):
467 467 """Create matchctx for a sub-tree narrowed by the given matcher"""
468 468 mctx = matchctx(self._basectx, self.ctx, self._badfn)
469 469 mctx._match = match
470 470 # leave wider status which we don't have to care
471 471 mctx._status = self._status
472 472 return mctx
473 473
474 474 def switch(self, basectx, ctx):
475 475 mctx = matchctx(basectx, ctx, self._badfn)
476 476 mctx._match = self._match
477 477 return mctx
478 478
479 479 def withstatus(self, keys):
480 480 """Create matchctx which has precomputed status specified by the keys"""
481 481 mctx = matchctx(self._basectx, self.ctx, self._badfn)
482 482 mctx._match = self._match
483 483 mctx._buildstatus(keys)
484 484 return mctx
485 485
486 486 def _buildstatus(self, keys):
487 487 self._status = self._basectx.status(self.ctx, self._match,
488 488 listignored='ignored' in keys,
489 489 listclean='clean' in keys,
490 490 listunknown='unknown' in keys)
491 491
492 492 def status(self):
493 493 return self._status
494 494
495 495 def matcher(self, patterns):
496 496 return self.ctx.match(patterns, badfn=self._badfn)
497 497
498 498 def predicate(self, predfn, predrepr=None, cache=False):
499 499 """Create a matcher to select files by predfn(filename)"""
500 500 if cache:
501 501 predfn = util.cachefunc(predfn)
502 502 repo = self.ctx.repo()
503 503 return matchmod.predicatematcher(repo.root, repo.getcwd(), predfn,
504 504 predrepr=predrepr, badfn=self._badfn)
505 505
506 506 def fpredicate(self, predfn, predrepr=None, cache=False):
507 507 """Create a matcher to select files by predfn(fctx) at the current
508 508 revision
509 509
510 510 Missing files are ignored.
511 511 """
512 512 ctx = self.ctx
513 513 if ctx.rev() is None:
514 514 def fctxpredfn(f):
515 515 try:
516 516 fctx = ctx[f]
517 517 except error.LookupError:
518 518 return False
519 519 try:
520 520 fctx.audit()
521 521 except error.Abort:
522 522 return False
523 523 try:
524 524 return predfn(fctx)
525 525 except (IOError, OSError) as e:
526 526 # open()-ing a directory fails with EACCES on Windows
527 527 if e.errno in (errno.ENOENT, errno.EACCES, errno.ENOTDIR,
528 528 errno.EISDIR):
529 529 return False
530 530 raise
531 531 else:
532 532 def fctxpredfn(f):
533 533 try:
534 534 fctx = ctx[f]
535 535 except error.LookupError:
536 536 return False
537 537 return predfn(fctx)
538 538 return self.predicate(fctxpredfn, predrepr=predrepr, cache=cache)
539 539
540 540 def never(self):
541 541 """Create a matcher to select nothing"""
542 542 repo = self.ctx.repo()
543 return matchmod.nevermatcher(repo.root, repo.getcwd(),
544 badfn=self._badfn)
543 return matchmod.never(repo.root, repo.getcwd(), badfn=self._badfn)
545 544
546 545 def match(ctx, expr, badfn=None):
547 546 """Create a matcher for a single fileset expression"""
548 547 tree = filesetlang.parse(expr)
549 548 tree = filesetlang.analyze(tree)
550 549 tree = filesetlang.optimize(tree)
551 550 mctx = matchctx(ctx.p1(), ctx, badfn=badfn)
552 551 return getmatch(mctx, tree)
553 552
554 553
555 554 def loadpredicate(ui, extname, registrarobj):
556 555 """Load fileset predicates from specified registrarobj
557 556 """
558 557 for name, func in registrarobj._table.iteritems():
559 558 symbols[name] = func
560 559
561 560 # tell hggettext to extract docstrings from these functions:
562 561 i18nfunctions = symbols.values()
@@ -1,1840 +1,1840 b''
1 1 # subrepo.py - sub-repository classes and factory
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import stat
16 16 import subprocess
17 17 import sys
18 18 import tarfile
19 19 import xml.dom.minidom
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 cmdutil,
24 24 encoding,
25 25 error,
26 26 exchange,
27 27 logcmdutil,
28 28 match as matchmod,
29 29 node,
30 30 pathutil,
31 31 phases,
32 32 pycompat,
33 33 scmutil,
34 34 subrepoutil,
35 35 util,
36 36 vfs as vfsmod,
37 37 )
38 38 from .utils import (
39 39 dateutil,
40 40 procutil,
41 41 stringutil,
42 42 )
43 43
44 44 hg = None
45 45 reporelpath = subrepoutil.reporelpath
46 46 subrelpath = subrepoutil.subrelpath
47 47 _abssource = subrepoutil._abssource
48 48 propertycache = util.propertycache
49 49
50 50 def _expandedabspath(path):
51 51 '''
52 52 get a path or url and if it is a path expand it and return an absolute path
53 53 '''
54 54 expandedpath = util.urllocalpath(util.expandpath(path))
55 55 u = util.url(expandedpath)
56 56 if not u.scheme:
57 57 path = util.normpath(os.path.abspath(u.path))
58 58 return path
59 59
60 60 def _getstorehashcachename(remotepath):
61 61 '''get a unique filename for the store hash cache of a remote repository'''
62 62 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
63 63
64 64 class SubrepoAbort(error.Abort):
65 65 """Exception class used to avoid handling a subrepo error more than once"""
66 66 def __init__(self, *args, **kw):
67 67 self.subrepo = kw.pop(r'subrepo', None)
68 68 self.cause = kw.pop(r'cause', None)
69 69 error.Abort.__init__(self, *args, **kw)
70 70
71 71 def annotatesubrepoerror(func):
72 72 def decoratedmethod(self, *args, **kargs):
73 73 try:
74 74 res = func(self, *args, **kargs)
75 75 except SubrepoAbort as ex:
76 76 # This exception has already been handled
77 77 raise ex
78 78 except error.Abort as ex:
79 79 subrepo = subrelpath(self)
80 80 errormsg = (stringutil.forcebytestr(ex) + ' '
81 81 + _('(in subrepository "%s")') % subrepo)
82 82 # avoid handling this exception by raising a SubrepoAbort exception
83 83 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
84 84 cause=sys.exc_info())
85 85 return res
86 86 return decoratedmethod
87 87
88 88 def _updateprompt(ui, sub, dirty, local, remote):
89 89 if dirty:
90 90 msg = (_(' subrepository sources for %s differ\n'
91 91 'use (l)ocal source (%s) or (r)emote source (%s)?'
92 92 '$$ &Local $$ &Remote')
93 93 % (subrelpath(sub), local, remote))
94 94 else:
95 95 msg = (_(' subrepository sources for %s differ (in checked out '
96 96 'version)\n'
97 97 'use (l)ocal source (%s) or (r)emote source (%s)?'
98 98 '$$ &Local $$ &Remote')
99 99 % (subrelpath(sub), local, remote))
100 100 return ui.promptchoice(msg, 0)
101 101
102 102 def _sanitize(ui, vfs, ignore):
103 103 for dirname, dirs, names in vfs.walk():
104 104 for i, d in enumerate(dirs):
105 105 if d.lower() == ignore:
106 106 del dirs[i]
107 107 break
108 108 if vfs.basename(dirname).lower() != '.hg':
109 109 continue
110 110 for f in names:
111 111 if f.lower() == 'hgrc':
112 112 ui.warn(_("warning: removing potentially hostile 'hgrc' "
113 113 "in '%s'\n") % vfs.join(dirname))
114 114 vfs.unlink(vfs.reljoin(dirname, f))
115 115
116 116 def _auditsubrepopath(repo, path):
117 117 # sanity check for potentially unsafe paths such as '~' and '$FOO'
118 118 if path.startswith('~') or '$' in path or util.expandpath(path) != path:
119 119 raise error.Abort(_('subrepo path contains illegal component: %s')
120 120 % path)
121 121 # auditor doesn't check if the path itself is a symlink
122 122 pathutil.pathauditor(repo.root)(path)
123 123 if repo.wvfs.islink(path):
124 124 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
125 125
126 126 SUBREPO_ALLOWED_DEFAULTS = {
127 127 'hg': True,
128 128 'git': False,
129 129 'svn': False,
130 130 }
131 131
132 132 def _checktype(ui, kind):
133 133 # subrepos.allowed is a master kill switch. If disabled, subrepos are
134 134 # disabled period.
135 135 if not ui.configbool('subrepos', 'allowed', True):
136 136 raise error.Abort(_('subrepos not enabled'),
137 137 hint=_("see 'hg help config.subrepos' for details"))
138 138
139 139 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
140 140 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
141 141 raise error.Abort(_('%s subrepos not allowed') % kind,
142 142 hint=_("see 'hg help config.subrepos' for details"))
143 143
144 144 if kind not in types:
145 145 raise error.Abort(_('unknown subrepo type %s') % kind)
146 146
147 147 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
148 148 """return instance of the right subrepo class for subrepo in path"""
149 149 # subrepo inherently violates our import layering rules
150 150 # because it wants to make repo objects from deep inside the stack
151 151 # so we manually delay the circular imports to not break
152 152 # scripts that don't use our demand-loading
153 153 global hg
154 154 from . import hg as h
155 155 hg = h
156 156
157 157 repo = ctx.repo()
158 158 _auditsubrepopath(repo, path)
159 159 state = ctx.substate[path]
160 160 _checktype(repo.ui, state[2])
161 161 if allowwdir:
162 162 state = (state[0], ctx.subrev(path), state[2])
163 163 return types[state[2]](ctx, path, state[:2], allowcreate)
164 164
165 165 def nullsubrepo(ctx, path, pctx):
166 166 """return an empty subrepo in pctx for the extant subrepo in ctx"""
167 167 # subrepo inherently violates our import layering rules
168 168 # because it wants to make repo objects from deep inside the stack
169 169 # so we manually delay the circular imports to not break
170 170 # scripts that don't use our demand-loading
171 171 global hg
172 172 from . import hg as h
173 173 hg = h
174 174
175 175 repo = ctx.repo()
176 176 _auditsubrepopath(repo, path)
177 177 state = ctx.substate[path]
178 178 _checktype(repo.ui, state[2])
179 179 subrev = ''
180 180 if state[2] == 'hg':
181 181 subrev = "0" * 40
182 182 return types[state[2]](pctx, path, (state[0], subrev), True)
183 183
184 184 # subrepo classes need to implement the following abstract class:
185 185
186 186 class abstractsubrepo(object):
187 187
188 188 def __init__(self, ctx, path):
189 189 """Initialize abstractsubrepo part
190 190
191 191 ``ctx`` is the context referring this subrepository in the
192 192 parent repository.
193 193
194 194 ``path`` is the path to this subrepository as seen from
195 195 innermost repository.
196 196 """
197 197 self.ui = ctx.repo().ui
198 198 self._ctx = ctx
199 199 self._path = path
200 200
201 201 def addwebdirpath(self, serverpath, webconf):
202 202 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
203 203
204 204 ``serverpath`` is the path component of the URL for this repo.
205 205
206 206 ``webconf`` is the dictionary of hgwebdir entries.
207 207 """
208 208 pass
209 209
210 210 def storeclean(self, path):
211 211 """
212 212 returns true if the repository has not changed since it was last
213 213 cloned from or pushed to a given repository.
214 214 """
215 215 return False
216 216
217 217 def dirty(self, ignoreupdate=False, missing=False):
218 218 """returns true if the dirstate of the subrepo is dirty or does not
219 219 match current stored state. If ignoreupdate is true, only check
220 220 whether the subrepo has uncommitted changes in its dirstate. If missing
221 221 is true, check for deleted files.
222 222 """
223 223 raise NotImplementedError
224 224
225 225 def dirtyreason(self, ignoreupdate=False, missing=False):
226 226 """return reason string if it is ``dirty()``
227 227
228 228 Returned string should have enough information for the message
229 229 of exception.
230 230
231 231 This returns None, otherwise.
232 232 """
233 233 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
234 234 return _('uncommitted changes in subrepository "%s"'
235 235 ) % subrelpath(self)
236 236
237 237 def bailifchanged(self, ignoreupdate=False, hint=None):
238 238 """raise Abort if subrepository is ``dirty()``
239 239 """
240 240 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
241 241 missing=True)
242 242 if dirtyreason:
243 243 raise error.Abort(dirtyreason, hint=hint)
244 244
245 245 def basestate(self):
246 246 """current working directory base state, disregarding .hgsubstate
247 247 state and working directory modifications"""
248 248 raise NotImplementedError
249 249
250 250 def checknested(self, path):
251 251 """check if path is a subrepository within this repository"""
252 252 return False
253 253
254 254 def commit(self, text, user, date):
255 255 """commit the current changes to the subrepo with the given
256 256 log message. Use given user and date if possible. Return the
257 257 new state of the subrepo.
258 258 """
259 259 raise NotImplementedError
260 260
261 261 def phase(self, state):
262 262 """returns phase of specified state in the subrepository.
263 263 """
264 264 return phases.public
265 265
266 266 def remove(self):
267 267 """remove the subrepo
268 268
269 269 (should verify the dirstate is not dirty first)
270 270 """
271 271 raise NotImplementedError
272 272
273 273 def get(self, state, overwrite=False):
274 274 """run whatever commands are needed to put the subrepo into
275 275 this state
276 276 """
277 277 raise NotImplementedError
278 278
279 279 def merge(self, state):
280 280 """merge currently-saved state with the new state."""
281 281 raise NotImplementedError
282 282
283 283 def push(self, opts):
284 284 """perform whatever action is analogous to 'hg push'
285 285
286 286 This may be a no-op on some systems.
287 287 """
288 288 raise NotImplementedError
289 289
290 290 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
291 291 return []
292 292
293 293 def addremove(self, matcher, prefix, uipathfn, opts):
294 294 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
295 295 return 1
296 296
297 297 def cat(self, match, fm, fntemplate, prefix, **opts):
298 298 return 1
299 299
300 300 def status(self, rev2, **opts):
301 301 return scmutil.status([], [], [], [], [], [], [])
302 302
303 303 def diff(self, ui, diffopts, node2, match, prefix, **opts):
304 304 pass
305 305
306 306 def outgoing(self, ui, dest, opts):
307 307 return 1
308 308
309 309 def incoming(self, ui, source, opts):
310 310 return 1
311 311
312 312 def files(self):
313 313 """return filename iterator"""
314 314 raise NotImplementedError
315 315
316 316 def filedata(self, name, decode):
317 317 """return file data, optionally passed through repo decoders"""
318 318 raise NotImplementedError
319 319
320 320 def fileflags(self, name):
321 321 """return file flags"""
322 322 return ''
323 323
324 324 def matchfileset(self, expr, badfn=None):
325 325 """Resolve the fileset expression for this repo"""
326 return matchmod.nevermatcher(self.wvfs.base, '', badfn=badfn)
326 return matchmod.never(self.wvfs.base, '', badfn=badfn)
327 327
328 328 def printfiles(self, ui, m, fm, fmt, subrepos):
329 329 """handle the files command for this subrepo"""
330 330 return 1
331 331
332 332 def archive(self, archiver, prefix, match=None, decode=True):
333 333 if match is not None:
334 334 files = [f for f in self.files() if match(f)]
335 335 else:
336 336 files = self.files()
337 337 total = len(files)
338 338 relpath = subrelpath(self)
339 339 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
340 340 unit=_('files'), total=total)
341 341 progress.update(0)
342 342 for name in files:
343 343 flags = self.fileflags(name)
344 344 mode = 'x' in flags and 0o755 or 0o644
345 345 symlink = 'l' in flags
346 346 archiver.addfile(prefix + name, mode, symlink,
347 347 self.filedata(name, decode))
348 348 progress.increment()
349 349 progress.complete()
350 350 return total
351 351
352 352 def walk(self, match):
353 353 '''
354 354 walk recursively through the directory tree, finding all files
355 355 matched by the match function
356 356 '''
357 357
358 358 def forget(self, match, prefix, uipathfn, dryrun, interactive):
359 359 return ([], [])
360 360
361 361 def removefiles(self, matcher, prefix, uipathfn, after, force, subrepos,
362 362 dryrun, warnings):
363 363 """remove the matched files from the subrepository and the filesystem,
364 364 possibly by force and/or after the file has been removed from the
365 365 filesystem. Return 0 on success, 1 on any warning.
366 366 """
367 367 warnings.append(_("warning: removefiles not implemented (%s)")
368 368 % self._path)
369 369 return 1
370 370
371 371 def revert(self, substate, *pats, **opts):
372 372 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
373 373 % (substate[0], substate[2]))
374 374 return []
375 375
376 376 def shortid(self, revid):
377 377 return revid
378 378
379 379 def unshare(self):
380 380 '''
381 381 convert this repository from shared to normal storage.
382 382 '''
383 383
384 384 def verify(self):
385 385 '''verify the integrity of the repository. Return 0 on success or
386 386 warning, 1 on any error.
387 387 '''
388 388 return 0
389 389
390 390 @propertycache
391 391 def wvfs(self):
392 392 """return vfs to access the working directory of this subrepository
393 393 """
394 394 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
395 395
396 396 @propertycache
397 397 def _relpath(self):
398 398 """return path to this subrepository as seen from outermost repository
399 399 """
400 400 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
401 401
402 402 class hgsubrepo(abstractsubrepo):
403 403 def __init__(self, ctx, path, state, allowcreate):
404 404 super(hgsubrepo, self).__init__(ctx, path)
405 405 self._state = state
406 406 r = ctx.repo()
407 407 root = r.wjoin(path)
408 408 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
409 409 # repository constructor does expand variables in path, which is
410 410 # unsafe since subrepo path might come from untrusted source.
411 411 if os.path.realpath(util.expandpath(root)) != root:
412 412 raise error.Abort(_('subrepo path contains illegal component: %s')
413 413 % path)
414 414 self._repo = hg.repository(r.baseui, root, create=create)
415 415 if self._repo.root != root:
416 416 raise error.ProgrammingError('failed to reject unsafe subrepo '
417 417 'path: %s (expanded to %s)'
418 418 % (root, self._repo.root))
419 419
420 420 # Propagate the parent's --hidden option
421 421 if r is r.unfiltered():
422 422 self._repo = self._repo.unfiltered()
423 423
424 424 self.ui = self._repo.ui
425 425 for s, k in [('ui', 'commitsubrepos')]:
426 426 v = r.ui.config(s, k)
427 427 if v:
428 428 self.ui.setconfig(s, k, v, 'subrepo')
429 429 # internal config: ui._usedassubrepo
430 430 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
431 431 self._initrepo(r, state[0], create)
432 432
433 433 @annotatesubrepoerror
434 434 def addwebdirpath(self, serverpath, webconf):
435 435 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
436 436
437 437 def storeclean(self, path):
438 438 with self._repo.lock():
439 439 return self._storeclean(path)
440 440
441 441 def _storeclean(self, path):
442 442 clean = True
443 443 itercache = self._calcstorehash(path)
444 444 for filehash in self._readstorehashcache(path):
445 445 if filehash != next(itercache, None):
446 446 clean = False
447 447 break
448 448 if clean:
449 449 # if not empty:
450 450 # the cached and current pull states have a different size
451 451 clean = next(itercache, None) is None
452 452 return clean
453 453
454 454 def _calcstorehash(self, remotepath):
455 455 '''calculate a unique "store hash"
456 456
457 457 This method is used to to detect when there are changes that may
458 458 require a push to a given remote path.'''
459 459 # sort the files that will be hashed in increasing (likely) file size
460 460 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
461 461 yield '# %s\n' % _expandedabspath(remotepath)
462 462 vfs = self._repo.vfs
463 463 for relname in filelist:
464 464 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
465 465 yield '%s = %s\n' % (relname, filehash)
466 466
467 467 @propertycache
468 468 def _cachestorehashvfs(self):
469 469 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
470 470
471 471 def _readstorehashcache(self, remotepath):
472 472 '''read the store hash cache for a given remote repository'''
473 473 cachefile = _getstorehashcachename(remotepath)
474 474 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
475 475
476 476 def _cachestorehash(self, remotepath):
477 477 '''cache the current store hash
478 478
479 479 Each remote repo requires its own store hash cache, because a subrepo
480 480 store may be "clean" versus a given remote repo, but not versus another
481 481 '''
482 482 cachefile = _getstorehashcachename(remotepath)
483 483 with self._repo.lock():
484 484 storehash = list(self._calcstorehash(remotepath))
485 485 vfs = self._cachestorehashvfs
486 486 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
487 487
488 488 def _getctx(self):
489 489 '''fetch the context for this subrepo revision, possibly a workingctx
490 490 '''
491 491 if self._ctx.rev() is None:
492 492 return self._repo[None] # workingctx if parent is workingctx
493 493 else:
494 494 rev = self._state[1]
495 495 return self._repo[rev]
496 496
497 497 @annotatesubrepoerror
498 498 def _initrepo(self, parentrepo, source, create):
499 499 self._repo._subparent = parentrepo
500 500 self._repo._subsource = source
501 501
502 502 if create:
503 503 lines = ['[paths]\n']
504 504
505 505 def addpathconfig(key, value):
506 506 if value:
507 507 lines.append('%s = %s\n' % (key, value))
508 508 self.ui.setconfig('paths', key, value, 'subrepo')
509 509
510 510 defpath = _abssource(self._repo, abort=False)
511 511 defpushpath = _abssource(self._repo, True, abort=False)
512 512 addpathconfig('default', defpath)
513 513 if defpath != defpushpath:
514 514 addpathconfig('default-push', defpushpath)
515 515
516 516 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
517 517
518 518 @annotatesubrepoerror
519 519 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
520 520 return cmdutil.add(ui, self._repo, match, prefix, uipathfn,
521 521 explicitonly, **opts)
522 522
523 523 @annotatesubrepoerror
524 524 def addremove(self, m, prefix, uipathfn, opts):
525 525 # In the same way as sub directories are processed, once in a subrepo,
526 526 # always entry any of its subrepos. Don't corrupt the options that will
527 527 # be used to process sibling subrepos however.
528 528 opts = copy.copy(opts)
529 529 opts['subrepos'] = True
530 530 return scmutil.addremove(self._repo, m, prefix, uipathfn, opts)
531 531
532 532 @annotatesubrepoerror
533 533 def cat(self, match, fm, fntemplate, prefix, **opts):
534 534 rev = self._state[1]
535 535 ctx = self._repo[rev]
536 536 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
537 537 prefix, **opts)
538 538
539 539 @annotatesubrepoerror
540 540 def status(self, rev2, **opts):
541 541 try:
542 542 rev1 = self._state[1]
543 543 ctx1 = self._repo[rev1]
544 544 ctx2 = self._repo[rev2]
545 545 return self._repo.status(ctx1, ctx2, **opts)
546 546 except error.RepoLookupError as inst:
547 547 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
548 548 % (inst, subrelpath(self)))
549 549 return scmutil.status([], [], [], [], [], [], [])
550 550
551 551 @annotatesubrepoerror
552 552 def diff(self, ui, diffopts, node2, match, prefix, **opts):
553 553 try:
554 554 node1 = node.bin(self._state[1])
555 555 # We currently expect node2 to come from substate and be
556 556 # in hex format
557 557 if node2 is not None:
558 558 node2 = node.bin(node2)
559 559 logcmdutil.diffordiffstat(ui, self._repo, diffopts, node1, node2,
560 560 match, prefix=prefix, listsubrepos=True,
561 561 **opts)
562 562 except error.RepoLookupError as inst:
563 563 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
564 564 % (inst, subrelpath(self)))
565 565
566 566 @annotatesubrepoerror
567 567 def archive(self, archiver, prefix, match=None, decode=True):
568 568 self._get(self._state + ('hg',))
569 569 files = self.files()
570 570 if match:
571 571 files = [f for f in files if match(f)]
572 572 rev = self._state[1]
573 573 ctx = self._repo[rev]
574 574 scmutil.prefetchfiles(self._repo, [ctx.rev()],
575 575 scmutil.matchfiles(self._repo, files))
576 576 total = abstractsubrepo.archive(self, archiver, prefix, match)
577 577 for subpath in ctx.substate:
578 578 s = subrepo(ctx, subpath, True)
579 579 submatch = matchmod.subdirmatcher(subpath, match)
580 580 subprefix = prefix + subpath + '/'
581 581 total += s.archive(archiver, subprefix, submatch,
582 582 decode)
583 583 return total
584 584
585 585 @annotatesubrepoerror
586 586 def dirty(self, ignoreupdate=False, missing=False):
587 587 r = self._state[1]
588 588 if r == '' and not ignoreupdate: # no state recorded
589 589 return True
590 590 w = self._repo[None]
591 591 if r != w.p1().hex() and not ignoreupdate:
592 592 # different version checked out
593 593 return True
594 594 return w.dirty(missing=missing) # working directory changed
595 595
596 596 def basestate(self):
597 597 return self._repo['.'].hex()
598 598
599 599 def checknested(self, path):
600 600 return self._repo._checknested(self._repo.wjoin(path))
601 601
602 602 @annotatesubrepoerror
603 603 def commit(self, text, user, date):
604 604 # don't bother committing in the subrepo if it's only been
605 605 # updated
606 606 if not self.dirty(True):
607 607 return self._repo['.'].hex()
608 608 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
609 609 n = self._repo.commit(text, user, date)
610 610 if not n:
611 611 return self._repo['.'].hex() # different version checked out
612 612 return node.hex(n)
613 613
614 614 @annotatesubrepoerror
615 615 def phase(self, state):
616 616 return self._repo[state or '.'].phase()
617 617
618 618 @annotatesubrepoerror
619 619 def remove(self):
620 620 # we can't fully delete the repository as it may contain
621 621 # local-only history
622 622 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
623 623 hg.clean(self._repo, node.nullid, False)
624 624
625 625 def _get(self, state):
626 626 source, revision, kind = state
627 627 parentrepo = self._repo._subparent
628 628
629 629 if revision in self._repo.unfiltered():
630 630 # Allow shared subrepos tracked at null to setup the sharedpath
631 631 if len(self._repo) != 0 or not parentrepo.shared():
632 632 return True
633 633 self._repo._subsource = source
634 634 srcurl = _abssource(self._repo)
635 635
636 636 # Defer creating the peer until after the status message is logged, in
637 637 # case there are network problems.
638 638 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
639 639
640 640 if len(self._repo) == 0:
641 641 # use self._repo.vfs instead of self.wvfs to remove .hg only
642 642 self._repo.vfs.rmtree()
643 643
644 644 # A remote subrepo could be shared if there is a local copy
645 645 # relative to the parent's share source. But clone pooling doesn't
646 646 # assemble the repos in a tree, so that can't be consistently done.
647 647 # A simpler option is for the user to configure clone pooling, and
648 648 # work with that.
649 649 if parentrepo.shared() and hg.islocal(srcurl):
650 650 self.ui.status(_('sharing subrepo %s from %s\n')
651 651 % (subrelpath(self), srcurl))
652 652 shared = hg.share(self._repo._subparent.baseui,
653 653 getpeer(), self._repo.root,
654 654 update=False, bookmarks=False)
655 655 self._repo = shared.local()
656 656 else:
657 657 # TODO: find a common place for this and this code in the
658 658 # share.py wrap of the clone command.
659 659 if parentrepo.shared():
660 660 pool = self.ui.config('share', 'pool')
661 661 if pool:
662 662 pool = util.expandpath(pool)
663 663
664 664 shareopts = {
665 665 'pool': pool,
666 666 'mode': self.ui.config('share', 'poolnaming'),
667 667 }
668 668 else:
669 669 shareopts = {}
670 670
671 671 self.ui.status(_('cloning subrepo %s from %s\n')
672 672 % (subrelpath(self), util.hidepassword(srcurl)))
673 673 other, cloned = hg.clone(self._repo._subparent.baseui, {},
674 674 getpeer(), self._repo.root,
675 675 update=False, shareopts=shareopts)
676 676 self._repo = cloned.local()
677 677 self._initrepo(parentrepo, source, create=True)
678 678 self._cachestorehash(srcurl)
679 679 else:
680 680 self.ui.status(_('pulling subrepo %s from %s\n')
681 681 % (subrelpath(self), util.hidepassword(srcurl)))
682 682 cleansub = self.storeclean(srcurl)
683 683 exchange.pull(self._repo, getpeer())
684 684 if cleansub:
685 685 # keep the repo clean after pull
686 686 self._cachestorehash(srcurl)
687 687 return False
688 688
689 689 @annotatesubrepoerror
690 690 def get(self, state, overwrite=False):
691 691 inrepo = self._get(state)
692 692 source, revision, kind = state
693 693 repo = self._repo
694 694 repo.ui.debug("getting subrepo %s\n" % self._path)
695 695 if inrepo:
696 696 urepo = repo.unfiltered()
697 697 ctx = urepo[revision]
698 698 if ctx.hidden():
699 699 urepo.ui.warn(
700 700 _('revision %s in subrepository "%s" is hidden\n') \
701 701 % (revision[0:12], self._path))
702 702 repo = urepo
703 703 hg.updaterepo(repo, revision, overwrite)
704 704
705 705 @annotatesubrepoerror
706 706 def merge(self, state):
707 707 self._get(state)
708 708 cur = self._repo['.']
709 709 dst = self._repo[state[1]]
710 710 anc = dst.ancestor(cur)
711 711
712 712 def mergefunc():
713 713 if anc == cur and dst.branch() == cur.branch():
714 714 self.ui.debug('updating subrepository "%s"\n'
715 715 % subrelpath(self))
716 716 hg.update(self._repo, state[1])
717 717 elif anc == dst:
718 718 self.ui.debug('skipping subrepository "%s"\n'
719 719 % subrelpath(self))
720 720 else:
721 721 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
722 722 hg.merge(self._repo, state[1], remind=False)
723 723
724 724 wctx = self._repo[None]
725 725 if self.dirty():
726 726 if anc != dst:
727 727 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
728 728 mergefunc()
729 729 else:
730 730 mergefunc()
731 731 else:
732 732 mergefunc()
733 733
734 734 @annotatesubrepoerror
735 735 def push(self, opts):
736 736 force = opts.get('force')
737 737 newbranch = opts.get('new_branch')
738 738 ssh = opts.get('ssh')
739 739
740 740 # push subrepos depth-first for coherent ordering
741 741 c = self._repo['.']
742 742 subs = c.substate # only repos that are committed
743 743 for s in sorted(subs):
744 744 if c.sub(s).push(opts) == 0:
745 745 return False
746 746
747 747 dsturl = _abssource(self._repo, True)
748 748 if not force:
749 749 if self.storeclean(dsturl):
750 750 self.ui.status(
751 751 _('no changes made to subrepo %s since last push to %s\n')
752 752 % (subrelpath(self), util.hidepassword(dsturl)))
753 753 return None
754 754 self.ui.status(_('pushing subrepo %s to %s\n') %
755 755 (subrelpath(self), util.hidepassword(dsturl)))
756 756 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
757 757 res = exchange.push(self._repo, other, force, newbranch=newbranch)
758 758
759 759 # the repo is now clean
760 760 self._cachestorehash(dsturl)
761 761 return res.cgresult
762 762
763 763 @annotatesubrepoerror
764 764 def outgoing(self, ui, dest, opts):
765 765 if 'rev' in opts or 'branch' in opts:
766 766 opts = copy.copy(opts)
767 767 opts.pop('rev', None)
768 768 opts.pop('branch', None)
769 769 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
770 770
771 771 @annotatesubrepoerror
772 772 def incoming(self, ui, source, opts):
773 773 if 'rev' in opts or 'branch' in opts:
774 774 opts = copy.copy(opts)
775 775 opts.pop('rev', None)
776 776 opts.pop('branch', None)
777 777 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
778 778
779 779 @annotatesubrepoerror
780 780 def files(self):
781 781 rev = self._state[1]
782 782 ctx = self._repo[rev]
783 783 return ctx.manifest().keys()
784 784
785 785 def filedata(self, name, decode):
786 786 rev = self._state[1]
787 787 data = self._repo[rev][name].data()
788 788 if decode:
789 789 data = self._repo.wwritedata(name, data)
790 790 return data
791 791
792 792 def fileflags(self, name):
793 793 rev = self._state[1]
794 794 ctx = self._repo[rev]
795 795 return ctx.flags(name)
796 796
797 797 @annotatesubrepoerror
798 798 def printfiles(self, ui, m, fm, fmt, subrepos):
799 799 # If the parent context is a workingctx, use the workingctx here for
800 800 # consistency.
801 801 if self._ctx.rev() is None:
802 802 ctx = self._repo[None]
803 803 else:
804 804 rev = self._state[1]
805 805 ctx = self._repo[rev]
806 806 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
807 807
808 808 @annotatesubrepoerror
809 809 def matchfileset(self, expr, badfn=None):
810 810 repo = self._repo
811 811 if self._ctx.rev() is None:
812 812 ctx = repo[None]
813 813 else:
814 814 rev = self._state[1]
815 815 ctx = repo[rev]
816 816
817 817 matchers = [ctx.matchfileset(expr, badfn=badfn)]
818 818
819 819 for subpath in ctx.substate:
820 820 sub = ctx.sub(subpath)
821 821
822 822 try:
823 823 sm = sub.matchfileset(expr, badfn=badfn)
824 824 pm = matchmod.prefixdirmatcher(repo.root, repo.getcwd(),
825 825 subpath, sm, badfn=badfn)
826 826 matchers.append(pm)
827 827 except error.LookupError:
828 828 self.ui.status(_("skipping missing subrepository: %s\n")
829 829 % self.wvfs.reljoin(reporelpath(self), subpath))
830 830 if len(matchers) == 1:
831 831 return matchers[0]
832 832 return matchmod.unionmatcher(matchers)
833 833
834 834 def walk(self, match):
835 835 ctx = self._repo[None]
836 836 return ctx.walk(match)
837 837
838 838 @annotatesubrepoerror
839 839 def forget(self, match, prefix, uipathfn, dryrun, interactive):
840 840 return cmdutil.forget(self.ui, self._repo, match, prefix, uipathfn,
841 841 True, dryrun=dryrun, interactive=interactive)
842 842
843 843 @annotatesubrepoerror
844 844 def removefiles(self, matcher, prefix, uipathfn, after, force, subrepos,
845 845 dryrun, warnings):
846 846 return cmdutil.remove(self.ui, self._repo, matcher, prefix, uipathfn,
847 847 after, force, subrepos, dryrun)
848 848
849 849 @annotatesubrepoerror
850 850 def revert(self, substate, *pats, **opts):
851 851 # reverting a subrepo is a 2 step process:
852 852 # 1. if the no_backup is not set, revert all modified
853 853 # files inside the subrepo
854 854 # 2. update the subrepo to the revision specified in
855 855 # the corresponding substate dictionary
856 856 self.ui.status(_('reverting subrepo %s\n') % substate[0])
857 857 if not opts.get(r'no_backup'):
858 858 # Revert all files on the subrepo, creating backups
859 859 # Note that this will not recursively revert subrepos
860 860 # We could do it if there was a set:subrepos() predicate
861 861 opts = opts.copy()
862 862 opts[r'date'] = None
863 863 opts[r'rev'] = substate[1]
864 864
865 865 self.filerevert(*pats, **opts)
866 866
867 867 # Update the repo to the revision specified in the given substate
868 868 if not opts.get(r'dry_run'):
869 869 self.get(substate, overwrite=True)
870 870
871 871 def filerevert(self, *pats, **opts):
872 872 ctx = self._repo[opts[r'rev']]
873 873 parents = self._repo.dirstate.parents()
874 874 if opts.get(r'all'):
875 875 pats = ['set:modified()']
876 876 else:
877 877 pats = []
878 878 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
879 879
880 880 def shortid(self, revid):
881 881 return revid[:12]
882 882
883 883 @annotatesubrepoerror
884 884 def unshare(self):
885 885 # subrepo inherently violates our import layering rules
886 886 # because it wants to make repo objects from deep inside the stack
887 887 # so we manually delay the circular imports to not break
888 888 # scripts that don't use our demand-loading
889 889 global hg
890 890 from . import hg as h
891 891 hg = h
892 892
893 893 # Nothing prevents a user from sharing in a repo, and then making that a
894 894 # subrepo. Alternately, the previous unshare attempt may have failed
895 895 # part way through. So recurse whether or not this layer is shared.
896 896 if self._repo.shared():
897 897 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
898 898
899 899 hg.unshare(self.ui, self._repo)
900 900
901 901 def verify(self):
902 902 try:
903 903 rev = self._state[1]
904 904 ctx = self._repo.unfiltered()[rev]
905 905 if ctx.hidden():
906 906 # Since hidden revisions aren't pushed/pulled, it seems worth an
907 907 # explicit warning.
908 908 ui = self._repo.ui
909 909 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
910 910 (self._relpath, node.short(self._ctx.node())))
911 911 return 0
912 912 except error.RepoLookupError:
913 913 # A missing subrepo revision may be a case of needing to pull it, so
914 914 # don't treat this as an error.
915 915 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
916 916 (self._relpath, node.short(self._ctx.node())))
917 917 return 0
918 918
919 919 @propertycache
920 920 def wvfs(self):
921 921 """return own wvfs for efficiency and consistency
922 922 """
923 923 return self._repo.wvfs
924 924
925 925 @propertycache
926 926 def _relpath(self):
927 927 """return path to this subrepository as seen from outermost repository
928 928 """
929 929 # Keep consistent dir separators by avoiding vfs.join(self._path)
930 930 return reporelpath(self._repo)
931 931
932 932 class svnsubrepo(abstractsubrepo):
933 933 def __init__(self, ctx, path, state, allowcreate):
934 934 super(svnsubrepo, self).__init__(ctx, path)
935 935 self._state = state
936 936 self._exe = procutil.findexe('svn')
937 937 if not self._exe:
938 938 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
939 939 % self._path)
940 940
941 941 def _svncommand(self, commands, filename='', failok=False):
942 942 cmd = [self._exe]
943 943 extrakw = {}
944 944 if not self.ui.interactive():
945 945 # Making stdin be a pipe should prevent svn from behaving
946 946 # interactively even if we can't pass --non-interactive.
947 947 extrakw[r'stdin'] = subprocess.PIPE
948 948 # Starting in svn 1.5 --non-interactive is a global flag
949 949 # instead of being per-command, but we need to support 1.4 so
950 950 # we have to be intelligent about what commands take
951 951 # --non-interactive.
952 952 if commands[0] in ('update', 'checkout', 'commit'):
953 953 cmd.append('--non-interactive')
954 954 cmd.extend(commands)
955 955 if filename is not None:
956 956 path = self.wvfs.reljoin(self._ctx.repo().origroot,
957 957 self._path, filename)
958 958 cmd.append(path)
959 959 env = dict(encoding.environ)
960 960 # Avoid localized output, preserve current locale for everything else.
961 961 lc_all = env.get('LC_ALL')
962 962 if lc_all:
963 963 env['LANG'] = lc_all
964 964 del env['LC_ALL']
965 965 env['LC_MESSAGES'] = 'C'
966 966 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr, cmd),
967 967 bufsize=-1, close_fds=procutil.closefds,
968 968 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
969 969 env=procutil.tonativeenv(env), **extrakw)
970 970 stdout, stderr = map(util.fromnativeeol, p.communicate())
971 971 stderr = stderr.strip()
972 972 if not failok:
973 973 if p.returncode:
974 974 raise error.Abort(stderr or 'exited with code %d'
975 975 % p.returncode)
976 976 if stderr:
977 977 self.ui.warn(stderr + '\n')
978 978 return stdout, stderr
979 979
980 980 @propertycache
981 981 def _svnversion(self):
982 982 output, err = self._svncommand(['--version', '--quiet'], filename=None)
983 983 m = re.search(br'^(\d+)\.(\d+)', output)
984 984 if not m:
985 985 raise error.Abort(_('cannot retrieve svn tool version'))
986 986 return (int(m.group(1)), int(m.group(2)))
987 987
988 988 def _svnmissing(self):
989 989 return not self.wvfs.exists('.svn')
990 990
991 991 def _wcrevs(self):
992 992 # Get the working directory revision as well as the last
993 993 # commit revision so we can compare the subrepo state with
994 994 # both. We used to store the working directory one.
995 995 output, err = self._svncommand(['info', '--xml'])
996 996 doc = xml.dom.minidom.parseString(output)
997 997 entries = doc.getElementsByTagName(r'entry')
998 998 lastrev, rev = '0', '0'
999 999 if entries:
1000 1000 rev = pycompat.bytestr(entries[0].getAttribute(r'revision')) or '0'
1001 1001 commits = entries[0].getElementsByTagName(r'commit')
1002 1002 if commits:
1003 1003 lastrev = pycompat.bytestr(
1004 1004 commits[0].getAttribute(r'revision')) or '0'
1005 1005 return (lastrev, rev)
1006 1006
1007 1007 def _wcrev(self):
1008 1008 return self._wcrevs()[0]
1009 1009
1010 1010 def _wcchanged(self):
1011 1011 """Return (changes, extchanges, missing) where changes is True
1012 1012 if the working directory was changed, extchanges is
1013 1013 True if any of these changes concern an external entry and missing
1014 1014 is True if any change is a missing entry.
1015 1015 """
1016 1016 output, err = self._svncommand(['status', '--xml'])
1017 1017 externals, changes, missing = [], [], []
1018 1018 doc = xml.dom.minidom.parseString(output)
1019 1019 for e in doc.getElementsByTagName(r'entry'):
1020 1020 s = e.getElementsByTagName(r'wc-status')
1021 1021 if not s:
1022 1022 continue
1023 1023 item = s[0].getAttribute(r'item')
1024 1024 props = s[0].getAttribute(r'props')
1025 1025 path = e.getAttribute(r'path').encode('utf8')
1026 1026 if item == r'external':
1027 1027 externals.append(path)
1028 1028 elif item == r'missing':
1029 1029 missing.append(path)
1030 1030 if (item not in (r'', r'normal', r'unversioned', r'external')
1031 1031 or props not in (r'', r'none', r'normal')):
1032 1032 changes.append(path)
1033 1033 for path in changes:
1034 1034 for ext in externals:
1035 1035 if path == ext or path.startswith(ext + pycompat.ossep):
1036 1036 return True, True, bool(missing)
1037 1037 return bool(changes), False, bool(missing)
1038 1038
1039 1039 @annotatesubrepoerror
1040 1040 def dirty(self, ignoreupdate=False, missing=False):
1041 1041 if self._svnmissing():
1042 1042 return self._state[1] != ''
1043 1043 wcchanged = self._wcchanged()
1044 1044 changed = wcchanged[0] or (missing and wcchanged[2])
1045 1045 if not changed:
1046 1046 if self._state[1] in self._wcrevs() or ignoreupdate:
1047 1047 return False
1048 1048 return True
1049 1049
1050 1050 def basestate(self):
1051 1051 lastrev, rev = self._wcrevs()
1052 1052 if lastrev != rev:
1053 1053 # Last committed rev is not the same than rev. We would
1054 1054 # like to take lastrev but we do not know if the subrepo
1055 1055 # URL exists at lastrev. Test it and fallback to rev it
1056 1056 # is not there.
1057 1057 try:
1058 1058 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1059 1059 return lastrev
1060 1060 except error.Abort:
1061 1061 pass
1062 1062 return rev
1063 1063
1064 1064 @annotatesubrepoerror
1065 1065 def commit(self, text, user, date):
1066 1066 # user and date are out of our hands since svn is centralized
1067 1067 changed, extchanged, missing = self._wcchanged()
1068 1068 if not changed:
1069 1069 return self.basestate()
1070 1070 if extchanged:
1071 1071 # Do not try to commit externals
1072 1072 raise error.Abort(_('cannot commit svn externals'))
1073 1073 if missing:
1074 1074 # svn can commit with missing entries but aborting like hg
1075 1075 # seems a better approach.
1076 1076 raise error.Abort(_('cannot commit missing svn entries'))
1077 1077 commitinfo, err = self._svncommand(['commit', '-m', text])
1078 1078 self.ui.status(commitinfo)
1079 1079 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1080 1080 if not newrev:
1081 1081 if not commitinfo.strip():
1082 1082 # Sometimes, our definition of "changed" differs from
1083 1083 # svn one. For instance, svn ignores missing files
1084 1084 # when committing. If there are only missing files, no
1085 1085 # commit is made, no output and no error code.
1086 1086 raise error.Abort(_('failed to commit svn changes'))
1087 1087 raise error.Abort(commitinfo.splitlines()[-1])
1088 1088 newrev = newrev.groups()[0]
1089 1089 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1090 1090 return newrev
1091 1091
1092 1092 @annotatesubrepoerror
1093 1093 def remove(self):
1094 1094 if self.dirty():
1095 1095 self.ui.warn(_('not removing repo %s because '
1096 1096 'it has changes.\n') % self._path)
1097 1097 return
1098 1098 self.ui.note(_('removing subrepo %s\n') % self._path)
1099 1099
1100 1100 self.wvfs.rmtree(forcibly=True)
1101 1101 try:
1102 1102 pwvfs = self._ctx.repo().wvfs
1103 1103 pwvfs.removedirs(pwvfs.dirname(self._path))
1104 1104 except OSError:
1105 1105 pass
1106 1106
1107 1107 @annotatesubrepoerror
1108 1108 def get(self, state, overwrite=False):
1109 1109 if overwrite:
1110 1110 self._svncommand(['revert', '--recursive'])
1111 1111 args = ['checkout']
1112 1112 if self._svnversion >= (1, 5):
1113 1113 args.append('--force')
1114 1114 # The revision must be specified at the end of the URL to properly
1115 1115 # update to a directory which has since been deleted and recreated.
1116 1116 args.append('%s@%s' % (state[0], state[1]))
1117 1117
1118 1118 # SEC: check that the ssh url is safe
1119 1119 util.checksafessh(state[0])
1120 1120
1121 1121 status, err = self._svncommand(args, failok=True)
1122 1122 _sanitize(self.ui, self.wvfs, '.svn')
1123 1123 if not re.search('Checked out revision [0-9]+.', status):
1124 1124 if ('is already a working copy for a different URL' in err
1125 1125 and (self._wcchanged()[:2] == (False, False))):
1126 1126 # obstructed but clean working copy, so just blow it away.
1127 1127 self.remove()
1128 1128 self.get(state, overwrite=False)
1129 1129 return
1130 1130 raise error.Abort((status or err).splitlines()[-1])
1131 1131 self.ui.status(status)
1132 1132
1133 1133 @annotatesubrepoerror
1134 1134 def merge(self, state):
1135 1135 old = self._state[1]
1136 1136 new = state[1]
1137 1137 wcrev = self._wcrev()
1138 1138 if new != wcrev:
1139 1139 dirty = old == wcrev or self._wcchanged()[0]
1140 1140 if _updateprompt(self.ui, self, dirty, wcrev, new):
1141 1141 self.get(state, False)
1142 1142
1143 1143 def push(self, opts):
1144 1144 # push is a no-op for SVN
1145 1145 return True
1146 1146
1147 1147 @annotatesubrepoerror
1148 1148 def files(self):
1149 1149 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1150 1150 doc = xml.dom.minidom.parseString(output)
1151 1151 paths = []
1152 1152 for e in doc.getElementsByTagName(r'entry'):
1153 1153 kind = pycompat.bytestr(e.getAttribute(r'kind'))
1154 1154 if kind != 'file':
1155 1155 continue
1156 1156 name = r''.join(c.data for c
1157 1157 in e.getElementsByTagName(r'name')[0].childNodes
1158 1158 if c.nodeType == c.TEXT_NODE)
1159 1159 paths.append(name.encode('utf8'))
1160 1160 return paths
1161 1161
1162 1162 def filedata(self, name, decode):
1163 1163 return self._svncommand(['cat'], name)[0]
1164 1164
1165 1165
1166 1166 class gitsubrepo(abstractsubrepo):
1167 1167 def __init__(self, ctx, path, state, allowcreate):
1168 1168 super(gitsubrepo, self).__init__(ctx, path)
1169 1169 self._state = state
1170 1170 self._abspath = ctx.repo().wjoin(path)
1171 1171 self._subparent = ctx.repo()
1172 1172 self._ensuregit()
1173 1173
1174 1174 def _ensuregit(self):
1175 1175 try:
1176 1176 self._gitexecutable = 'git'
1177 1177 out, err = self._gitnodir(['--version'])
1178 1178 except OSError as e:
1179 1179 genericerror = _("error executing git for subrepo '%s': %s")
1180 1180 notfoundhint = _("check git is installed and in your PATH")
1181 1181 if e.errno != errno.ENOENT:
1182 1182 raise error.Abort(genericerror % (
1183 1183 self._path, encoding.strtolocal(e.strerror)))
1184 1184 elif pycompat.iswindows:
1185 1185 try:
1186 1186 self._gitexecutable = 'git.cmd'
1187 1187 out, err = self._gitnodir(['--version'])
1188 1188 except OSError as e2:
1189 1189 if e2.errno == errno.ENOENT:
1190 1190 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1191 1191 " for subrepo '%s'") % self._path,
1192 1192 hint=notfoundhint)
1193 1193 else:
1194 1194 raise error.Abort(genericerror % (self._path,
1195 1195 encoding.strtolocal(e2.strerror)))
1196 1196 else:
1197 1197 raise error.Abort(_("couldn't find git for subrepo '%s'")
1198 1198 % self._path, hint=notfoundhint)
1199 1199 versionstatus = self._checkversion(out)
1200 1200 if versionstatus == 'unknown':
1201 1201 self.ui.warn(_('cannot retrieve git version\n'))
1202 1202 elif versionstatus == 'abort':
1203 1203 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1204 1204 elif versionstatus == 'warning':
1205 1205 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1206 1206
1207 1207 @staticmethod
1208 1208 def _gitversion(out):
1209 1209 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1210 1210 if m:
1211 1211 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1212 1212
1213 1213 m = re.search(br'^git version (\d+)\.(\d+)', out)
1214 1214 if m:
1215 1215 return (int(m.group(1)), int(m.group(2)), 0)
1216 1216
1217 1217 return -1
1218 1218
1219 1219 @staticmethod
1220 1220 def _checkversion(out):
1221 1221 '''ensure git version is new enough
1222 1222
1223 1223 >>> _checkversion = gitsubrepo._checkversion
1224 1224 >>> _checkversion(b'git version 1.6.0')
1225 1225 'ok'
1226 1226 >>> _checkversion(b'git version 1.8.5')
1227 1227 'ok'
1228 1228 >>> _checkversion(b'git version 1.4.0')
1229 1229 'abort'
1230 1230 >>> _checkversion(b'git version 1.5.0')
1231 1231 'warning'
1232 1232 >>> _checkversion(b'git version 1.9-rc0')
1233 1233 'ok'
1234 1234 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1235 1235 'ok'
1236 1236 >>> _checkversion(b'git version 1.9.0.GIT')
1237 1237 'ok'
1238 1238 >>> _checkversion(b'git version 12345')
1239 1239 'unknown'
1240 1240 >>> _checkversion(b'no')
1241 1241 'unknown'
1242 1242 '''
1243 1243 version = gitsubrepo._gitversion(out)
1244 1244 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1245 1245 # despite the docstring comment. For now, error on 1.4.0, warn on
1246 1246 # 1.5.0 but attempt to continue.
1247 1247 if version == -1:
1248 1248 return 'unknown'
1249 1249 if version < (1, 5, 0):
1250 1250 return 'abort'
1251 1251 elif version < (1, 6, 0):
1252 1252 return 'warning'
1253 1253 return 'ok'
1254 1254
1255 1255 def _gitcommand(self, commands, env=None, stream=False):
1256 1256 return self._gitdir(commands, env=env, stream=stream)[0]
1257 1257
1258 1258 def _gitdir(self, commands, env=None, stream=False):
1259 1259 return self._gitnodir(commands, env=env, stream=stream,
1260 1260 cwd=self._abspath)
1261 1261
1262 1262 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1263 1263 """Calls the git command
1264 1264
1265 1265 The methods tries to call the git command. versions prior to 1.6.0
1266 1266 are not supported and very probably fail.
1267 1267 """
1268 1268 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1269 1269 if env is None:
1270 1270 env = encoding.environ.copy()
1271 1271 # disable localization for Git output (issue5176)
1272 1272 env['LC_ALL'] = 'C'
1273 1273 # fix for Git CVE-2015-7545
1274 1274 if 'GIT_ALLOW_PROTOCOL' not in env:
1275 1275 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1276 1276 # unless ui.quiet is set, print git's stderr,
1277 1277 # which is mostly progress and useful info
1278 1278 errpipe = None
1279 1279 if self.ui.quiet:
1280 1280 errpipe = open(os.devnull, 'w')
1281 1281 if self.ui._colormode and len(commands) and commands[0] == "diff":
1282 1282 # insert the argument in the front,
1283 1283 # the end of git diff arguments is used for paths
1284 1284 commands.insert(1, '--color')
1285 1285 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr,
1286 1286 [self._gitexecutable] + commands),
1287 1287 bufsize=-1,
1288 1288 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1289 1289 env=procutil.tonativeenv(env),
1290 1290 close_fds=procutil.closefds,
1291 1291 stdout=subprocess.PIPE, stderr=errpipe)
1292 1292 if stream:
1293 1293 return p.stdout, None
1294 1294
1295 1295 retdata = p.stdout.read().strip()
1296 1296 # wait for the child to exit to avoid race condition.
1297 1297 p.wait()
1298 1298
1299 1299 if p.returncode != 0 and p.returncode != 1:
1300 1300 # there are certain error codes that are ok
1301 1301 command = commands[0]
1302 1302 if command in ('cat-file', 'symbolic-ref'):
1303 1303 return retdata, p.returncode
1304 1304 # for all others, abort
1305 1305 raise error.Abort(_('git %s error %d in %s') %
1306 1306 (command, p.returncode, self._relpath))
1307 1307
1308 1308 return retdata, p.returncode
1309 1309
1310 1310 def _gitmissing(self):
1311 1311 return not self.wvfs.exists('.git')
1312 1312
1313 1313 def _gitstate(self):
1314 1314 return self._gitcommand(['rev-parse', 'HEAD'])
1315 1315
1316 1316 def _gitcurrentbranch(self):
1317 1317 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1318 1318 if err:
1319 1319 current = None
1320 1320 return current
1321 1321
1322 1322 def _gitremote(self, remote):
1323 1323 out = self._gitcommand(['remote', 'show', '-n', remote])
1324 1324 line = out.split('\n')[1]
1325 1325 i = line.index('URL: ') + len('URL: ')
1326 1326 return line[i:]
1327 1327
1328 1328 def _githavelocally(self, revision):
1329 1329 out, code = self._gitdir(['cat-file', '-e', revision])
1330 1330 return code == 0
1331 1331
1332 1332 def _gitisancestor(self, r1, r2):
1333 1333 base = self._gitcommand(['merge-base', r1, r2])
1334 1334 return base == r1
1335 1335
1336 1336 def _gitisbare(self):
1337 1337 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1338 1338
1339 1339 def _gitupdatestat(self):
1340 1340 """This must be run before git diff-index.
1341 1341 diff-index only looks at changes to file stat;
1342 1342 this command looks at file contents and updates the stat."""
1343 1343 self._gitcommand(['update-index', '-q', '--refresh'])
1344 1344
1345 1345 def _gitbranchmap(self):
1346 1346 '''returns 2 things:
1347 1347 a map from git branch to revision
1348 1348 a map from revision to branches'''
1349 1349 branch2rev = {}
1350 1350 rev2branch = {}
1351 1351
1352 1352 out = self._gitcommand(['for-each-ref', '--format',
1353 1353 '%(objectname) %(refname)'])
1354 1354 for line in out.split('\n'):
1355 1355 revision, ref = line.split(' ')
1356 1356 if (not ref.startswith('refs/heads/') and
1357 1357 not ref.startswith('refs/remotes/')):
1358 1358 continue
1359 1359 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1360 1360 continue # ignore remote/HEAD redirects
1361 1361 branch2rev[ref] = revision
1362 1362 rev2branch.setdefault(revision, []).append(ref)
1363 1363 return branch2rev, rev2branch
1364 1364
1365 1365 def _gittracking(self, branches):
1366 1366 'return map of remote branch to local tracking branch'
1367 1367 # assumes no more than one local tracking branch for each remote
1368 1368 tracking = {}
1369 1369 for b in branches:
1370 1370 if b.startswith('refs/remotes/'):
1371 1371 continue
1372 1372 bname = b.split('/', 2)[2]
1373 1373 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1374 1374 if remote:
1375 1375 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1376 1376 tracking['refs/remotes/%s/%s' %
1377 1377 (remote, ref.split('/', 2)[2])] = b
1378 1378 return tracking
1379 1379
1380 1380 def _abssource(self, source):
1381 1381 if '://' not in source:
1382 1382 # recognize the scp syntax as an absolute source
1383 1383 colon = source.find(':')
1384 1384 if colon != -1 and '/' not in source[:colon]:
1385 1385 return source
1386 1386 self._subsource = source
1387 1387 return _abssource(self)
1388 1388
1389 1389 def _fetch(self, source, revision):
1390 1390 if self._gitmissing():
1391 1391 # SEC: check for safe ssh url
1392 1392 util.checksafessh(source)
1393 1393
1394 1394 source = self._abssource(source)
1395 1395 self.ui.status(_('cloning subrepo %s from %s\n') %
1396 1396 (self._relpath, source))
1397 1397 self._gitnodir(['clone', source, self._abspath])
1398 1398 if self._githavelocally(revision):
1399 1399 return
1400 1400 self.ui.status(_('pulling subrepo %s from %s\n') %
1401 1401 (self._relpath, self._gitremote('origin')))
1402 1402 # try only origin: the originally cloned repo
1403 1403 self._gitcommand(['fetch'])
1404 1404 if not self._githavelocally(revision):
1405 1405 raise error.Abort(_('revision %s does not exist in subrepository '
1406 1406 '"%s"\n') % (revision, self._relpath))
1407 1407
1408 1408 @annotatesubrepoerror
1409 1409 def dirty(self, ignoreupdate=False, missing=False):
1410 1410 if self._gitmissing():
1411 1411 return self._state[1] != ''
1412 1412 if self._gitisbare():
1413 1413 return True
1414 1414 if not ignoreupdate and self._state[1] != self._gitstate():
1415 1415 # different version checked out
1416 1416 return True
1417 1417 # check for staged changes or modified files; ignore untracked files
1418 1418 self._gitupdatestat()
1419 1419 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1420 1420 return code == 1
1421 1421
1422 1422 def basestate(self):
1423 1423 return self._gitstate()
1424 1424
1425 1425 @annotatesubrepoerror
1426 1426 def get(self, state, overwrite=False):
1427 1427 source, revision, kind = state
1428 1428 if not revision:
1429 1429 self.remove()
1430 1430 return
1431 1431 self._fetch(source, revision)
1432 1432 # if the repo was set to be bare, unbare it
1433 1433 if self._gitisbare():
1434 1434 self._gitcommand(['config', 'core.bare', 'false'])
1435 1435 if self._gitstate() == revision:
1436 1436 self._gitcommand(['reset', '--hard', 'HEAD'])
1437 1437 return
1438 1438 elif self._gitstate() == revision:
1439 1439 if overwrite:
1440 1440 # first reset the index to unmark new files for commit, because
1441 1441 # reset --hard will otherwise throw away files added for commit,
1442 1442 # not just unmark them.
1443 1443 self._gitcommand(['reset', 'HEAD'])
1444 1444 self._gitcommand(['reset', '--hard', 'HEAD'])
1445 1445 return
1446 1446 branch2rev, rev2branch = self._gitbranchmap()
1447 1447
1448 1448 def checkout(args):
1449 1449 cmd = ['checkout']
1450 1450 if overwrite:
1451 1451 # first reset the index to unmark new files for commit, because
1452 1452 # the -f option will otherwise throw away files added for
1453 1453 # commit, not just unmark them.
1454 1454 self._gitcommand(['reset', 'HEAD'])
1455 1455 cmd.append('-f')
1456 1456 self._gitcommand(cmd + args)
1457 1457 _sanitize(self.ui, self.wvfs, '.git')
1458 1458
1459 1459 def rawcheckout():
1460 1460 # no branch to checkout, check it out with no branch
1461 1461 self.ui.warn(_('checking out detached HEAD in '
1462 1462 'subrepository "%s"\n') % self._relpath)
1463 1463 self.ui.warn(_('check out a git branch if you intend '
1464 1464 'to make changes\n'))
1465 1465 checkout(['-q', revision])
1466 1466
1467 1467 if revision not in rev2branch:
1468 1468 rawcheckout()
1469 1469 return
1470 1470 branches = rev2branch[revision]
1471 1471 firstlocalbranch = None
1472 1472 for b in branches:
1473 1473 if b == 'refs/heads/master':
1474 1474 # master trumps all other branches
1475 1475 checkout(['refs/heads/master'])
1476 1476 return
1477 1477 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1478 1478 firstlocalbranch = b
1479 1479 if firstlocalbranch:
1480 1480 checkout([firstlocalbranch])
1481 1481 return
1482 1482
1483 1483 tracking = self._gittracking(branch2rev.keys())
1484 1484 # choose a remote branch already tracked if possible
1485 1485 remote = branches[0]
1486 1486 if remote not in tracking:
1487 1487 for b in branches:
1488 1488 if b in tracking:
1489 1489 remote = b
1490 1490 break
1491 1491
1492 1492 if remote not in tracking:
1493 1493 # create a new local tracking branch
1494 1494 local = remote.split('/', 3)[3]
1495 1495 checkout(['-b', local, remote])
1496 1496 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1497 1497 # When updating to a tracked remote branch,
1498 1498 # if the local tracking branch is downstream of it,
1499 1499 # a normal `git pull` would have performed a "fast-forward merge"
1500 1500 # which is equivalent to updating the local branch to the remote.
1501 1501 # Since we are only looking at branching at update, we need to
1502 1502 # detect this situation and perform this action lazily.
1503 1503 if tracking[remote] != self._gitcurrentbranch():
1504 1504 checkout([tracking[remote]])
1505 1505 self._gitcommand(['merge', '--ff', remote])
1506 1506 _sanitize(self.ui, self.wvfs, '.git')
1507 1507 else:
1508 1508 # a real merge would be required, just checkout the revision
1509 1509 rawcheckout()
1510 1510
1511 1511 @annotatesubrepoerror
1512 1512 def commit(self, text, user, date):
1513 1513 if self._gitmissing():
1514 1514 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1515 1515 cmd = ['commit', '-a', '-m', text]
1516 1516 env = encoding.environ.copy()
1517 1517 if user:
1518 1518 cmd += ['--author', user]
1519 1519 if date:
1520 1520 # git's date parser silently ignores when seconds < 1e9
1521 1521 # convert to ISO8601
1522 1522 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1523 1523 '%Y-%m-%dT%H:%M:%S %1%2')
1524 1524 self._gitcommand(cmd, env=env)
1525 1525 # make sure commit works otherwise HEAD might not exist under certain
1526 1526 # circumstances
1527 1527 return self._gitstate()
1528 1528
1529 1529 @annotatesubrepoerror
1530 1530 def merge(self, state):
1531 1531 source, revision, kind = state
1532 1532 self._fetch(source, revision)
1533 1533 base = self._gitcommand(['merge-base', revision, self._state[1]])
1534 1534 self._gitupdatestat()
1535 1535 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1536 1536
1537 1537 def mergefunc():
1538 1538 if base == revision:
1539 1539 self.get(state) # fast forward merge
1540 1540 elif base != self._state[1]:
1541 1541 self._gitcommand(['merge', '--no-commit', revision])
1542 1542 _sanitize(self.ui, self.wvfs, '.git')
1543 1543
1544 1544 if self.dirty():
1545 1545 if self._gitstate() != revision:
1546 1546 dirty = self._gitstate() == self._state[1] or code != 0
1547 1547 if _updateprompt(self.ui, self, dirty,
1548 1548 self._state[1][:7], revision[:7]):
1549 1549 mergefunc()
1550 1550 else:
1551 1551 mergefunc()
1552 1552
1553 1553 @annotatesubrepoerror
1554 1554 def push(self, opts):
1555 1555 force = opts.get('force')
1556 1556
1557 1557 if not self._state[1]:
1558 1558 return True
1559 1559 if self._gitmissing():
1560 1560 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1561 1561 # if a branch in origin contains the revision, nothing to do
1562 1562 branch2rev, rev2branch = self._gitbranchmap()
1563 1563 if self._state[1] in rev2branch:
1564 1564 for b in rev2branch[self._state[1]]:
1565 1565 if b.startswith('refs/remotes/origin/'):
1566 1566 return True
1567 1567 for b, revision in branch2rev.iteritems():
1568 1568 if b.startswith('refs/remotes/origin/'):
1569 1569 if self._gitisancestor(self._state[1], revision):
1570 1570 return True
1571 1571 # otherwise, try to push the currently checked out branch
1572 1572 cmd = ['push']
1573 1573 if force:
1574 1574 cmd.append('--force')
1575 1575
1576 1576 current = self._gitcurrentbranch()
1577 1577 if current:
1578 1578 # determine if the current branch is even useful
1579 1579 if not self._gitisancestor(self._state[1], current):
1580 1580 self.ui.warn(_('unrelated git branch checked out '
1581 1581 'in subrepository "%s"\n') % self._relpath)
1582 1582 return False
1583 1583 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1584 1584 (current.split('/', 2)[2], self._relpath))
1585 1585 ret = self._gitdir(cmd + ['origin', current])
1586 1586 return ret[1] == 0
1587 1587 else:
1588 1588 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1589 1589 'cannot push revision %s\n') %
1590 1590 (self._relpath, self._state[1]))
1591 1591 return False
1592 1592
1593 1593 @annotatesubrepoerror
1594 1594 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
1595 1595 if self._gitmissing():
1596 1596 return []
1597 1597
1598 1598 s = self.status(None, unknown=True, clean=True)
1599 1599
1600 1600 tracked = set()
1601 1601 # dirstates 'amn' warn, 'r' is added again
1602 1602 for l in (s.modified, s.added, s.deleted, s.clean):
1603 1603 tracked.update(l)
1604 1604
1605 1605 # Unknown files not of interest will be rejected by the matcher
1606 1606 files = s.unknown
1607 1607 files.extend(match.files())
1608 1608
1609 1609 rejected = []
1610 1610
1611 1611 files = [f for f in sorted(set(files)) if match(f)]
1612 1612 for f in files:
1613 1613 exact = match.exact(f)
1614 1614 command = ["add"]
1615 1615 if exact:
1616 1616 command.append("-f") #should be added, even if ignored
1617 1617 if ui.verbose or not exact:
1618 1618 ui.status(_('adding %s\n') % uipathfn(f))
1619 1619
1620 1620 if f in tracked: # hg prints 'adding' even if already tracked
1621 1621 if exact:
1622 1622 rejected.append(f)
1623 1623 continue
1624 1624 if not opts.get(r'dry_run'):
1625 1625 self._gitcommand(command + [f])
1626 1626
1627 1627 for f in rejected:
1628 1628 ui.warn(_("%s already tracked!\n") % uipathfn(f))
1629 1629
1630 1630 return rejected
1631 1631
1632 1632 @annotatesubrepoerror
1633 1633 def remove(self):
1634 1634 if self._gitmissing():
1635 1635 return
1636 1636 if self.dirty():
1637 1637 self.ui.warn(_('not removing repo %s because '
1638 1638 'it has changes.\n') % self._relpath)
1639 1639 return
1640 1640 # we can't fully delete the repository as it may contain
1641 1641 # local-only history
1642 1642 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1643 1643 self._gitcommand(['config', 'core.bare', 'true'])
1644 1644 for f, kind in self.wvfs.readdir():
1645 1645 if f == '.git':
1646 1646 continue
1647 1647 if kind == stat.S_IFDIR:
1648 1648 self.wvfs.rmtree(f)
1649 1649 else:
1650 1650 self.wvfs.unlink(f)
1651 1651
1652 1652 def archive(self, archiver, prefix, match=None, decode=True):
1653 1653 total = 0
1654 1654 source, revision = self._state
1655 1655 if not revision:
1656 1656 return total
1657 1657 self._fetch(source, revision)
1658 1658
1659 1659 # Parse git's native archive command.
1660 1660 # This should be much faster than manually traversing the trees
1661 1661 # and objects with many subprocess calls.
1662 1662 tarstream = self._gitcommand(['archive', revision], stream=True)
1663 1663 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1664 1664 relpath = subrelpath(self)
1665 1665 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
1666 1666 unit=_('files'))
1667 1667 progress.update(0)
1668 1668 for info in tar:
1669 1669 if info.isdir():
1670 1670 continue
1671 1671 bname = pycompat.fsencode(info.name)
1672 1672 if match and not match(bname):
1673 1673 continue
1674 1674 if info.issym():
1675 1675 data = info.linkname
1676 1676 else:
1677 1677 data = tar.extractfile(info).read()
1678 1678 archiver.addfile(prefix + bname, info.mode, info.issym(), data)
1679 1679 total += 1
1680 1680 progress.increment()
1681 1681 progress.complete()
1682 1682 return total
1683 1683
1684 1684
1685 1685 @annotatesubrepoerror
1686 1686 def cat(self, match, fm, fntemplate, prefix, **opts):
1687 1687 rev = self._state[1]
1688 1688 if match.anypats():
1689 1689 return 1 #No support for include/exclude yet
1690 1690
1691 1691 if not match.files():
1692 1692 return 1
1693 1693
1694 1694 # TODO: add support for non-plain formatter (see cmdutil.cat())
1695 1695 for f in match.files():
1696 1696 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1697 1697 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1698 1698 pathname=self.wvfs.reljoin(prefix, f))
1699 1699 fp.write(output)
1700 1700 fp.close()
1701 1701 return 0
1702 1702
1703 1703
1704 1704 @annotatesubrepoerror
1705 1705 def status(self, rev2, **opts):
1706 1706 rev1 = self._state[1]
1707 1707 if self._gitmissing() or not rev1:
1708 1708 # if the repo is missing, return no results
1709 1709 return scmutil.status([], [], [], [], [], [], [])
1710 1710 modified, added, removed = [], [], []
1711 1711 self._gitupdatestat()
1712 1712 if rev2:
1713 1713 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1714 1714 else:
1715 1715 command = ['diff-index', '--no-renames', rev1]
1716 1716 out = self._gitcommand(command)
1717 1717 for line in out.split('\n'):
1718 1718 tab = line.find('\t')
1719 1719 if tab == -1:
1720 1720 continue
1721 1721 status, f = line[tab - 1:tab], line[tab + 1:]
1722 1722 if status == 'M':
1723 1723 modified.append(f)
1724 1724 elif status == 'A':
1725 1725 added.append(f)
1726 1726 elif status == 'D':
1727 1727 removed.append(f)
1728 1728
1729 1729 deleted, unknown, ignored, clean = [], [], [], []
1730 1730
1731 1731 command = ['status', '--porcelain', '-z']
1732 1732 if opts.get(r'unknown'):
1733 1733 command += ['--untracked-files=all']
1734 1734 if opts.get(r'ignored'):
1735 1735 command += ['--ignored']
1736 1736 out = self._gitcommand(command)
1737 1737
1738 1738 changedfiles = set()
1739 1739 changedfiles.update(modified)
1740 1740 changedfiles.update(added)
1741 1741 changedfiles.update(removed)
1742 1742 for line in out.split('\0'):
1743 1743 if not line:
1744 1744 continue
1745 1745 st = line[0:2]
1746 1746 #moves and copies show 2 files on one line
1747 1747 if line.find('\0') >= 0:
1748 1748 filename1, filename2 = line[3:].split('\0')
1749 1749 else:
1750 1750 filename1 = line[3:]
1751 1751 filename2 = None
1752 1752
1753 1753 changedfiles.add(filename1)
1754 1754 if filename2:
1755 1755 changedfiles.add(filename2)
1756 1756
1757 1757 if st == '??':
1758 1758 unknown.append(filename1)
1759 1759 elif st == '!!':
1760 1760 ignored.append(filename1)
1761 1761
1762 1762 if opts.get(r'clean'):
1763 1763 out = self._gitcommand(['ls-files'])
1764 1764 for f in out.split('\n'):
1765 1765 if not f in changedfiles:
1766 1766 clean.append(f)
1767 1767
1768 1768 return scmutil.status(modified, added, removed, deleted,
1769 1769 unknown, ignored, clean)
1770 1770
1771 1771 @annotatesubrepoerror
1772 1772 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1773 1773 node1 = self._state[1]
1774 1774 cmd = ['diff', '--no-renames']
1775 1775 if opts[r'stat']:
1776 1776 cmd.append('--stat')
1777 1777 else:
1778 1778 # for Git, this also implies '-p'
1779 1779 cmd.append('-U%d' % diffopts.context)
1780 1780
1781 1781 if diffopts.noprefix:
1782 1782 cmd.extend(['--src-prefix=%s/' % prefix,
1783 1783 '--dst-prefix=%s/' % prefix])
1784 1784 else:
1785 1785 cmd.extend(['--src-prefix=a/%s/' % prefix,
1786 1786 '--dst-prefix=b/%s/' % prefix])
1787 1787
1788 1788 if diffopts.ignorews:
1789 1789 cmd.append('--ignore-all-space')
1790 1790 if diffopts.ignorewsamount:
1791 1791 cmd.append('--ignore-space-change')
1792 1792 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1793 1793 and diffopts.ignoreblanklines:
1794 1794 cmd.append('--ignore-blank-lines')
1795 1795
1796 1796 cmd.append(node1)
1797 1797 if node2:
1798 1798 cmd.append(node2)
1799 1799
1800 1800 output = ""
1801 1801 if match.always():
1802 1802 output += self._gitcommand(cmd) + '\n'
1803 1803 else:
1804 1804 st = self.status(node2)[:3]
1805 1805 files = [f for sublist in st for f in sublist]
1806 1806 for f in files:
1807 1807 if match(f):
1808 1808 output += self._gitcommand(cmd + ['--', f]) + '\n'
1809 1809
1810 1810 if output.strip():
1811 1811 ui.write(output)
1812 1812
1813 1813 @annotatesubrepoerror
1814 1814 def revert(self, substate, *pats, **opts):
1815 1815 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1816 1816 if not opts.get(r'no_backup'):
1817 1817 status = self.status(None)
1818 1818 names = status.modified
1819 1819 for name in names:
1820 1820 # backuppath() expects a path relative to the parent repo (the
1821 1821 # repo that ui.origbackuppath is relative to)
1822 1822 parentname = os.path.join(self._path, name)
1823 1823 bakname = scmutil.backuppath(self.ui, self._subparent,
1824 1824 parentname)
1825 1825 self.ui.note(_('saving current version of %s as %s\n') %
1826 1826 (name, os.path.relpath(bakname)))
1827 1827 util.rename(self.wvfs.join(name), bakname)
1828 1828
1829 1829 if not opts.get(r'dry_run'):
1830 1830 self.get(substate, overwrite=True)
1831 1831 return []
1832 1832
1833 1833 def shortid(self, revid):
1834 1834 return revid[:7]
1835 1835
1836 1836 types = {
1837 1837 'hg': hgsubrepo,
1838 1838 'svn': svnsubrepo,
1839 1839 'git': gitsubrepo,
1840 1840 }
General Comments 0
You need to be logged in to leave comments. Login now