##// END OF EJS Templates
typing: fix directives mangled by black...
Matt Harbison -
r47545:70f8c648 stable
parent child Browse files
Show More
@@ -1,1703 +1,1704 b''
1 # changegroup.py - Mercurial changegroup manipulation functions
1 # changegroup.py - Mercurial changegroup manipulation functions
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import struct
11 import struct
12 import weakref
12 import weakref
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 hex,
16 hex,
17 nullid,
17 nullid,
18 nullrev,
18 nullrev,
19 short,
19 short,
20 )
20 )
21 from .pycompat import open
21 from .pycompat import open
22
22
23 from . import (
23 from . import (
24 error,
24 error,
25 match as matchmod,
25 match as matchmod,
26 mdiff,
26 mdiff,
27 phases,
27 phases,
28 pycompat,
28 pycompat,
29 requirements,
29 requirements,
30 scmutil,
30 scmutil,
31 util,
31 util,
32 )
32 )
33
33
34 from .interfaces import repository
34 from .interfaces import repository
35
35
36 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct(b"20s20s20s20s")
36 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct(b"20s20s20s20s")
37 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct(b"20s20s20s20s20s")
37 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct(b"20s20s20s20s20s")
38 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(b">20s20s20s20s20sH")
38 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(b">20s20s20s20s20sH")
39
39
40 LFS_REQUIREMENT = b'lfs'
40 LFS_REQUIREMENT = b'lfs'
41
41
42 readexactly = util.readexactly
42 readexactly = util.readexactly
43
43
44
44
45 def getchunk(stream):
45 def getchunk(stream):
46 """return the next chunk from stream as a string"""
46 """return the next chunk from stream as a string"""
47 d = readexactly(stream, 4)
47 d = readexactly(stream, 4)
48 l = struct.unpack(b">l", d)[0]
48 l = struct.unpack(b">l", d)[0]
49 if l <= 4:
49 if l <= 4:
50 if l:
50 if l:
51 raise error.Abort(_(b"invalid chunk length %d") % l)
51 raise error.Abort(_(b"invalid chunk length %d") % l)
52 return b""
52 return b""
53 return readexactly(stream, l - 4)
53 return readexactly(stream, l - 4)
54
54
55
55
56 def chunkheader(length):
56 def chunkheader(length):
57 """return a changegroup chunk header (string)"""
57 """return a changegroup chunk header (string)"""
58 return struct.pack(b">l", length + 4)
58 return struct.pack(b">l", length + 4)
59
59
60
60
61 def closechunk():
61 def closechunk():
62 """return a changegroup chunk header (string) for a zero-length chunk"""
62 """return a changegroup chunk header (string) for a zero-length chunk"""
63 return struct.pack(b">l", 0)
63 return struct.pack(b">l", 0)
64
64
65
65
66 def _fileheader(path):
66 def _fileheader(path):
67 """Obtain a changegroup chunk header for a named path."""
67 """Obtain a changegroup chunk header for a named path."""
68 return chunkheader(len(path)) + path
68 return chunkheader(len(path)) + path
69
69
70
70
71 def writechunks(ui, chunks, filename, vfs=None):
71 def writechunks(ui, chunks, filename, vfs=None):
72 """Write chunks to a file and return its filename.
72 """Write chunks to a file and return its filename.
73
73
74 The stream is assumed to be a bundle file.
74 The stream is assumed to be a bundle file.
75 Existing files will not be overwritten.
75 Existing files will not be overwritten.
76 If no filename is specified, a temporary file is created.
76 If no filename is specified, a temporary file is created.
77 """
77 """
78 fh = None
78 fh = None
79 cleanup = None
79 cleanup = None
80 try:
80 try:
81 if filename:
81 if filename:
82 if vfs:
82 if vfs:
83 fh = vfs.open(filename, b"wb")
83 fh = vfs.open(filename, b"wb")
84 else:
84 else:
85 # Increase default buffer size because default is usually
85 # Increase default buffer size because default is usually
86 # small (4k is common on Linux).
86 # small (4k is common on Linux).
87 fh = open(filename, b"wb", 131072)
87 fh = open(filename, b"wb", 131072)
88 else:
88 else:
89 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
89 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
90 fh = os.fdopen(fd, "wb")
90 fh = os.fdopen(fd, "wb")
91 cleanup = filename
91 cleanup = filename
92 for c in chunks:
92 for c in chunks:
93 fh.write(c)
93 fh.write(c)
94 cleanup = None
94 cleanup = None
95 return filename
95 return filename
96 finally:
96 finally:
97 if fh is not None:
97 if fh is not None:
98 fh.close()
98 fh.close()
99 if cleanup is not None:
99 if cleanup is not None:
100 if filename and vfs:
100 if filename and vfs:
101 vfs.unlink(cleanup)
101 vfs.unlink(cleanup)
102 else:
102 else:
103 os.unlink(cleanup)
103 os.unlink(cleanup)
104
104
105
105
106 class cg1unpacker(object):
106 class cg1unpacker(object):
107 """Unpacker for cg1 changegroup streams.
107 """Unpacker for cg1 changegroup streams.
108
108
109 A changegroup unpacker handles the framing of the revision data in
109 A changegroup unpacker handles the framing of the revision data in
110 the wire format. Most consumers will want to use the apply()
110 the wire format. Most consumers will want to use the apply()
111 method to add the changes from the changegroup to a repository.
111 method to add the changes from the changegroup to a repository.
112
112
113 If you're forwarding a changegroup unmodified to another consumer,
113 If you're forwarding a changegroup unmodified to another consumer,
114 use getchunks(), which returns an iterator of changegroup
114 use getchunks(), which returns an iterator of changegroup
115 chunks. This is mostly useful for cases where you need to know the
115 chunks. This is mostly useful for cases where you need to know the
116 data stream has ended by observing the end of the changegroup.
116 data stream has ended by observing the end of the changegroup.
117
117
118 deltachunk() is useful only if you're applying delta data. Most
118 deltachunk() is useful only if you're applying delta data. Most
119 consumers should prefer apply() instead.
119 consumers should prefer apply() instead.
120
120
121 A few other public methods exist. Those are used only for
121 A few other public methods exist. Those are used only for
122 bundlerepo and some debug commands - their use is discouraged.
122 bundlerepo and some debug commands - their use is discouraged.
123 """
123 """
124
124
125 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
125 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
126 deltaheadersize = deltaheader.size
126 deltaheadersize = deltaheader.size
127 version = b'01'
127 version = b'01'
128 _grouplistcount = 1 # One list of files after the manifests
128 _grouplistcount = 1 # One list of files after the manifests
129
129
130 def __init__(self, fh, alg, extras=None):
130 def __init__(self, fh, alg, extras=None):
131 if alg is None:
131 if alg is None:
132 alg = b'UN'
132 alg = b'UN'
133 if alg not in util.compengines.supportedbundletypes:
133 if alg not in util.compengines.supportedbundletypes:
134 raise error.Abort(_(b'unknown stream compression type: %s') % alg)
134 raise error.Abort(_(b'unknown stream compression type: %s') % alg)
135 if alg == b'BZ':
135 if alg == b'BZ':
136 alg = b'_truncatedBZ'
136 alg = b'_truncatedBZ'
137
137
138 compengine = util.compengines.forbundletype(alg)
138 compengine = util.compengines.forbundletype(alg)
139 self._stream = compengine.decompressorreader(fh)
139 self._stream = compengine.decompressorreader(fh)
140 self._type = alg
140 self._type = alg
141 self.extras = extras or {}
141 self.extras = extras or {}
142 self.callback = None
142 self.callback = None
143
143
144 # These methods (compressed, read, seek, tell) all appear to only
144 # These methods (compressed, read, seek, tell) all appear to only
145 # be used by bundlerepo, but it's a little hard to tell.
145 # be used by bundlerepo, but it's a little hard to tell.
146 def compressed(self):
146 def compressed(self):
147 return self._type is not None and self._type != b'UN'
147 return self._type is not None and self._type != b'UN'
148
148
149 def read(self, l):
149 def read(self, l):
150 return self._stream.read(l)
150 return self._stream.read(l)
151
151
152 def seek(self, pos):
152 def seek(self, pos):
153 return self._stream.seek(pos)
153 return self._stream.seek(pos)
154
154
155 def tell(self):
155 def tell(self):
156 return self._stream.tell()
156 return self._stream.tell()
157
157
158 def close(self):
158 def close(self):
159 return self._stream.close()
159 return self._stream.close()
160
160
161 def _chunklength(self):
161 def _chunklength(self):
162 d = readexactly(self._stream, 4)
162 d = readexactly(self._stream, 4)
163 l = struct.unpack(b">l", d)[0]
163 l = struct.unpack(b">l", d)[0]
164 if l <= 4:
164 if l <= 4:
165 if l:
165 if l:
166 raise error.Abort(_(b"invalid chunk length %d") % l)
166 raise error.Abort(_(b"invalid chunk length %d") % l)
167 return 0
167 return 0
168 if self.callback:
168 if self.callback:
169 self.callback()
169 self.callback()
170 return l - 4
170 return l - 4
171
171
172 def changelogheader(self):
172 def changelogheader(self):
173 """v10 does not have a changelog header chunk"""
173 """v10 does not have a changelog header chunk"""
174 return {}
174 return {}
175
175
176 def manifestheader(self):
176 def manifestheader(self):
177 """v10 does not have a manifest header chunk"""
177 """v10 does not have a manifest header chunk"""
178 return {}
178 return {}
179
179
180 def filelogheader(self):
180 def filelogheader(self):
181 """return the header of the filelogs chunk, v10 only has the filename"""
181 """return the header of the filelogs chunk, v10 only has the filename"""
182 l = self._chunklength()
182 l = self._chunklength()
183 if not l:
183 if not l:
184 return {}
184 return {}
185 fname = readexactly(self._stream, l)
185 fname = readexactly(self._stream, l)
186 return {b'filename': fname}
186 return {b'filename': fname}
187
187
188 def _deltaheader(self, headertuple, prevnode):
188 def _deltaheader(self, headertuple, prevnode):
189 node, p1, p2, cs = headertuple
189 node, p1, p2, cs = headertuple
190 if prevnode is None:
190 if prevnode is None:
191 deltabase = p1
191 deltabase = p1
192 else:
192 else:
193 deltabase = prevnode
193 deltabase = prevnode
194 flags = 0
194 flags = 0
195 return node, p1, p2, deltabase, cs, flags
195 return node, p1, p2, deltabase, cs, flags
196
196
197 def deltachunk(self, prevnode):
197 def deltachunk(self, prevnode):
198 l = self._chunklength()
198 l = self._chunklength()
199 if not l:
199 if not l:
200 return {}
200 return {}
201 headerdata = readexactly(self._stream, self.deltaheadersize)
201 headerdata = readexactly(self._stream, self.deltaheadersize)
202 header = self.deltaheader.unpack(headerdata)
202 header = self.deltaheader.unpack(headerdata)
203 delta = readexactly(self._stream, l - self.deltaheadersize)
203 delta = readexactly(self._stream, l - self.deltaheadersize)
204 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
204 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
205 return (node, p1, p2, cs, deltabase, delta, flags)
205 return (node, p1, p2, cs, deltabase, delta, flags)
206
206
207 def getchunks(self):
207 def getchunks(self):
208 """returns all the chunks contains in the bundle
208 """returns all the chunks contains in the bundle
209
209
210 Used when you need to forward the binary stream to a file or another
210 Used when you need to forward the binary stream to a file or another
211 network API. To do so, it parse the changegroup data, otherwise it will
211 network API. To do so, it parse the changegroup data, otherwise it will
212 block in case of sshrepo because it don't know the end of the stream.
212 block in case of sshrepo because it don't know the end of the stream.
213 """
213 """
214 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
214 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
215 # and a list of filelogs. For changegroup 3, we expect 4 parts:
215 # and a list of filelogs. For changegroup 3, we expect 4 parts:
216 # changelog, manifestlog, a list of tree manifestlogs, and a list of
216 # changelog, manifestlog, a list of tree manifestlogs, and a list of
217 # filelogs.
217 # filelogs.
218 #
218 #
219 # Changelog and manifestlog parts are terminated with empty chunks. The
219 # Changelog and manifestlog parts are terminated with empty chunks. The
220 # tree and file parts are a list of entry sections. Each entry section
220 # tree and file parts are a list of entry sections. Each entry section
221 # is a series of chunks terminating in an empty chunk. The list of these
221 # is a series of chunks terminating in an empty chunk. The list of these
222 # entry sections is terminated in yet another empty chunk, so we know
222 # entry sections is terminated in yet another empty chunk, so we know
223 # we've reached the end of the tree/file list when we reach an empty
223 # we've reached the end of the tree/file list when we reach an empty
224 # chunk that was proceeded by no non-empty chunks.
224 # chunk that was proceeded by no non-empty chunks.
225
225
226 parts = 0
226 parts = 0
227 while parts < 2 + self._grouplistcount:
227 while parts < 2 + self._grouplistcount:
228 noentries = True
228 noentries = True
229 while True:
229 while True:
230 chunk = getchunk(self)
230 chunk = getchunk(self)
231 if not chunk:
231 if not chunk:
232 # The first two empty chunks represent the end of the
232 # The first two empty chunks represent the end of the
233 # changelog and the manifestlog portions. The remaining
233 # changelog and the manifestlog portions. The remaining
234 # empty chunks represent either A) the end of individual
234 # empty chunks represent either A) the end of individual
235 # tree or file entries in the file list, or B) the end of
235 # tree or file entries in the file list, or B) the end of
236 # the entire list. It's the end of the entire list if there
236 # the entire list. It's the end of the entire list if there
237 # were no entries (i.e. noentries is True).
237 # were no entries (i.e. noentries is True).
238 if parts < 2:
238 if parts < 2:
239 parts += 1
239 parts += 1
240 elif noentries:
240 elif noentries:
241 parts += 1
241 parts += 1
242 break
242 break
243 noentries = False
243 noentries = False
244 yield chunkheader(len(chunk))
244 yield chunkheader(len(chunk))
245 pos = 0
245 pos = 0
246 while pos < len(chunk):
246 while pos < len(chunk):
247 next = pos + 2 ** 20
247 next = pos + 2 ** 20
248 yield chunk[pos:next]
248 yield chunk[pos:next]
249 pos = next
249 pos = next
250 yield closechunk()
250 yield closechunk()
251
251
252 def _unpackmanifests(self, repo, revmap, trp, prog):
252 def _unpackmanifests(self, repo, revmap, trp, prog):
253 self.callback = prog.increment
253 self.callback = prog.increment
254 # no need to check for empty manifest group here:
254 # no need to check for empty manifest group here:
255 # if the result of the merge of 1 and 2 is the same in 3 and 4,
255 # if the result of the merge of 1 and 2 is the same in 3 and 4,
256 # no new manifest will be created and the manifest group will
256 # no new manifest will be created and the manifest group will
257 # be empty during the pull
257 # be empty during the pull
258 self.manifestheader()
258 self.manifestheader()
259 deltas = self.deltaiter()
259 deltas = self.deltaiter()
260 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
260 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
261 prog.complete()
261 prog.complete()
262 self.callback = None
262 self.callback = None
263
263
264 def apply(
264 def apply(
265 self,
265 self,
266 repo,
266 repo,
267 tr,
267 tr,
268 srctype,
268 srctype,
269 url,
269 url,
270 targetphase=phases.draft,
270 targetphase=phases.draft,
271 expectedtotal=None,
271 expectedtotal=None,
272 ):
272 ):
273 """Add the changegroup returned by source.read() to this repo.
273 """Add the changegroup returned by source.read() to this repo.
274 srctype is a string like 'push', 'pull', or 'unbundle'. url is
274 srctype is a string like 'push', 'pull', or 'unbundle'. url is
275 the URL of the repo where this changegroup is coming from.
275 the URL of the repo where this changegroup is coming from.
276
276
277 Return an integer summarizing the change to this repo:
277 Return an integer summarizing the change to this repo:
278 - nothing changed or no source: 0
278 - nothing changed or no source: 0
279 - more heads than before: 1+added heads (2..n)
279 - more heads than before: 1+added heads (2..n)
280 - fewer heads than before: -1-removed heads (-2..-n)
280 - fewer heads than before: -1-removed heads (-2..-n)
281 - number of heads stays the same: 1
281 - number of heads stays the same: 1
282 """
282 """
283 repo = repo.unfiltered()
283 repo = repo.unfiltered()
284
284
285 def csmap(x):
285 def csmap(x):
286 repo.ui.debug(b"add changeset %s\n" % short(x))
286 repo.ui.debug(b"add changeset %s\n" % short(x))
287 return len(cl)
287 return len(cl)
288
288
289 def revmap(x):
289 def revmap(x):
290 return cl.rev(x)
290 return cl.rev(x)
291
291
292 try:
292 try:
293 # The transaction may already carry source information. In this
293 # The transaction may already carry source information. In this
294 # case we use the top level data. We overwrite the argument
294 # case we use the top level data. We overwrite the argument
295 # because we need to use the top level value (if they exist)
295 # because we need to use the top level value (if they exist)
296 # in this function.
296 # in this function.
297 srctype = tr.hookargs.setdefault(b'source', srctype)
297 srctype = tr.hookargs.setdefault(b'source', srctype)
298 tr.hookargs.setdefault(b'url', url)
298 tr.hookargs.setdefault(b'url', url)
299 repo.hook(
299 repo.hook(
300 b'prechangegroup', throw=True, **pycompat.strkwargs(tr.hookargs)
300 b'prechangegroup', throw=True, **pycompat.strkwargs(tr.hookargs)
301 )
301 )
302
302
303 # write changelog data to temp files so concurrent readers
303 # write changelog data to temp files so concurrent readers
304 # will not see an inconsistent view
304 # will not see an inconsistent view
305 cl = repo.changelog
305 cl = repo.changelog
306 cl.delayupdate(tr)
306 cl.delayupdate(tr)
307 oldheads = set(cl.heads())
307 oldheads = set(cl.heads())
308
308
309 trp = weakref.proxy(tr)
309 trp = weakref.proxy(tr)
310 # pull off the changeset group
310 # pull off the changeset group
311 repo.ui.status(_(b"adding changesets\n"))
311 repo.ui.status(_(b"adding changesets\n"))
312 clstart = len(cl)
312 clstart = len(cl)
313 progress = repo.ui.makeprogress(
313 progress = repo.ui.makeprogress(
314 _(b'changesets'), unit=_(b'chunks'), total=expectedtotal
314 _(b'changesets'), unit=_(b'chunks'), total=expectedtotal
315 )
315 )
316 self.callback = progress.increment
316 self.callback = progress.increment
317
317
318 efilesset = set()
318 efilesset = set()
319 cgnodes = []
319 cgnodes = []
320
320
321 def ondupchangelog(cl, node):
321 def ondupchangelog(cl, node):
322 if cl.rev(node) < clstart:
322 if cl.rev(node) < clstart:
323 cgnodes.append(node)
323 cgnodes.append(node)
324
324
325 def onchangelog(cl, node):
325 def onchangelog(cl, node):
326 efilesset.update(cl.readfiles(node))
326 efilesset.update(cl.readfiles(node))
327
327
328 self.changelogheader()
328 self.changelogheader()
329 deltas = self.deltaiter()
329 deltas = self.deltaiter()
330 if not cl.addgroup(
330 if not cl.addgroup(
331 deltas,
331 deltas,
332 csmap,
332 csmap,
333 trp,
333 trp,
334 addrevisioncb=onchangelog,
334 addrevisioncb=onchangelog,
335 duplicaterevisioncb=ondupchangelog,
335 duplicaterevisioncb=ondupchangelog,
336 ):
336 ):
337 repo.ui.develwarn(
337 repo.ui.develwarn(
338 b'applied empty changelog from changegroup',
338 b'applied empty changelog from changegroup',
339 config=b'warn-empty-changegroup',
339 config=b'warn-empty-changegroup',
340 )
340 )
341 efiles = len(efilesset)
341 efiles = len(efilesset)
342 clend = len(cl)
342 clend = len(cl)
343 changesets = clend - clstart
343 changesets = clend - clstart
344 progress.complete()
344 progress.complete()
345 del deltas
345 del deltas
346 # TODO Python 2.7 removal
346 # TODO Python 2.7 removal
347 # del efilesset
347 # del efilesset
348 efilesset = None
348 efilesset = None
349 self.callback = None
349 self.callback = None
350
350
351 # pull off the manifest group
351 # pull off the manifest group
352 repo.ui.status(_(b"adding manifests\n"))
352 repo.ui.status(_(b"adding manifests\n"))
353 # We know that we'll never have more manifests than we had
353 # We know that we'll never have more manifests than we had
354 # changesets.
354 # changesets.
355 progress = repo.ui.makeprogress(
355 progress = repo.ui.makeprogress(
356 _(b'manifests'), unit=_(b'chunks'), total=changesets
356 _(b'manifests'), unit=_(b'chunks'), total=changesets
357 )
357 )
358 self._unpackmanifests(repo, revmap, trp, progress)
358 self._unpackmanifests(repo, revmap, trp, progress)
359
359
360 needfiles = {}
360 needfiles = {}
361 if repo.ui.configbool(b'server', b'validate'):
361 if repo.ui.configbool(b'server', b'validate'):
362 cl = repo.changelog
362 cl = repo.changelog
363 ml = repo.manifestlog
363 ml = repo.manifestlog
364 # validate incoming csets have their manifests
364 # validate incoming csets have their manifests
365 for cset in pycompat.xrange(clstart, clend):
365 for cset in pycompat.xrange(clstart, clend):
366 mfnode = cl.changelogrevision(cset).manifest
366 mfnode = cl.changelogrevision(cset).manifest
367 mfest = ml[mfnode].readdelta()
367 mfest = ml[mfnode].readdelta()
368 # store file nodes we must see
368 # store file nodes we must see
369 for f, n in pycompat.iteritems(mfest):
369 for f, n in pycompat.iteritems(mfest):
370 needfiles.setdefault(f, set()).add(n)
370 needfiles.setdefault(f, set()).add(n)
371
371
372 # process the files
372 # process the files
373 repo.ui.status(_(b"adding file changes\n"))
373 repo.ui.status(_(b"adding file changes\n"))
374 newrevs, newfiles = _addchangegroupfiles(
374 newrevs, newfiles = _addchangegroupfiles(
375 repo, self, revmap, trp, efiles, needfiles
375 repo, self, revmap, trp, efiles, needfiles
376 )
376 )
377
377
378 # making sure the value exists
378 # making sure the value exists
379 tr.changes.setdefault(b'changegroup-count-changesets', 0)
379 tr.changes.setdefault(b'changegroup-count-changesets', 0)
380 tr.changes.setdefault(b'changegroup-count-revisions', 0)
380 tr.changes.setdefault(b'changegroup-count-revisions', 0)
381 tr.changes.setdefault(b'changegroup-count-files', 0)
381 tr.changes.setdefault(b'changegroup-count-files', 0)
382 tr.changes.setdefault(b'changegroup-count-heads', 0)
382 tr.changes.setdefault(b'changegroup-count-heads', 0)
383
383
384 # some code use bundle operation for internal purpose. They usually
384 # some code use bundle operation for internal purpose. They usually
385 # set `ui.quiet` to do this outside of user sight. Size the report
385 # set `ui.quiet` to do this outside of user sight. Size the report
386 # of such operation now happens at the end of the transaction, that
386 # of such operation now happens at the end of the transaction, that
387 # ui.quiet has not direct effect on the output.
387 # ui.quiet has not direct effect on the output.
388 #
388 #
389 # To preserve this intend use an inelegant hack, we fail to report
389 # To preserve this intend use an inelegant hack, we fail to report
390 # the change if `quiet` is set. We should probably move to
390 # the change if `quiet` is set. We should probably move to
391 # something better, but this is a good first step to allow the "end
391 # something better, but this is a good first step to allow the "end
392 # of transaction report" to pass tests.
392 # of transaction report" to pass tests.
393 if not repo.ui.quiet:
393 if not repo.ui.quiet:
394 tr.changes[b'changegroup-count-changesets'] += changesets
394 tr.changes[b'changegroup-count-changesets'] += changesets
395 tr.changes[b'changegroup-count-revisions'] += newrevs
395 tr.changes[b'changegroup-count-revisions'] += newrevs
396 tr.changes[b'changegroup-count-files'] += newfiles
396 tr.changes[b'changegroup-count-files'] += newfiles
397
397
398 deltaheads = 0
398 deltaheads = 0
399 if oldheads:
399 if oldheads:
400 heads = cl.heads()
400 heads = cl.heads()
401 deltaheads += len(heads) - len(oldheads)
401 deltaheads += len(heads) - len(oldheads)
402 for h in heads:
402 for h in heads:
403 if h not in oldheads and repo[h].closesbranch():
403 if h not in oldheads and repo[h].closesbranch():
404 deltaheads -= 1
404 deltaheads -= 1
405
405
406 # see previous comment about checking ui.quiet
406 # see previous comment about checking ui.quiet
407 if not repo.ui.quiet:
407 if not repo.ui.quiet:
408 tr.changes[b'changegroup-count-heads'] += deltaheads
408 tr.changes[b'changegroup-count-heads'] += deltaheads
409 repo.invalidatevolatilesets()
409 repo.invalidatevolatilesets()
410
410
411 if changesets > 0:
411 if changesets > 0:
412 if b'node' not in tr.hookargs:
412 if b'node' not in tr.hookargs:
413 tr.hookargs[b'node'] = hex(cl.node(clstart))
413 tr.hookargs[b'node'] = hex(cl.node(clstart))
414 tr.hookargs[b'node_last'] = hex(cl.node(clend - 1))
414 tr.hookargs[b'node_last'] = hex(cl.node(clend - 1))
415 hookargs = dict(tr.hookargs)
415 hookargs = dict(tr.hookargs)
416 else:
416 else:
417 hookargs = dict(tr.hookargs)
417 hookargs = dict(tr.hookargs)
418 hookargs[b'node'] = hex(cl.node(clstart))
418 hookargs[b'node'] = hex(cl.node(clstart))
419 hookargs[b'node_last'] = hex(cl.node(clend - 1))
419 hookargs[b'node_last'] = hex(cl.node(clend - 1))
420 repo.hook(
420 repo.hook(
421 b'pretxnchangegroup',
421 b'pretxnchangegroup',
422 throw=True,
422 throw=True,
423 **pycompat.strkwargs(hookargs)
423 **pycompat.strkwargs(hookargs)
424 )
424 )
425
425
426 added = pycompat.xrange(clstart, clend)
426 added = pycompat.xrange(clstart, clend)
427 phaseall = None
427 phaseall = None
428 if srctype in (b'push', b'serve'):
428 if srctype in (b'push', b'serve'):
429 # Old servers can not push the boundary themselves.
429 # Old servers can not push the boundary themselves.
430 # New servers won't push the boundary if changeset already
430 # New servers won't push the boundary if changeset already
431 # exists locally as secret
431 # exists locally as secret
432 #
432 #
433 # We should not use added here but the list of all change in
433 # We should not use added here but the list of all change in
434 # the bundle
434 # the bundle
435 if repo.publishing():
435 if repo.publishing():
436 targetphase = phaseall = phases.public
436 targetphase = phaseall = phases.public
437 else:
437 else:
438 # closer target phase computation
438 # closer target phase computation
439
439
440 # Those changesets have been pushed from the
440 # Those changesets have been pushed from the
441 # outside, their phases are going to be pushed
441 # outside, their phases are going to be pushed
442 # alongside. Therefor `targetphase` is
442 # alongside. Therefor `targetphase` is
443 # ignored.
443 # ignored.
444 targetphase = phaseall = phases.draft
444 targetphase = phaseall = phases.draft
445 if added:
445 if added:
446 phases.registernew(repo, tr, targetphase, added)
446 phases.registernew(repo, tr, targetphase, added)
447 if phaseall is not None:
447 if phaseall is not None:
448 phases.advanceboundary(repo, tr, phaseall, cgnodes, revs=added)
448 phases.advanceboundary(repo, tr, phaseall, cgnodes, revs=added)
449 cgnodes = []
449 cgnodes = []
450
450
451 if changesets > 0:
451 if changesets > 0:
452
452
453 def runhooks(unused_success):
453 def runhooks(unused_success):
454 # These hooks run when the lock releases, not when the
454 # These hooks run when the lock releases, not when the
455 # transaction closes. So it's possible for the changelog
455 # transaction closes. So it's possible for the changelog
456 # to have changed since we last saw it.
456 # to have changed since we last saw it.
457 if clstart >= len(repo):
457 if clstart >= len(repo):
458 return
458 return
459
459
460 repo.hook(b"changegroup", **pycompat.strkwargs(hookargs))
460 repo.hook(b"changegroup", **pycompat.strkwargs(hookargs))
461
461
462 for rev in added:
462 for rev in added:
463 args = hookargs.copy()
463 args = hookargs.copy()
464 args[b'node'] = hex(cl.node(rev))
464 args[b'node'] = hex(cl.node(rev))
465 del args[b'node_last']
465 del args[b'node_last']
466 repo.hook(b"incoming", **pycompat.strkwargs(args))
466 repo.hook(b"incoming", **pycompat.strkwargs(args))
467
467
468 newheads = [h for h in repo.heads() if h not in oldheads]
468 newheads = [h for h in repo.heads() if h not in oldheads]
469 repo.ui.log(
469 repo.ui.log(
470 b"incoming",
470 b"incoming",
471 b"%d incoming changes - new heads: %s\n",
471 b"%d incoming changes - new heads: %s\n",
472 len(added),
472 len(added),
473 b', '.join([hex(c[:6]) for c in newheads]),
473 b', '.join([hex(c[:6]) for c in newheads]),
474 )
474 )
475
475
476 tr.addpostclose(
476 tr.addpostclose(
477 b'changegroup-runhooks-%020i' % clstart,
477 b'changegroup-runhooks-%020i' % clstart,
478 lambda tr: repo._afterlock(runhooks),
478 lambda tr: repo._afterlock(runhooks),
479 )
479 )
480 finally:
480 finally:
481 repo.ui.flush()
481 repo.ui.flush()
482 # never return 0 here:
482 # never return 0 here:
483 if deltaheads < 0:
483 if deltaheads < 0:
484 ret = deltaheads - 1
484 ret = deltaheads - 1
485 else:
485 else:
486 ret = deltaheads + 1
486 ret = deltaheads + 1
487 return ret
487 return ret
488
488
489 def deltaiter(self):
489 def deltaiter(self):
490 """
490 """
491 returns an iterator of the deltas in this changegroup
491 returns an iterator of the deltas in this changegroup
492
492
493 Useful for passing to the underlying storage system to be stored.
493 Useful for passing to the underlying storage system to be stored.
494 """
494 """
495 chain = None
495 chain = None
496 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
496 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
497 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
497 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
498 yield chunkdata
498 yield chunkdata
499 chain = chunkdata[0]
499 chain = chunkdata[0]
500
500
501
501
502 class cg2unpacker(cg1unpacker):
502 class cg2unpacker(cg1unpacker):
503 """Unpacker for cg2 streams.
503 """Unpacker for cg2 streams.
504
504
505 cg2 streams add support for generaldelta, so the delta header
505 cg2 streams add support for generaldelta, so the delta header
506 format is slightly different. All other features about the data
506 format is slightly different. All other features about the data
507 remain the same.
507 remain the same.
508 """
508 """
509
509
510 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
510 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
511 deltaheadersize = deltaheader.size
511 deltaheadersize = deltaheader.size
512 version = b'02'
512 version = b'02'
513
513
514 def _deltaheader(self, headertuple, prevnode):
514 def _deltaheader(self, headertuple, prevnode):
515 node, p1, p2, deltabase, cs = headertuple
515 node, p1, p2, deltabase, cs = headertuple
516 flags = 0
516 flags = 0
517 return node, p1, p2, deltabase, cs, flags
517 return node, p1, p2, deltabase, cs, flags
518
518
519
519
520 class cg3unpacker(cg2unpacker):
520 class cg3unpacker(cg2unpacker):
521 """Unpacker for cg3 streams.
521 """Unpacker for cg3 streams.
522
522
523 cg3 streams add support for exchanging treemanifests and revlog
523 cg3 streams add support for exchanging treemanifests and revlog
524 flags. It adds the revlog flags to the delta header and an empty chunk
524 flags. It adds the revlog flags to the delta header and an empty chunk
525 separating manifests and files.
525 separating manifests and files.
526 """
526 """
527
527
528 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
528 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
529 deltaheadersize = deltaheader.size
529 deltaheadersize = deltaheader.size
530 version = b'03'
530 version = b'03'
531 _grouplistcount = 2 # One list of manifests and one list of files
531 _grouplistcount = 2 # One list of manifests and one list of files
532
532
533 def _deltaheader(self, headertuple, prevnode):
533 def _deltaheader(self, headertuple, prevnode):
534 node, p1, p2, deltabase, cs, flags = headertuple
534 node, p1, p2, deltabase, cs, flags = headertuple
535 return node, p1, p2, deltabase, cs, flags
535 return node, p1, p2, deltabase, cs, flags
536
536
537 def _unpackmanifests(self, repo, revmap, trp, prog):
537 def _unpackmanifests(self, repo, revmap, trp, prog):
538 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
538 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
539 for chunkdata in iter(self.filelogheader, {}):
539 for chunkdata in iter(self.filelogheader, {}):
540 # If we get here, there are directory manifests in the changegroup
540 # If we get here, there are directory manifests in the changegroup
541 d = chunkdata[b"filename"]
541 d = chunkdata[b"filename"]
542 repo.ui.debug(b"adding %s revisions\n" % d)
542 repo.ui.debug(b"adding %s revisions\n" % d)
543 deltas = self.deltaiter()
543 deltas = self.deltaiter()
544 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
544 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
545 raise error.Abort(_(b"received dir revlog group is empty"))
545 raise error.Abort(_(b"received dir revlog group is empty"))
546
546
547
547
548 class headerlessfixup(object):
548 class headerlessfixup(object):
549 def __init__(self, fh, h):
549 def __init__(self, fh, h):
550 self._h = h
550 self._h = h
551 self._fh = fh
551 self._fh = fh
552
552
553 def read(self, n):
553 def read(self, n):
554 if self._h:
554 if self._h:
555 d, self._h = self._h[:n], self._h[n:]
555 d, self._h = self._h[:n], self._h[n:]
556 if len(d) < n:
556 if len(d) < n:
557 d += readexactly(self._fh, n - len(d))
557 d += readexactly(self._fh, n - len(d))
558 return d
558 return d
559 return readexactly(self._fh, n)
559 return readexactly(self._fh, n)
560
560
561
561
562 def _revisiondeltatochunks(delta, headerfn):
562 def _revisiondeltatochunks(delta, headerfn):
563 """Serialize a revisiondelta to changegroup chunks."""
563 """Serialize a revisiondelta to changegroup chunks."""
564
564
565 # The captured revision delta may be encoded as a delta against
565 # The captured revision delta may be encoded as a delta against
566 # a base revision or as a full revision. The changegroup format
566 # a base revision or as a full revision. The changegroup format
567 # requires that everything on the wire be deltas. So for full
567 # requires that everything on the wire be deltas. So for full
568 # revisions, we need to invent a header that says to rewrite
568 # revisions, we need to invent a header that says to rewrite
569 # data.
569 # data.
570
570
571 if delta.delta is not None:
571 if delta.delta is not None:
572 prefix, data = b'', delta.delta
572 prefix, data = b'', delta.delta
573 elif delta.basenode == nullid:
573 elif delta.basenode == nullid:
574 data = delta.revision
574 data = delta.revision
575 prefix = mdiff.trivialdiffheader(len(data))
575 prefix = mdiff.trivialdiffheader(len(data))
576 else:
576 else:
577 data = delta.revision
577 data = delta.revision
578 prefix = mdiff.replacediffheader(delta.baserevisionsize, len(data))
578 prefix = mdiff.replacediffheader(delta.baserevisionsize, len(data))
579
579
580 meta = headerfn(delta)
580 meta = headerfn(delta)
581
581
582 yield chunkheader(len(meta) + len(prefix) + len(data))
582 yield chunkheader(len(meta) + len(prefix) + len(data))
583 yield meta
583 yield meta
584 if prefix:
584 if prefix:
585 yield prefix
585 yield prefix
586 yield data
586 yield data
587
587
588
588
589 def _sortnodesellipsis(store, nodes, cl, lookup):
589 def _sortnodesellipsis(store, nodes, cl, lookup):
590 """Sort nodes for changegroup generation."""
590 """Sort nodes for changegroup generation."""
591 # Ellipses serving mode.
591 # Ellipses serving mode.
592 #
592 #
593 # In a perfect world, we'd generate better ellipsis-ified graphs
593 # In a perfect world, we'd generate better ellipsis-ified graphs
594 # for non-changelog revlogs. In practice, we haven't started doing
594 # for non-changelog revlogs. In practice, we haven't started doing
595 # that yet, so the resulting DAGs for the manifestlog and filelogs
595 # that yet, so the resulting DAGs for the manifestlog and filelogs
596 # are actually full of bogus parentage on all the ellipsis
596 # are actually full of bogus parentage on all the ellipsis
597 # nodes. This has the side effect that, while the contents are
597 # nodes. This has the side effect that, while the contents are
598 # correct, the individual DAGs might be completely out of whack in
598 # correct, the individual DAGs might be completely out of whack in
599 # a case like 882681bc3166 and its ancestors (back about 10
599 # a case like 882681bc3166 and its ancestors (back about 10
600 # revisions or so) in the main hg repo.
600 # revisions or so) in the main hg repo.
601 #
601 #
602 # The one invariant we *know* holds is that the new (potentially
602 # The one invariant we *know* holds is that the new (potentially
603 # bogus) DAG shape will be valid if we order the nodes in the
603 # bogus) DAG shape will be valid if we order the nodes in the
604 # order that they're introduced in dramatis personae by the
604 # order that they're introduced in dramatis personae by the
605 # changelog, so what we do is we sort the non-changelog histories
605 # changelog, so what we do is we sort the non-changelog histories
606 # by the order in which they are used by the changelog.
606 # by the order in which they are used by the changelog.
607 key = lambda n: cl.rev(lookup(n))
607 key = lambda n: cl.rev(lookup(n))
608 return sorted(nodes, key=key)
608 return sorted(nodes, key=key)
609
609
610
610
611 def _resolvenarrowrevisioninfo(
611 def _resolvenarrowrevisioninfo(
612 cl,
612 cl,
613 store,
613 store,
614 ischangelog,
614 ischangelog,
615 rev,
615 rev,
616 linkrev,
616 linkrev,
617 linknode,
617 linknode,
618 clrevtolocalrev,
618 clrevtolocalrev,
619 fullclnodes,
619 fullclnodes,
620 precomputedellipsis,
620 precomputedellipsis,
621 ):
621 ):
622 linkparents = precomputedellipsis[linkrev]
622 linkparents = precomputedellipsis[linkrev]
623
623
624 def local(clrev):
624 def local(clrev):
625 """Turn a changelog revnum into a local revnum.
625 """Turn a changelog revnum into a local revnum.
626
626
627 The ellipsis dag is stored as revnums on the changelog,
627 The ellipsis dag is stored as revnums on the changelog,
628 but when we're producing ellipsis entries for
628 but when we're producing ellipsis entries for
629 non-changelog revlogs, we need to turn those numbers into
629 non-changelog revlogs, we need to turn those numbers into
630 something local. This does that for us, and during the
630 something local. This does that for us, and during the
631 changelog sending phase will also expand the stored
631 changelog sending phase will also expand the stored
632 mappings as needed.
632 mappings as needed.
633 """
633 """
634 if clrev == nullrev:
634 if clrev == nullrev:
635 return nullrev
635 return nullrev
636
636
637 if ischangelog:
637 if ischangelog:
638 return clrev
638 return clrev
639
639
640 # Walk the ellipsis-ized changelog breadth-first looking for a
640 # Walk the ellipsis-ized changelog breadth-first looking for a
641 # change that has been linked from the current revlog.
641 # change that has been linked from the current revlog.
642 #
642 #
643 # For a flat manifest revlog only a single step should be necessary
643 # For a flat manifest revlog only a single step should be necessary
644 # as all relevant changelog entries are relevant to the flat
644 # as all relevant changelog entries are relevant to the flat
645 # manifest.
645 # manifest.
646 #
646 #
647 # For a filelog or tree manifest dirlog however not every changelog
647 # For a filelog or tree manifest dirlog however not every changelog
648 # entry will have been relevant, so we need to skip some changelog
648 # entry will have been relevant, so we need to skip some changelog
649 # nodes even after ellipsis-izing.
649 # nodes even after ellipsis-izing.
650 walk = [clrev]
650 walk = [clrev]
651 while walk:
651 while walk:
652 p = walk[0]
652 p = walk[0]
653 walk = walk[1:]
653 walk = walk[1:]
654 if p in clrevtolocalrev:
654 if p in clrevtolocalrev:
655 return clrevtolocalrev[p]
655 return clrevtolocalrev[p]
656 elif p in fullclnodes:
656 elif p in fullclnodes:
657 walk.extend([pp for pp in cl.parentrevs(p) if pp != nullrev])
657 walk.extend([pp for pp in cl.parentrevs(p) if pp != nullrev])
658 elif p in precomputedellipsis:
658 elif p in precomputedellipsis:
659 walk.extend(
659 walk.extend(
660 [pp for pp in precomputedellipsis[p] if pp != nullrev]
660 [pp for pp in precomputedellipsis[p] if pp != nullrev]
661 )
661 )
662 else:
662 else:
663 # In this case, we've got an ellipsis with parents
663 # In this case, we've got an ellipsis with parents
664 # outside the current bundle (likely an
664 # outside the current bundle (likely an
665 # incremental pull). We "know" that we can use the
665 # incremental pull). We "know" that we can use the
666 # value of this same revlog at whatever revision
666 # value of this same revlog at whatever revision
667 # is pointed to by linknode. "Know" is in scare
667 # is pointed to by linknode. "Know" is in scare
668 # quotes because I haven't done enough examination
668 # quotes because I haven't done enough examination
669 # of edge cases to convince myself this is really
669 # of edge cases to convince myself this is really
670 # a fact - it works for all the (admittedly
670 # a fact - it works for all the (admittedly
671 # thorough) cases in our testsuite, but I would be
671 # thorough) cases in our testsuite, but I would be
672 # somewhat unsurprised to find a case in the wild
672 # somewhat unsurprised to find a case in the wild
673 # where this breaks down a bit. That said, I don't
673 # where this breaks down a bit. That said, I don't
674 # know if it would hurt anything.
674 # know if it would hurt anything.
675 for i in pycompat.xrange(rev, 0, -1):
675 for i in pycompat.xrange(rev, 0, -1):
676 if store.linkrev(i) == clrev:
676 if store.linkrev(i) == clrev:
677 return i
677 return i
678 # We failed to resolve a parent for this node, so
678 # We failed to resolve a parent for this node, so
679 # we crash the changegroup construction.
679 # we crash the changegroup construction.
680 raise error.Abort(
680 raise error.Abort(
681 b'unable to resolve parent while packing %r %r'
681 b'unable to resolve parent while packing %r %r'
682 b' for changeset %r' % (store.indexfile, rev, clrev)
682 b' for changeset %r' % (store.indexfile, rev, clrev)
683 )
683 )
684
684
685 return nullrev
685 return nullrev
686
686
687 if not linkparents or (store.parentrevs(rev) == (nullrev, nullrev)):
687 if not linkparents or (store.parentrevs(rev) == (nullrev, nullrev)):
688 p1, p2 = nullrev, nullrev
688 p1, p2 = nullrev, nullrev
689 elif len(linkparents) == 1:
689 elif len(linkparents) == 1:
690 (p1,) = sorted(local(p) for p in linkparents)
690 (p1,) = sorted(local(p) for p in linkparents)
691 p2 = nullrev
691 p2 = nullrev
692 else:
692 else:
693 p1, p2 = sorted(local(p) for p in linkparents)
693 p1, p2 = sorted(local(p) for p in linkparents)
694
694
695 p1node, p2node = store.node(p1), store.node(p2)
695 p1node, p2node = store.node(p1), store.node(p2)
696
696
697 return p1node, p2node, linknode
697 return p1node, p2node, linknode
698
698
699
699
700 def deltagroup(
700 def deltagroup(
701 repo,
701 repo,
702 store,
702 store,
703 nodes,
703 nodes,
704 ischangelog,
704 ischangelog,
705 lookup,
705 lookup,
706 forcedeltaparentprev,
706 forcedeltaparentprev,
707 topic=None,
707 topic=None,
708 ellipses=False,
708 ellipses=False,
709 clrevtolocalrev=None,
709 clrevtolocalrev=None,
710 fullclnodes=None,
710 fullclnodes=None,
711 precomputedellipsis=None,
711 precomputedellipsis=None,
712 ):
712 ):
713 """Calculate deltas for a set of revisions.
713 """Calculate deltas for a set of revisions.
714
714
715 Is a generator of ``revisiondelta`` instances.
715 Is a generator of ``revisiondelta`` instances.
716
716
717 If topic is not None, progress detail will be generated using this
717 If topic is not None, progress detail will be generated using this
718 topic name (e.g. changesets, manifests, etc).
718 topic name (e.g. changesets, manifests, etc).
719 """
719 """
720 if not nodes:
720 if not nodes:
721 return
721 return
722
722
723 cl = repo.changelog
723 cl = repo.changelog
724
724
725 if ischangelog:
725 if ischangelog:
726 # `hg log` shows changesets in storage order. To preserve order
726 # `hg log` shows changesets in storage order. To preserve order
727 # across clones, send out changesets in storage order.
727 # across clones, send out changesets in storage order.
728 nodesorder = b'storage'
728 nodesorder = b'storage'
729 elif ellipses:
729 elif ellipses:
730 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
730 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
731 nodesorder = b'nodes'
731 nodesorder = b'nodes'
732 else:
732 else:
733 nodesorder = None
733 nodesorder = None
734
734
735 # Perform ellipses filtering and revision massaging. We do this before
735 # Perform ellipses filtering and revision massaging. We do this before
736 # emitrevisions() because a) filtering out revisions creates less work
736 # emitrevisions() because a) filtering out revisions creates less work
737 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
737 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
738 # assumptions about delta choices and we would possibly send a delta
738 # assumptions about delta choices and we would possibly send a delta
739 # referencing a missing base revision.
739 # referencing a missing base revision.
740 #
740 #
741 # Also, calling lookup() has side-effects with regards to populating
741 # Also, calling lookup() has side-effects with regards to populating
742 # data structures. If we don't call lookup() for each node or if we call
742 # data structures. If we don't call lookup() for each node or if we call
743 # lookup() after the first pass through each node, things can break -
743 # lookup() after the first pass through each node, things can break -
744 # possibly intermittently depending on the python hash seed! For that
744 # possibly intermittently depending on the python hash seed! For that
745 # reason, we store a mapping of all linknodes during the initial node
745 # reason, we store a mapping of all linknodes during the initial node
746 # pass rather than use lookup() on the output side.
746 # pass rather than use lookup() on the output side.
747 if ellipses:
747 if ellipses:
748 filtered = []
748 filtered = []
749 adjustedparents = {}
749 adjustedparents = {}
750 linknodes = {}
750 linknodes = {}
751
751
752 for node in nodes:
752 for node in nodes:
753 rev = store.rev(node)
753 rev = store.rev(node)
754 linknode = lookup(node)
754 linknode = lookup(node)
755 linkrev = cl.rev(linknode)
755 linkrev = cl.rev(linknode)
756 clrevtolocalrev[linkrev] = rev
756 clrevtolocalrev[linkrev] = rev
757
757
758 # If linknode is in fullclnodes, it means the corresponding
758 # If linknode is in fullclnodes, it means the corresponding
759 # changeset was a full changeset and is being sent unaltered.
759 # changeset was a full changeset and is being sent unaltered.
760 if linknode in fullclnodes:
760 if linknode in fullclnodes:
761 linknodes[node] = linknode
761 linknodes[node] = linknode
762
762
763 # If the corresponding changeset wasn't in the set computed
763 # If the corresponding changeset wasn't in the set computed
764 # as relevant to us, it should be dropped outright.
764 # as relevant to us, it should be dropped outright.
765 elif linkrev not in precomputedellipsis:
765 elif linkrev not in precomputedellipsis:
766 continue
766 continue
767
767
768 else:
768 else:
769 # We could probably do this later and avoid the dict
769 # We could probably do this later and avoid the dict
770 # holding state. But it likely doesn't matter.
770 # holding state. But it likely doesn't matter.
771 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
771 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
772 cl,
772 cl,
773 store,
773 store,
774 ischangelog,
774 ischangelog,
775 rev,
775 rev,
776 linkrev,
776 linkrev,
777 linknode,
777 linknode,
778 clrevtolocalrev,
778 clrevtolocalrev,
779 fullclnodes,
779 fullclnodes,
780 precomputedellipsis,
780 precomputedellipsis,
781 )
781 )
782
782
783 adjustedparents[node] = (p1node, p2node)
783 adjustedparents[node] = (p1node, p2node)
784 linknodes[node] = linknode
784 linknodes[node] = linknode
785
785
786 filtered.append(node)
786 filtered.append(node)
787
787
788 nodes = filtered
788 nodes = filtered
789
789
790 # We expect the first pass to be fast, so we only engage the progress
790 # We expect the first pass to be fast, so we only engage the progress
791 # meter for constructing the revision deltas.
791 # meter for constructing the revision deltas.
792 progress = None
792 progress = None
793 if topic is not None:
793 if topic is not None:
794 progress = repo.ui.makeprogress(
794 progress = repo.ui.makeprogress(
795 topic, unit=_(b'chunks'), total=len(nodes)
795 topic, unit=_(b'chunks'), total=len(nodes)
796 )
796 )
797
797
798 configtarget = repo.ui.config(b'devel', b'bundle.delta')
798 configtarget = repo.ui.config(b'devel', b'bundle.delta')
799 if configtarget not in (b'', b'p1', b'full'):
799 if configtarget not in (b'', b'p1', b'full'):
800 msg = _(b"""config "devel.bundle.delta" as unknown value: %s""")
800 msg = _(b"""config "devel.bundle.delta" as unknown value: %s""")
801 repo.ui.warn(msg % configtarget)
801 repo.ui.warn(msg % configtarget)
802
802
803 deltamode = repository.CG_DELTAMODE_STD
803 deltamode = repository.CG_DELTAMODE_STD
804 if forcedeltaparentprev:
804 if forcedeltaparentprev:
805 deltamode = repository.CG_DELTAMODE_PREV
805 deltamode = repository.CG_DELTAMODE_PREV
806 elif configtarget == b'p1':
806 elif configtarget == b'p1':
807 deltamode = repository.CG_DELTAMODE_P1
807 deltamode = repository.CG_DELTAMODE_P1
808 elif configtarget == b'full':
808 elif configtarget == b'full':
809 deltamode = repository.CG_DELTAMODE_FULL
809 deltamode = repository.CG_DELTAMODE_FULL
810
810
811 revisions = store.emitrevisions(
811 revisions = store.emitrevisions(
812 nodes,
812 nodes,
813 nodesorder=nodesorder,
813 nodesorder=nodesorder,
814 revisiondata=True,
814 revisiondata=True,
815 assumehaveparentrevisions=not ellipses,
815 assumehaveparentrevisions=not ellipses,
816 deltamode=deltamode,
816 deltamode=deltamode,
817 )
817 )
818
818
819 for i, revision in enumerate(revisions):
819 for i, revision in enumerate(revisions):
820 if progress:
820 if progress:
821 progress.update(i + 1)
821 progress.update(i + 1)
822
822
823 if ellipses:
823 if ellipses:
824 linknode = linknodes[revision.node]
824 linknode = linknodes[revision.node]
825
825
826 if revision.node in adjustedparents:
826 if revision.node in adjustedparents:
827 p1node, p2node = adjustedparents[revision.node]
827 p1node, p2node = adjustedparents[revision.node]
828 revision.p1node = p1node
828 revision.p1node = p1node
829 revision.p2node = p2node
829 revision.p2node = p2node
830 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
830 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
831
831
832 else:
832 else:
833 linknode = lookup(revision.node)
833 linknode = lookup(revision.node)
834
834
835 revision.linknode = linknode
835 revision.linknode = linknode
836 yield revision
836 yield revision
837
837
838 if progress:
838 if progress:
839 progress.complete()
839 progress.complete()
840
840
841
841
842 class cgpacker(object):
842 class cgpacker(object):
843 def __init__(
843 def __init__(
844 self,
844 self,
845 repo,
845 repo,
846 oldmatcher,
846 oldmatcher,
847 matcher,
847 matcher,
848 version,
848 version,
849 builddeltaheader,
849 builddeltaheader,
850 manifestsend,
850 manifestsend,
851 forcedeltaparentprev=False,
851 forcedeltaparentprev=False,
852 bundlecaps=None,
852 bundlecaps=None,
853 ellipses=False,
853 ellipses=False,
854 shallow=False,
854 shallow=False,
855 ellipsisroots=None,
855 ellipsisroots=None,
856 fullnodes=None,
856 fullnodes=None,
857 ):
857 ):
858 """Given a source repo, construct a bundler.
858 """Given a source repo, construct a bundler.
859
859
860 oldmatcher is a matcher that matches on files the client already has.
860 oldmatcher is a matcher that matches on files the client already has.
861 These will not be included in the changegroup.
861 These will not be included in the changegroup.
862
862
863 matcher is a matcher that matches on files to include in the
863 matcher is a matcher that matches on files to include in the
864 changegroup. Used to facilitate sparse changegroups.
864 changegroup. Used to facilitate sparse changegroups.
865
865
866 forcedeltaparentprev indicates whether delta parents must be against
866 forcedeltaparentprev indicates whether delta parents must be against
867 the previous revision in a delta group. This should only be used for
867 the previous revision in a delta group. This should only be used for
868 compatibility with changegroup version 1.
868 compatibility with changegroup version 1.
869
869
870 builddeltaheader is a callable that constructs the header for a group
870 builddeltaheader is a callable that constructs the header for a group
871 delta.
871 delta.
872
872
873 manifestsend is a chunk to send after manifests have been fully emitted.
873 manifestsend is a chunk to send after manifests have been fully emitted.
874
874
875 ellipses indicates whether ellipsis serving mode is enabled.
875 ellipses indicates whether ellipsis serving mode is enabled.
876
876
877 bundlecaps is optional and can be used to specify the set of
877 bundlecaps is optional and can be used to specify the set of
878 capabilities which can be used to build the bundle. While bundlecaps is
878 capabilities which can be used to build the bundle. While bundlecaps is
879 unused in core Mercurial, extensions rely on this feature to communicate
879 unused in core Mercurial, extensions rely on this feature to communicate
880 capabilities to customize the changegroup packer.
880 capabilities to customize the changegroup packer.
881
881
882 shallow indicates whether shallow data might be sent. The packer may
882 shallow indicates whether shallow data might be sent. The packer may
883 need to pack file contents not introduced by the changes being packed.
883 need to pack file contents not introduced by the changes being packed.
884
884
885 fullnodes is the set of changelog nodes which should not be ellipsis
885 fullnodes is the set of changelog nodes which should not be ellipsis
886 nodes. We store this rather than the set of nodes that should be
886 nodes. We store this rather than the set of nodes that should be
887 ellipsis because for very large histories we expect this to be
887 ellipsis because for very large histories we expect this to be
888 significantly smaller.
888 significantly smaller.
889 """
889 """
890 assert oldmatcher
890 assert oldmatcher
891 assert matcher
891 assert matcher
892 self._oldmatcher = oldmatcher
892 self._oldmatcher = oldmatcher
893 self._matcher = matcher
893 self._matcher = matcher
894
894
895 self.version = version
895 self.version = version
896 self._forcedeltaparentprev = forcedeltaparentprev
896 self._forcedeltaparentprev = forcedeltaparentprev
897 self._builddeltaheader = builddeltaheader
897 self._builddeltaheader = builddeltaheader
898 self._manifestsend = manifestsend
898 self._manifestsend = manifestsend
899 self._ellipses = ellipses
899 self._ellipses = ellipses
900
900
901 # Set of capabilities we can use to build the bundle.
901 # Set of capabilities we can use to build the bundle.
902 if bundlecaps is None:
902 if bundlecaps is None:
903 bundlecaps = set()
903 bundlecaps = set()
904 self._bundlecaps = bundlecaps
904 self._bundlecaps = bundlecaps
905 self._isshallow = shallow
905 self._isshallow = shallow
906 self._fullclnodes = fullnodes
906 self._fullclnodes = fullnodes
907
907
908 # Maps ellipsis revs to their roots at the changelog level.
908 # Maps ellipsis revs to their roots at the changelog level.
909 self._precomputedellipsis = ellipsisroots
909 self._precomputedellipsis = ellipsisroots
910
910
911 self._repo = repo
911 self._repo = repo
912
912
913 if self._repo.ui.verbose and not self._repo.ui.debugflag:
913 if self._repo.ui.verbose and not self._repo.ui.debugflag:
914 self._verbosenote = self._repo.ui.note
914 self._verbosenote = self._repo.ui.note
915 else:
915 else:
916 self._verbosenote = lambda s: None
916 self._verbosenote = lambda s: None
917
917
918 def generate(
918 def generate(
919 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True
919 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True
920 ):
920 ):
921 """Yield a sequence of changegroup byte chunks.
921 """Yield a sequence of changegroup byte chunks.
922 If changelog is False, changelog data won't be added to changegroup
922 If changelog is False, changelog data won't be added to changegroup
923 """
923 """
924
924
925 repo = self._repo
925 repo = self._repo
926 cl = repo.changelog
926 cl = repo.changelog
927
927
928 self._verbosenote(_(b'uncompressed size of bundle content:\n'))
928 self._verbosenote(_(b'uncompressed size of bundle content:\n'))
929 size = 0
929 size = 0
930
930
931 clstate, deltas = self._generatechangelog(
931 clstate, deltas = self._generatechangelog(
932 cl, clnodes, generate=changelog
932 cl, clnodes, generate=changelog
933 )
933 )
934 for delta in deltas:
934 for delta in deltas:
935 for chunk in _revisiondeltatochunks(delta, self._builddeltaheader):
935 for chunk in _revisiondeltatochunks(delta, self._builddeltaheader):
936 size += len(chunk)
936 size += len(chunk)
937 yield chunk
937 yield chunk
938
938
939 close = closechunk()
939 close = closechunk()
940 size += len(close)
940 size += len(close)
941 yield closechunk()
941 yield closechunk()
942
942
943 self._verbosenote(_(b'%8.i (changelog)\n') % size)
943 self._verbosenote(_(b'%8.i (changelog)\n') % size)
944
944
945 clrevorder = clstate[b'clrevorder']
945 clrevorder = clstate[b'clrevorder']
946 manifests = clstate[b'manifests']
946 manifests = clstate[b'manifests']
947 changedfiles = clstate[b'changedfiles']
947 changedfiles = clstate[b'changedfiles']
948
948
949 # We need to make sure that the linkrev in the changegroup refers to
949 # We need to make sure that the linkrev in the changegroup refers to
950 # the first changeset that introduced the manifest or file revision.
950 # the first changeset that introduced the manifest or file revision.
951 # The fastpath is usually safer than the slowpath, because the filelogs
951 # The fastpath is usually safer than the slowpath, because the filelogs
952 # are walked in revlog order.
952 # are walked in revlog order.
953 #
953 #
954 # When taking the slowpath when the manifest revlog uses generaldelta,
954 # When taking the slowpath when the manifest revlog uses generaldelta,
955 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
955 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
956 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
956 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
957 #
957 #
958 # When taking the fastpath, we are only vulnerable to reordering
958 # When taking the fastpath, we are only vulnerable to reordering
959 # of the changelog itself. The changelog never uses generaldelta and is
959 # of the changelog itself. The changelog never uses generaldelta and is
960 # never reordered. To handle this case, we simply take the slowpath,
960 # never reordered. To handle this case, we simply take the slowpath,
961 # which already has the 'clrevorder' logic. This was also fixed in
961 # which already has the 'clrevorder' logic. This was also fixed in
962 # cc0ff93d0c0c.
962 # cc0ff93d0c0c.
963
963
964 # Treemanifests don't work correctly with fastpathlinkrev
964 # Treemanifests don't work correctly with fastpathlinkrev
965 # either, because we don't discover which directory nodes to
965 # either, because we don't discover which directory nodes to
966 # send along with files. This could probably be fixed.
966 # send along with files. This could probably be fixed.
967 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo)
967 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo)
968
968
969 fnodes = {} # needed file nodes
969 fnodes = {} # needed file nodes
970
970
971 size = 0
971 size = 0
972 it = self.generatemanifests(
972 it = self.generatemanifests(
973 commonrevs,
973 commonrevs,
974 clrevorder,
974 clrevorder,
975 fastpathlinkrev,
975 fastpathlinkrev,
976 manifests,
976 manifests,
977 fnodes,
977 fnodes,
978 source,
978 source,
979 clstate[b'clrevtomanifestrev'],
979 clstate[b'clrevtomanifestrev'],
980 )
980 )
981
981
982 for tree, deltas in it:
982 for tree, deltas in it:
983 if tree:
983 if tree:
984 assert self.version == b'03'
984 assert self.version == b'03'
985 chunk = _fileheader(tree)
985 chunk = _fileheader(tree)
986 size += len(chunk)
986 size += len(chunk)
987 yield chunk
987 yield chunk
988
988
989 for delta in deltas:
989 for delta in deltas:
990 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
990 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
991 for chunk in chunks:
991 for chunk in chunks:
992 size += len(chunk)
992 size += len(chunk)
993 yield chunk
993 yield chunk
994
994
995 close = closechunk()
995 close = closechunk()
996 size += len(close)
996 size += len(close)
997 yield close
997 yield close
998
998
999 self._verbosenote(_(b'%8.i (manifests)\n') % size)
999 self._verbosenote(_(b'%8.i (manifests)\n') % size)
1000 yield self._manifestsend
1000 yield self._manifestsend
1001
1001
1002 mfdicts = None
1002 mfdicts = None
1003 if self._ellipses and self._isshallow:
1003 if self._ellipses and self._isshallow:
1004 mfdicts = [
1004 mfdicts = [
1005 (self._repo.manifestlog[n].read(), lr)
1005 (self._repo.manifestlog[n].read(), lr)
1006 for (n, lr) in pycompat.iteritems(manifests)
1006 for (n, lr) in pycompat.iteritems(manifests)
1007 ]
1007 ]
1008
1008
1009 manifests.clear()
1009 manifests.clear()
1010 clrevs = {cl.rev(x) for x in clnodes}
1010 clrevs = {cl.rev(x) for x in clnodes}
1011
1011
1012 it = self.generatefiles(
1012 it = self.generatefiles(
1013 changedfiles,
1013 changedfiles,
1014 commonrevs,
1014 commonrevs,
1015 source,
1015 source,
1016 mfdicts,
1016 mfdicts,
1017 fastpathlinkrev,
1017 fastpathlinkrev,
1018 fnodes,
1018 fnodes,
1019 clrevs,
1019 clrevs,
1020 )
1020 )
1021
1021
1022 for path, deltas in it:
1022 for path, deltas in it:
1023 h = _fileheader(path)
1023 h = _fileheader(path)
1024 size = len(h)
1024 size = len(h)
1025 yield h
1025 yield h
1026
1026
1027 for delta in deltas:
1027 for delta in deltas:
1028 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
1028 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
1029 for chunk in chunks:
1029 for chunk in chunks:
1030 size += len(chunk)
1030 size += len(chunk)
1031 yield chunk
1031 yield chunk
1032
1032
1033 close = closechunk()
1033 close = closechunk()
1034 size += len(close)
1034 size += len(close)
1035 yield close
1035 yield close
1036
1036
1037 self._verbosenote(_(b'%8.i %s\n') % (size, path))
1037 self._verbosenote(_(b'%8.i %s\n') % (size, path))
1038
1038
1039 yield closechunk()
1039 yield closechunk()
1040
1040
1041 if clnodes:
1041 if clnodes:
1042 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source)
1042 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source)
1043
1043
1044 def _generatechangelog(self, cl, nodes, generate=True):
1044 def _generatechangelog(self, cl, nodes, generate=True):
1045 """Generate data for changelog chunks.
1045 """Generate data for changelog chunks.
1046
1046
1047 Returns a 2-tuple of a dict containing state and an iterable of
1047 Returns a 2-tuple of a dict containing state and an iterable of
1048 byte chunks. The state will not be fully populated until the
1048 byte chunks. The state will not be fully populated until the
1049 chunk stream has been fully consumed.
1049 chunk stream has been fully consumed.
1050
1050
1051 if generate is False, the state will be fully populated and no chunk
1051 if generate is False, the state will be fully populated and no chunk
1052 stream will be yielded
1052 stream will be yielded
1053 """
1053 """
1054 clrevorder = {}
1054 clrevorder = {}
1055 manifests = {}
1055 manifests = {}
1056 mfl = self._repo.manifestlog
1056 mfl = self._repo.manifestlog
1057 changedfiles = set()
1057 changedfiles = set()
1058 clrevtomanifestrev = {}
1058 clrevtomanifestrev = {}
1059
1059
1060 state = {
1060 state = {
1061 b'clrevorder': clrevorder,
1061 b'clrevorder': clrevorder,
1062 b'manifests': manifests,
1062 b'manifests': manifests,
1063 b'changedfiles': changedfiles,
1063 b'changedfiles': changedfiles,
1064 b'clrevtomanifestrev': clrevtomanifestrev,
1064 b'clrevtomanifestrev': clrevtomanifestrev,
1065 }
1065 }
1066
1066
1067 if not (generate or self._ellipses):
1067 if not (generate or self._ellipses):
1068 # sort the nodes in storage order
1068 # sort the nodes in storage order
1069 nodes = sorted(nodes, key=cl.rev)
1069 nodes = sorted(nodes, key=cl.rev)
1070 for node in nodes:
1070 for node in nodes:
1071 c = cl.changelogrevision(node)
1071 c = cl.changelogrevision(node)
1072 clrevorder[node] = len(clrevorder)
1072 clrevorder[node] = len(clrevorder)
1073 # record the first changeset introducing this manifest version
1073 # record the first changeset introducing this manifest version
1074 manifests.setdefault(c.manifest, node)
1074 manifests.setdefault(c.manifest, node)
1075 # Record a complete list of potentially-changed files in
1075 # Record a complete list of potentially-changed files in
1076 # this manifest.
1076 # this manifest.
1077 changedfiles.update(c.files)
1077 changedfiles.update(c.files)
1078
1078
1079 return state, ()
1079 return state, ()
1080
1080
1081 # Callback for the changelog, used to collect changed files and
1081 # Callback for the changelog, used to collect changed files and
1082 # manifest nodes.
1082 # manifest nodes.
1083 # Returns the linkrev node (identity in the changelog case).
1083 # Returns the linkrev node (identity in the changelog case).
1084 def lookupcl(x):
1084 def lookupcl(x):
1085 c = cl.changelogrevision(x)
1085 c = cl.changelogrevision(x)
1086 clrevorder[x] = len(clrevorder)
1086 clrevorder[x] = len(clrevorder)
1087
1087
1088 if self._ellipses:
1088 if self._ellipses:
1089 # Only update manifests if x is going to be sent. Otherwise we
1089 # Only update manifests if x is going to be sent. Otherwise we
1090 # end up with bogus linkrevs specified for manifests and
1090 # end up with bogus linkrevs specified for manifests and
1091 # we skip some manifest nodes that we should otherwise
1091 # we skip some manifest nodes that we should otherwise
1092 # have sent.
1092 # have sent.
1093 if (
1093 if (
1094 x in self._fullclnodes
1094 x in self._fullclnodes
1095 or cl.rev(x) in self._precomputedellipsis
1095 or cl.rev(x) in self._precomputedellipsis
1096 ):
1096 ):
1097
1097
1098 manifestnode = c.manifest
1098 manifestnode = c.manifest
1099 # Record the first changeset introducing this manifest
1099 # Record the first changeset introducing this manifest
1100 # version.
1100 # version.
1101 manifests.setdefault(manifestnode, x)
1101 manifests.setdefault(manifestnode, x)
1102 # Set this narrow-specific dict so we have the lowest
1102 # Set this narrow-specific dict so we have the lowest
1103 # manifest revnum to look up for this cl revnum. (Part of
1103 # manifest revnum to look up for this cl revnum. (Part of
1104 # mapping changelog ellipsis parents to manifest ellipsis
1104 # mapping changelog ellipsis parents to manifest ellipsis
1105 # parents)
1105 # parents)
1106 clrevtomanifestrev.setdefault(
1106 clrevtomanifestrev.setdefault(
1107 cl.rev(x), mfl.rev(manifestnode)
1107 cl.rev(x), mfl.rev(manifestnode)
1108 )
1108 )
1109 # We can't trust the changed files list in the changeset if the
1109 # We can't trust the changed files list in the changeset if the
1110 # client requested a shallow clone.
1110 # client requested a shallow clone.
1111 if self._isshallow:
1111 if self._isshallow:
1112 changedfiles.update(mfl[c.manifest].read().keys())
1112 changedfiles.update(mfl[c.manifest].read().keys())
1113 else:
1113 else:
1114 changedfiles.update(c.files)
1114 changedfiles.update(c.files)
1115 else:
1115 else:
1116 # record the first changeset introducing this manifest version
1116 # record the first changeset introducing this manifest version
1117 manifests.setdefault(c.manifest, x)
1117 manifests.setdefault(c.manifest, x)
1118 # Record a complete list of potentially-changed files in
1118 # Record a complete list of potentially-changed files in
1119 # this manifest.
1119 # this manifest.
1120 changedfiles.update(c.files)
1120 changedfiles.update(c.files)
1121
1121
1122 return x
1122 return x
1123
1123
1124 gen = deltagroup(
1124 gen = deltagroup(
1125 self._repo,
1125 self._repo,
1126 cl,
1126 cl,
1127 nodes,
1127 nodes,
1128 True,
1128 True,
1129 lookupcl,
1129 lookupcl,
1130 self._forcedeltaparentprev,
1130 self._forcedeltaparentprev,
1131 ellipses=self._ellipses,
1131 ellipses=self._ellipses,
1132 topic=_(b'changesets'),
1132 topic=_(b'changesets'),
1133 clrevtolocalrev={},
1133 clrevtolocalrev={},
1134 fullclnodes=self._fullclnodes,
1134 fullclnodes=self._fullclnodes,
1135 precomputedellipsis=self._precomputedellipsis,
1135 precomputedellipsis=self._precomputedellipsis,
1136 )
1136 )
1137
1137
1138 return state, gen
1138 return state, gen
1139
1139
1140 def generatemanifests(
1140 def generatemanifests(
1141 self,
1141 self,
1142 commonrevs,
1142 commonrevs,
1143 clrevorder,
1143 clrevorder,
1144 fastpathlinkrev,
1144 fastpathlinkrev,
1145 manifests,
1145 manifests,
1146 fnodes,
1146 fnodes,
1147 source,
1147 source,
1148 clrevtolocalrev,
1148 clrevtolocalrev,
1149 ):
1149 ):
1150 """Returns an iterator of changegroup chunks containing manifests.
1150 """Returns an iterator of changegroup chunks containing manifests.
1151
1151
1152 `source` is unused here, but is used by extensions like remotefilelog to
1152 `source` is unused here, but is used by extensions like remotefilelog to
1153 change what is sent based in pulls vs pushes, etc.
1153 change what is sent based in pulls vs pushes, etc.
1154 """
1154 """
1155 repo = self._repo
1155 repo = self._repo
1156 mfl = repo.manifestlog
1156 mfl = repo.manifestlog
1157 tmfnodes = {b'': manifests}
1157 tmfnodes = {b'': manifests}
1158
1158
1159 # Callback for the manifest, used to collect linkrevs for filelog
1159 # Callback for the manifest, used to collect linkrevs for filelog
1160 # revisions.
1160 # revisions.
1161 # Returns the linkrev node (collected in lookupcl).
1161 # Returns the linkrev node (collected in lookupcl).
1162 def makelookupmflinknode(tree, nodes):
1162 def makelookupmflinknode(tree, nodes):
1163 if fastpathlinkrev:
1163 if fastpathlinkrev:
1164 assert not tree
1164 assert not tree
1165 return (
1165
1166 manifests.__getitem__
1166 # pytype: disable=unsupported-operands
1167 ) # pytype: disable=unsupported-operands
1167 return manifests.__getitem__
1168 # pytype: enable=unsupported-operands
1168
1169
1169 def lookupmflinknode(x):
1170 def lookupmflinknode(x):
1170 """Callback for looking up the linknode for manifests.
1171 """Callback for looking up the linknode for manifests.
1171
1172
1172 Returns the linkrev node for the specified manifest.
1173 Returns the linkrev node for the specified manifest.
1173
1174
1174 SIDE EFFECT:
1175 SIDE EFFECT:
1175
1176
1176 1) fclnodes gets populated with the list of relevant
1177 1) fclnodes gets populated with the list of relevant
1177 file nodes if we're not using fastpathlinkrev
1178 file nodes if we're not using fastpathlinkrev
1178 2) When treemanifests are in use, collects treemanifest nodes
1179 2) When treemanifests are in use, collects treemanifest nodes
1179 to send
1180 to send
1180
1181
1181 Note that this means manifests must be completely sent to
1182 Note that this means manifests must be completely sent to
1182 the client before you can trust the list of files and
1183 the client before you can trust the list of files and
1183 treemanifests to send.
1184 treemanifests to send.
1184 """
1185 """
1185 clnode = nodes[x]
1186 clnode = nodes[x]
1186 mdata = mfl.get(tree, x).readfast(shallow=True)
1187 mdata = mfl.get(tree, x).readfast(shallow=True)
1187 for p, n, fl in mdata.iterentries():
1188 for p, n, fl in mdata.iterentries():
1188 if fl == b't': # subdirectory manifest
1189 if fl == b't': # subdirectory manifest
1189 subtree = tree + p + b'/'
1190 subtree = tree + p + b'/'
1190 tmfclnodes = tmfnodes.setdefault(subtree, {})
1191 tmfclnodes = tmfnodes.setdefault(subtree, {})
1191 tmfclnode = tmfclnodes.setdefault(n, clnode)
1192 tmfclnode = tmfclnodes.setdefault(n, clnode)
1192 if clrevorder[clnode] < clrevorder[tmfclnode]:
1193 if clrevorder[clnode] < clrevorder[tmfclnode]:
1193 tmfclnodes[n] = clnode
1194 tmfclnodes[n] = clnode
1194 else:
1195 else:
1195 f = tree + p
1196 f = tree + p
1196 fclnodes = fnodes.setdefault(f, {})
1197 fclnodes = fnodes.setdefault(f, {})
1197 fclnode = fclnodes.setdefault(n, clnode)
1198 fclnode = fclnodes.setdefault(n, clnode)
1198 if clrevorder[clnode] < clrevorder[fclnode]:
1199 if clrevorder[clnode] < clrevorder[fclnode]:
1199 fclnodes[n] = clnode
1200 fclnodes[n] = clnode
1200 return clnode
1201 return clnode
1201
1202
1202 return lookupmflinknode
1203 return lookupmflinknode
1203
1204
1204 while tmfnodes:
1205 while tmfnodes:
1205 tree, nodes = tmfnodes.popitem()
1206 tree, nodes = tmfnodes.popitem()
1206
1207
1207 should_visit = self._matcher.visitdir(tree[:-1])
1208 should_visit = self._matcher.visitdir(tree[:-1])
1208 if tree and not should_visit:
1209 if tree and not should_visit:
1209 continue
1210 continue
1210
1211
1211 store = mfl.getstorage(tree)
1212 store = mfl.getstorage(tree)
1212
1213
1213 if not should_visit:
1214 if not should_visit:
1214 # No nodes to send because this directory is out of
1215 # No nodes to send because this directory is out of
1215 # the client's view of the repository (probably
1216 # the client's view of the repository (probably
1216 # because of narrow clones). Do this even for the root
1217 # because of narrow clones). Do this even for the root
1217 # directory (tree=='')
1218 # directory (tree=='')
1218 prunednodes = []
1219 prunednodes = []
1219 else:
1220 else:
1220 # Avoid sending any manifest nodes we can prove the
1221 # Avoid sending any manifest nodes we can prove the
1221 # client already has by checking linkrevs. See the
1222 # client already has by checking linkrevs. See the
1222 # related comment in generatefiles().
1223 # related comment in generatefiles().
1223 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1224 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1224
1225
1225 if tree and not prunednodes:
1226 if tree and not prunednodes:
1226 continue
1227 continue
1227
1228
1228 lookupfn = makelookupmflinknode(tree, nodes)
1229 lookupfn = makelookupmflinknode(tree, nodes)
1229
1230
1230 deltas = deltagroup(
1231 deltas = deltagroup(
1231 self._repo,
1232 self._repo,
1232 store,
1233 store,
1233 prunednodes,
1234 prunednodes,
1234 False,
1235 False,
1235 lookupfn,
1236 lookupfn,
1236 self._forcedeltaparentprev,
1237 self._forcedeltaparentprev,
1237 ellipses=self._ellipses,
1238 ellipses=self._ellipses,
1238 topic=_(b'manifests'),
1239 topic=_(b'manifests'),
1239 clrevtolocalrev=clrevtolocalrev,
1240 clrevtolocalrev=clrevtolocalrev,
1240 fullclnodes=self._fullclnodes,
1241 fullclnodes=self._fullclnodes,
1241 precomputedellipsis=self._precomputedellipsis,
1242 precomputedellipsis=self._precomputedellipsis,
1242 )
1243 )
1243
1244
1244 if not self._oldmatcher.visitdir(store.tree[:-1]):
1245 if not self._oldmatcher.visitdir(store.tree[:-1]):
1245 yield tree, deltas
1246 yield tree, deltas
1246 else:
1247 else:
1247 # 'deltas' is a generator and we need to consume it even if
1248 # 'deltas' is a generator and we need to consume it even if
1248 # we are not going to send it because a side-effect is that
1249 # we are not going to send it because a side-effect is that
1249 # it updates tmdnodes (via lookupfn)
1250 # it updates tmdnodes (via lookupfn)
1250 for d in deltas:
1251 for d in deltas:
1251 pass
1252 pass
1252 if not tree:
1253 if not tree:
1253 yield tree, []
1254 yield tree, []
1254
1255
1255 def _prunemanifests(self, store, nodes, commonrevs):
1256 def _prunemanifests(self, store, nodes, commonrevs):
1256 if not self._ellipses:
1257 if not self._ellipses:
1257 # In non-ellipses case and large repositories, it is better to
1258 # In non-ellipses case and large repositories, it is better to
1258 # prevent calling of store.rev and store.linkrev on a lot of
1259 # prevent calling of store.rev and store.linkrev on a lot of
1259 # nodes as compared to sending some extra data
1260 # nodes as compared to sending some extra data
1260 return nodes.copy()
1261 return nodes.copy()
1261 # This is split out as a separate method to allow filtering
1262 # This is split out as a separate method to allow filtering
1262 # commonrevs in extension code.
1263 # commonrevs in extension code.
1263 #
1264 #
1264 # TODO(augie): this shouldn't be required, instead we should
1265 # TODO(augie): this shouldn't be required, instead we should
1265 # make filtering of revisions to send delegated to the store
1266 # make filtering of revisions to send delegated to the store
1266 # layer.
1267 # layer.
1267 frev, flr = store.rev, store.linkrev
1268 frev, flr = store.rev, store.linkrev
1268 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1269 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1269
1270
1270 # The 'source' parameter is useful for extensions
1271 # The 'source' parameter is useful for extensions
1271 def generatefiles(
1272 def generatefiles(
1272 self,
1273 self,
1273 changedfiles,
1274 changedfiles,
1274 commonrevs,
1275 commonrevs,
1275 source,
1276 source,
1276 mfdicts,
1277 mfdicts,
1277 fastpathlinkrev,
1278 fastpathlinkrev,
1278 fnodes,
1279 fnodes,
1279 clrevs,
1280 clrevs,
1280 ):
1281 ):
1281 changedfiles = [
1282 changedfiles = [
1282 f
1283 f
1283 for f in changedfiles
1284 for f in changedfiles
1284 if self._matcher(f) and not self._oldmatcher(f)
1285 if self._matcher(f) and not self._oldmatcher(f)
1285 ]
1286 ]
1286
1287
1287 if not fastpathlinkrev:
1288 if not fastpathlinkrev:
1288
1289
1289 def normallinknodes(unused, fname):
1290 def normallinknodes(unused, fname):
1290 return fnodes.get(fname, {})
1291 return fnodes.get(fname, {})
1291
1292
1292 else:
1293 else:
1293 cln = self._repo.changelog.node
1294 cln = self._repo.changelog.node
1294
1295
1295 def normallinknodes(store, fname):
1296 def normallinknodes(store, fname):
1296 flinkrev = store.linkrev
1297 flinkrev = store.linkrev
1297 fnode = store.node
1298 fnode = store.node
1298 revs = ((r, flinkrev(r)) for r in store)
1299 revs = ((r, flinkrev(r)) for r in store)
1299 return {fnode(r): cln(lr) for r, lr in revs if lr in clrevs}
1300 return {fnode(r): cln(lr) for r, lr in revs if lr in clrevs}
1300
1301
1301 clrevtolocalrev = {}
1302 clrevtolocalrev = {}
1302
1303
1303 if self._isshallow:
1304 if self._isshallow:
1304 # In a shallow clone, the linknodes callback needs to also include
1305 # In a shallow clone, the linknodes callback needs to also include
1305 # those file nodes that are in the manifests we sent but weren't
1306 # those file nodes that are in the manifests we sent but weren't
1306 # introduced by those manifests.
1307 # introduced by those manifests.
1307 commonctxs = [self._repo[c] for c in commonrevs]
1308 commonctxs = [self._repo[c] for c in commonrevs]
1308 clrev = self._repo.changelog.rev
1309 clrev = self._repo.changelog.rev
1309
1310
1310 def linknodes(flog, fname):
1311 def linknodes(flog, fname):
1311 for c in commonctxs:
1312 for c in commonctxs:
1312 try:
1313 try:
1313 fnode = c.filenode(fname)
1314 fnode = c.filenode(fname)
1314 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1315 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1315 except error.ManifestLookupError:
1316 except error.ManifestLookupError:
1316 pass
1317 pass
1317 links = normallinknodes(flog, fname)
1318 links = normallinknodes(flog, fname)
1318 if len(links) != len(mfdicts):
1319 if len(links) != len(mfdicts):
1319 for mf, lr in mfdicts:
1320 for mf, lr in mfdicts:
1320 fnode = mf.get(fname, None)
1321 fnode = mf.get(fname, None)
1321 if fnode in links:
1322 if fnode in links:
1322 links[fnode] = min(links[fnode], lr, key=clrev)
1323 links[fnode] = min(links[fnode], lr, key=clrev)
1323 elif fnode:
1324 elif fnode:
1324 links[fnode] = lr
1325 links[fnode] = lr
1325 return links
1326 return links
1326
1327
1327 else:
1328 else:
1328 linknodes = normallinknodes
1329 linknodes = normallinknodes
1329
1330
1330 repo = self._repo
1331 repo = self._repo
1331 progress = repo.ui.makeprogress(
1332 progress = repo.ui.makeprogress(
1332 _(b'files'), unit=_(b'files'), total=len(changedfiles)
1333 _(b'files'), unit=_(b'files'), total=len(changedfiles)
1333 )
1334 )
1334 for i, fname in enumerate(sorted(changedfiles)):
1335 for i, fname in enumerate(sorted(changedfiles)):
1335 filerevlog = repo.file(fname)
1336 filerevlog = repo.file(fname)
1336 if not filerevlog:
1337 if not filerevlog:
1337 raise error.Abort(
1338 raise error.Abort(
1338 _(b"empty or missing file data for %s") % fname
1339 _(b"empty or missing file data for %s") % fname
1339 )
1340 )
1340
1341
1341 clrevtolocalrev.clear()
1342 clrevtolocalrev.clear()
1342
1343
1343 linkrevnodes = linknodes(filerevlog, fname)
1344 linkrevnodes = linknodes(filerevlog, fname)
1344 # Lookup for filenodes, we collected the linkrev nodes above in the
1345 # Lookup for filenodes, we collected the linkrev nodes above in the
1345 # fastpath case and with lookupmf in the slowpath case.
1346 # fastpath case and with lookupmf in the slowpath case.
1346 def lookupfilelog(x):
1347 def lookupfilelog(x):
1347 return linkrevnodes[x]
1348 return linkrevnodes[x]
1348
1349
1349 frev, flr = filerevlog.rev, filerevlog.linkrev
1350 frev, flr = filerevlog.rev, filerevlog.linkrev
1350 # Skip sending any filenode we know the client already
1351 # Skip sending any filenode we know the client already
1351 # has. This avoids over-sending files relatively
1352 # has. This avoids over-sending files relatively
1352 # inexpensively, so it's not a problem if we under-filter
1353 # inexpensively, so it's not a problem if we under-filter
1353 # here.
1354 # here.
1354 filenodes = [
1355 filenodes = [
1355 n for n in linkrevnodes if flr(frev(n)) not in commonrevs
1356 n for n in linkrevnodes if flr(frev(n)) not in commonrevs
1356 ]
1357 ]
1357
1358
1358 if not filenodes:
1359 if not filenodes:
1359 continue
1360 continue
1360
1361
1361 progress.update(i + 1, item=fname)
1362 progress.update(i + 1, item=fname)
1362
1363
1363 deltas = deltagroup(
1364 deltas = deltagroup(
1364 self._repo,
1365 self._repo,
1365 filerevlog,
1366 filerevlog,
1366 filenodes,
1367 filenodes,
1367 False,
1368 False,
1368 lookupfilelog,
1369 lookupfilelog,
1369 self._forcedeltaparentprev,
1370 self._forcedeltaparentprev,
1370 ellipses=self._ellipses,
1371 ellipses=self._ellipses,
1371 clrevtolocalrev=clrevtolocalrev,
1372 clrevtolocalrev=clrevtolocalrev,
1372 fullclnodes=self._fullclnodes,
1373 fullclnodes=self._fullclnodes,
1373 precomputedellipsis=self._precomputedellipsis,
1374 precomputedellipsis=self._precomputedellipsis,
1374 )
1375 )
1375
1376
1376 yield fname, deltas
1377 yield fname, deltas
1377
1378
1378 progress.complete()
1379 progress.complete()
1379
1380
1380
1381
1381 def _makecg1packer(
1382 def _makecg1packer(
1382 repo,
1383 repo,
1383 oldmatcher,
1384 oldmatcher,
1384 matcher,
1385 matcher,
1385 bundlecaps,
1386 bundlecaps,
1386 ellipses=False,
1387 ellipses=False,
1387 shallow=False,
1388 shallow=False,
1388 ellipsisroots=None,
1389 ellipsisroots=None,
1389 fullnodes=None,
1390 fullnodes=None,
1390 ):
1391 ):
1391 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1392 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1392 d.node, d.p1node, d.p2node, d.linknode
1393 d.node, d.p1node, d.p2node, d.linknode
1393 )
1394 )
1394
1395
1395 return cgpacker(
1396 return cgpacker(
1396 repo,
1397 repo,
1397 oldmatcher,
1398 oldmatcher,
1398 matcher,
1399 matcher,
1399 b'01',
1400 b'01',
1400 builddeltaheader=builddeltaheader,
1401 builddeltaheader=builddeltaheader,
1401 manifestsend=b'',
1402 manifestsend=b'',
1402 forcedeltaparentprev=True,
1403 forcedeltaparentprev=True,
1403 bundlecaps=bundlecaps,
1404 bundlecaps=bundlecaps,
1404 ellipses=ellipses,
1405 ellipses=ellipses,
1405 shallow=shallow,
1406 shallow=shallow,
1406 ellipsisroots=ellipsisroots,
1407 ellipsisroots=ellipsisroots,
1407 fullnodes=fullnodes,
1408 fullnodes=fullnodes,
1408 )
1409 )
1409
1410
1410
1411
1411 def _makecg2packer(
1412 def _makecg2packer(
1412 repo,
1413 repo,
1413 oldmatcher,
1414 oldmatcher,
1414 matcher,
1415 matcher,
1415 bundlecaps,
1416 bundlecaps,
1416 ellipses=False,
1417 ellipses=False,
1417 shallow=False,
1418 shallow=False,
1418 ellipsisroots=None,
1419 ellipsisroots=None,
1419 fullnodes=None,
1420 fullnodes=None,
1420 ):
1421 ):
1421 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1422 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1422 d.node, d.p1node, d.p2node, d.basenode, d.linknode
1423 d.node, d.p1node, d.p2node, d.basenode, d.linknode
1423 )
1424 )
1424
1425
1425 return cgpacker(
1426 return cgpacker(
1426 repo,
1427 repo,
1427 oldmatcher,
1428 oldmatcher,
1428 matcher,
1429 matcher,
1429 b'02',
1430 b'02',
1430 builddeltaheader=builddeltaheader,
1431 builddeltaheader=builddeltaheader,
1431 manifestsend=b'',
1432 manifestsend=b'',
1432 bundlecaps=bundlecaps,
1433 bundlecaps=bundlecaps,
1433 ellipses=ellipses,
1434 ellipses=ellipses,
1434 shallow=shallow,
1435 shallow=shallow,
1435 ellipsisroots=ellipsisroots,
1436 ellipsisroots=ellipsisroots,
1436 fullnodes=fullnodes,
1437 fullnodes=fullnodes,
1437 )
1438 )
1438
1439
1439
1440
1440 def _makecg3packer(
1441 def _makecg3packer(
1441 repo,
1442 repo,
1442 oldmatcher,
1443 oldmatcher,
1443 matcher,
1444 matcher,
1444 bundlecaps,
1445 bundlecaps,
1445 ellipses=False,
1446 ellipses=False,
1446 shallow=False,
1447 shallow=False,
1447 ellipsisroots=None,
1448 ellipsisroots=None,
1448 fullnodes=None,
1449 fullnodes=None,
1449 ):
1450 ):
1450 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1451 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1451 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags
1452 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags
1452 )
1453 )
1453
1454
1454 return cgpacker(
1455 return cgpacker(
1455 repo,
1456 repo,
1456 oldmatcher,
1457 oldmatcher,
1457 matcher,
1458 matcher,
1458 b'03',
1459 b'03',
1459 builddeltaheader=builddeltaheader,
1460 builddeltaheader=builddeltaheader,
1460 manifestsend=closechunk(),
1461 manifestsend=closechunk(),
1461 bundlecaps=bundlecaps,
1462 bundlecaps=bundlecaps,
1462 ellipses=ellipses,
1463 ellipses=ellipses,
1463 shallow=shallow,
1464 shallow=shallow,
1464 ellipsisroots=ellipsisroots,
1465 ellipsisroots=ellipsisroots,
1465 fullnodes=fullnodes,
1466 fullnodes=fullnodes,
1466 )
1467 )
1467
1468
1468
1469
1469 _packermap = {
1470 _packermap = {
1470 b'01': (_makecg1packer, cg1unpacker),
1471 b'01': (_makecg1packer, cg1unpacker),
1471 # cg2 adds support for exchanging generaldelta
1472 # cg2 adds support for exchanging generaldelta
1472 b'02': (_makecg2packer, cg2unpacker),
1473 b'02': (_makecg2packer, cg2unpacker),
1473 # cg3 adds support for exchanging revlog flags and treemanifests
1474 # cg3 adds support for exchanging revlog flags and treemanifests
1474 b'03': (_makecg3packer, cg3unpacker),
1475 b'03': (_makecg3packer, cg3unpacker),
1475 }
1476 }
1476
1477
1477
1478
1478 def allsupportedversions(repo):
1479 def allsupportedversions(repo):
1479 versions = set(_packermap.keys())
1480 versions = set(_packermap.keys())
1480 needv03 = False
1481 needv03 = False
1481 if (
1482 if (
1482 repo.ui.configbool(b'experimental', b'changegroup3')
1483 repo.ui.configbool(b'experimental', b'changegroup3')
1483 or repo.ui.configbool(b'experimental', b'treemanifest')
1484 or repo.ui.configbool(b'experimental', b'treemanifest')
1484 or scmutil.istreemanifest(repo)
1485 or scmutil.istreemanifest(repo)
1485 ):
1486 ):
1486 # we keep version 03 because we need to to exchange treemanifest data
1487 # we keep version 03 because we need to to exchange treemanifest data
1487 #
1488 #
1488 # we also keep vresion 01 and 02, because it is possible for repo to
1489 # we also keep vresion 01 and 02, because it is possible for repo to
1489 # contains both normal and tree manifest at the same time. so using
1490 # contains both normal and tree manifest at the same time. so using
1490 # older version to pull data is viable
1491 # older version to pull data is viable
1491 #
1492 #
1492 # (or even to push subset of history)
1493 # (or even to push subset of history)
1493 needv03 = True
1494 needv03 = True
1494 if b'exp-sidedata-flag' in repo.requirements:
1495 if b'exp-sidedata-flag' in repo.requirements:
1495 needv03 = True
1496 needv03 = True
1496 # don't attempt to use 01/02 until we do sidedata cleaning
1497 # don't attempt to use 01/02 until we do sidedata cleaning
1497 versions.discard(b'01')
1498 versions.discard(b'01')
1498 versions.discard(b'02')
1499 versions.discard(b'02')
1499 if not needv03:
1500 if not needv03:
1500 versions.discard(b'03')
1501 versions.discard(b'03')
1501 return versions
1502 return versions
1502
1503
1503
1504
1504 # Changegroup versions that can be applied to the repo
1505 # Changegroup versions that can be applied to the repo
1505 def supportedincomingversions(repo):
1506 def supportedincomingversions(repo):
1506 return allsupportedversions(repo)
1507 return allsupportedversions(repo)
1507
1508
1508
1509
1509 # Changegroup versions that can be created from the repo
1510 # Changegroup versions that can be created from the repo
1510 def supportedoutgoingversions(repo):
1511 def supportedoutgoingversions(repo):
1511 versions = allsupportedversions(repo)
1512 versions = allsupportedversions(repo)
1512 if scmutil.istreemanifest(repo):
1513 if scmutil.istreemanifest(repo):
1513 # Versions 01 and 02 support only flat manifests and it's just too
1514 # Versions 01 and 02 support only flat manifests and it's just too
1514 # expensive to convert between the flat manifest and tree manifest on
1515 # expensive to convert between the flat manifest and tree manifest on
1515 # the fly. Since tree manifests are hashed differently, all of history
1516 # the fly. Since tree manifests are hashed differently, all of history
1516 # would have to be converted. Instead, we simply don't even pretend to
1517 # would have to be converted. Instead, we simply don't even pretend to
1517 # support versions 01 and 02.
1518 # support versions 01 and 02.
1518 versions.discard(b'01')
1519 versions.discard(b'01')
1519 versions.discard(b'02')
1520 versions.discard(b'02')
1520 if requirements.NARROW_REQUIREMENT in repo.requirements:
1521 if requirements.NARROW_REQUIREMENT in repo.requirements:
1521 # Versions 01 and 02 don't support revlog flags, and we need to
1522 # Versions 01 and 02 don't support revlog flags, and we need to
1522 # support that for stripping and unbundling to work.
1523 # support that for stripping and unbundling to work.
1523 versions.discard(b'01')
1524 versions.discard(b'01')
1524 versions.discard(b'02')
1525 versions.discard(b'02')
1525 if LFS_REQUIREMENT in repo.requirements:
1526 if LFS_REQUIREMENT in repo.requirements:
1526 # Versions 01 and 02 don't support revlog flags, and we need to
1527 # Versions 01 and 02 don't support revlog flags, and we need to
1527 # mark LFS entries with REVIDX_EXTSTORED.
1528 # mark LFS entries with REVIDX_EXTSTORED.
1528 versions.discard(b'01')
1529 versions.discard(b'01')
1529 versions.discard(b'02')
1530 versions.discard(b'02')
1530
1531
1531 return versions
1532 return versions
1532
1533
1533
1534
1534 def localversion(repo):
1535 def localversion(repo):
1535 # Finds the best version to use for bundles that are meant to be used
1536 # Finds the best version to use for bundles that are meant to be used
1536 # locally, such as those from strip and shelve, and temporary bundles.
1537 # locally, such as those from strip and shelve, and temporary bundles.
1537 return max(supportedoutgoingversions(repo))
1538 return max(supportedoutgoingversions(repo))
1538
1539
1539
1540
1540 def safeversion(repo):
1541 def safeversion(repo):
1541 # Finds the smallest version that it's safe to assume clients of the repo
1542 # Finds the smallest version that it's safe to assume clients of the repo
1542 # will support. For example, all hg versions that support generaldelta also
1543 # will support. For example, all hg versions that support generaldelta also
1543 # support changegroup 02.
1544 # support changegroup 02.
1544 versions = supportedoutgoingversions(repo)
1545 versions = supportedoutgoingversions(repo)
1545 if b'generaldelta' in repo.requirements:
1546 if b'generaldelta' in repo.requirements:
1546 versions.discard(b'01')
1547 versions.discard(b'01')
1547 assert versions
1548 assert versions
1548 return min(versions)
1549 return min(versions)
1549
1550
1550
1551
1551 def getbundler(
1552 def getbundler(
1552 version,
1553 version,
1553 repo,
1554 repo,
1554 bundlecaps=None,
1555 bundlecaps=None,
1555 oldmatcher=None,
1556 oldmatcher=None,
1556 matcher=None,
1557 matcher=None,
1557 ellipses=False,
1558 ellipses=False,
1558 shallow=False,
1559 shallow=False,
1559 ellipsisroots=None,
1560 ellipsisroots=None,
1560 fullnodes=None,
1561 fullnodes=None,
1561 ):
1562 ):
1562 assert version in supportedoutgoingversions(repo)
1563 assert version in supportedoutgoingversions(repo)
1563
1564
1564 if matcher is None:
1565 if matcher is None:
1565 matcher = matchmod.always()
1566 matcher = matchmod.always()
1566 if oldmatcher is None:
1567 if oldmatcher is None:
1567 oldmatcher = matchmod.never()
1568 oldmatcher = matchmod.never()
1568
1569
1569 if version == b'01' and not matcher.always():
1570 if version == b'01' and not matcher.always():
1570 raise error.ProgrammingError(
1571 raise error.ProgrammingError(
1571 b'version 01 changegroups do not support sparse file matchers'
1572 b'version 01 changegroups do not support sparse file matchers'
1572 )
1573 )
1573
1574
1574 if ellipses and version in (b'01', b'02'):
1575 if ellipses and version in (b'01', b'02'):
1575 raise error.Abort(
1576 raise error.Abort(
1576 _(
1577 _(
1577 b'ellipsis nodes require at least cg3 on client and server, '
1578 b'ellipsis nodes require at least cg3 on client and server, '
1578 b'but negotiated version %s'
1579 b'but negotiated version %s'
1579 )
1580 )
1580 % version
1581 % version
1581 )
1582 )
1582
1583
1583 # Requested files could include files not in the local store. So
1584 # Requested files could include files not in the local store. So
1584 # filter those out.
1585 # filter those out.
1585 matcher = repo.narrowmatch(matcher)
1586 matcher = repo.narrowmatch(matcher)
1586
1587
1587 fn = _packermap[version][0]
1588 fn = _packermap[version][0]
1588 return fn(
1589 return fn(
1589 repo,
1590 repo,
1590 oldmatcher,
1591 oldmatcher,
1591 matcher,
1592 matcher,
1592 bundlecaps,
1593 bundlecaps,
1593 ellipses=ellipses,
1594 ellipses=ellipses,
1594 shallow=shallow,
1595 shallow=shallow,
1595 ellipsisroots=ellipsisroots,
1596 ellipsisroots=ellipsisroots,
1596 fullnodes=fullnodes,
1597 fullnodes=fullnodes,
1597 )
1598 )
1598
1599
1599
1600
1600 def getunbundler(version, fh, alg, extras=None):
1601 def getunbundler(version, fh, alg, extras=None):
1601 return _packermap[version][1](fh, alg, extras=extras)
1602 return _packermap[version][1](fh, alg, extras=extras)
1602
1603
1603
1604
1604 def _changegroupinfo(repo, nodes, source):
1605 def _changegroupinfo(repo, nodes, source):
1605 if repo.ui.verbose or source == b'bundle':
1606 if repo.ui.verbose or source == b'bundle':
1606 repo.ui.status(_(b"%d changesets found\n") % len(nodes))
1607 repo.ui.status(_(b"%d changesets found\n") % len(nodes))
1607 if repo.ui.debugflag:
1608 if repo.ui.debugflag:
1608 repo.ui.debug(b"list of changesets:\n")
1609 repo.ui.debug(b"list of changesets:\n")
1609 for node in nodes:
1610 for node in nodes:
1610 repo.ui.debug(b"%s\n" % hex(node))
1611 repo.ui.debug(b"%s\n" % hex(node))
1611
1612
1612
1613
1613 def makechangegroup(
1614 def makechangegroup(
1614 repo, outgoing, version, source, fastpath=False, bundlecaps=None
1615 repo, outgoing, version, source, fastpath=False, bundlecaps=None
1615 ):
1616 ):
1616 cgstream = makestream(
1617 cgstream = makestream(
1617 repo,
1618 repo,
1618 outgoing,
1619 outgoing,
1619 version,
1620 version,
1620 source,
1621 source,
1621 fastpath=fastpath,
1622 fastpath=fastpath,
1622 bundlecaps=bundlecaps,
1623 bundlecaps=bundlecaps,
1623 )
1624 )
1624 return getunbundler(
1625 return getunbundler(
1625 version,
1626 version,
1626 util.chunkbuffer(cgstream),
1627 util.chunkbuffer(cgstream),
1627 None,
1628 None,
1628 {b'clcount': len(outgoing.missing)},
1629 {b'clcount': len(outgoing.missing)},
1629 )
1630 )
1630
1631
1631
1632
1632 def makestream(
1633 def makestream(
1633 repo,
1634 repo,
1634 outgoing,
1635 outgoing,
1635 version,
1636 version,
1636 source,
1637 source,
1637 fastpath=False,
1638 fastpath=False,
1638 bundlecaps=None,
1639 bundlecaps=None,
1639 matcher=None,
1640 matcher=None,
1640 ):
1641 ):
1641 bundler = getbundler(version, repo, bundlecaps=bundlecaps, matcher=matcher)
1642 bundler = getbundler(version, repo, bundlecaps=bundlecaps, matcher=matcher)
1642
1643
1643 repo = repo.unfiltered()
1644 repo = repo.unfiltered()
1644 commonrevs = outgoing.common
1645 commonrevs = outgoing.common
1645 csets = outgoing.missing
1646 csets = outgoing.missing
1646 heads = outgoing.ancestorsof
1647 heads = outgoing.ancestorsof
1647 # We go through the fast path if we get told to, or if all (unfiltered
1648 # We go through the fast path if we get told to, or if all (unfiltered
1648 # heads have been requested (since we then know there all linkrevs will
1649 # heads have been requested (since we then know there all linkrevs will
1649 # be pulled by the client).
1650 # be pulled by the client).
1650 heads.sort()
1651 heads.sort()
1651 fastpathlinkrev = fastpath or (
1652 fastpathlinkrev = fastpath or (
1652 repo.filtername is None and heads == sorted(repo.heads())
1653 repo.filtername is None and heads == sorted(repo.heads())
1653 )
1654 )
1654
1655
1655 repo.hook(b'preoutgoing', throw=True, source=source)
1656 repo.hook(b'preoutgoing', throw=True, source=source)
1656 _changegroupinfo(repo, csets, source)
1657 _changegroupinfo(repo, csets, source)
1657 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1658 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1658
1659
1659
1660
1660 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1661 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1661 revisions = 0
1662 revisions = 0
1662 files = 0
1663 files = 0
1663 progress = repo.ui.makeprogress(
1664 progress = repo.ui.makeprogress(
1664 _(b'files'), unit=_(b'files'), total=expectedfiles
1665 _(b'files'), unit=_(b'files'), total=expectedfiles
1665 )
1666 )
1666 for chunkdata in iter(source.filelogheader, {}):
1667 for chunkdata in iter(source.filelogheader, {}):
1667 files += 1
1668 files += 1
1668 f = chunkdata[b"filename"]
1669 f = chunkdata[b"filename"]
1669 repo.ui.debug(b"adding %s revisions\n" % f)
1670 repo.ui.debug(b"adding %s revisions\n" % f)
1670 progress.increment()
1671 progress.increment()
1671 fl = repo.file(f)
1672 fl = repo.file(f)
1672 o = len(fl)
1673 o = len(fl)
1673 try:
1674 try:
1674 deltas = source.deltaiter()
1675 deltas = source.deltaiter()
1675 if not fl.addgroup(deltas, revmap, trp):
1676 if not fl.addgroup(deltas, revmap, trp):
1676 raise error.Abort(_(b"received file revlog group is empty"))
1677 raise error.Abort(_(b"received file revlog group is empty"))
1677 except error.CensoredBaseError as e:
1678 except error.CensoredBaseError as e:
1678 raise error.Abort(_(b"received delta base is censored: %s") % e)
1679 raise error.Abort(_(b"received delta base is censored: %s") % e)
1679 revisions += len(fl) - o
1680 revisions += len(fl) - o
1680 if f in needfiles:
1681 if f in needfiles:
1681 needs = needfiles[f]
1682 needs = needfiles[f]
1682 for new in pycompat.xrange(o, len(fl)):
1683 for new in pycompat.xrange(o, len(fl)):
1683 n = fl.node(new)
1684 n = fl.node(new)
1684 if n in needs:
1685 if n in needs:
1685 needs.remove(n)
1686 needs.remove(n)
1686 else:
1687 else:
1687 raise error.Abort(_(b"received spurious file revlog entry"))
1688 raise error.Abort(_(b"received spurious file revlog entry"))
1688 if not needs:
1689 if not needs:
1689 del needfiles[f]
1690 del needfiles[f]
1690 progress.complete()
1691 progress.complete()
1691
1692
1692 for f, needs in pycompat.iteritems(needfiles):
1693 for f, needs in pycompat.iteritems(needfiles):
1693 fl = repo.file(f)
1694 fl = repo.file(f)
1694 for n in needs:
1695 for n in needs:
1695 try:
1696 try:
1696 fl.rev(n)
1697 fl.rev(n)
1697 except error.LookupError:
1698 except error.LookupError:
1698 raise error.Abort(
1699 raise error.Abort(
1699 _(b'missing file data for %s:%s - run hg verify')
1700 _(b'missing file data for %s:%s - run hg verify')
1700 % (f, hex(n))
1701 % (f, hex(n))
1701 )
1702 )
1702
1703
1703 return revisions, files
1704 return revisions, files
@@ -1,516 +1,516 b''
1 # dagparser.py - parser and generator for concise description of DAGs
1 # dagparser.py - parser and generator for concise description of DAGs
2 #
2 #
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11 import string
11 import string
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 )
17 )
18 from .utils import stringutil
18 from .utils import stringutil
19
19
20
20
21 def parsedag(desc):
21 def parsedag(desc):
22 '''parses a DAG from a concise textual description; generates events
22 '''parses a DAG from a concise textual description; generates events
23
23
24 "+n" is a linear run of n nodes based on the current default parent
24 "+n" is a linear run of n nodes based on the current default parent
25 "." is a single node based on the current default parent
25 "." is a single node based on the current default parent
26 "$" resets the default parent to -1 (implied at the start);
26 "$" resets the default parent to -1 (implied at the start);
27 otherwise the default parent is always the last node created
27 otherwise the default parent is always the last node created
28 "<p" sets the default parent to the backref p
28 "<p" sets the default parent to the backref p
29 "*p" is a fork at parent p, where p is a backref
29 "*p" is a fork at parent p, where p is a backref
30 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
30 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
31 "/p2/.../pn" is a merge of the preceding node and p2..pn
31 "/p2/.../pn" is a merge of the preceding node and p2..pn
32 ":name" defines a label for the preceding node; labels can be redefined
32 ":name" defines a label for the preceding node; labels can be redefined
33 "@text" emits an annotation event for text
33 "@text" emits an annotation event for text
34 "!command" emits an action event for the current node
34 "!command" emits an action event for the current node
35 "!!my command\n" is like "!", but to the end of the line
35 "!!my command\n" is like "!", but to the end of the line
36 "#...\n" is a comment up to the end of the line
36 "#...\n" is a comment up to the end of the line
37
37
38 Whitespace between the above elements is ignored.
38 Whitespace between the above elements is ignored.
39
39
40 A backref is either
40 A backref is either
41 * a number n, which references the node curr-n, where curr is the current
41 * a number n, which references the node curr-n, where curr is the current
42 node, or
42 node, or
43 * the name of a label you placed earlier using ":name", or
43 * the name of a label you placed earlier using ":name", or
44 * empty to denote the default parent.
44 * empty to denote the default parent.
45
45
46 All string valued-elements are either strictly alphanumeric, or must
46 All string valued-elements are either strictly alphanumeric, or must
47 be enclosed in double quotes ("..."), with "\" as escape character.
47 be enclosed in double quotes ("..."), with "\" as escape character.
48
48
49 Generates sequence of
49 Generates sequence of
50
50
51 ('n', (id, [parentids])) for node creation
51 ('n', (id, [parentids])) for node creation
52 ('l', (id, labelname)) for labels on nodes
52 ('l', (id, labelname)) for labels on nodes
53 ('a', text) for annotations
53 ('a', text) for annotations
54 ('c', command) for actions (!)
54 ('c', command) for actions (!)
55 ('C', command) for line actions (!!)
55 ('C', command) for line actions (!!)
56
56
57 Examples
57 Examples
58 --------
58 --------
59
59
60 Example of a complex graph (output not shown for brevity):
60 Example of a complex graph (output not shown for brevity):
61
61
62 >>> len(list(parsedag(b"""
62 >>> len(list(parsedag(b"""
63 ...
63 ...
64 ... +3 # 3 nodes in linear run
64 ... +3 # 3 nodes in linear run
65 ... :forkhere # a label for the last of the 3 nodes from above
65 ... :forkhere # a label for the last of the 3 nodes from above
66 ... +5 # 5 more nodes on one branch
66 ... +5 # 5 more nodes on one branch
67 ... :mergethis # label again
67 ... :mergethis # label again
68 ... <forkhere # set default parent to labeled fork node
68 ... <forkhere # set default parent to labeled fork node
69 ... +10 # 10 more nodes on a parallel branch
69 ... +10 # 10 more nodes on a parallel branch
70 ... @stable # following nodes will be annotated as "stable"
70 ... @stable # following nodes will be annotated as "stable"
71 ... +5 # 5 nodes in stable
71 ... +5 # 5 nodes in stable
72 ... !addfile # custom command; could trigger new file in next node
72 ... !addfile # custom command; could trigger new file in next node
73 ... +2 # two more nodes
73 ... +2 # two more nodes
74 ... /mergethis # merge last node with labeled node
74 ... /mergethis # merge last node with labeled node
75 ... +4 # 4 more nodes descending from merge node
75 ... +4 # 4 more nodes descending from merge node
76 ...
76 ...
77 ... """)))
77 ... """)))
78 34
78 34
79
79
80 Empty list:
80 Empty list:
81
81
82 >>> list(parsedag(b""))
82 >>> list(parsedag(b""))
83 []
83 []
84
84
85 A simple linear run:
85 A simple linear run:
86
86
87 >>> list(parsedag(b"+3"))
87 >>> list(parsedag(b"+3"))
88 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
88 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
89
89
90 Some non-standard ways to define such runs:
90 Some non-standard ways to define such runs:
91
91
92 >>> list(parsedag(b"+1+2"))
92 >>> list(parsedag(b"+1+2"))
93 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
93 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
94
94
95 >>> list(parsedag(b"+1*1*"))
95 >>> list(parsedag(b"+1*1*"))
96 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
96 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
97
97
98 >>> list(parsedag(b"*"))
98 >>> list(parsedag(b"*"))
99 [('n', (0, [-1]))]
99 [('n', (0, [-1]))]
100
100
101 >>> list(parsedag(b"..."))
101 >>> list(parsedag(b"..."))
102 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
102 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
103
103
104 A fork and a join, using numeric back references:
104 A fork and a join, using numeric back references:
105
105
106 >>> list(parsedag(b"+2*2*/2"))
106 >>> list(parsedag(b"+2*2*/2"))
107 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
107 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
108
108
109 >>> list(parsedag(b"+2<2+1/2"))
109 >>> list(parsedag(b"+2<2+1/2"))
110 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
110 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
111
111
112 Placing a label:
112 Placing a label:
113
113
114 >>> list(parsedag(b"+1 :mylabel +1"))
114 >>> list(parsedag(b"+1 :mylabel +1"))
115 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
115 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
116
116
117 An empty label (silly, really):
117 An empty label (silly, really):
118
118
119 >>> list(parsedag(b"+1:+1"))
119 >>> list(parsedag(b"+1:+1"))
120 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
120 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
121
121
122 Fork and join, but with labels instead of numeric back references:
122 Fork and join, but with labels instead of numeric back references:
123
123
124 >>> list(parsedag(b"+1:f +1:p2 *f */p2"))
124 >>> list(parsedag(b"+1:f +1:p2 *f */p2"))
125 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
125 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
126 ('n', (2, [0])), ('n', (3, [2, 1]))]
126 ('n', (2, [0])), ('n', (3, [2, 1]))]
127
127
128 >>> list(parsedag(b"+1:f +1:p2 <f +1 /p2"))
128 >>> list(parsedag(b"+1:f +1:p2 <f +1 /p2"))
129 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
129 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
130 ('n', (2, [0])), ('n', (3, [2, 1]))]
130 ('n', (2, [0])), ('n', (3, [2, 1]))]
131
131
132 Restarting from the root:
132 Restarting from the root:
133
133
134 >>> list(parsedag(b"+1 $ +1"))
134 >>> list(parsedag(b"+1 $ +1"))
135 [('n', (0, [-1])), ('n', (1, [-1]))]
135 [('n', (0, [-1])), ('n', (1, [-1]))]
136
136
137 Annotations, which are meant to introduce sticky state for subsequent nodes:
137 Annotations, which are meant to introduce sticky state for subsequent nodes:
138
138
139 >>> list(parsedag(b"+1 @ann +1"))
139 >>> list(parsedag(b"+1 @ann +1"))
140 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
140 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
141
141
142 >>> list(parsedag(b'+1 @"my annotation" +1'))
142 >>> list(parsedag(b'+1 @"my annotation" +1'))
143 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
143 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
144
144
145 Commands, which are meant to operate on the most recently created node:
145 Commands, which are meant to operate on the most recently created node:
146
146
147 >>> list(parsedag(b"+1 !cmd +1"))
147 >>> list(parsedag(b"+1 !cmd +1"))
148 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
148 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
149
149
150 >>> list(parsedag(b'+1 !"my command" +1'))
150 >>> list(parsedag(b'+1 !"my command" +1'))
151 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
151 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
152
152
153 >>> list(parsedag(b'+1 !!my command line\\n +1'))
153 >>> list(parsedag(b'+1 !!my command line\\n +1'))
154 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
154 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
155
155
156 Comments, which extend to the end of the line:
156 Comments, which extend to the end of the line:
157
157
158 >>> list(parsedag(b'+1 # comment\\n+1'))
158 >>> list(parsedag(b'+1 # comment\\n+1'))
159 [('n', (0, [-1])), ('n', (1, [0]))]
159 [('n', (0, [-1])), ('n', (1, [0]))]
160
160
161 Error:
161 Error:
162
162
163 >>> try: list(parsedag(b'+1 bad'))
163 >>> try: list(parsedag(b'+1 bad'))
164 ... except Exception as e: print(pycompat.sysstr(bytes(e)))
164 ... except Exception as e: print(pycompat.sysstr(bytes(e)))
165 invalid character in dag description: bad...
165 invalid character in dag description: bad...
166
166
167 '''
167 '''
168 if not desc:
168 if not desc:
169 return
169 return
170
170
171 wordchars = pycompat.bytestr(
171 # pytype: disable=wrong-arg-types
172 string.ascii_letters + string.digits
172 wordchars = pycompat.bytestr(string.ascii_letters + string.digits)
173 ) # pytype: disable=wrong-arg-types
173 # pytype: enable=wrong-arg-types
174
174
175 labels = {}
175 labels = {}
176 p1 = -1
176 p1 = -1
177 r = 0
177 r = 0
178
178
179 def resolve(ref):
179 def resolve(ref):
180 if not ref:
180 if not ref:
181 return p1
181 return p1
182 elif ref[0] in pycompat.bytestr(
182 # pytype: disable=wrong-arg-types
183 string.digits
183 elif ref[0] in pycompat.bytestr(string.digits):
184 ): # pytype: disable=wrong-arg-types
184 # pytype: enable=wrong-arg-types
185 return r - int(ref)
185 return r - int(ref)
186 else:
186 else:
187 return labels[ref]
187 return labels[ref]
188
188
189 chiter = pycompat.iterbytestr(desc)
189 chiter = pycompat.iterbytestr(desc)
190
190
191 def nextch():
191 def nextch():
192 return next(chiter, b'\0')
192 return next(chiter, b'\0')
193
193
194 def nextrun(c, allow):
194 def nextrun(c, allow):
195 s = b''
195 s = b''
196 while c in allow:
196 while c in allow:
197 s += c
197 s += c
198 c = nextch()
198 c = nextch()
199 return c, s
199 return c, s
200
200
201 def nextdelimited(c, limit, escape):
201 def nextdelimited(c, limit, escape):
202 s = b''
202 s = b''
203 while c != limit:
203 while c != limit:
204 if c == escape:
204 if c == escape:
205 c = nextch()
205 c = nextch()
206 s += c
206 s += c
207 c = nextch()
207 c = nextch()
208 return nextch(), s
208 return nextch(), s
209
209
210 def nextstring(c):
210 def nextstring(c):
211 if c == b'"':
211 if c == b'"':
212 return nextdelimited(nextch(), b'"', b'\\')
212 return nextdelimited(nextch(), b'"', b'\\')
213 else:
213 else:
214 return nextrun(c, wordchars)
214 return nextrun(c, wordchars)
215
215
216 c = nextch()
216 c = nextch()
217 while c != b'\0':
217 while c != b'\0':
218 while c in pycompat.bytestr(
218 # pytype: disable=wrong-arg-types
219 string.whitespace
219 while c in pycompat.bytestr(string.whitespace):
220 ): # pytype: disable=wrong-arg-types
220 # pytype: enable=wrong-arg-types
221 c = nextch()
221 c = nextch()
222 if c == b'.':
222 if c == b'.':
223 yield b'n', (r, [p1])
223 yield b'n', (r, [p1])
224 p1 = r
224 p1 = r
225 r += 1
225 r += 1
226 c = nextch()
226 c = nextch()
227 elif c == b'+':
227 elif c == b'+':
228 c, digs = nextrun(
228 # pytype: disable=wrong-arg-types
229 nextch(), pycompat.bytestr(string.digits)
229 c, digs = nextrun(nextch(), pycompat.bytestr(string.digits))
230 ) # pytype: disable=wrong-arg-types
230 # pytype: enable=wrong-arg-types
231 n = int(digs)
231 n = int(digs)
232 for i in pycompat.xrange(0, n):
232 for i in pycompat.xrange(0, n):
233 yield b'n', (r, [p1])
233 yield b'n', (r, [p1])
234 p1 = r
234 p1 = r
235 r += 1
235 r += 1
236 elif c in b'*/':
236 elif c in b'*/':
237 if c == b'*':
237 if c == b'*':
238 c = nextch()
238 c = nextch()
239 c, pref = nextstring(c)
239 c, pref = nextstring(c)
240 prefs = [pref]
240 prefs = [pref]
241 while c == b'/':
241 while c == b'/':
242 c, pref = nextstring(nextch())
242 c, pref = nextstring(nextch())
243 prefs.append(pref)
243 prefs.append(pref)
244 ps = [resolve(ref) for ref in prefs]
244 ps = [resolve(ref) for ref in prefs]
245 yield b'n', (r, ps)
245 yield b'n', (r, ps)
246 p1 = r
246 p1 = r
247 r += 1
247 r += 1
248 elif c == b'<':
248 elif c == b'<':
249 c, ref = nextstring(nextch())
249 c, ref = nextstring(nextch())
250 p1 = resolve(ref)
250 p1 = resolve(ref)
251 elif c == b':':
251 elif c == b':':
252 c, name = nextstring(nextch())
252 c, name = nextstring(nextch())
253 labels[name] = p1
253 labels[name] = p1
254 yield b'l', (p1, name)
254 yield b'l', (p1, name)
255 elif c == b'@':
255 elif c == b'@':
256 c, text = nextstring(nextch())
256 c, text = nextstring(nextch())
257 yield b'a', text
257 yield b'a', text
258 elif c == b'!':
258 elif c == b'!':
259 c = nextch()
259 c = nextch()
260 if c == b'!':
260 if c == b'!':
261 cmd = b''
261 cmd = b''
262 c = nextch()
262 c = nextch()
263 while c not in b'\n\r\0':
263 while c not in b'\n\r\0':
264 cmd += c
264 cmd += c
265 c = nextch()
265 c = nextch()
266 yield b'C', cmd
266 yield b'C', cmd
267 else:
267 else:
268 c, cmd = nextstring(c)
268 c, cmd = nextstring(c)
269 yield b'c', cmd
269 yield b'c', cmd
270 elif c == b'#':
270 elif c == b'#':
271 while c not in b'\n\r\0':
271 while c not in b'\n\r\0':
272 c = nextch()
272 c = nextch()
273 elif c == b'$':
273 elif c == b'$':
274 p1 = -1
274 p1 = -1
275 c = nextch()
275 c = nextch()
276 elif c == b'\0':
276 elif c == b'\0':
277 return # in case it was preceded by whitespace
277 return # in case it was preceded by whitespace
278 else:
278 else:
279 s = b''
279 s = b''
280 i = 0
280 i = 0
281 while c != b'\0' and i < 10:
281 while c != b'\0' and i < 10:
282 s += c
282 s += c
283 i += 1
283 i += 1
284 c = nextch()
284 c = nextch()
285 raise error.Abort(
285 raise error.Abort(
286 _(b'invalid character in dag description: %s...') % s
286 _(b'invalid character in dag description: %s...') % s
287 )
287 )
288
288
289
289
290 def dagtextlines(
290 def dagtextlines(
291 events,
291 events,
292 addspaces=True,
292 addspaces=True,
293 wraplabels=False,
293 wraplabels=False,
294 wrapannotations=False,
294 wrapannotations=False,
295 wrapcommands=False,
295 wrapcommands=False,
296 wrapnonlinear=False,
296 wrapnonlinear=False,
297 usedots=False,
297 usedots=False,
298 maxlinewidth=70,
298 maxlinewidth=70,
299 ):
299 ):
300 '''generates single lines for dagtext()'''
300 '''generates single lines for dagtext()'''
301
301
302 def wrapstring(text):
302 def wrapstring(text):
303 if re.match(b"^[0-9a-z]*$", text):
303 if re.match(b"^[0-9a-z]*$", text):
304 return text
304 return text
305 return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\"') + b'"'
305 return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\"') + b'"'
306
306
307 def gen():
307 def gen():
308 labels = {}
308 labels = {}
309 run = 0
309 run = 0
310 wantr = 0
310 wantr = 0
311 needroot = False
311 needroot = False
312 for kind, data in events:
312 for kind, data in events:
313 if kind == b'n':
313 if kind == b'n':
314 r, ps = data
314 r, ps = data
315
315
316 # sanity check
316 # sanity check
317 if r != wantr:
317 if r != wantr:
318 raise error.Abort(_(b"expected id %i, got %i") % (wantr, r))
318 raise error.Abort(_(b"expected id %i, got %i") % (wantr, r))
319 if not ps:
319 if not ps:
320 ps = [-1]
320 ps = [-1]
321 else:
321 else:
322 for p in ps:
322 for p in ps:
323 if p >= r:
323 if p >= r:
324 raise error.Abort(
324 raise error.Abort(
325 _(
325 _(
326 b"parent id %i is larger than "
326 b"parent id %i is larger than "
327 b"current id %i"
327 b"current id %i"
328 )
328 )
329 % (p, r)
329 % (p, r)
330 )
330 )
331 wantr += 1
331 wantr += 1
332
332
333 # new root?
333 # new root?
334 p1 = r - 1
334 p1 = r - 1
335 if len(ps) == 1 and ps[0] == -1:
335 if len(ps) == 1 and ps[0] == -1:
336 if needroot:
336 if needroot:
337 if run:
337 if run:
338 yield b'+%d' % run
338 yield b'+%d' % run
339 run = 0
339 run = 0
340 if wrapnonlinear:
340 if wrapnonlinear:
341 yield b'\n'
341 yield b'\n'
342 yield b'$'
342 yield b'$'
343 p1 = -1
343 p1 = -1
344 else:
344 else:
345 needroot = True
345 needroot = True
346 if len(ps) == 1 and ps[0] == p1:
346 if len(ps) == 1 and ps[0] == p1:
347 if usedots:
347 if usedots:
348 yield b"."
348 yield b"."
349 else:
349 else:
350 run += 1
350 run += 1
351 else:
351 else:
352 if run:
352 if run:
353 yield b'+%d' % run
353 yield b'+%d' % run
354 run = 0
354 run = 0
355 if wrapnonlinear:
355 if wrapnonlinear:
356 yield b'\n'
356 yield b'\n'
357 prefs = []
357 prefs = []
358 for p in ps:
358 for p in ps:
359 if p == p1:
359 if p == p1:
360 prefs.append(b'')
360 prefs.append(b'')
361 elif p in labels:
361 elif p in labels:
362 prefs.append(labels[p])
362 prefs.append(labels[p])
363 else:
363 else:
364 prefs.append(b'%d' % (r - p))
364 prefs.append(b'%d' % (r - p))
365 yield b'*' + b'/'.join(prefs)
365 yield b'*' + b'/'.join(prefs)
366 else:
366 else:
367 if run:
367 if run:
368 yield b'+%d' % run
368 yield b'+%d' % run
369 run = 0
369 run = 0
370 if kind == b'l':
370 if kind == b'l':
371 rid, name = data
371 rid, name = data
372 labels[rid] = name
372 labels[rid] = name
373 yield b':' + name
373 yield b':' + name
374 if wraplabels:
374 if wraplabels:
375 yield b'\n'
375 yield b'\n'
376 elif kind == b'c':
376 elif kind == b'c':
377 yield b'!' + wrapstring(data)
377 yield b'!' + wrapstring(data)
378 if wrapcommands:
378 if wrapcommands:
379 yield b'\n'
379 yield b'\n'
380 elif kind == b'C':
380 elif kind == b'C':
381 yield b'!!' + data
381 yield b'!!' + data
382 yield b'\n'
382 yield b'\n'
383 elif kind == b'a':
383 elif kind == b'a':
384 if wrapannotations:
384 if wrapannotations:
385 yield b'\n'
385 yield b'\n'
386 yield b'@' + wrapstring(data)
386 yield b'@' + wrapstring(data)
387 elif kind == b'#':
387 elif kind == b'#':
388 yield b'#' + data
388 yield b'#' + data
389 yield b'\n'
389 yield b'\n'
390 else:
390 else:
391 raise error.Abort(
391 raise error.Abort(
392 _(b"invalid event type in dag: ('%s', '%s')")
392 _(b"invalid event type in dag: ('%s', '%s')")
393 % (
393 % (
394 stringutil.escapestr(kind),
394 stringutil.escapestr(kind),
395 stringutil.escapestr(data),
395 stringutil.escapestr(data),
396 )
396 )
397 )
397 )
398 if run:
398 if run:
399 yield b'+%d' % run
399 yield b'+%d' % run
400
400
401 line = b''
401 line = b''
402 for part in gen():
402 for part in gen():
403 if part == b'\n':
403 if part == b'\n':
404 if line:
404 if line:
405 yield line
405 yield line
406 line = b''
406 line = b''
407 else:
407 else:
408 if len(line) + len(part) >= maxlinewidth:
408 if len(line) + len(part) >= maxlinewidth:
409 yield line
409 yield line
410 line = b''
410 line = b''
411 elif addspaces and line and part != b'.':
411 elif addspaces and line and part != b'.':
412 line += b' '
412 line += b' '
413 line += part
413 line += part
414 if line:
414 if line:
415 yield line
415 yield line
416
416
417
417
418 def dagtext(
418 def dagtext(
419 dag,
419 dag,
420 addspaces=True,
420 addspaces=True,
421 wraplabels=False,
421 wraplabels=False,
422 wrapannotations=False,
422 wrapannotations=False,
423 wrapcommands=False,
423 wrapcommands=False,
424 wrapnonlinear=False,
424 wrapnonlinear=False,
425 usedots=False,
425 usedots=False,
426 maxlinewidth=70,
426 maxlinewidth=70,
427 ):
427 ):
428 """generates lines of a textual representation for a dag event stream
428 """generates lines of a textual representation for a dag event stream
429
429
430 events should generate what parsedag() does, so:
430 events should generate what parsedag() does, so:
431
431
432 ('n', (id, [parentids])) for node creation
432 ('n', (id, [parentids])) for node creation
433 ('l', (id, labelname)) for labels on nodes
433 ('l', (id, labelname)) for labels on nodes
434 ('a', text) for annotations
434 ('a', text) for annotations
435 ('c', text) for commands
435 ('c', text) for commands
436 ('C', text) for line commands ('!!')
436 ('C', text) for line commands ('!!')
437 ('#', text) for comment lines
437 ('#', text) for comment lines
438
438
439 Parent nodes must come before child nodes.
439 Parent nodes must come before child nodes.
440
440
441 Examples
441 Examples
442 --------
442 --------
443
443
444 Linear run:
444 Linear run:
445
445
446 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0]))])
446 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0]))])
447 '+2'
447 '+2'
448
448
449 Two roots:
449 Two roots:
450
450
451 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [-1]))])
451 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [-1]))])
452 '+1 $ +1'
452 '+1 $ +1'
453
453
454 Fork and join:
454 Fork and join:
455
455
456 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0])), (b'n', (2, [0])),
456 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0])), (b'n', (2, [0])),
457 ... (b'n', (3, [2, 1]))])
457 ... (b'n', (3, [2, 1]))])
458 '+2 *2 */2'
458 '+2 *2 */2'
459
459
460 Fork and join with labels:
460 Fork and join with labels:
461
461
462 >>> dagtext([(b'n', (0, [-1])), (b'l', (0, b'f')), (b'n', (1, [0])),
462 >>> dagtext([(b'n', (0, [-1])), (b'l', (0, b'f')), (b'n', (1, [0])),
463 ... (b'l', (1, b'p2')), (b'n', (2, [0])), (b'n', (3, [2, 1]))])
463 ... (b'l', (1, b'p2')), (b'n', (2, [0])), (b'n', (3, [2, 1]))])
464 '+1 :f +1 :p2 *f */p2'
464 '+1 :f +1 :p2 *f */p2'
465
465
466 Annotations:
466 Annotations:
467
467
468 >>> dagtext([(b'n', (0, [-1])), (b'a', b'ann'), (b'n', (1, [0]))])
468 >>> dagtext([(b'n', (0, [-1])), (b'a', b'ann'), (b'n', (1, [0]))])
469 '+1 @ann +1'
469 '+1 @ann +1'
470
470
471 >>> dagtext([(b'n', (0, [-1])),
471 >>> dagtext([(b'n', (0, [-1])),
472 ... (b'a', b'my annotation'),
472 ... (b'a', b'my annotation'),
473 ... (b'n', (1, [0]))])
473 ... (b'n', (1, [0]))])
474 '+1 @"my annotation" +1'
474 '+1 @"my annotation" +1'
475
475
476 Commands:
476 Commands:
477
477
478 >>> dagtext([(b'n', (0, [-1])), (b'c', b'cmd'), (b'n', (1, [0]))])
478 >>> dagtext([(b'n', (0, [-1])), (b'c', b'cmd'), (b'n', (1, [0]))])
479 '+1 !cmd +1'
479 '+1 !cmd +1'
480
480
481 >>> dagtext([(b'n', (0, [-1])),
481 >>> dagtext([(b'n', (0, [-1])),
482 ... (b'c', b'my command'),
482 ... (b'c', b'my command'),
483 ... (b'n', (1, [0]))])
483 ... (b'n', (1, [0]))])
484 '+1 !"my command" +1'
484 '+1 !"my command" +1'
485
485
486 >>> dagtext([(b'n', (0, [-1])),
486 >>> dagtext([(b'n', (0, [-1])),
487 ... (b'C', b'my command line'),
487 ... (b'C', b'my command line'),
488 ... (b'n', (1, [0]))])
488 ... (b'n', (1, [0]))])
489 '+1 !!my command line\\n+1'
489 '+1 !!my command line\\n+1'
490
490
491 Comments:
491 Comments:
492
492
493 >>> dagtext([(b'n', (0, [-1])), (b'#', b' comment'), (b'n', (1, [0]))])
493 >>> dagtext([(b'n', (0, [-1])), (b'#', b' comment'), (b'n', (1, [0]))])
494 '+1 # comment\\n+1'
494 '+1 # comment\\n+1'
495
495
496 >>> dagtext([])
496 >>> dagtext([])
497 ''
497 ''
498
498
499 Combining parsedag and dagtext:
499 Combining parsedag and dagtext:
500
500
501 >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2'))
501 >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2'))
502 '+1 :f +1 :p2 *f */p2'
502 '+1 :f +1 :p2 *f */p2'
503
503
504 """
504 """
505 return b"\n".join(
505 return b"\n".join(
506 dagtextlines(
506 dagtextlines(
507 dag,
507 dag,
508 addspaces,
508 addspaces,
509 wraplabels,
509 wraplabels,
510 wrapannotations,
510 wrapannotations,
511 wrapcommands,
511 wrapcommands,
512 wrapnonlinear,
512 wrapnonlinear,
513 usedots,
513 usedots,
514 maxlinewidth,
514 maxlinewidth,
515 )
515 )
516 )
516 )
General Comments 0
You need to be logged in to leave comments. Login now