##// END OF EJS Templates
obsolete: explode if metadata contains invalid UTF-8 sequence (API)...
Yuya Nishihara -
r38730:ff1182d1 default
parent child Browse files
Show More
@@ -1,1022 +1,1033 b''
1 # obsolete.py - obsolete markers handling
1 # obsolete.py - obsolete markers handling
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 """Obsolete marker handling
9 """Obsolete marker handling
10
10
11 An obsolete marker maps an old changeset to a list of new
11 An obsolete marker maps an old changeset to a list of new
12 changesets. If the list of new changesets is empty, the old changeset
12 changesets. If the list of new changesets is empty, the old changeset
13 is said to be "killed". Otherwise, the old changeset is being
13 is said to be "killed". Otherwise, the old changeset is being
14 "replaced" by the new changesets.
14 "replaced" by the new changesets.
15
15
16 Obsolete markers can be used to record and distribute changeset graph
16 Obsolete markers can be used to record and distribute changeset graph
17 transformations performed by history rewrite operations, and help
17 transformations performed by history rewrite operations, and help
18 building new tools to reconcile conflicting rewrite actions. To
18 building new tools to reconcile conflicting rewrite actions. To
19 facilitate conflict resolution, markers include various annotations
19 facilitate conflict resolution, markers include various annotations
20 besides old and news changeset identifiers, such as creation date or
20 besides old and news changeset identifiers, such as creation date or
21 author name.
21 author name.
22
22
23 The old obsoleted changeset is called a "predecessor" and possible
23 The old obsoleted changeset is called a "predecessor" and possible
24 replacements are called "successors". Markers that used changeset X as
24 replacements are called "successors". Markers that used changeset X as
25 a predecessor are called "successor markers of X" because they hold
25 a predecessor are called "successor markers of X" because they hold
26 information about the successors of X. Markers that use changeset Y as
26 information about the successors of X. Markers that use changeset Y as
27 a successors are call "predecessor markers of Y" because they hold
27 a successors are call "predecessor markers of Y" because they hold
28 information about the predecessors of Y.
28 information about the predecessors of Y.
29
29
30 Examples:
30 Examples:
31
31
32 - When changeset A is replaced by changeset A', one marker is stored:
32 - When changeset A is replaced by changeset A', one marker is stored:
33
33
34 (A, (A',))
34 (A, (A',))
35
35
36 - When changesets A and B are folded into a new changeset C, two markers are
36 - When changesets A and B are folded into a new changeset C, two markers are
37 stored:
37 stored:
38
38
39 (A, (C,)) and (B, (C,))
39 (A, (C,)) and (B, (C,))
40
40
41 - When changeset A is simply "pruned" from the graph, a marker is created:
41 - When changeset A is simply "pruned" from the graph, a marker is created:
42
42
43 (A, ())
43 (A, ())
44
44
45 - When changeset A is split into B and C, a single marker is used:
45 - When changeset A is split into B and C, a single marker is used:
46
46
47 (A, (B, C))
47 (A, (B, C))
48
48
49 We use a single marker to distinguish the "split" case from the "divergence"
49 We use a single marker to distinguish the "split" case from the "divergence"
50 case. If two independent operations rewrite the same changeset A in to A' and
50 case. If two independent operations rewrite the same changeset A in to A' and
51 A'', we have an error case: divergent rewriting. We can detect it because
51 A'', we have an error case: divergent rewriting. We can detect it because
52 two markers will be created independently:
52 two markers will be created independently:
53
53
54 (A, (B,)) and (A, (C,))
54 (A, (B,)) and (A, (C,))
55
55
56 Format
56 Format
57 ------
57 ------
58
58
59 Markers are stored in an append-only file stored in
59 Markers are stored in an append-only file stored in
60 '.hg/store/obsstore'.
60 '.hg/store/obsstore'.
61
61
62 The file starts with a version header:
62 The file starts with a version header:
63
63
64 - 1 unsigned byte: version number, starting at zero.
64 - 1 unsigned byte: version number, starting at zero.
65
65
66 The header is followed by the markers. Marker format depend of the version. See
66 The header is followed by the markers. Marker format depend of the version. See
67 comment associated with each format for details.
67 comment associated with each format for details.
68
68
69 """
69 """
70 from __future__ import absolute_import
70 from __future__ import absolute_import
71
71
72 import errno
72 import errno
73 import struct
73 import struct
74
74
75 from .i18n import _
75 from .i18n import _
76 from . import (
76 from . import (
77 encoding,
77 encoding,
78 error,
78 error,
79 node,
79 node,
80 obsutil,
80 obsutil,
81 phases,
81 phases,
82 policy,
82 policy,
83 pycompat,
83 util,
84 util,
84 )
85 )
85 from .utils import dateutil
86 from .utils import dateutil
86
87
87 parsers = policy.importmod(r'parsers')
88 parsers = policy.importmod(r'parsers')
88
89
89 _pack = struct.pack
90 _pack = struct.pack
90 _unpack = struct.unpack
91 _unpack = struct.unpack
91 _calcsize = struct.calcsize
92 _calcsize = struct.calcsize
92 propertycache = util.propertycache
93 propertycache = util.propertycache
93
94
94 # the obsolete feature is not mature enough to be enabled by default.
95 # the obsolete feature is not mature enough to be enabled by default.
95 # you have to rely on third party extension extension to enable this.
96 # you have to rely on third party extension extension to enable this.
96 _enabled = False
97 _enabled = False
97
98
98 # Options for obsolescence
99 # Options for obsolescence
99 createmarkersopt = 'createmarkers'
100 createmarkersopt = 'createmarkers'
100 allowunstableopt = 'allowunstable'
101 allowunstableopt = 'allowunstable'
101 exchangeopt = 'exchange'
102 exchangeopt = 'exchange'
102
103
103 def _getoptionvalue(repo, option):
104 def _getoptionvalue(repo, option):
104 """Returns True if the given repository has the given obsolete option
105 """Returns True if the given repository has the given obsolete option
105 enabled.
106 enabled.
106 """
107 """
107 configkey = 'evolution.%s' % option
108 configkey = 'evolution.%s' % option
108 newconfig = repo.ui.configbool('experimental', configkey)
109 newconfig = repo.ui.configbool('experimental', configkey)
109
110
110 # Return the value only if defined
111 # Return the value only if defined
111 if newconfig is not None:
112 if newconfig is not None:
112 return newconfig
113 return newconfig
113
114
114 # Fallback on generic option
115 # Fallback on generic option
115 try:
116 try:
116 return repo.ui.configbool('experimental', 'evolution')
117 return repo.ui.configbool('experimental', 'evolution')
117 except (error.ConfigError, AttributeError):
118 except (error.ConfigError, AttributeError):
118 # Fallback on old-fashion config
119 # Fallback on old-fashion config
119 # inconsistent config: experimental.evolution
120 # inconsistent config: experimental.evolution
120 result = set(repo.ui.configlist('experimental', 'evolution'))
121 result = set(repo.ui.configlist('experimental', 'evolution'))
121
122
122 if 'all' in result:
123 if 'all' in result:
123 return True
124 return True
124
125
125 # For migration purposes, temporarily return true if the config hasn't
126 # For migration purposes, temporarily return true if the config hasn't
126 # been set but _enabled is true.
127 # been set but _enabled is true.
127 if len(result) == 0 and _enabled:
128 if len(result) == 0 and _enabled:
128 return True
129 return True
129
130
130 # Temporary hack for next check
131 # Temporary hack for next check
131 newconfig = repo.ui.config('experimental', 'evolution.createmarkers')
132 newconfig = repo.ui.config('experimental', 'evolution.createmarkers')
132 if newconfig:
133 if newconfig:
133 result.add('createmarkers')
134 result.add('createmarkers')
134
135
135 return option in result
136 return option in result
136
137
137 def getoptions(repo):
138 def getoptions(repo):
138 """Returns dicts showing state of obsolescence features."""
139 """Returns dicts showing state of obsolescence features."""
139
140
140 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
141 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
141 unstablevalue = _getoptionvalue(repo, allowunstableopt)
142 unstablevalue = _getoptionvalue(repo, allowunstableopt)
142 exchangevalue = _getoptionvalue(repo, exchangeopt)
143 exchangevalue = _getoptionvalue(repo, exchangeopt)
143
144
144 # createmarkers must be enabled if other options are enabled
145 # createmarkers must be enabled if other options are enabled
145 if ((unstablevalue or exchangevalue) and not createmarkersvalue):
146 if ((unstablevalue or exchangevalue) and not createmarkersvalue):
146 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
147 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
147 "if other obsolete options are enabled"))
148 "if other obsolete options are enabled"))
148
149
149 return {
150 return {
150 createmarkersopt: createmarkersvalue,
151 createmarkersopt: createmarkersvalue,
151 allowunstableopt: unstablevalue,
152 allowunstableopt: unstablevalue,
152 exchangeopt: exchangevalue,
153 exchangeopt: exchangevalue,
153 }
154 }
154
155
155 def isenabled(repo, option):
156 def isenabled(repo, option):
156 """Returns True if the given repository has the given obsolete option
157 """Returns True if the given repository has the given obsolete option
157 enabled.
158 enabled.
158 """
159 """
159 return getoptions(repo)[option]
160 return getoptions(repo)[option]
160
161
161 # Creating aliases for marker flags because evolve extension looks for
162 # Creating aliases for marker flags because evolve extension looks for
162 # bumpedfix in obsolete.py
163 # bumpedfix in obsolete.py
163 bumpedfix = obsutil.bumpedfix
164 bumpedfix = obsutil.bumpedfix
164 usingsha256 = obsutil.usingsha256
165 usingsha256 = obsutil.usingsha256
165
166
166 ## Parsing and writing of version "0"
167 ## Parsing and writing of version "0"
167 #
168 #
168 # The header is followed by the markers. Each marker is made of:
169 # The header is followed by the markers. Each marker is made of:
169 #
170 #
170 # - 1 uint8 : number of new changesets "N", can be zero.
171 # - 1 uint8 : number of new changesets "N", can be zero.
171 #
172 #
172 # - 1 uint32: metadata size "M" in bytes.
173 # - 1 uint32: metadata size "M" in bytes.
173 #
174 #
174 # - 1 byte: a bit field. It is reserved for flags used in common
175 # - 1 byte: a bit field. It is reserved for flags used in common
175 # obsolete marker operations, to avoid repeated decoding of metadata
176 # obsolete marker operations, to avoid repeated decoding of metadata
176 # entries.
177 # entries.
177 #
178 #
178 # - 20 bytes: obsoleted changeset identifier.
179 # - 20 bytes: obsoleted changeset identifier.
179 #
180 #
180 # - N*20 bytes: new changesets identifiers.
181 # - N*20 bytes: new changesets identifiers.
181 #
182 #
182 # - M bytes: metadata as a sequence of nul-terminated strings. Each
183 # - M bytes: metadata as a sequence of nul-terminated strings. Each
183 # string contains a key and a value, separated by a colon ':', without
184 # string contains a key and a value, separated by a colon ':', without
184 # additional encoding. Keys cannot contain '\0' or ':' and values
185 # additional encoding. Keys cannot contain '\0' or ':' and values
185 # cannot contain '\0'.
186 # cannot contain '\0'.
186 _fm0version = 0
187 _fm0version = 0
187 _fm0fixed = '>BIB20s'
188 _fm0fixed = '>BIB20s'
188 _fm0node = '20s'
189 _fm0node = '20s'
189 _fm0fsize = _calcsize(_fm0fixed)
190 _fm0fsize = _calcsize(_fm0fixed)
190 _fm0fnodesize = _calcsize(_fm0node)
191 _fm0fnodesize = _calcsize(_fm0node)
191
192
192 def _fm0readmarkers(data, off, stop):
193 def _fm0readmarkers(data, off, stop):
193 # Loop on markers
194 # Loop on markers
194 while off < stop:
195 while off < stop:
195 # read fixed part
196 # read fixed part
196 cur = data[off:off + _fm0fsize]
197 cur = data[off:off + _fm0fsize]
197 off += _fm0fsize
198 off += _fm0fsize
198 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
199 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
199 # read replacement
200 # read replacement
200 sucs = ()
201 sucs = ()
201 if numsuc:
202 if numsuc:
202 s = (_fm0fnodesize * numsuc)
203 s = (_fm0fnodesize * numsuc)
203 cur = data[off:off + s]
204 cur = data[off:off + s]
204 sucs = _unpack(_fm0node * numsuc, cur)
205 sucs = _unpack(_fm0node * numsuc, cur)
205 off += s
206 off += s
206 # read metadata
207 # read metadata
207 # (metadata will be decoded on demand)
208 # (metadata will be decoded on demand)
208 metadata = data[off:off + mdsize]
209 metadata = data[off:off + mdsize]
209 if len(metadata) != mdsize:
210 if len(metadata) != mdsize:
210 raise error.Abort(_('parsing obsolete marker: metadata is too '
211 raise error.Abort(_('parsing obsolete marker: metadata is too '
211 'short, %d bytes expected, got %d')
212 'short, %d bytes expected, got %d')
212 % (mdsize, len(metadata)))
213 % (mdsize, len(metadata)))
213 off += mdsize
214 off += mdsize
214 metadata = _fm0decodemeta(metadata)
215 metadata = _fm0decodemeta(metadata)
215 try:
216 try:
216 when, offset = metadata.pop('date', '0 0').split(' ')
217 when, offset = metadata.pop('date', '0 0').split(' ')
217 date = float(when), int(offset)
218 date = float(when), int(offset)
218 except ValueError:
219 except ValueError:
219 date = (0., 0)
220 date = (0., 0)
220 parents = None
221 parents = None
221 if 'p2' in metadata:
222 if 'p2' in metadata:
222 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
223 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
223 elif 'p1' in metadata:
224 elif 'p1' in metadata:
224 parents = (metadata.pop('p1', None),)
225 parents = (metadata.pop('p1', None),)
225 elif 'p0' in metadata:
226 elif 'p0' in metadata:
226 parents = ()
227 parents = ()
227 if parents is not None:
228 if parents is not None:
228 try:
229 try:
229 parents = tuple(node.bin(p) for p in parents)
230 parents = tuple(node.bin(p) for p in parents)
230 # if parent content is not a nodeid, drop the data
231 # if parent content is not a nodeid, drop the data
231 for p in parents:
232 for p in parents:
232 if len(p) != 20:
233 if len(p) != 20:
233 parents = None
234 parents = None
234 break
235 break
235 except TypeError:
236 except TypeError:
236 # if content cannot be translated to nodeid drop the data.
237 # if content cannot be translated to nodeid drop the data.
237 parents = None
238 parents = None
238
239
239 metadata = tuple(sorted(metadata.iteritems()))
240 metadata = tuple(sorted(metadata.iteritems()))
240
241
241 yield (pre, sucs, flags, metadata, date, parents)
242 yield (pre, sucs, flags, metadata, date, parents)
242
243
243 def _fm0encodeonemarker(marker):
244 def _fm0encodeonemarker(marker):
244 pre, sucs, flags, metadata, date, parents = marker
245 pre, sucs, flags, metadata, date, parents = marker
245 if flags & usingsha256:
246 if flags & usingsha256:
246 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
247 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
247 metadata = dict(metadata)
248 metadata = dict(metadata)
248 time, tz = date
249 time, tz = date
249 metadata['date'] = '%r %i' % (time, tz)
250 metadata['date'] = '%r %i' % (time, tz)
250 if parents is not None:
251 if parents is not None:
251 if not parents:
252 if not parents:
252 # mark that we explicitly recorded no parents
253 # mark that we explicitly recorded no parents
253 metadata['p0'] = ''
254 metadata['p0'] = ''
254 for i, p in enumerate(parents, 1):
255 for i, p in enumerate(parents, 1):
255 metadata['p%i' % i] = node.hex(p)
256 metadata['p%i' % i] = node.hex(p)
256 metadata = _fm0encodemeta(metadata)
257 metadata = _fm0encodemeta(metadata)
257 numsuc = len(sucs)
258 numsuc = len(sucs)
258 format = _fm0fixed + (_fm0node * numsuc)
259 format = _fm0fixed + (_fm0node * numsuc)
259 data = [numsuc, len(metadata), flags, pre]
260 data = [numsuc, len(metadata), flags, pre]
260 data.extend(sucs)
261 data.extend(sucs)
261 return _pack(format, *data) + metadata
262 return _pack(format, *data) + metadata
262
263
263 def _fm0encodemeta(meta):
264 def _fm0encodemeta(meta):
264 """Return encoded metadata string to string mapping.
265 """Return encoded metadata string to string mapping.
265
266
266 Assume no ':' in key and no '\0' in both key and value."""
267 Assume no ':' in key and no '\0' in both key and value."""
267 for key, value in meta.iteritems():
268 for key, value in meta.iteritems():
268 if ':' in key or '\0' in key:
269 if ':' in key or '\0' in key:
269 raise ValueError("':' and '\0' are forbidden in metadata key'")
270 raise ValueError("':' and '\0' are forbidden in metadata key'")
270 if '\0' in value:
271 if '\0' in value:
271 raise ValueError("':' is forbidden in metadata value'")
272 raise ValueError("':' is forbidden in metadata value'")
272 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
273 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
273
274
274 def _fm0decodemeta(data):
275 def _fm0decodemeta(data):
275 """Return string to string dictionary from encoded version."""
276 """Return string to string dictionary from encoded version."""
276 d = {}
277 d = {}
277 for l in data.split('\0'):
278 for l in data.split('\0'):
278 if l:
279 if l:
279 key, value = l.split(':')
280 key, value = l.split(':')
280 d[key] = value
281 d[key] = value
281 return d
282 return d
282
283
283 ## Parsing and writing of version "1"
284 ## Parsing and writing of version "1"
284 #
285 #
285 # The header is followed by the markers. Each marker is made of:
286 # The header is followed by the markers. Each marker is made of:
286 #
287 #
287 # - uint32: total size of the marker (including this field)
288 # - uint32: total size of the marker (including this field)
288 #
289 #
289 # - float64: date in seconds since epoch
290 # - float64: date in seconds since epoch
290 #
291 #
291 # - int16: timezone offset in minutes
292 # - int16: timezone offset in minutes
292 #
293 #
293 # - uint16: a bit field. It is reserved for flags used in common
294 # - uint16: a bit field. It is reserved for flags used in common
294 # obsolete marker operations, to avoid repeated decoding of metadata
295 # obsolete marker operations, to avoid repeated decoding of metadata
295 # entries.
296 # entries.
296 #
297 #
297 # - uint8: number of successors "N", can be zero.
298 # - uint8: number of successors "N", can be zero.
298 #
299 #
299 # - uint8: number of parents "P", can be zero.
300 # - uint8: number of parents "P", can be zero.
300 #
301 #
301 # 0: parents data stored but no parent,
302 # 0: parents data stored but no parent,
302 # 1: one parent stored,
303 # 1: one parent stored,
303 # 2: two parents stored,
304 # 2: two parents stored,
304 # 3: no parent data stored
305 # 3: no parent data stored
305 #
306 #
306 # - uint8: number of metadata entries M
307 # - uint8: number of metadata entries M
307 #
308 #
308 # - 20 or 32 bytes: predecessor changeset identifier.
309 # - 20 or 32 bytes: predecessor changeset identifier.
309 #
310 #
310 # - N*(20 or 32) bytes: successors changesets identifiers.
311 # - N*(20 or 32) bytes: successors changesets identifiers.
311 #
312 #
312 # - P*(20 or 32) bytes: parents of the predecessors changesets.
313 # - P*(20 or 32) bytes: parents of the predecessors changesets.
313 #
314 #
314 # - M*(uint8, uint8): size of all metadata entries (key and value)
315 # - M*(uint8, uint8): size of all metadata entries (key and value)
315 #
316 #
316 # - remaining bytes: the metadata, each (key, value) pair after the other.
317 # - remaining bytes: the metadata, each (key, value) pair after the other.
317 _fm1version = 1
318 _fm1version = 1
318 _fm1fixed = '>IdhHBBB20s'
319 _fm1fixed = '>IdhHBBB20s'
319 _fm1nodesha1 = '20s'
320 _fm1nodesha1 = '20s'
320 _fm1nodesha256 = '32s'
321 _fm1nodesha256 = '32s'
321 _fm1nodesha1size = _calcsize(_fm1nodesha1)
322 _fm1nodesha1size = _calcsize(_fm1nodesha1)
322 _fm1nodesha256size = _calcsize(_fm1nodesha256)
323 _fm1nodesha256size = _calcsize(_fm1nodesha256)
323 _fm1fsize = _calcsize(_fm1fixed)
324 _fm1fsize = _calcsize(_fm1fixed)
324 _fm1parentnone = 3
325 _fm1parentnone = 3
325 _fm1parentshift = 14
326 _fm1parentshift = 14
326 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
327 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
327 _fm1metapair = 'BB'
328 _fm1metapair = 'BB'
328 _fm1metapairsize = _calcsize(_fm1metapair)
329 _fm1metapairsize = _calcsize(_fm1metapair)
329
330
330 def _fm1purereadmarkers(data, off, stop):
331 def _fm1purereadmarkers(data, off, stop):
331 # make some global constants local for performance
332 # make some global constants local for performance
332 noneflag = _fm1parentnone
333 noneflag = _fm1parentnone
333 sha2flag = usingsha256
334 sha2flag = usingsha256
334 sha1size = _fm1nodesha1size
335 sha1size = _fm1nodesha1size
335 sha2size = _fm1nodesha256size
336 sha2size = _fm1nodesha256size
336 sha1fmt = _fm1nodesha1
337 sha1fmt = _fm1nodesha1
337 sha2fmt = _fm1nodesha256
338 sha2fmt = _fm1nodesha256
338 metasize = _fm1metapairsize
339 metasize = _fm1metapairsize
339 metafmt = _fm1metapair
340 metafmt = _fm1metapair
340 fsize = _fm1fsize
341 fsize = _fm1fsize
341 unpack = _unpack
342 unpack = _unpack
342
343
343 # Loop on markers
344 # Loop on markers
344 ufixed = struct.Struct(_fm1fixed).unpack
345 ufixed = struct.Struct(_fm1fixed).unpack
345
346
346 while off < stop:
347 while off < stop:
347 # read fixed part
348 # read fixed part
348 o1 = off + fsize
349 o1 = off + fsize
349 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
350 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
350
351
351 if flags & sha2flag:
352 if flags & sha2flag:
352 # FIXME: prec was read as a SHA1, needs to be amended
353 # FIXME: prec was read as a SHA1, needs to be amended
353
354
354 # read 0 or more successors
355 # read 0 or more successors
355 if numsuc == 1:
356 if numsuc == 1:
356 o2 = o1 + sha2size
357 o2 = o1 + sha2size
357 sucs = (data[o1:o2],)
358 sucs = (data[o1:o2],)
358 else:
359 else:
359 o2 = o1 + sha2size * numsuc
360 o2 = o1 + sha2size * numsuc
360 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
361 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
361
362
362 # read parents
363 # read parents
363 if numpar == noneflag:
364 if numpar == noneflag:
364 o3 = o2
365 o3 = o2
365 parents = None
366 parents = None
366 elif numpar == 1:
367 elif numpar == 1:
367 o3 = o2 + sha2size
368 o3 = o2 + sha2size
368 parents = (data[o2:o3],)
369 parents = (data[o2:o3],)
369 else:
370 else:
370 o3 = o2 + sha2size * numpar
371 o3 = o2 + sha2size * numpar
371 parents = unpack(sha2fmt * numpar, data[o2:o3])
372 parents = unpack(sha2fmt * numpar, data[o2:o3])
372 else:
373 else:
373 # read 0 or more successors
374 # read 0 or more successors
374 if numsuc == 1:
375 if numsuc == 1:
375 o2 = o1 + sha1size
376 o2 = o1 + sha1size
376 sucs = (data[o1:o2],)
377 sucs = (data[o1:o2],)
377 else:
378 else:
378 o2 = o1 + sha1size * numsuc
379 o2 = o1 + sha1size * numsuc
379 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
380 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
380
381
381 # read parents
382 # read parents
382 if numpar == noneflag:
383 if numpar == noneflag:
383 o3 = o2
384 o3 = o2
384 parents = None
385 parents = None
385 elif numpar == 1:
386 elif numpar == 1:
386 o3 = o2 + sha1size
387 o3 = o2 + sha1size
387 parents = (data[o2:o3],)
388 parents = (data[o2:o3],)
388 else:
389 else:
389 o3 = o2 + sha1size * numpar
390 o3 = o2 + sha1size * numpar
390 parents = unpack(sha1fmt * numpar, data[o2:o3])
391 parents = unpack(sha1fmt * numpar, data[o2:o3])
391
392
392 # read metadata
393 # read metadata
393 off = o3 + metasize * nummeta
394 off = o3 + metasize * nummeta
394 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
395 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
395 metadata = []
396 metadata = []
396 for idx in xrange(0, len(metapairsize), 2):
397 for idx in xrange(0, len(metapairsize), 2):
397 o1 = off + metapairsize[idx]
398 o1 = off + metapairsize[idx]
398 o2 = o1 + metapairsize[idx + 1]
399 o2 = o1 + metapairsize[idx + 1]
399 metadata.append((data[off:o1], data[o1:o2]))
400 metadata.append((data[off:o1], data[o1:o2]))
400 off = o2
401 off = o2
401
402
402 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
403 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
403
404
404 def _fm1encodeonemarker(marker):
405 def _fm1encodeonemarker(marker):
405 pre, sucs, flags, metadata, date, parents = marker
406 pre, sucs, flags, metadata, date, parents = marker
406 # determine node size
407 # determine node size
407 _fm1node = _fm1nodesha1
408 _fm1node = _fm1nodesha1
408 if flags & usingsha256:
409 if flags & usingsha256:
409 _fm1node = _fm1nodesha256
410 _fm1node = _fm1nodesha256
410 numsuc = len(sucs)
411 numsuc = len(sucs)
411 numextranodes = numsuc
412 numextranodes = numsuc
412 if parents is None:
413 if parents is None:
413 numpar = _fm1parentnone
414 numpar = _fm1parentnone
414 else:
415 else:
415 numpar = len(parents)
416 numpar = len(parents)
416 numextranodes += numpar
417 numextranodes += numpar
417 formatnodes = _fm1node * numextranodes
418 formatnodes = _fm1node * numextranodes
418 formatmeta = _fm1metapair * len(metadata)
419 formatmeta = _fm1metapair * len(metadata)
419 format = _fm1fixed + formatnodes + formatmeta
420 format = _fm1fixed + formatnodes + formatmeta
420 # tz is stored in minutes so we divide by 60
421 # tz is stored in minutes so we divide by 60
421 tz = date[1]//60
422 tz = date[1]//60
422 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
423 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
423 data.extend(sucs)
424 data.extend(sucs)
424 if parents is not None:
425 if parents is not None:
425 data.extend(parents)
426 data.extend(parents)
426 totalsize = _calcsize(format)
427 totalsize = _calcsize(format)
427 for key, value in metadata:
428 for key, value in metadata:
428 lk = len(key)
429 lk = len(key)
429 lv = len(value)
430 lv = len(value)
430 if lk > 255:
431 if lk > 255:
431 msg = ('obsstore metadata key cannot be longer than 255 bytes'
432 msg = ('obsstore metadata key cannot be longer than 255 bytes'
432 ' (key "%s" is %u bytes)') % (key, lk)
433 ' (key "%s" is %u bytes)') % (key, lk)
433 raise error.ProgrammingError(msg)
434 raise error.ProgrammingError(msg)
434 if lv > 255:
435 if lv > 255:
435 msg = ('obsstore metadata value cannot be longer than 255 bytes'
436 msg = ('obsstore metadata value cannot be longer than 255 bytes'
436 ' (value "%s" for key "%s" is %u bytes)') % (value, key, lv)
437 ' (value "%s" for key "%s" is %u bytes)') % (value, key, lv)
437 raise error.ProgrammingError(msg)
438 raise error.ProgrammingError(msg)
438 data.append(lk)
439 data.append(lk)
439 data.append(lv)
440 data.append(lv)
440 totalsize += lk + lv
441 totalsize += lk + lv
441 data[0] = totalsize
442 data[0] = totalsize
442 data = [_pack(format, *data)]
443 data = [_pack(format, *data)]
443 for key, value in metadata:
444 for key, value in metadata:
444 data.append(key)
445 data.append(key)
445 data.append(value)
446 data.append(value)
446 return ''.join(data)
447 return ''.join(data)
447
448
448 def _fm1readmarkers(data, off, stop):
449 def _fm1readmarkers(data, off, stop):
449 native = getattr(parsers, 'fm1readmarkers', None)
450 native = getattr(parsers, 'fm1readmarkers', None)
450 if not native:
451 if not native:
451 return _fm1purereadmarkers(data, off, stop)
452 return _fm1purereadmarkers(data, off, stop)
452 return native(data, off, stop)
453 return native(data, off, stop)
453
454
454 # mapping to read/write various marker formats
455 # mapping to read/write various marker formats
455 # <version> -> (decoder, encoder)
456 # <version> -> (decoder, encoder)
456 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
457 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
457 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
458 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
458
459
459 def _readmarkerversion(data):
460 def _readmarkerversion(data):
460 return _unpack('>B', data[0:1])[0]
461 return _unpack('>B', data[0:1])[0]
461
462
462 @util.nogc
463 @util.nogc
463 def _readmarkers(data, off=None, stop=None):
464 def _readmarkers(data, off=None, stop=None):
464 """Read and enumerate markers from raw data"""
465 """Read and enumerate markers from raw data"""
465 diskversion = _readmarkerversion(data)
466 diskversion = _readmarkerversion(data)
466 if not off:
467 if not off:
467 off = 1 # skip 1 byte version number
468 off = 1 # skip 1 byte version number
468 if stop is None:
469 if stop is None:
469 stop = len(data)
470 stop = len(data)
470 if diskversion not in formats:
471 if diskversion not in formats:
471 msg = _('parsing obsolete marker: unknown version %r') % diskversion
472 msg = _('parsing obsolete marker: unknown version %r') % diskversion
472 raise error.UnknownVersion(msg, version=diskversion)
473 raise error.UnknownVersion(msg, version=diskversion)
473 return diskversion, formats[diskversion][0](data, off, stop)
474 return diskversion, formats[diskversion][0](data, off, stop)
474
475
475 def encodeheader(version=_fm0version):
476 def encodeheader(version=_fm0version):
476 return _pack('>B', version)
477 return _pack('>B', version)
477
478
478 def encodemarkers(markers, addheader=False, version=_fm0version):
479 def encodemarkers(markers, addheader=False, version=_fm0version):
479 # Kept separate from flushmarkers(), it will be reused for
480 # Kept separate from flushmarkers(), it will be reused for
480 # markers exchange.
481 # markers exchange.
481 encodeone = formats[version][1]
482 encodeone = formats[version][1]
482 if addheader:
483 if addheader:
483 yield encodeheader(version)
484 yield encodeheader(version)
484 for marker in markers:
485 for marker in markers:
485 yield encodeone(marker)
486 yield encodeone(marker)
486
487
487 @util.nogc
488 @util.nogc
488 def _addsuccessors(successors, markers):
489 def _addsuccessors(successors, markers):
489 for mark in markers:
490 for mark in markers:
490 successors.setdefault(mark[0], set()).add(mark)
491 successors.setdefault(mark[0], set()).add(mark)
491
492
492 @util.nogc
493 @util.nogc
493 def _addpredecessors(predecessors, markers):
494 def _addpredecessors(predecessors, markers):
494 for mark in markers:
495 for mark in markers:
495 for suc in mark[1]:
496 for suc in mark[1]:
496 predecessors.setdefault(suc, set()).add(mark)
497 predecessors.setdefault(suc, set()).add(mark)
497
498
498 @util.nogc
499 @util.nogc
499 def _addchildren(children, markers):
500 def _addchildren(children, markers):
500 for mark in markers:
501 for mark in markers:
501 parents = mark[5]
502 parents = mark[5]
502 if parents is not None:
503 if parents is not None:
503 for p in parents:
504 for p in parents:
504 children.setdefault(p, set()).add(mark)
505 children.setdefault(p, set()).add(mark)
505
506
506 def _checkinvalidmarkers(markers):
507 def _checkinvalidmarkers(markers):
507 """search for marker with invalid data and raise error if needed
508 """search for marker with invalid data and raise error if needed
508
509
509 Exist as a separated function to allow the evolve extension for a more
510 Exist as a separated function to allow the evolve extension for a more
510 subtle handling.
511 subtle handling.
511 """
512 """
512 for mark in markers:
513 for mark in markers:
513 if node.nullid in mark[1]:
514 if node.nullid in mark[1]:
514 raise error.Abort(_('bad obsolescence marker detected: '
515 raise error.Abort(_('bad obsolescence marker detected: '
515 'invalid successors nullid'))
516 'invalid successors nullid'))
516
517
517 class obsstore(object):
518 class obsstore(object):
518 """Store obsolete markers
519 """Store obsolete markers
519
520
520 Markers can be accessed with two mappings:
521 Markers can be accessed with two mappings:
521 - predecessors[x] -> set(markers on predecessors edges of x)
522 - predecessors[x] -> set(markers on predecessors edges of x)
522 - successors[x] -> set(markers on successors edges of x)
523 - successors[x] -> set(markers on successors edges of x)
523 - children[x] -> set(markers on predecessors edges of children(x)
524 - children[x] -> set(markers on predecessors edges of children(x)
524 """
525 """
525
526
526 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
527 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
527 # prec: nodeid, predecessors changesets
528 # prec: nodeid, predecessors changesets
528 # succs: tuple of nodeid, successor changesets (0-N length)
529 # succs: tuple of nodeid, successor changesets (0-N length)
529 # flag: integer, flag field carrying modifier for the markers (see doc)
530 # flag: integer, flag field carrying modifier for the markers (see doc)
530 # meta: binary blob in UTF-8, encoded metadata dictionary
531 # meta: binary blob in UTF-8, encoded metadata dictionary
531 # date: (float, int) tuple, date of marker creation
532 # date: (float, int) tuple, date of marker creation
532 # parents: (tuple of nodeid) or None, parents of predecessors
533 # parents: (tuple of nodeid) or None, parents of predecessors
533 # None is used when no data has been recorded
534 # None is used when no data has been recorded
534
535
535 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
536 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
536 # caches for various obsolescence related cache
537 # caches for various obsolescence related cache
537 self.caches = {}
538 self.caches = {}
538 self.svfs = svfs
539 self.svfs = svfs
539 self._defaultformat = defaultformat
540 self._defaultformat = defaultformat
540 self._readonly = readonly
541 self._readonly = readonly
541
542
542 def __iter__(self):
543 def __iter__(self):
543 return iter(self._all)
544 return iter(self._all)
544
545
545 def __len__(self):
546 def __len__(self):
546 return len(self._all)
547 return len(self._all)
547
548
548 def __nonzero__(self):
549 def __nonzero__(self):
549 if not self._cached(r'_all'):
550 if not self._cached(r'_all'):
550 try:
551 try:
551 return self.svfs.stat('obsstore').st_size > 1
552 return self.svfs.stat('obsstore').st_size > 1
552 except OSError as inst:
553 except OSError as inst:
553 if inst.errno != errno.ENOENT:
554 if inst.errno != errno.ENOENT:
554 raise
555 raise
555 # just build an empty _all list if no obsstore exists, which
556 # just build an empty _all list if no obsstore exists, which
556 # avoids further stat() syscalls
557 # avoids further stat() syscalls
557 return bool(self._all)
558 return bool(self._all)
558
559
559 __bool__ = __nonzero__
560 __bool__ = __nonzero__
560
561
561 @property
562 @property
562 def readonly(self):
563 def readonly(self):
563 """True if marker creation is disabled
564 """True if marker creation is disabled
564
565
565 Remove me in the future when obsolete marker is always on."""
566 Remove me in the future when obsolete marker is always on."""
566 return self._readonly
567 return self._readonly
567
568
568 def create(self, transaction, prec, succs=(), flag=0, parents=None,
569 def create(self, transaction, prec, succs=(), flag=0, parents=None,
569 date=None, metadata=None, ui=None):
570 date=None, metadata=None, ui=None):
570 """obsolete: add a new obsolete marker
571 """obsolete: add a new obsolete marker
571
572
572 * ensuring it is hashable
573 * ensuring it is hashable
573 * check mandatory metadata
574 * check mandatory metadata
574 * encode metadata
575 * encode metadata
575
576
576 If you are a human writing code creating marker you want to use the
577 If you are a human writing code creating marker you want to use the
577 `createmarkers` function in this module instead.
578 `createmarkers` function in this module instead.
578
579
579 return True if a new marker have been added, False if the markers
580 return True if a new marker have been added, False if the markers
580 already existed (no op).
581 already existed (no op).
581 """
582 """
582 if metadata is None:
583 if metadata is None:
583 metadata = {}
584 metadata = {}
584 if date is None:
585 if date is None:
585 if 'date' in metadata:
586 if 'date' in metadata:
586 # as a courtesy for out-of-tree extensions
587 # as a courtesy for out-of-tree extensions
587 date = dateutil.parsedate(metadata.pop('date'))
588 date = dateutil.parsedate(metadata.pop('date'))
588 elif ui is not None:
589 elif ui is not None:
589 date = ui.configdate('devel', 'default-date')
590 date = ui.configdate('devel', 'default-date')
590 if date is None:
591 if date is None:
591 date = dateutil.makedate()
592 date = dateutil.makedate()
592 else:
593 else:
593 date = dateutil.makedate()
594 date = dateutil.makedate()
594 if len(prec) != 20:
595 if len(prec) != 20:
595 raise ValueError(prec)
596 raise ValueError(prec)
596 for succ in succs:
597 for succ in succs:
597 if len(succ) != 20:
598 if len(succ) != 20:
598 raise ValueError(succ)
599 raise ValueError(succ)
599 if prec in succs:
600 if prec in succs:
600 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
601 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
601
602
602 metadata = tuple(sorted(metadata.iteritems()))
603 metadata = tuple(sorted(metadata.iteritems()))
604 for k, v in metadata:
605 try:
606 # might be better to reject non-ASCII keys
607 k.decode('utf-8')
608 v.decode('utf-8')
609 except UnicodeDecodeError:
610 raise error.ProgrammingError(
611 'obsstore metadata must be valid UTF-8 sequence '
612 '(key = %r, value = %r)'
613 % (pycompat.bytestr(k), pycompat.bytestr(v)))
603
614
604 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
615 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
605 return bool(self.add(transaction, [marker]))
616 return bool(self.add(transaction, [marker]))
606
617
607 def add(self, transaction, markers):
618 def add(self, transaction, markers):
608 """Add new markers to the store
619 """Add new markers to the store
609
620
610 Take care of filtering duplicate.
621 Take care of filtering duplicate.
611 Return the number of new marker."""
622 Return the number of new marker."""
612 if self._readonly:
623 if self._readonly:
613 raise error.Abort(_('creating obsolete markers is not enabled on '
624 raise error.Abort(_('creating obsolete markers is not enabled on '
614 'this repo'))
625 'this repo'))
615 known = set()
626 known = set()
616 getsuccessors = self.successors.get
627 getsuccessors = self.successors.get
617 new = []
628 new = []
618 for m in markers:
629 for m in markers:
619 if m not in getsuccessors(m[0], ()) and m not in known:
630 if m not in getsuccessors(m[0], ()) and m not in known:
620 known.add(m)
631 known.add(m)
621 new.append(m)
632 new.append(m)
622 if new:
633 if new:
623 f = self.svfs('obsstore', 'ab')
634 f = self.svfs('obsstore', 'ab')
624 try:
635 try:
625 offset = f.tell()
636 offset = f.tell()
626 transaction.add('obsstore', offset)
637 transaction.add('obsstore', offset)
627 # offset == 0: new file - add the version header
638 # offset == 0: new file - add the version header
628 data = b''.join(encodemarkers(new, offset == 0, self._version))
639 data = b''.join(encodemarkers(new, offset == 0, self._version))
629 f.write(data)
640 f.write(data)
630 finally:
641 finally:
631 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
642 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
632 # call 'filecacheentry.refresh()' here
643 # call 'filecacheentry.refresh()' here
633 f.close()
644 f.close()
634 addedmarkers = transaction.changes.get('obsmarkers')
645 addedmarkers = transaction.changes.get('obsmarkers')
635 if addedmarkers is not None:
646 if addedmarkers is not None:
636 addedmarkers.update(new)
647 addedmarkers.update(new)
637 self._addmarkers(new, data)
648 self._addmarkers(new, data)
638 # new marker *may* have changed several set. invalidate the cache.
649 # new marker *may* have changed several set. invalidate the cache.
639 self.caches.clear()
650 self.caches.clear()
640 # records the number of new markers for the transaction hooks
651 # records the number of new markers for the transaction hooks
641 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
652 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
642 transaction.hookargs['new_obsmarkers'] = '%d' % (previous + len(new))
653 transaction.hookargs['new_obsmarkers'] = '%d' % (previous + len(new))
643 return len(new)
654 return len(new)
644
655
645 def mergemarkers(self, transaction, data):
656 def mergemarkers(self, transaction, data):
646 """merge a binary stream of markers inside the obsstore
657 """merge a binary stream of markers inside the obsstore
647
658
648 Returns the number of new markers added."""
659 Returns the number of new markers added."""
649 version, markers = _readmarkers(data)
660 version, markers = _readmarkers(data)
650 return self.add(transaction, markers)
661 return self.add(transaction, markers)
651
662
652 @propertycache
663 @propertycache
653 def _data(self):
664 def _data(self):
654 return self.svfs.tryread('obsstore')
665 return self.svfs.tryread('obsstore')
655
666
656 @propertycache
667 @propertycache
657 def _version(self):
668 def _version(self):
658 if len(self._data) >= 1:
669 if len(self._data) >= 1:
659 return _readmarkerversion(self._data)
670 return _readmarkerversion(self._data)
660 else:
671 else:
661 return self._defaultformat
672 return self._defaultformat
662
673
663 @propertycache
674 @propertycache
664 def _all(self):
675 def _all(self):
665 data = self._data
676 data = self._data
666 if not data:
677 if not data:
667 return []
678 return []
668 self._version, markers = _readmarkers(data)
679 self._version, markers = _readmarkers(data)
669 markers = list(markers)
680 markers = list(markers)
670 _checkinvalidmarkers(markers)
681 _checkinvalidmarkers(markers)
671 return markers
682 return markers
672
683
673 @propertycache
684 @propertycache
674 def successors(self):
685 def successors(self):
675 successors = {}
686 successors = {}
676 _addsuccessors(successors, self._all)
687 _addsuccessors(successors, self._all)
677 return successors
688 return successors
678
689
679 @propertycache
690 @propertycache
680 def predecessors(self):
691 def predecessors(self):
681 predecessors = {}
692 predecessors = {}
682 _addpredecessors(predecessors, self._all)
693 _addpredecessors(predecessors, self._all)
683 return predecessors
694 return predecessors
684
695
685 @propertycache
696 @propertycache
686 def children(self):
697 def children(self):
687 children = {}
698 children = {}
688 _addchildren(children, self._all)
699 _addchildren(children, self._all)
689 return children
700 return children
690
701
691 def _cached(self, attr):
702 def _cached(self, attr):
692 return attr in self.__dict__
703 return attr in self.__dict__
693
704
694 def _addmarkers(self, markers, rawdata):
705 def _addmarkers(self, markers, rawdata):
695 markers = list(markers) # to allow repeated iteration
706 markers = list(markers) # to allow repeated iteration
696 self._data = self._data + rawdata
707 self._data = self._data + rawdata
697 self._all.extend(markers)
708 self._all.extend(markers)
698 if self._cached(r'successors'):
709 if self._cached(r'successors'):
699 _addsuccessors(self.successors, markers)
710 _addsuccessors(self.successors, markers)
700 if self._cached(r'predecessors'):
711 if self._cached(r'predecessors'):
701 _addpredecessors(self.predecessors, markers)
712 _addpredecessors(self.predecessors, markers)
702 if self._cached(r'children'):
713 if self._cached(r'children'):
703 _addchildren(self.children, markers)
714 _addchildren(self.children, markers)
704 _checkinvalidmarkers(markers)
715 _checkinvalidmarkers(markers)
705
716
706 def relevantmarkers(self, nodes):
717 def relevantmarkers(self, nodes):
707 """return a set of all obsolescence markers relevant to a set of nodes.
718 """return a set of all obsolescence markers relevant to a set of nodes.
708
719
709 "relevant" to a set of nodes mean:
720 "relevant" to a set of nodes mean:
710
721
711 - marker that use this changeset as successor
722 - marker that use this changeset as successor
712 - prune marker of direct children on this changeset
723 - prune marker of direct children on this changeset
713 - recursive application of the two rules on predecessors of these
724 - recursive application of the two rules on predecessors of these
714 markers
725 markers
715
726
716 It is a set so you cannot rely on order."""
727 It is a set so you cannot rely on order."""
717
728
718 pendingnodes = set(nodes)
729 pendingnodes = set(nodes)
719 seenmarkers = set()
730 seenmarkers = set()
720 seennodes = set(pendingnodes)
731 seennodes = set(pendingnodes)
721 precursorsmarkers = self.predecessors
732 precursorsmarkers = self.predecessors
722 succsmarkers = self.successors
733 succsmarkers = self.successors
723 children = self.children
734 children = self.children
724 while pendingnodes:
735 while pendingnodes:
725 direct = set()
736 direct = set()
726 for current in pendingnodes:
737 for current in pendingnodes:
727 direct.update(precursorsmarkers.get(current, ()))
738 direct.update(precursorsmarkers.get(current, ()))
728 pruned = [m for m in children.get(current, ()) if not m[1]]
739 pruned = [m for m in children.get(current, ()) if not m[1]]
729 direct.update(pruned)
740 direct.update(pruned)
730 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
741 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
731 direct.update(pruned)
742 direct.update(pruned)
732 direct -= seenmarkers
743 direct -= seenmarkers
733 pendingnodes = set([m[0] for m in direct])
744 pendingnodes = set([m[0] for m in direct])
734 seenmarkers |= direct
745 seenmarkers |= direct
735 pendingnodes -= seennodes
746 pendingnodes -= seennodes
736 seennodes |= pendingnodes
747 seennodes |= pendingnodes
737 return seenmarkers
748 return seenmarkers
738
749
739 def makestore(ui, repo):
750 def makestore(ui, repo):
740 """Create an obsstore instance from a repo."""
751 """Create an obsstore instance from a repo."""
741 # read default format for new obsstore.
752 # read default format for new obsstore.
742 # developer config: format.obsstore-version
753 # developer config: format.obsstore-version
743 defaultformat = ui.configint('format', 'obsstore-version')
754 defaultformat = ui.configint('format', 'obsstore-version')
744 # rely on obsstore class default when possible.
755 # rely on obsstore class default when possible.
745 kwargs = {}
756 kwargs = {}
746 if defaultformat is not None:
757 if defaultformat is not None:
747 kwargs[r'defaultformat'] = defaultformat
758 kwargs[r'defaultformat'] = defaultformat
748 readonly = not isenabled(repo, createmarkersopt)
759 readonly = not isenabled(repo, createmarkersopt)
749 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
760 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
750 if store and readonly:
761 if store and readonly:
751 ui.warn(_('obsolete feature not enabled but %i markers found!\n')
762 ui.warn(_('obsolete feature not enabled but %i markers found!\n')
752 % len(list(store)))
763 % len(list(store)))
753 return store
764 return store
754
765
755 def commonversion(versions):
766 def commonversion(versions):
756 """Return the newest version listed in both versions and our local formats.
767 """Return the newest version listed in both versions and our local formats.
757
768
758 Returns None if no common version exists.
769 Returns None if no common version exists.
759 """
770 """
760 versions.sort(reverse=True)
771 versions.sort(reverse=True)
761 # search for highest version known on both side
772 # search for highest version known on both side
762 for v in versions:
773 for v in versions:
763 if v in formats:
774 if v in formats:
764 return v
775 return v
765 return None
776 return None
766
777
767 # arbitrary picked to fit into 8K limit from HTTP server
778 # arbitrary picked to fit into 8K limit from HTTP server
768 # you have to take in account:
779 # you have to take in account:
769 # - the version header
780 # - the version header
770 # - the base85 encoding
781 # - the base85 encoding
771 _maxpayload = 5300
782 _maxpayload = 5300
772
783
773 def _pushkeyescape(markers):
784 def _pushkeyescape(markers):
774 """encode markers into a dict suitable for pushkey exchange
785 """encode markers into a dict suitable for pushkey exchange
775
786
776 - binary data is base85 encoded
787 - binary data is base85 encoded
777 - split in chunks smaller than 5300 bytes"""
788 - split in chunks smaller than 5300 bytes"""
778 keys = {}
789 keys = {}
779 parts = []
790 parts = []
780 currentlen = _maxpayload * 2 # ensure we create a new part
791 currentlen = _maxpayload * 2 # ensure we create a new part
781 for marker in markers:
792 for marker in markers:
782 nextdata = _fm0encodeonemarker(marker)
793 nextdata = _fm0encodeonemarker(marker)
783 if (len(nextdata) + currentlen > _maxpayload):
794 if (len(nextdata) + currentlen > _maxpayload):
784 currentpart = []
795 currentpart = []
785 currentlen = 0
796 currentlen = 0
786 parts.append(currentpart)
797 parts.append(currentpart)
787 currentpart.append(nextdata)
798 currentpart.append(nextdata)
788 currentlen += len(nextdata)
799 currentlen += len(nextdata)
789 for idx, part in enumerate(reversed(parts)):
800 for idx, part in enumerate(reversed(parts)):
790 data = ''.join([_pack('>B', _fm0version)] + part)
801 data = ''.join([_pack('>B', _fm0version)] + part)
791 keys['dump%i' % idx] = util.b85encode(data)
802 keys['dump%i' % idx] = util.b85encode(data)
792 return keys
803 return keys
793
804
794 def listmarkers(repo):
805 def listmarkers(repo):
795 """List markers over pushkey"""
806 """List markers over pushkey"""
796 if not repo.obsstore:
807 if not repo.obsstore:
797 return {}
808 return {}
798 return _pushkeyescape(sorted(repo.obsstore))
809 return _pushkeyescape(sorted(repo.obsstore))
799
810
800 def pushmarker(repo, key, old, new):
811 def pushmarker(repo, key, old, new):
801 """Push markers over pushkey"""
812 """Push markers over pushkey"""
802 if not key.startswith('dump'):
813 if not key.startswith('dump'):
803 repo.ui.warn(_('unknown key: %r') % key)
814 repo.ui.warn(_('unknown key: %r') % key)
804 return False
815 return False
805 if old:
816 if old:
806 repo.ui.warn(_('unexpected old value for %r') % key)
817 repo.ui.warn(_('unexpected old value for %r') % key)
807 return False
818 return False
808 data = util.b85decode(new)
819 data = util.b85decode(new)
809 with repo.lock(), repo.transaction('pushkey: obsolete markers') as tr:
820 with repo.lock(), repo.transaction('pushkey: obsolete markers') as tr:
810 repo.obsstore.mergemarkers(tr, data)
821 repo.obsstore.mergemarkers(tr, data)
811 repo.invalidatevolatilesets()
822 repo.invalidatevolatilesets()
812 return True
823 return True
813
824
814 # mapping of 'set-name' -> <function to compute this set>
825 # mapping of 'set-name' -> <function to compute this set>
815 cachefuncs = {}
826 cachefuncs = {}
816 def cachefor(name):
827 def cachefor(name):
817 """Decorator to register a function as computing the cache for a set"""
828 """Decorator to register a function as computing the cache for a set"""
818 def decorator(func):
829 def decorator(func):
819 if name in cachefuncs:
830 if name in cachefuncs:
820 msg = "duplicated registration for volatileset '%s' (existing: %r)"
831 msg = "duplicated registration for volatileset '%s' (existing: %r)"
821 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
832 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
822 cachefuncs[name] = func
833 cachefuncs[name] = func
823 return func
834 return func
824 return decorator
835 return decorator
825
836
826 def getrevs(repo, name):
837 def getrevs(repo, name):
827 """Return the set of revision that belong to the <name> set
838 """Return the set of revision that belong to the <name> set
828
839
829 Such access may compute the set and cache it for future use"""
840 Such access may compute the set and cache it for future use"""
830 repo = repo.unfiltered()
841 repo = repo.unfiltered()
831 if not repo.obsstore:
842 if not repo.obsstore:
832 return frozenset()
843 return frozenset()
833 if name not in repo.obsstore.caches:
844 if name not in repo.obsstore.caches:
834 repo.obsstore.caches[name] = cachefuncs[name](repo)
845 repo.obsstore.caches[name] = cachefuncs[name](repo)
835 return repo.obsstore.caches[name]
846 return repo.obsstore.caches[name]
836
847
837 # To be simple we need to invalidate obsolescence cache when:
848 # To be simple we need to invalidate obsolescence cache when:
838 #
849 #
839 # - new changeset is added:
850 # - new changeset is added:
840 # - public phase is changed
851 # - public phase is changed
841 # - obsolescence marker are added
852 # - obsolescence marker are added
842 # - strip is used a repo
853 # - strip is used a repo
843 def clearobscaches(repo):
854 def clearobscaches(repo):
844 """Remove all obsolescence related cache from a repo
855 """Remove all obsolescence related cache from a repo
845
856
846 This remove all cache in obsstore is the obsstore already exist on the
857 This remove all cache in obsstore is the obsstore already exist on the
847 repo.
858 repo.
848
859
849 (We could be smarter here given the exact event that trigger the cache
860 (We could be smarter here given the exact event that trigger the cache
850 clearing)"""
861 clearing)"""
851 # only clear cache is there is obsstore data in this repo
862 # only clear cache is there is obsstore data in this repo
852 if 'obsstore' in repo._filecache:
863 if 'obsstore' in repo._filecache:
853 repo.obsstore.caches.clear()
864 repo.obsstore.caches.clear()
854
865
855 def _mutablerevs(repo):
866 def _mutablerevs(repo):
856 """the set of mutable revision in the repository"""
867 """the set of mutable revision in the repository"""
857 return repo._phasecache.getrevset(repo, phases.mutablephases)
868 return repo._phasecache.getrevset(repo, phases.mutablephases)
858
869
859 @cachefor('obsolete')
870 @cachefor('obsolete')
860 def _computeobsoleteset(repo):
871 def _computeobsoleteset(repo):
861 """the set of obsolete revisions"""
872 """the set of obsolete revisions"""
862 getnode = repo.changelog.node
873 getnode = repo.changelog.node
863 notpublic = _mutablerevs(repo)
874 notpublic = _mutablerevs(repo)
864 isobs = repo.obsstore.successors.__contains__
875 isobs = repo.obsstore.successors.__contains__
865 obs = set(r for r in notpublic if isobs(getnode(r)))
876 obs = set(r for r in notpublic if isobs(getnode(r)))
866 return obs
877 return obs
867
878
868 @cachefor('orphan')
879 @cachefor('orphan')
869 def _computeorphanset(repo):
880 def _computeorphanset(repo):
870 """the set of non obsolete revisions with obsolete parents"""
881 """the set of non obsolete revisions with obsolete parents"""
871 pfunc = repo.changelog.parentrevs
882 pfunc = repo.changelog.parentrevs
872 mutable = _mutablerevs(repo)
883 mutable = _mutablerevs(repo)
873 obsolete = getrevs(repo, 'obsolete')
884 obsolete = getrevs(repo, 'obsolete')
874 others = mutable - obsolete
885 others = mutable - obsolete
875 unstable = set()
886 unstable = set()
876 for r in sorted(others):
887 for r in sorted(others):
877 # A rev is unstable if one of its parent is obsolete or unstable
888 # A rev is unstable if one of its parent is obsolete or unstable
878 # this works since we traverse following growing rev order
889 # this works since we traverse following growing rev order
879 for p in pfunc(r):
890 for p in pfunc(r):
880 if p in obsolete or p in unstable:
891 if p in obsolete or p in unstable:
881 unstable.add(r)
892 unstable.add(r)
882 break
893 break
883 return unstable
894 return unstable
884
895
885 @cachefor('suspended')
896 @cachefor('suspended')
886 def _computesuspendedset(repo):
897 def _computesuspendedset(repo):
887 """the set of obsolete parents with non obsolete descendants"""
898 """the set of obsolete parents with non obsolete descendants"""
888 suspended = repo.changelog.ancestors(getrevs(repo, 'orphan'))
899 suspended = repo.changelog.ancestors(getrevs(repo, 'orphan'))
889 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
900 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
890
901
891 @cachefor('extinct')
902 @cachefor('extinct')
892 def _computeextinctset(repo):
903 def _computeextinctset(repo):
893 """the set of obsolete parents without non obsolete descendants"""
904 """the set of obsolete parents without non obsolete descendants"""
894 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
905 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
895
906
896 @cachefor('phasedivergent')
907 @cachefor('phasedivergent')
897 def _computephasedivergentset(repo):
908 def _computephasedivergentset(repo):
898 """the set of revs trying to obsolete public revisions"""
909 """the set of revs trying to obsolete public revisions"""
899 bumped = set()
910 bumped = set()
900 # util function (avoid attribute lookup in the loop)
911 # util function (avoid attribute lookup in the loop)
901 phase = repo._phasecache.phase # would be faster to grab the full list
912 phase = repo._phasecache.phase # would be faster to grab the full list
902 public = phases.public
913 public = phases.public
903 cl = repo.changelog
914 cl = repo.changelog
904 torev = cl.nodemap.get
915 torev = cl.nodemap.get
905 tonode = cl.node
916 tonode = cl.node
906 for rev in repo.revs('(not public()) and (not obsolete())'):
917 for rev in repo.revs('(not public()) and (not obsolete())'):
907 # We only evaluate mutable, non-obsolete revision
918 # We only evaluate mutable, non-obsolete revision
908 node = tonode(rev)
919 node = tonode(rev)
909 # (future) A cache of predecessors may worth if split is very common
920 # (future) A cache of predecessors may worth if split is very common
910 for pnode in obsutil.allpredecessors(repo.obsstore, [node],
921 for pnode in obsutil.allpredecessors(repo.obsstore, [node],
911 ignoreflags=bumpedfix):
922 ignoreflags=bumpedfix):
912 prev = torev(pnode) # unfiltered! but so is phasecache
923 prev = torev(pnode) # unfiltered! but so is phasecache
913 if (prev is not None) and (phase(repo, prev) <= public):
924 if (prev is not None) and (phase(repo, prev) <= public):
914 # we have a public predecessor
925 # we have a public predecessor
915 bumped.add(rev)
926 bumped.add(rev)
916 break # Next draft!
927 break # Next draft!
917 return bumped
928 return bumped
918
929
919 @cachefor('contentdivergent')
930 @cachefor('contentdivergent')
920 def _computecontentdivergentset(repo):
931 def _computecontentdivergentset(repo):
921 """the set of rev that compete to be the final successors of some revision.
932 """the set of rev that compete to be the final successors of some revision.
922 """
933 """
923 divergent = set()
934 divergent = set()
924 obsstore = repo.obsstore
935 obsstore = repo.obsstore
925 newermap = {}
936 newermap = {}
926 tonode = repo.changelog.node
937 tonode = repo.changelog.node
927 for rev in repo.revs('(not public()) - obsolete()'):
938 for rev in repo.revs('(not public()) - obsolete()'):
928 node = tonode(rev)
939 node = tonode(rev)
929 mark = obsstore.predecessors.get(node, ())
940 mark = obsstore.predecessors.get(node, ())
930 toprocess = set(mark)
941 toprocess = set(mark)
931 seen = set()
942 seen = set()
932 while toprocess:
943 while toprocess:
933 prec = toprocess.pop()[0]
944 prec = toprocess.pop()[0]
934 if prec in seen:
945 if prec in seen:
935 continue # emergency cycle hanging prevention
946 continue # emergency cycle hanging prevention
936 seen.add(prec)
947 seen.add(prec)
937 if prec not in newermap:
948 if prec not in newermap:
938 obsutil.successorssets(repo, prec, cache=newermap)
949 obsutil.successorssets(repo, prec, cache=newermap)
939 newer = [n for n in newermap[prec] if n]
950 newer = [n for n in newermap[prec] if n]
940 if len(newer) > 1:
951 if len(newer) > 1:
941 divergent.add(rev)
952 divergent.add(rev)
942 break
953 break
943 toprocess.update(obsstore.predecessors.get(prec, ()))
954 toprocess.update(obsstore.predecessors.get(prec, ()))
944 return divergent
955 return divergent
945
956
946
957
947 def createmarkers(repo, relations, flag=0, date=None, metadata=None,
958 def createmarkers(repo, relations, flag=0, date=None, metadata=None,
948 operation=None):
959 operation=None):
949 """Add obsolete markers between changesets in a repo
960 """Add obsolete markers between changesets in a repo
950
961
951 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
962 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
952 tuple. `old` and `news` are changectx. metadata is an optional dictionary
963 tuple. `old` and `news` are changectx. metadata is an optional dictionary
953 containing metadata for this marker only. It is merged with the global
964 containing metadata for this marker only. It is merged with the global
954 metadata specified through the `metadata` argument of this function.
965 metadata specified through the `metadata` argument of this function.
955 Any string values in metadata must be UTF-8 bytes.
966 Any string values in metadata must be UTF-8 bytes.
956
967
957 Trying to obsolete a public changeset will raise an exception.
968 Trying to obsolete a public changeset will raise an exception.
958
969
959 Current user and date are used except if specified otherwise in the
970 Current user and date are used except if specified otherwise in the
960 metadata attribute.
971 metadata attribute.
961
972
962 This function operates within a transaction of its own, but does
973 This function operates within a transaction of its own, but does
963 not take any lock on the repo.
974 not take any lock on the repo.
964 """
975 """
965 # prepare metadata
976 # prepare metadata
966 if metadata is None:
977 if metadata is None:
967 metadata = {}
978 metadata = {}
968 if 'user' not in metadata:
979 if 'user' not in metadata:
969 luser = repo.ui.config('devel', 'user.obsmarker') or repo.ui.username()
980 luser = repo.ui.config('devel', 'user.obsmarker') or repo.ui.username()
970 metadata['user'] = encoding.fromlocal(luser)
981 metadata['user'] = encoding.fromlocal(luser)
971
982
972 # Operation metadata handling
983 # Operation metadata handling
973 useoperation = repo.ui.configbool('experimental',
984 useoperation = repo.ui.configbool('experimental',
974 'evolution.track-operation')
985 'evolution.track-operation')
975 if useoperation and operation:
986 if useoperation and operation:
976 metadata['operation'] = operation
987 metadata['operation'] = operation
977
988
978 # Effect flag metadata handling
989 # Effect flag metadata handling
979 saveeffectflag = repo.ui.configbool('experimental',
990 saveeffectflag = repo.ui.configbool('experimental',
980 'evolution.effect-flags')
991 'evolution.effect-flags')
981
992
982 with repo.transaction('add-obsolescence-marker') as tr:
993 with repo.transaction('add-obsolescence-marker') as tr:
983 markerargs = []
994 markerargs = []
984 for rel in relations:
995 for rel in relations:
985 prec = rel[0]
996 prec = rel[0]
986 sucs = rel[1]
997 sucs = rel[1]
987 localmetadata = metadata.copy()
998 localmetadata = metadata.copy()
988 if 2 < len(rel):
999 if 2 < len(rel):
989 localmetadata.update(rel[2])
1000 localmetadata.update(rel[2])
990
1001
991 if not prec.mutable():
1002 if not prec.mutable():
992 raise error.Abort(_("cannot obsolete public changeset: %s")
1003 raise error.Abort(_("cannot obsolete public changeset: %s")
993 % prec,
1004 % prec,
994 hint="see 'hg help phases' for details")
1005 hint="see 'hg help phases' for details")
995 nprec = prec.node()
1006 nprec = prec.node()
996 nsucs = tuple(s.node() for s in sucs)
1007 nsucs = tuple(s.node() for s in sucs)
997 npare = None
1008 npare = None
998 if not nsucs:
1009 if not nsucs:
999 npare = tuple(p.node() for p in prec.parents())
1010 npare = tuple(p.node() for p in prec.parents())
1000 if nprec in nsucs:
1011 if nprec in nsucs:
1001 raise error.Abort(_("changeset %s cannot obsolete itself")
1012 raise error.Abort(_("changeset %s cannot obsolete itself")
1002 % prec)
1013 % prec)
1003
1014
1004 # Effect flag can be different by relation
1015 # Effect flag can be different by relation
1005 if saveeffectflag:
1016 if saveeffectflag:
1006 # The effect flag is saved in a versioned field name for future
1017 # The effect flag is saved in a versioned field name for future
1007 # evolution
1018 # evolution
1008 effectflag = obsutil.geteffectflag(rel)
1019 effectflag = obsutil.geteffectflag(rel)
1009 localmetadata[obsutil.EFFECTFLAGFIELD] = "%d" % effectflag
1020 localmetadata[obsutil.EFFECTFLAGFIELD] = "%d" % effectflag
1010
1021
1011 # Creating the marker causes the hidden cache to become invalid,
1022 # Creating the marker causes the hidden cache to become invalid,
1012 # which causes recomputation when we ask for prec.parents() above.
1023 # which causes recomputation when we ask for prec.parents() above.
1013 # Resulting in n^2 behavior. So let's prepare all of the args
1024 # Resulting in n^2 behavior. So let's prepare all of the args
1014 # first, then create the markers.
1025 # first, then create the markers.
1015 markerargs.append((nprec, nsucs, npare, localmetadata))
1026 markerargs.append((nprec, nsucs, npare, localmetadata))
1016
1027
1017 for args in markerargs:
1028 for args in markerargs:
1018 nprec, nsucs, npare, localmetadata = args
1029 nprec, nsucs, npare, localmetadata = args
1019 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1030 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1020 date=date, metadata=localmetadata,
1031 date=date, metadata=localmetadata,
1021 ui=repo.ui)
1032 ui=repo.ui)
1022 repo.filteredrevcache.clear()
1033 repo.filteredrevcache.clear()
General Comments 0
You need to be logged in to leave comments. Login now