##// END OF EJS Templates
obsutil: rename allprecursors into allpredecessors...
Boris Feld -
r33701:e6d8ee3c default
parent child Browse files
Show More
@@ -1,632 +1,632 b''
1 1 # phabricator.py - simple Phabricator integration
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """simple Phabricator integration
8 8
9 9 This extension provides a ``phabsend`` command which sends a stack of
10 10 changesets to Phabricator without amending commit messages, and a ``phabread``
11 11 command which prints a stack of revisions in a format suitable
12 12 for :hg:`import`.
13 13
14 14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 15 changeset from being sent. The requirement could be disabled by changing
16 16 ``differential.require-test-plan-field`` config server side.
17 17
18 18 Config::
19 19
20 20 [phabricator]
21 21 # Phabricator URL
22 22 url = https://phab.example.com/
23 23
24 24 # API token. Get it from https://$HOST/conduit/login/
25 25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
26 26
27 27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 28 # callsign is "FOO".
29 29 callsign = FOO
30 30
31 31 """
32 32
33 33 from __future__ import absolute_import
34 34
35 35 import json
36 36 import re
37 37
38 38 from mercurial.node import bin, nullid
39 39 from mercurial.i18n import _
40 40 from mercurial import (
41 41 encoding,
42 42 error,
43 43 mdiff,
44 obsolete,
44 obsutil,
45 45 patch,
46 46 registrar,
47 47 scmutil,
48 48 tags,
49 49 url as urlmod,
50 50 util,
51 51 )
52 52
53 53 cmdtable = {}
54 54 command = registrar.command(cmdtable)
55 55
56 56 def urlencodenested(params):
57 57 """like urlencode, but works with nested parameters.
58 58
59 59 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
60 60 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
61 61 urlencode. Note: the encoding is consistent with PHP's http_build_query.
62 62 """
63 63 flatparams = util.sortdict()
64 64 def process(prefix, obj):
65 65 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
66 66 if items is None:
67 67 flatparams[prefix] = obj
68 68 else:
69 69 for k, v in items(obj):
70 70 if prefix:
71 71 process('%s[%s]' % (prefix, k), v)
72 72 else:
73 73 process(k, v)
74 74 process('', params)
75 75 return util.urlreq.urlencode(flatparams)
76 76
77 77 def readurltoken(repo):
78 78 """return conduit url, token and make sure they exist
79 79
80 80 Currently read from [phabricator] config section. In the future, it might
81 81 make sense to read from .arcconfig and .arcrc as well.
82 82 """
83 83 values = []
84 84 section = 'phabricator'
85 85 for name in ['url', 'token']:
86 86 value = repo.ui.config(section, name)
87 87 if not value:
88 88 raise error.Abort(_('config %s.%s is required') % (section, name))
89 89 values.append(value)
90 90 return values
91 91
92 92 def callconduit(repo, name, params):
93 93 """call Conduit API, params is a dict. return json.loads result, or None"""
94 94 host, token = readurltoken(repo)
95 95 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
96 96 urlopener = urlmod.opener(repo.ui, authinfo)
97 97 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
98 98 params = params.copy()
99 99 params['api.token'] = token
100 100 request = util.urlreq.request(url, data=urlencodenested(params))
101 101 body = urlopener.open(request).read()
102 102 repo.ui.debug('Conduit Response: %s\n' % body)
103 103 parsed = json.loads(body)
104 104 if parsed.get(r'error_code'):
105 105 msg = (_('Conduit Error (%s): %s')
106 106 % (parsed[r'error_code'], parsed[r'error_info']))
107 107 raise error.Abort(msg)
108 108 return parsed[r'result']
109 109
110 110 @command('debugcallconduit', [], _('METHOD'))
111 111 def debugcallconduit(ui, repo, name):
112 112 """call Conduit API
113 113
114 114 Call parameters are read from stdin as a JSON blob. Result will be written
115 115 to stdout as a JSON blob.
116 116 """
117 117 params = json.loads(ui.fin.read())
118 118 result = callconduit(repo, name, params)
119 119 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
120 120 ui.write('%s\n' % s)
121 121
122 122 def getrepophid(repo):
123 123 """given callsign, return repository PHID or None"""
124 124 # developer config: phabricator.repophid
125 125 repophid = repo.ui.config('phabricator', 'repophid')
126 126 if repophid:
127 127 return repophid
128 128 callsign = repo.ui.config('phabricator', 'callsign')
129 129 if not callsign:
130 130 return None
131 131 query = callconduit(repo, 'diffusion.repository.search',
132 132 {'constraints': {'callsigns': [callsign]}})
133 133 if len(query[r'data']) == 0:
134 134 return None
135 135 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
136 136 repo.ui.setconfig('phabricator', 'repophid', repophid)
137 137 return repophid
138 138
139 139 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
140 140 _differentialrevisiondescre = re.compile(
141 141 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
142 142
143 143 def getoldnodedrevmap(repo, nodelist):
144 144 """find previous nodes that has been sent to Phabricator
145 145
146 146 return {node: (oldnode, Differential diff, Differential Revision ID)}
147 147 for node in nodelist with known previous sent versions, or associated
148 148 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
149 149 be ``None``.
150 150
151 151 Examines commit messages like "Differential Revision:" to get the
152 152 association information.
153 153
154 154 If such commit message line is not found, examines all precursors and their
155 155 tags. Tags with format like "D1234" are considered a match and the node
156 156 with that tag, and the number after "D" (ex. 1234) will be returned.
157 157
158 158 The ``old node``, if not None, is guaranteed to be the last diff of
159 159 corresponding Differential Revision, and exist in the repo.
160 160 """
161 161 url, token = readurltoken(repo)
162 162 unfi = repo.unfiltered()
163 163 nodemap = unfi.changelog.nodemap
164 164
165 165 result = {} # {node: (oldnode?, lastdiff?, drev)}
166 166 toconfirm = {} # {node: (force, {precnode}, drev)}
167 167 for node in nodelist:
168 168 ctx = unfi[node]
169 169 # For tags like "D123", put them into "toconfirm" to verify later
170 precnodes = list(obsolete.allprecursors(unfi.obsstore, [node]))
170 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
171 171 for n in precnodes:
172 172 if n in nodemap:
173 173 for tag in unfi.nodetags(n):
174 174 m = _differentialrevisiontagre.match(tag)
175 175 if m:
176 176 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
177 177 continue
178 178
179 179 # Check commit message
180 180 m = _differentialrevisiondescre.search(ctx.description())
181 181 if m:
182 182 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
183 183
184 184 # Double check if tags are genuine by collecting all old nodes from
185 185 # Phabricator, and expect precursors overlap with it.
186 186 if toconfirm:
187 187 drevs = [drev for force, precs, drev in toconfirm.values()]
188 188 alldiffs = callconduit(unfi, 'differential.querydiffs',
189 189 {'revisionIDs': drevs})
190 190 getnode = lambda d: bin(encoding.unitolocal(
191 191 getdiffmeta(d).get(r'node', ''))) or None
192 192 for newnode, (force, precset, drev) in toconfirm.items():
193 193 diffs = [d for d in alldiffs.values()
194 194 if int(d[r'revisionID']) == drev]
195 195
196 196 # "precursors" as known by Phabricator
197 197 phprecset = set(getnode(d) for d in diffs)
198 198
199 199 # Ignore if precursors (Phabricator and local repo) do not overlap,
200 200 # and force is not set (when commit message says nothing)
201 201 if not force and not bool(phprecset & precset):
202 202 tagname = 'D%d' % drev
203 203 tags.tag(repo, tagname, nullid, message=None, user=None,
204 204 date=None, local=True)
205 205 unfi.ui.warn(_('D%s: local tag removed - does not match '
206 206 'Differential history\n') % drev)
207 207 continue
208 208
209 209 # Find the last node using Phabricator metadata, and make sure it
210 210 # exists in the repo
211 211 oldnode = lastdiff = None
212 212 if diffs:
213 213 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
214 214 oldnode = getnode(lastdiff)
215 215 if oldnode and oldnode not in nodemap:
216 216 oldnode = None
217 217
218 218 result[newnode] = (oldnode, lastdiff, drev)
219 219
220 220 return result
221 221
222 222 def getdiff(ctx, diffopts):
223 223 """plain-text diff without header (user, commit message, etc)"""
224 224 output = util.stringio()
225 225 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
226 226 None, opts=diffopts):
227 227 output.write(chunk)
228 228 return output.getvalue()
229 229
230 230 def creatediff(ctx):
231 231 """create a Differential Diff"""
232 232 repo = ctx.repo()
233 233 repophid = getrepophid(repo)
234 234 # Create a "Differential Diff" via "differential.createrawdiff" API
235 235 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
236 236 if repophid:
237 237 params['repositoryPHID'] = repophid
238 238 diff = callconduit(repo, 'differential.createrawdiff', params)
239 239 if not diff:
240 240 raise error.Abort(_('cannot create diff for %s') % ctx)
241 241 return diff
242 242
243 243 def writediffproperties(ctx, diff):
244 244 """write metadata to diff so patches could be applied losslessly"""
245 245 params = {
246 246 'diff_id': diff[r'id'],
247 247 'name': 'hg:meta',
248 248 'data': json.dumps({
249 249 'user': ctx.user(),
250 250 'date': '%d %d' % ctx.date(),
251 251 'node': ctx.hex(),
252 252 'parent': ctx.p1().hex(),
253 253 }),
254 254 }
255 255 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
256 256
257 257 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
258 258 olddiff=None, actions=None):
259 259 """create or update a Differential Revision
260 260
261 261 If revid is None, create a new Differential Revision, otherwise update
262 262 revid. If parentrevid is not None, set it as a dependency.
263 263
264 264 If oldnode is not None, check if the patch content (without commit message
265 265 and metadata) has changed before creating another diff.
266 266
267 267 If actions is not None, they will be appended to the transaction.
268 268 """
269 269 repo = ctx.repo()
270 270 if oldnode:
271 271 diffopts = mdiff.diffopts(git=True, context=1)
272 272 oldctx = repo.unfiltered()[oldnode]
273 273 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
274 274 else:
275 275 neednewdiff = True
276 276
277 277 transactions = []
278 278 if neednewdiff:
279 279 diff = creatediff(ctx)
280 280 writediffproperties(ctx, diff)
281 281 transactions.append({'type': 'update', 'value': diff[r'phid']})
282 282 else:
283 283 # Even if we don't need to upload a new diff because the patch content
284 284 # does not change. We might still need to update its metadata so
285 285 # pushers could know the correct node metadata.
286 286 assert olddiff
287 287 diff = olddiff
288 288 writediffproperties(ctx, diff)
289 289
290 290 # Use a temporary summary to set dependency. There might be better ways but
291 291 # I cannot find them for now. But do not do that if we are updating an
292 292 # existing revision (revid is not None) since that introduces visible
293 293 # churns (someone edited "Summary" twice) on the web page.
294 294 if parentrevid and revid is None:
295 295 summary = 'Depends on D%s' % parentrevid
296 296 transactions += [{'type': 'summary', 'value': summary},
297 297 {'type': 'summary', 'value': ' '}]
298 298
299 299 if actions:
300 300 transactions += actions
301 301
302 302 # Parse commit message and update related fields.
303 303 desc = ctx.description()
304 304 info = callconduit(repo, 'differential.parsecommitmessage',
305 305 {'corpus': desc})
306 306 for k, v in info[r'fields'].items():
307 307 if k in ['title', 'summary', 'testPlan']:
308 308 transactions.append({'type': k, 'value': v})
309 309
310 310 params = {'transactions': transactions}
311 311 if revid is not None:
312 312 # Update an existing Differential Revision
313 313 params['objectIdentifier'] = revid
314 314
315 315 revision = callconduit(repo, 'differential.revision.edit', params)
316 316 if not revision:
317 317 raise error.Abort(_('cannot create revision for %s') % ctx)
318 318
319 319 return revision
320 320
321 321 def userphids(repo, names):
322 322 """convert user names to PHIDs"""
323 323 query = {'constraints': {'usernames': names}}
324 324 result = callconduit(repo, 'user.search', query)
325 325 # username not found is not an error of the API. So check if we have missed
326 326 # some names here.
327 327 data = result[r'data']
328 328 resolved = set(entry[r'fields'][r'username'] for entry in data)
329 329 unresolved = set(names) - resolved
330 330 if unresolved:
331 331 raise error.Abort(_('unknown username: %s')
332 332 % ' '.join(sorted(unresolved)))
333 333 return [entry[r'phid'] for entry in data]
334 334
335 335 @command('phabsend',
336 336 [('r', 'rev', [], _('revisions to send'), _('REV')),
337 337 ('', 'reviewer', [], _('specify reviewers')),
338 338 ('', 'confirm', None, _('ask for confirmation before sending'))],
339 339 _('REV [OPTIONS]'))
340 340 def phabsend(ui, repo, *revs, **opts):
341 341 """upload changesets to Phabricator
342 342
343 343 If there are multiple revisions specified, they will be send as a stack
344 344 with a linear dependencies relationship using the order specified by the
345 345 revset.
346 346
347 347 For the first time uploading changesets, local tags will be created to
348 348 maintain the association. After the first time, phabsend will check
349 349 obsstore and tags information so it can figure out whether to update an
350 350 existing Differential Revision, or create a new one.
351 351
352 352 The --confirm option lets you confirm changesets before sending them. You
353 353 can also add following to your configuration file to make it default
354 354 behaviour.
355 355
356 356 [phabsend]
357 357 confirm = true
358 358 """
359 359 revs = list(revs) + opts.get('rev', [])
360 360 revs = scmutil.revrange(repo, revs)
361 361
362 362 if not revs:
363 363 raise error.Abort(_('phabsend requires at least one changeset'))
364 364
365 365 confirm = ui.configbool('phabsend', 'confirm')
366 366 confirm |= bool(opts.get('confirm'))
367 367 if confirm:
368 368 confirmed = _confirmbeforesend(repo, revs)
369 369 if not confirmed:
370 370 raise error.Abort(_('phabsend cancelled'))
371 371
372 372 actions = []
373 373 reviewers = opts.get('reviewer', [])
374 374 if reviewers:
375 375 phids = userphids(repo, reviewers)
376 376 actions.append({'type': 'reviewers.add', 'value': phids})
377 377
378 378 # {newnode: (oldnode, olddiff, olddrev}
379 379 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
380 380
381 381 # Send patches one by one so we know their Differential Revision IDs and
382 382 # can provide dependency relationship
383 383 lastrevid = None
384 384 for rev in revs:
385 385 ui.debug('sending rev %d\n' % rev)
386 386 ctx = repo[rev]
387 387
388 388 # Get Differential Revision ID
389 389 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
390 390 if oldnode != ctx.node():
391 391 # Create or update Differential Revision
392 392 revision = createdifferentialrevision(ctx, revid, lastrevid,
393 393 oldnode, olddiff, actions)
394 394 newrevid = int(revision[r'object'][r'id'])
395 395 if revid:
396 396 action = _('updated')
397 397 else:
398 398 action = _('created')
399 399
400 400 # Create a local tag to note the association
401 401 tagname = 'D%d' % newrevid
402 402 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
403 403 date=None, local=True)
404 404 else:
405 405 # Nothing changed. But still set "newrevid" so the next revision
406 406 # could depend on this one.
407 407 newrevid = revid
408 408 action = _('skipped')
409 409
410 410 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
411 411 ctx.description().split('\n')[0]))
412 412 lastrevid = newrevid
413 413
414 414 # Map from "hg:meta" keys to header understood by "hg import". The order is
415 415 # consistent with "hg export" output.
416 416 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
417 417 (r'node', 'Node ID'), (r'parent', 'Parent ')])
418 418
419 419 def _confirmbeforesend(repo, revs):
420 420 ui = repo.ui
421 421 for rev in revs:
422 422 ctx = repo[rev]
423 423 desc = ctx.description().splitlines()[0]
424 424 ui.write(('%d: ' % rev), label='phabsend.revnumber')
425 425 ui.write(('%s\n' % desc), label='phabsend.desc')
426 426
427 427 if ui.promptchoice(_('Phabsend the above changes (yn)?'
428 428 '$$ &Yes $$ &No')):
429 429 return False
430 430
431 431 return True
432 432
433 433 def querydrev(repo, params, stack=False):
434 434 """return a list of "Differential Revision" dicts
435 435
436 436 params is the input of "differential.query" API, and is expected to match
437 437 just a single Differential Revision.
438 438
439 439 A "Differential Revision dict" looks like:
440 440
441 441 {
442 442 "id": "2",
443 443 "phid": "PHID-DREV-672qvysjcczopag46qty",
444 444 "title": "example",
445 445 "uri": "https://phab.example.com/D2",
446 446 "dateCreated": "1499181406",
447 447 "dateModified": "1499182103",
448 448 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
449 449 "status": "0",
450 450 "statusName": "Needs Review",
451 451 "properties": [],
452 452 "branch": null,
453 453 "summary": "",
454 454 "testPlan": "",
455 455 "lineCount": "2",
456 456 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
457 457 "diffs": [
458 458 "3",
459 459 "4",
460 460 ],
461 461 "commits": [],
462 462 "reviewers": [],
463 463 "ccs": [],
464 464 "hashes": [],
465 465 "auxiliary": {
466 466 "phabricator:projects": [],
467 467 "phabricator:depends-on": [
468 468 "PHID-DREV-gbapp366kutjebt7agcd"
469 469 ]
470 470 },
471 471 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
472 472 "sourcePath": null
473 473 }
474 474
475 475 If stack is True, return a list of "Differential Revision dict"s in an
476 476 order that the latter ones depend on the former ones. Otherwise, return a
477 477 list of a unique "Differential Revision dict".
478 478 """
479 479 prefetched = {} # {id or phid: drev}
480 480 def fetch(params):
481 481 """params -> single drev or None"""
482 482 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
483 483 if key in prefetched:
484 484 return prefetched[key]
485 485 # Otherwise, send the request. If we're fetching a stack, be smarter
486 486 # and fetch more ids in one batch, even if it could be unnecessary.
487 487 batchparams = params
488 488 if stack and len(params.get(r'ids', [])) == 1:
489 489 i = int(params[r'ids'][0])
490 490 # developer config: phabricator.batchsize
491 491 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
492 492 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
493 493 drevs = callconduit(repo, 'differential.query', batchparams)
494 494 # Fill prefetched with the result
495 495 for drev in drevs:
496 496 prefetched[drev[r'phid']] = drev
497 497 prefetched[int(drev[r'id'])] = drev
498 498 if key not in prefetched:
499 499 raise error.Abort(_('cannot get Differential Revision %r') % params)
500 500 return prefetched[key]
501 501
502 502 visited = set()
503 503 result = []
504 504 queue = [params]
505 505 while queue:
506 506 params = queue.pop()
507 507 drev = fetch(params)
508 508 if drev[r'id'] in visited:
509 509 continue
510 510 visited.add(drev[r'id'])
511 511 result.append(drev)
512 512 if stack:
513 513 auxiliary = drev.get(r'auxiliary', {})
514 514 depends = auxiliary.get(r'phabricator:depends-on', [])
515 515 for phid in depends:
516 516 queue.append({'phids': [phid]})
517 517 result.reverse()
518 518 return result
519 519
520 520 def getdescfromdrev(drev):
521 521 """get description (commit message) from "Differential Revision"
522 522
523 523 This is similar to differential.getcommitmessage API. But we only care
524 524 about limited fields: title, summary, test plan, and URL.
525 525 """
526 526 title = drev[r'title']
527 527 summary = drev[r'summary'].rstrip()
528 528 testplan = drev[r'testPlan'].rstrip()
529 529 if testplan:
530 530 testplan = 'Test Plan:\n%s' % testplan
531 531 uri = 'Differential Revision: %s' % drev[r'uri']
532 532 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
533 533
534 534 def getdiffmeta(diff):
535 535 """get commit metadata (date, node, user, p1) from a diff object
536 536
537 537 The metadata could be "hg:meta", sent by phabsend, like:
538 538
539 539 "properties": {
540 540 "hg:meta": {
541 541 "date": "1499571514 25200",
542 542 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
543 543 "user": "Foo Bar <foo@example.com>",
544 544 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
545 545 }
546 546 }
547 547
548 548 Or converted from "local:commits", sent by "arc", like:
549 549
550 550 "properties": {
551 551 "local:commits": {
552 552 "98c08acae292b2faf60a279b4189beb6cff1414d": {
553 553 "author": "Foo Bar",
554 554 "time": 1499546314,
555 555 "branch": "default",
556 556 "tag": "",
557 557 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
558 558 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
559 559 "local": "1000",
560 560 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
561 561 "summary": "...",
562 562 "message": "...",
563 563 "authorEmail": "foo@example.com"
564 564 }
565 565 }
566 566 }
567 567
568 568 Note: metadata extracted from "local:commits" will lose time zone
569 569 information.
570 570 """
571 571 props = diff.get(r'properties') or {}
572 572 meta = props.get(r'hg:meta')
573 573 if not meta and props.get(r'local:commits'):
574 574 commit = sorted(props[r'local:commits'].values())[0]
575 575 meta = {
576 576 r'date': r'%d 0' % commit[r'time'],
577 577 r'node': commit[r'rev'],
578 578 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
579 579 }
580 580 if len(commit.get(r'parents', ())) >= 1:
581 581 meta[r'parent'] = commit[r'parents'][0]
582 582 return meta or {}
583 583
584 584 def readpatch(repo, params, write, stack=False):
585 585 """generate plain-text patch readable by 'hg import'
586 586
587 587 write is usually ui.write. params is passed to "differential.query". If
588 588 stack is True, also write dependent patches.
589 589 """
590 590 # Differential Revisions
591 591 drevs = querydrev(repo, params, stack)
592 592
593 593 # Prefetch hg:meta property for all diffs
594 594 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
595 595 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
596 596
597 597 # Generate patch for each drev
598 598 for drev in drevs:
599 599 repo.ui.note(_('reading D%s\n') % drev[r'id'])
600 600
601 601 diffid = max(int(v) for v in drev[r'diffs'])
602 602 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
603 603 desc = getdescfromdrev(drev)
604 604 header = '# HG changeset patch\n'
605 605
606 606 # Try to preserve metadata from hg:meta property. Write hg patch
607 607 # headers that can be read by the "import" command. See patchheadermap
608 608 # and extract in mercurial/patch.py for supported headers.
609 609 meta = getdiffmeta(diffs[str(diffid)])
610 610 for k in _metanamemap.keys():
611 611 if k in meta:
612 612 header += '# %s %s\n' % (_metanamemap[k], meta[k])
613 613
614 614 content = '%s%s\n%s' % (header, desc, body)
615 615 write(encoding.unitolocal(content))
616 616
617 617 @command('phabread',
618 618 [('', 'stack', False, _('read dependencies'))],
619 619 _('REVID [OPTIONS]'))
620 620 def phabread(ui, repo, revid, **opts):
621 621 """print patches from Phabricator suitable for importing
622 622
623 623 REVID could be a Differential Revision identity, like ``D123``, or just the
624 624 number ``123``, or a full URL like ``https://phab.example.com/D123``.
625 625
626 626 If --stack is given, follow dependencies information and read all patches.
627 627 """
628 628 try:
629 629 revid = int(revid.split('/')[-1].replace('D', ''))
630 630 except ValueError:
631 631 raise error.Abort(_('invalid Revision ID: %s') % revid)
632 632 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
@@ -1,1050 +1,1050 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete marker handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewrite operations, and help
18 18 building new tools to reconcile conflicting rewrite actions. To
19 19 facilitate conflict resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called a "predecessor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a predecessor are called "successor markers of X" because they hold
26 26 information about the successors of X. Markers that use changeset Y as
27 27 a successors are call "predecessor markers of Y" because they hold
28 28 information about the predecessors of Y.
29 29
30 30 Examples:
31 31
32 32 - When changeset A is replaced by changeset A', one marker is stored:
33 33
34 34 (A, (A',))
35 35
36 36 - When changesets A and B are folded into a new changeset C, two markers are
37 37 stored:
38 38
39 39 (A, (C,)) and (B, (C,))
40 40
41 41 - When changeset A is simply "pruned" from the graph, a marker is created:
42 42
43 43 (A, ())
44 44
45 45 - When changeset A is split into B and C, a single marker is used:
46 46
47 47 (A, (B, C))
48 48
49 49 We use a single marker to distinguish the "split" case from the "divergence"
50 50 case. If two independent operations rewrite the same changeset A in to A' and
51 51 A'', we have an error case: divergent rewriting. We can detect it because
52 52 two markers will be created independently:
53 53
54 54 (A, (B,)) and (A, (C,))
55 55
56 56 Format
57 57 ------
58 58
59 59 Markers are stored in an append-only file stored in
60 60 '.hg/store/obsstore'.
61 61
62 62 The file starts with a version header:
63 63
64 64 - 1 unsigned byte: version number, starting at zero.
65 65
66 66 The header is followed by the markers. Marker format depend of the version. See
67 67 comment associated with each format for details.
68 68
69 69 """
70 70 from __future__ import absolute_import
71 71
72 72 import errno
73 73 import struct
74 74
75 75 from .i18n import _
76 76 from . import (
77 77 error,
78 78 node,
79 79 obsutil,
80 80 phases,
81 81 policy,
82 82 util,
83 83 )
84 84
85 85 parsers = policy.importmod(r'parsers')
86 86
87 87 _pack = struct.pack
88 88 _unpack = struct.unpack
89 89 _calcsize = struct.calcsize
90 90 propertycache = util.propertycache
91 91
92 92 # the obsolete feature is not mature enough to be enabled by default.
93 93 # you have to rely on third party extension extension to enable this.
94 94 _enabled = False
95 95
96 96 # Options for obsolescence
97 97 createmarkersopt = 'createmarkers'
98 98 allowunstableopt = 'allowunstable'
99 99 exchangeopt = 'exchange'
100 100
101 101 def isenabled(repo, option):
102 102 """Returns True if the given repository has the given obsolete option
103 103 enabled.
104 104 """
105 105 result = set(repo.ui.configlist('experimental', 'evolution'))
106 106 if 'all' in result:
107 107 return True
108 108
109 109 # For migration purposes, temporarily return true if the config hasn't been
110 110 # set but _enabled is true.
111 111 if len(result) == 0 and _enabled:
112 112 return True
113 113
114 114 # createmarkers must be enabled if other options are enabled
115 115 if ((allowunstableopt in result or exchangeopt in result) and
116 116 not createmarkersopt in result):
117 117 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
118 118 "if other obsolete options are enabled"))
119 119
120 120 return option in result
121 121
122 122 ### obsolescence marker flag
123 123
124 124 ## bumpedfix flag
125 125 #
126 126 # When a changeset A' succeed to a changeset A which became public, we call A'
127 127 # "bumped" because it's a successors of a public changesets
128 128 #
129 129 # o A' (bumped)
130 130 # |`:
131 131 # | o A
132 132 # |/
133 133 # o Z
134 134 #
135 135 # The way to solve this situation is to create a new changeset Ad as children
136 136 # of A. This changeset have the same content than A'. So the diff from A to A'
137 137 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
138 138 #
139 139 # o Ad
140 140 # |`:
141 141 # | x A'
142 142 # |'|
143 143 # o | A
144 144 # |/
145 145 # o Z
146 146 #
147 147 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
148 148 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
149 149 # This flag mean that the successors express the changes between the public and
150 150 # bumped version and fix the situation, breaking the transitivity of
151 151 # "bumped" here.
152 152 bumpedfix = 1
153 153 usingsha256 = 2
154 154
155 155 ## Parsing and writing of version "0"
156 156 #
157 157 # The header is followed by the markers. Each marker is made of:
158 158 #
159 159 # - 1 uint8 : number of new changesets "N", can be zero.
160 160 #
161 161 # - 1 uint32: metadata size "M" in bytes.
162 162 #
163 163 # - 1 byte: a bit field. It is reserved for flags used in common
164 164 # obsolete marker operations, to avoid repeated decoding of metadata
165 165 # entries.
166 166 #
167 167 # - 20 bytes: obsoleted changeset identifier.
168 168 #
169 169 # - N*20 bytes: new changesets identifiers.
170 170 #
171 171 # - M bytes: metadata as a sequence of nul-terminated strings. Each
172 172 # string contains a key and a value, separated by a colon ':', without
173 173 # additional encoding. Keys cannot contain '\0' or ':' and values
174 174 # cannot contain '\0'.
175 175 _fm0version = 0
176 176 _fm0fixed = '>BIB20s'
177 177 _fm0node = '20s'
178 178 _fm0fsize = _calcsize(_fm0fixed)
179 179 _fm0fnodesize = _calcsize(_fm0node)
180 180
181 181 def _fm0readmarkers(data, off, stop):
182 182 # Loop on markers
183 183 while off < stop:
184 184 # read fixed part
185 185 cur = data[off:off + _fm0fsize]
186 186 off += _fm0fsize
187 187 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
188 188 # read replacement
189 189 sucs = ()
190 190 if numsuc:
191 191 s = (_fm0fnodesize * numsuc)
192 192 cur = data[off:off + s]
193 193 sucs = _unpack(_fm0node * numsuc, cur)
194 194 off += s
195 195 # read metadata
196 196 # (metadata will be decoded on demand)
197 197 metadata = data[off:off + mdsize]
198 198 if len(metadata) != mdsize:
199 199 raise error.Abort(_('parsing obsolete marker: metadata is too '
200 200 'short, %d bytes expected, got %d')
201 201 % (mdsize, len(metadata)))
202 202 off += mdsize
203 203 metadata = _fm0decodemeta(metadata)
204 204 try:
205 205 when, offset = metadata.pop('date', '0 0').split(' ')
206 206 date = float(when), int(offset)
207 207 except ValueError:
208 208 date = (0., 0)
209 209 parents = None
210 210 if 'p2' in metadata:
211 211 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
212 212 elif 'p1' in metadata:
213 213 parents = (metadata.pop('p1', None),)
214 214 elif 'p0' in metadata:
215 215 parents = ()
216 216 if parents is not None:
217 217 try:
218 218 parents = tuple(node.bin(p) for p in parents)
219 219 # if parent content is not a nodeid, drop the data
220 220 for p in parents:
221 221 if len(p) != 20:
222 222 parents = None
223 223 break
224 224 except TypeError:
225 225 # if content cannot be translated to nodeid drop the data.
226 226 parents = None
227 227
228 228 metadata = tuple(sorted(metadata.iteritems()))
229 229
230 230 yield (pre, sucs, flags, metadata, date, parents)
231 231
232 232 def _fm0encodeonemarker(marker):
233 233 pre, sucs, flags, metadata, date, parents = marker
234 234 if flags & usingsha256:
235 235 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
236 236 metadata = dict(metadata)
237 237 time, tz = date
238 238 metadata['date'] = '%r %i' % (time, tz)
239 239 if parents is not None:
240 240 if not parents:
241 241 # mark that we explicitly recorded no parents
242 242 metadata['p0'] = ''
243 243 for i, p in enumerate(parents, 1):
244 244 metadata['p%i' % i] = node.hex(p)
245 245 metadata = _fm0encodemeta(metadata)
246 246 numsuc = len(sucs)
247 247 format = _fm0fixed + (_fm0node * numsuc)
248 248 data = [numsuc, len(metadata), flags, pre]
249 249 data.extend(sucs)
250 250 return _pack(format, *data) + metadata
251 251
252 252 def _fm0encodemeta(meta):
253 253 """Return encoded metadata string to string mapping.
254 254
255 255 Assume no ':' in key and no '\0' in both key and value."""
256 256 for key, value in meta.iteritems():
257 257 if ':' in key or '\0' in key:
258 258 raise ValueError("':' and '\0' are forbidden in metadata key'")
259 259 if '\0' in value:
260 260 raise ValueError("':' is forbidden in metadata value'")
261 261 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
262 262
263 263 def _fm0decodemeta(data):
264 264 """Return string to string dictionary from encoded version."""
265 265 d = {}
266 266 for l in data.split('\0'):
267 267 if l:
268 268 key, value = l.split(':')
269 269 d[key] = value
270 270 return d
271 271
272 272 ## Parsing and writing of version "1"
273 273 #
274 274 # The header is followed by the markers. Each marker is made of:
275 275 #
276 276 # - uint32: total size of the marker (including this field)
277 277 #
278 278 # - float64: date in seconds since epoch
279 279 #
280 280 # - int16: timezone offset in minutes
281 281 #
282 282 # - uint16: a bit field. It is reserved for flags used in common
283 283 # obsolete marker operations, to avoid repeated decoding of metadata
284 284 # entries.
285 285 #
286 286 # - uint8: number of successors "N", can be zero.
287 287 #
288 288 # - uint8: number of parents "P", can be zero.
289 289 #
290 290 # 0: parents data stored but no parent,
291 291 # 1: one parent stored,
292 292 # 2: two parents stored,
293 293 # 3: no parent data stored
294 294 #
295 295 # - uint8: number of metadata entries M
296 296 #
297 297 # - 20 or 32 bytes: predecessor changeset identifier.
298 298 #
299 299 # - N*(20 or 32) bytes: successors changesets identifiers.
300 300 #
301 301 # - P*(20 or 32) bytes: parents of the predecessors changesets.
302 302 #
303 303 # - M*(uint8, uint8): size of all metadata entries (key and value)
304 304 #
305 305 # - remaining bytes: the metadata, each (key, value) pair after the other.
306 306 _fm1version = 1
307 307 _fm1fixed = '>IdhHBBB20s'
308 308 _fm1nodesha1 = '20s'
309 309 _fm1nodesha256 = '32s'
310 310 _fm1nodesha1size = _calcsize(_fm1nodesha1)
311 311 _fm1nodesha256size = _calcsize(_fm1nodesha256)
312 312 _fm1fsize = _calcsize(_fm1fixed)
313 313 _fm1parentnone = 3
314 314 _fm1parentshift = 14
315 315 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
316 316 _fm1metapair = 'BB'
317 317 _fm1metapairsize = _calcsize(_fm1metapair)
318 318
319 319 def _fm1purereadmarkers(data, off, stop):
320 320 # make some global constants local for performance
321 321 noneflag = _fm1parentnone
322 322 sha2flag = usingsha256
323 323 sha1size = _fm1nodesha1size
324 324 sha2size = _fm1nodesha256size
325 325 sha1fmt = _fm1nodesha1
326 326 sha2fmt = _fm1nodesha256
327 327 metasize = _fm1metapairsize
328 328 metafmt = _fm1metapair
329 329 fsize = _fm1fsize
330 330 unpack = _unpack
331 331
332 332 # Loop on markers
333 333 ufixed = struct.Struct(_fm1fixed).unpack
334 334
335 335 while off < stop:
336 336 # read fixed part
337 337 o1 = off + fsize
338 338 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
339 339
340 340 if flags & sha2flag:
341 341 # FIXME: prec was read as a SHA1, needs to be amended
342 342
343 343 # read 0 or more successors
344 344 if numsuc == 1:
345 345 o2 = o1 + sha2size
346 346 sucs = (data[o1:o2],)
347 347 else:
348 348 o2 = o1 + sha2size * numsuc
349 349 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
350 350
351 351 # read parents
352 352 if numpar == noneflag:
353 353 o3 = o2
354 354 parents = None
355 355 elif numpar == 1:
356 356 o3 = o2 + sha2size
357 357 parents = (data[o2:o3],)
358 358 else:
359 359 o3 = o2 + sha2size * numpar
360 360 parents = unpack(sha2fmt * numpar, data[o2:o3])
361 361 else:
362 362 # read 0 or more successors
363 363 if numsuc == 1:
364 364 o2 = o1 + sha1size
365 365 sucs = (data[o1:o2],)
366 366 else:
367 367 o2 = o1 + sha1size * numsuc
368 368 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
369 369
370 370 # read parents
371 371 if numpar == noneflag:
372 372 o3 = o2
373 373 parents = None
374 374 elif numpar == 1:
375 375 o3 = o2 + sha1size
376 376 parents = (data[o2:o3],)
377 377 else:
378 378 o3 = o2 + sha1size * numpar
379 379 parents = unpack(sha1fmt * numpar, data[o2:o3])
380 380
381 381 # read metadata
382 382 off = o3 + metasize * nummeta
383 383 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
384 384 metadata = []
385 385 for idx in xrange(0, len(metapairsize), 2):
386 386 o1 = off + metapairsize[idx]
387 387 o2 = o1 + metapairsize[idx + 1]
388 388 metadata.append((data[off:o1], data[o1:o2]))
389 389 off = o2
390 390
391 391 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
392 392
393 393 def _fm1encodeonemarker(marker):
394 394 pre, sucs, flags, metadata, date, parents = marker
395 395 # determine node size
396 396 _fm1node = _fm1nodesha1
397 397 if flags & usingsha256:
398 398 _fm1node = _fm1nodesha256
399 399 numsuc = len(sucs)
400 400 numextranodes = numsuc
401 401 if parents is None:
402 402 numpar = _fm1parentnone
403 403 else:
404 404 numpar = len(parents)
405 405 numextranodes += numpar
406 406 formatnodes = _fm1node * numextranodes
407 407 formatmeta = _fm1metapair * len(metadata)
408 408 format = _fm1fixed + formatnodes + formatmeta
409 409 # tz is stored in minutes so we divide by 60
410 410 tz = date[1]//60
411 411 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
412 412 data.extend(sucs)
413 413 if parents is not None:
414 414 data.extend(parents)
415 415 totalsize = _calcsize(format)
416 416 for key, value in metadata:
417 417 lk = len(key)
418 418 lv = len(value)
419 419 data.append(lk)
420 420 data.append(lv)
421 421 totalsize += lk + lv
422 422 data[0] = totalsize
423 423 data = [_pack(format, *data)]
424 424 for key, value in metadata:
425 425 data.append(key)
426 426 data.append(value)
427 427 return ''.join(data)
428 428
429 429 def _fm1readmarkers(data, off, stop):
430 430 native = getattr(parsers, 'fm1readmarkers', None)
431 431 if not native:
432 432 return _fm1purereadmarkers(data, off, stop)
433 433 return native(data, off, stop)
434 434
435 435 # mapping to read/write various marker formats
436 436 # <version> -> (decoder, encoder)
437 437 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
438 438 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
439 439
440 440 def _readmarkerversion(data):
441 441 return _unpack('>B', data[0:1])[0]
442 442
443 443 @util.nogc
444 444 def _readmarkers(data, off=None, stop=None):
445 445 """Read and enumerate markers from raw data"""
446 446 diskversion = _readmarkerversion(data)
447 447 if not off:
448 448 off = 1 # skip 1 byte version number
449 449 if stop is None:
450 450 stop = len(data)
451 451 if diskversion not in formats:
452 452 msg = _('parsing obsolete marker: unknown version %r') % diskversion
453 453 raise error.UnknownVersion(msg, version=diskversion)
454 454 return diskversion, formats[diskversion][0](data, off, stop)
455 455
456 456 def encodeheader(version=_fm0version):
457 457 return _pack('>B', version)
458 458
459 459 def encodemarkers(markers, addheader=False, version=_fm0version):
460 460 # Kept separate from flushmarkers(), it will be reused for
461 461 # markers exchange.
462 462 encodeone = formats[version][1]
463 463 if addheader:
464 464 yield encodeheader(version)
465 465 for marker in markers:
466 466 yield encodeone(marker)
467 467
468 468 @util.nogc
469 469 def _addsuccessors(successors, markers):
470 470 for mark in markers:
471 471 successors.setdefault(mark[0], set()).add(mark)
472 472
473 473 def _addprecursors(*args, **kwargs):
474 474 msg = ("'obsolete._addprecursors' is deprecated, "
475 475 "use 'obsolete._addpredecessors'")
476 476 util.nouideprecwarn(msg, '4.4')
477 477
478 478 return _addpredecessors(*args, **kwargs)
479 479
480 480 @util.nogc
481 481 def _addpredecessors(predecessors, markers):
482 482 for mark in markers:
483 483 for suc in mark[1]:
484 484 predecessors.setdefault(suc, set()).add(mark)
485 485
486 486 @util.nogc
487 487 def _addchildren(children, markers):
488 488 for mark in markers:
489 489 parents = mark[5]
490 490 if parents is not None:
491 491 for p in parents:
492 492 children.setdefault(p, set()).add(mark)
493 493
494 494 def _checkinvalidmarkers(markers):
495 495 """search for marker with invalid data and raise error if needed
496 496
497 497 Exist as a separated function to allow the evolve extension for a more
498 498 subtle handling.
499 499 """
500 500 for mark in markers:
501 501 if node.nullid in mark[1]:
502 502 raise error.Abort(_('bad obsolescence marker detected: '
503 503 'invalid successors nullid'))
504 504
505 505 class obsstore(object):
506 506 """Store obsolete markers
507 507
508 508 Markers can be accessed with two mappings:
509 509 - predecessors[x] -> set(markers on predecessors edges of x)
510 510 - successors[x] -> set(markers on successors edges of x)
511 511 - children[x] -> set(markers on predecessors edges of children(x)
512 512 """
513 513
514 514 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
515 515 # prec: nodeid, predecessors changesets
516 516 # succs: tuple of nodeid, successor changesets (0-N length)
517 517 # flag: integer, flag field carrying modifier for the markers (see doc)
518 518 # meta: binary blob, encoded metadata dictionary
519 519 # date: (float, int) tuple, date of marker creation
520 520 # parents: (tuple of nodeid) or None, parents of predecessors
521 521 # None is used when no data has been recorded
522 522
523 523 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
524 524 # caches for various obsolescence related cache
525 525 self.caches = {}
526 526 self.svfs = svfs
527 527 self._defaultformat = defaultformat
528 528 self._readonly = readonly
529 529
530 530 def __iter__(self):
531 531 return iter(self._all)
532 532
533 533 def __len__(self):
534 534 return len(self._all)
535 535
536 536 def __nonzero__(self):
537 537 if not self._cached('_all'):
538 538 try:
539 539 return self.svfs.stat('obsstore').st_size > 1
540 540 except OSError as inst:
541 541 if inst.errno != errno.ENOENT:
542 542 raise
543 543 # just build an empty _all list if no obsstore exists, which
544 544 # avoids further stat() syscalls
545 545 pass
546 546 return bool(self._all)
547 547
548 548 __bool__ = __nonzero__
549 549
550 550 @property
551 551 def readonly(self):
552 552 """True if marker creation is disabled
553 553
554 554 Remove me in the future when obsolete marker is always on."""
555 555 return self._readonly
556 556
557 557 def create(self, transaction, prec, succs=(), flag=0, parents=None,
558 558 date=None, metadata=None, ui=None):
559 559 """obsolete: add a new obsolete marker
560 560
561 561 * ensuring it is hashable
562 562 * check mandatory metadata
563 563 * encode metadata
564 564
565 565 If you are a human writing code creating marker you want to use the
566 566 `createmarkers` function in this module instead.
567 567
568 568 return True if a new marker have been added, False if the markers
569 569 already existed (no op).
570 570 """
571 571 if metadata is None:
572 572 metadata = {}
573 573 if date is None:
574 574 if 'date' in metadata:
575 575 # as a courtesy for out-of-tree extensions
576 576 date = util.parsedate(metadata.pop('date'))
577 577 elif ui is not None:
578 578 date = ui.configdate('devel', 'default-date')
579 579 if date is None:
580 580 date = util.makedate()
581 581 else:
582 582 date = util.makedate()
583 583 if len(prec) != 20:
584 584 raise ValueError(prec)
585 585 for succ in succs:
586 586 if len(succ) != 20:
587 587 raise ValueError(succ)
588 588 if prec in succs:
589 589 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
590 590
591 591 metadata = tuple(sorted(metadata.iteritems()))
592 592
593 593 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
594 594 return bool(self.add(transaction, [marker]))
595 595
596 596 def add(self, transaction, markers):
597 597 """Add new markers to the store
598 598
599 599 Take care of filtering duplicate.
600 600 Return the number of new marker."""
601 601 if self._readonly:
602 602 raise error.Abort(_('creating obsolete markers is not enabled on '
603 603 'this repo'))
604 604 known = set()
605 605 getsuccessors = self.successors.get
606 606 new = []
607 607 for m in markers:
608 608 if m not in getsuccessors(m[0], ()) and m not in known:
609 609 known.add(m)
610 610 new.append(m)
611 611 if new:
612 612 f = self.svfs('obsstore', 'ab')
613 613 try:
614 614 offset = f.tell()
615 615 transaction.add('obsstore', offset)
616 616 # offset == 0: new file - add the version header
617 617 data = b''.join(encodemarkers(new, offset == 0, self._version))
618 618 f.write(data)
619 619 finally:
620 620 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
621 621 # call 'filecacheentry.refresh()' here
622 622 f.close()
623 623 addedmarkers = transaction.changes.get('obsmarkers')
624 624 if addedmarkers is not None:
625 625 addedmarkers.update(new)
626 626 self._addmarkers(new, data)
627 627 # new marker *may* have changed several set. invalidate the cache.
628 628 self.caches.clear()
629 629 # records the number of new markers for the transaction hooks
630 630 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
631 631 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
632 632 return len(new)
633 633
634 634 def mergemarkers(self, transaction, data):
635 635 """merge a binary stream of markers inside the obsstore
636 636
637 637 Returns the number of new markers added."""
638 638 version, markers = _readmarkers(data)
639 639 return self.add(transaction, markers)
640 640
641 641 @propertycache
642 642 def _data(self):
643 643 return self.svfs.tryread('obsstore')
644 644
645 645 @propertycache
646 646 def _version(self):
647 647 if len(self._data) >= 1:
648 648 return _readmarkerversion(self._data)
649 649 else:
650 650 return self._defaultformat
651 651
652 652 @propertycache
653 653 def _all(self):
654 654 data = self._data
655 655 if not data:
656 656 return []
657 657 self._version, markers = _readmarkers(data)
658 658 markers = list(markers)
659 659 _checkinvalidmarkers(markers)
660 660 return markers
661 661
662 662 @propertycache
663 663 def successors(self):
664 664 successors = {}
665 665 _addsuccessors(successors, self._all)
666 666 return successors
667 667
668 668 @property
669 669 def precursors(self):
670 670 msg = ("'obsstore.precursors' is deprecated, "
671 671 "use 'obsstore.predecessors'")
672 672 util.nouideprecwarn(msg, '4.4')
673 673
674 674 return self.predecessors
675 675
676 676 @propertycache
677 677 def predecessors(self):
678 678 predecessors = {}
679 679 _addpredecessors(predecessors, self._all)
680 680 return predecessors
681 681
682 682 @propertycache
683 683 def children(self):
684 684 children = {}
685 685 _addchildren(children, self._all)
686 686 return children
687 687
688 688 def _cached(self, attr):
689 689 return attr in self.__dict__
690 690
691 691 def _addmarkers(self, markers, rawdata):
692 692 markers = list(markers) # to allow repeated iteration
693 693 self._data = self._data + rawdata
694 694 self._all.extend(markers)
695 695 if self._cached('successors'):
696 696 _addsuccessors(self.successors, markers)
697 697 if self._cached('predecessors'):
698 698 _addpredecessors(self.predecessors, markers)
699 699 if self._cached('children'):
700 700 _addchildren(self.children, markers)
701 701 _checkinvalidmarkers(markers)
702 702
703 703 def relevantmarkers(self, nodes):
704 704 """return a set of all obsolescence markers relevant to a set of nodes.
705 705
706 706 "relevant" to a set of nodes mean:
707 707
708 708 - marker that use this changeset as successor
709 709 - prune marker of direct children on this changeset
710 710 - recursive application of the two rules on predecessors of these
711 711 markers
712 712
713 713 It is a set so you cannot rely on order."""
714 714
715 715 pendingnodes = set(nodes)
716 716 seenmarkers = set()
717 717 seennodes = set(pendingnodes)
718 718 precursorsmarkers = self.predecessors
719 719 succsmarkers = self.successors
720 720 children = self.children
721 721 while pendingnodes:
722 722 direct = set()
723 723 for current in pendingnodes:
724 724 direct.update(precursorsmarkers.get(current, ()))
725 725 pruned = [m for m in children.get(current, ()) if not m[1]]
726 726 direct.update(pruned)
727 727 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
728 728 direct.update(pruned)
729 729 direct -= seenmarkers
730 730 pendingnodes = set([m[0] for m in direct])
731 731 seenmarkers |= direct
732 732 pendingnodes -= seennodes
733 733 seennodes |= pendingnodes
734 734 return seenmarkers
735 735
736 736 def makestore(ui, repo):
737 737 """Create an obsstore instance from a repo."""
738 738 # read default format for new obsstore.
739 739 # developer config: format.obsstore-version
740 740 defaultformat = ui.configint('format', 'obsstore-version')
741 741 # rely on obsstore class default when possible.
742 742 kwargs = {}
743 743 if defaultformat is not None:
744 744 kwargs['defaultformat'] = defaultformat
745 745 readonly = not isenabled(repo, createmarkersopt)
746 746 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
747 747 if store and readonly:
748 748 ui.warn(_('obsolete feature not enabled but %i markers found!\n')
749 749 % len(list(store)))
750 750 return store
751 751
752 752 def commonversion(versions):
753 753 """Return the newest version listed in both versions and our local formats.
754 754
755 755 Returns None if no common version exists.
756 756 """
757 757 versions.sort(reverse=True)
758 758 # search for highest version known on both side
759 759 for v in versions:
760 760 if v in formats:
761 761 return v
762 762 return None
763 763
764 764 # arbitrary picked to fit into 8K limit from HTTP server
765 765 # you have to take in account:
766 766 # - the version header
767 767 # - the base85 encoding
768 768 _maxpayload = 5300
769 769
770 770 def _pushkeyescape(markers):
771 771 """encode markers into a dict suitable for pushkey exchange
772 772
773 773 - binary data is base85 encoded
774 774 - split in chunks smaller than 5300 bytes"""
775 775 keys = {}
776 776 parts = []
777 777 currentlen = _maxpayload * 2 # ensure we create a new part
778 778 for marker in markers:
779 779 nextdata = _fm0encodeonemarker(marker)
780 780 if (len(nextdata) + currentlen > _maxpayload):
781 781 currentpart = []
782 782 currentlen = 0
783 783 parts.append(currentpart)
784 784 currentpart.append(nextdata)
785 785 currentlen += len(nextdata)
786 786 for idx, part in enumerate(reversed(parts)):
787 787 data = ''.join([_pack('>B', _fm0version)] + part)
788 788 keys['dump%i' % idx] = util.b85encode(data)
789 789 return keys
790 790
791 791 def listmarkers(repo):
792 792 """List markers over pushkey"""
793 793 if not repo.obsstore:
794 794 return {}
795 795 return _pushkeyescape(sorted(repo.obsstore))
796 796
797 797 def pushmarker(repo, key, old, new):
798 798 """Push markers over pushkey"""
799 799 if not key.startswith('dump'):
800 800 repo.ui.warn(_('unknown key: %r') % key)
801 801 return False
802 802 if old:
803 803 repo.ui.warn(_('unexpected old value for %r') % key)
804 804 return False
805 805 data = util.b85decode(new)
806 806 lock = repo.lock()
807 807 try:
808 808 tr = repo.transaction('pushkey: obsolete markers')
809 809 try:
810 810 repo.obsstore.mergemarkers(tr, data)
811 811 repo.invalidatevolatilesets()
812 812 tr.close()
813 813 return True
814 814 finally:
815 815 tr.release()
816 816 finally:
817 817 lock.release()
818 818
819 819 # keep compatibility for the 4.3 cycle
820 820 def allprecursors(obsstore, nodes, ignoreflags=0):
821 821 movemsg = 'obsolete.allprecursors moved to obsutil.allprecursors'
822 822 util.nouideprecwarn(movemsg, '4.3')
823 823 return obsutil.allprecursors(obsstore, nodes, ignoreflags)
824 824
825 825 def allsuccessors(obsstore, nodes, ignoreflags=0):
826 826 movemsg = 'obsolete.allsuccessors moved to obsutil.allsuccessors'
827 827 util.nouideprecwarn(movemsg, '4.3')
828 828 return obsutil.allsuccessors(obsstore, nodes, ignoreflags)
829 829
830 830 def marker(repo, data):
831 831 movemsg = 'obsolete.marker moved to obsutil.marker'
832 832 repo.ui.deprecwarn(movemsg, '4.3')
833 833 return obsutil.marker(repo, data)
834 834
835 835 def getmarkers(repo, nodes=None, exclusive=False):
836 836 movemsg = 'obsolete.getmarkers moved to obsutil.getmarkers'
837 837 repo.ui.deprecwarn(movemsg, '4.3')
838 838 return obsutil.getmarkers(repo, nodes=nodes, exclusive=exclusive)
839 839
840 840 def exclusivemarkers(repo, nodes):
841 841 movemsg = 'obsolete.exclusivemarkers moved to obsutil.exclusivemarkers'
842 842 repo.ui.deprecwarn(movemsg, '4.3')
843 843 return obsutil.exclusivemarkers(repo, nodes)
844 844
845 845 def foreground(repo, nodes):
846 846 movemsg = 'obsolete.foreground moved to obsutil.foreground'
847 847 repo.ui.deprecwarn(movemsg, '4.3')
848 848 return obsutil.foreground(repo, nodes)
849 849
850 850 def successorssets(repo, initialnode, cache=None):
851 851 movemsg = 'obsolete.successorssets moved to obsutil.successorssets'
852 852 repo.ui.deprecwarn(movemsg, '4.3')
853 853 return obsutil.successorssets(repo, initialnode, cache=cache)
854 854
855 855 # mapping of 'set-name' -> <function to compute this set>
856 856 cachefuncs = {}
857 857 def cachefor(name):
858 858 """Decorator to register a function as computing the cache for a set"""
859 859 def decorator(func):
860 860 if name in cachefuncs:
861 861 msg = "duplicated registration for volatileset '%s' (existing: %r)"
862 862 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
863 863 cachefuncs[name] = func
864 864 return func
865 865 return decorator
866 866
867 867 def getrevs(repo, name):
868 868 """Return the set of revision that belong to the <name> set
869 869
870 870 Such access may compute the set and cache it for future use"""
871 871 repo = repo.unfiltered()
872 872 if not repo.obsstore:
873 873 return frozenset()
874 874 if name not in repo.obsstore.caches:
875 875 repo.obsstore.caches[name] = cachefuncs[name](repo)
876 876 return repo.obsstore.caches[name]
877 877
878 878 # To be simple we need to invalidate obsolescence cache when:
879 879 #
880 880 # - new changeset is added:
881 881 # - public phase is changed
882 882 # - obsolescence marker are added
883 883 # - strip is used a repo
884 884 def clearobscaches(repo):
885 885 """Remove all obsolescence related cache from a repo
886 886
887 887 This remove all cache in obsstore is the obsstore already exist on the
888 888 repo.
889 889
890 890 (We could be smarter here given the exact event that trigger the cache
891 891 clearing)"""
892 892 # only clear cache is there is obsstore data in this repo
893 893 if 'obsstore' in repo._filecache:
894 894 repo.obsstore.caches.clear()
895 895
896 896 def _mutablerevs(repo):
897 897 """the set of mutable revision in the repository"""
898 898 return repo._phasecache.getrevset(repo, (phases.draft, phases.secret))
899 899
900 900 @cachefor('obsolete')
901 901 def _computeobsoleteset(repo):
902 902 """the set of obsolete revisions"""
903 903 getnode = repo.changelog.node
904 904 notpublic = _mutablerevs(repo)
905 905 isobs = repo.obsstore.successors.__contains__
906 906 obs = set(r for r in notpublic if isobs(getnode(r)))
907 907 return obs
908 908
909 909 @cachefor('unstable')
910 910 def _computeunstableset(repo):
911 911 """the set of non obsolete revisions with obsolete parents"""
912 912 pfunc = repo.changelog.parentrevs
913 913 mutable = _mutablerevs(repo)
914 914 obsolete = getrevs(repo, 'obsolete')
915 915 others = mutable - obsolete
916 916 unstable = set()
917 917 for r in sorted(others):
918 918 # A rev is unstable if one of its parent is obsolete or unstable
919 919 # this works since we traverse following growing rev order
920 920 for p in pfunc(r):
921 921 if p in obsolete or p in unstable:
922 922 unstable.add(r)
923 923 break
924 924 return unstable
925 925
926 926 @cachefor('suspended')
927 927 def _computesuspendedset(repo):
928 928 """the set of obsolete parents with non obsolete descendants"""
929 929 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
930 930 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
931 931
932 932 @cachefor('extinct')
933 933 def _computeextinctset(repo):
934 934 """the set of obsolete parents without non obsolete descendants"""
935 935 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
936 936
937 937
938 938 @cachefor('bumped')
939 939 def _computebumpedset(repo):
940 940 """the set of revs trying to obsolete public revisions"""
941 941 bumped = set()
942 942 # util function (avoid attribute lookup in the loop)
943 943 phase = repo._phasecache.phase # would be faster to grab the full list
944 944 public = phases.public
945 945 cl = repo.changelog
946 946 torev = cl.nodemap.get
947 947 for ctx in repo.set('(not public()) and (not obsolete())'):
948 948 rev = ctx.rev()
949 949 # We only evaluate mutable, non-obsolete revision
950 950 node = ctx.node()
951 951 # (future) A cache of predecessors may worth if split is very common
952 for pnode in obsutil.allprecursors(repo.obsstore, [node],
952 for pnode in obsutil.allpredecessors(repo.obsstore, [node],
953 953 ignoreflags=bumpedfix):
954 954 prev = torev(pnode) # unfiltered! but so is phasecache
955 955 if (prev is not None) and (phase(repo, prev) <= public):
956 956 # we have a public predecessor
957 957 bumped.add(rev)
958 958 break # Next draft!
959 959 return bumped
960 960
961 961 @cachefor('divergent')
962 962 def _computedivergentset(repo):
963 963 """the set of rev that compete to be the final successors of some revision.
964 964 """
965 965 divergent = set()
966 966 obsstore = repo.obsstore
967 967 newermap = {}
968 968 for ctx in repo.set('(not public()) - obsolete()'):
969 969 mark = obsstore.predecessors.get(ctx.node(), ())
970 970 toprocess = set(mark)
971 971 seen = set()
972 972 while toprocess:
973 973 prec = toprocess.pop()[0]
974 974 if prec in seen:
975 975 continue # emergency cycle hanging prevention
976 976 seen.add(prec)
977 977 if prec not in newermap:
978 978 obsutil.successorssets(repo, prec, cache=newermap)
979 979 newer = [n for n in newermap[prec] if n]
980 980 if len(newer) > 1:
981 981 divergent.add(ctx.rev())
982 982 break
983 983 toprocess.update(obsstore.predecessors.get(prec, ()))
984 984 return divergent
985 985
986 986
987 987 def createmarkers(repo, relations, flag=0, date=None, metadata=None,
988 988 operation=None):
989 989 """Add obsolete markers between changesets in a repo
990 990
991 991 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
992 992 tuple. `old` and `news` are changectx. metadata is an optional dictionary
993 993 containing metadata for this marker only. It is merged with the global
994 994 metadata specified through the `metadata` argument of this function,
995 995
996 996 Trying to obsolete a public changeset will raise an exception.
997 997
998 998 Current user and date are used except if specified otherwise in the
999 999 metadata attribute.
1000 1000
1001 1001 This function operates within a transaction of its own, but does
1002 1002 not take any lock on the repo.
1003 1003 """
1004 1004 # prepare metadata
1005 1005 if metadata is None:
1006 1006 metadata = {}
1007 1007 if 'user' not in metadata:
1008 1008 metadata['user'] = repo.ui.username()
1009 1009 useoperation = repo.ui.configbool('experimental',
1010 1010 'evolution.track-operation')
1011 1011 if useoperation and operation:
1012 1012 metadata['operation'] = operation
1013 1013 tr = repo.transaction('add-obsolescence-marker')
1014 1014 try:
1015 1015 markerargs = []
1016 1016 for rel in relations:
1017 1017 prec = rel[0]
1018 1018 sucs = rel[1]
1019 1019 localmetadata = metadata.copy()
1020 1020 if 2 < len(rel):
1021 1021 localmetadata.update(rel[2])
1022 1022
1023 1023 if not prec.mutable():
1024 1024 raise error.Abort(_("cannot obsolete public changeset: %s")
1025 1025 % prec,
1026 1026 hint="see 'hg help phases' for details")
1027 1027 nprec = prec.node()
1028 1028 nsucs = tuple(s.node() for s in sucs)
1029 1029 npare = None
1030 1030 if not nsucs:
1031 1031 npare = tuple(p.node() for p in prec.parents())
1032 1032 if nprec in nsucs:
1033 1033 raise error.Abort(_("changeset %s cannot obsolete itself")
1034 1034 % prec)
1035 1035
1036 1036 # Creating the marker causes the hidden cache to become invalid,
1037 1037 # which causes recomputation when we ask for prec.parents() above.
1038 1038 # Resulting in n^2 behavior. So let's prepare all of the args
1039 1039 # first, then create the markers.
1040 1040 markerargs.append((nprec, nsucs, npare, localmetadata))
1041 1041
1042 1042 for args in markerargs:
1043 1043 nprec, nsucs, npare, localmetadata = args
1044 1044 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1045 1045 date=date, metadata=localmetadata,
1046 1046 ui=repo.ui)
1047 1047 repo.filteredrevcache.clear()
1048 1048 tr.close()
1049 1049 finally:
1050 1050 tr.release()
@@ -1,544 +1,553 b''
1 1 # obsutil.py - utility functions for obsolescence
2 2 #
3 3 # Copyright 2017 Boris Feld <boris.feld@octobus.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from . import (
11 11 phases,
12 12 util
13 13 )
14 14
15 15 class marker(object):
16 16 """Wrap obsolete marker raw data"""
17 17
18 18 def __init__(self, repo, data):
19 19 # the repo argument will be used to create changectx in later version
20 20 self._repo = repo
21 21 self._data = data
22 22 self._decodedmeta = None
23 23
24 24 def __hash__(self):
25 25 return hash(self._data)
26 26
27 27 def __eq__(self, other):
28 28 if type(other) != type(self):
29 29 return False
30 30 return self._data == other._data
31 31
32 32 def precnode(self):
33 33 msg = ("'marker.precnode' is deprecated, "
34 34 "use 'marker.precnode'")
35 35 util.nouideprecwarn(msg, '4.4')
36 36 return self.prednode()
37 37
38 38 def prednode(self):
39 39 """Predecessor changeset node identifier"""
40 40 return self._data[0]
41 41
42 42 def succnodes(self):
43 43 """List of successor changesets node identifiers"""
44 44 return self._data[1]
45 45
46 46 def parentnodes(self):
47 47 """Parents of the predecessors (None if not recorded)"""
48 48 return self._data[5]
49 49
50 50 def metadata(self):
51 51 """Decoded metadata dictionary"""
52 52 return dict(self._data[3])
53 53
54 54 def date(self):
55 55 """Creation date as (unixtime, offset)"""
56 56 return self._data[4]
57 57
58 58 def flags(self):
59 59 """The flags field of the marker"""
60 60 return self._data[2]
61 61
62 62 def getmarkers(repo, nodes=None, exclusive=False):
63 63 """returns markers known in a repository
64 64
65 65 If <nodes> is specified, only markers "relevant" to those nodes are are
66 66 returned"""
67 67 if nodes is None:
68 68 rawmarkers = repo.obsstore
69 69 elif exclusive:
70 70 rawmarkers = exclusivemarkers(repo, nodes)
71 71 else:
72 72 rawmarkers = repo.obsstore.relevantmarkers(nodes)
73 73
74 74 for markerdata in rawmarkers:
75 75 yield marker(repo, markerdata)
76 76
77 77 def closestpredecessors(repo, nodeid):
78 78 """yield the list of next predecessors pointing on visible changectx nodes
79 79
80 80 This function respect the repoview filtering, filtered revision will be
81 81 considered missing.
82 82 """
83 83
84 84 precursors = repo.obsstore.predecessors
85 85 stack = [nodeid]
86 86 seen = set(stack)
87 87
88 88 while stack:
89 89 current = stack.pop()
90 90 currentpreccs = precursors.get(current, ())
91 91
92 92 for prec in currentpreccs:
93 93 precnodeid = prec[0]
94 94
95 95 # Basic cycle protection
96 96 if precnodeid in seen:
97 97 continue
98 98 seen.add(precnodeid)
99 99
100 100 if precnodeid in repo:
101 101 yield precnodeid
102 102 else:
103 103 stack.append(precnodeid)
104 104
105 def allprecursors(obsstore, nodes, ignoreflags=0):
105 def allprecursors(*args, **kwargs):
106 """ (DEPRECATED)
107 """
108 msg = ("'obsutil.allprecursors' is deprecated, "
109 "use 'obsutil.allpredecessors'")
110 util.nouideprecwarn(msg, '4.4')
111
112 return allpredecessors(*args, **kwargs)
113
114 def allpredecessors(obsstore, nodes, ignoreflags=0):
106 115 """Yield node for every precursors of <nodes>.
107 116
108 117 Some precursors may be unknown locally.
109 118
110 119 This is a linear yield unsuited to detecting folded changesets. It includes
111 120 initial nodes too."""
112 121
113 122 remaining = set(nodes)
114 123 seen = set(remaining)
115 124 while remaining:
116 125 current = remaining.pop()
117 126 yield current
118 127 for mark in obsstore.predecessors.get(current, ()):
119 128 # ignore marker flagged with specified flag
120 129 if mark[2] & ignoreflags:
121 130 continue
122 131 suc = mark[0]
123 132 if suc not in seen:
124 133 seen.add(suc)
125 134 remaining.add(suc)
126 135
127 136 def allsuccessors(obsstore, nodes, ignoreflags=0):
128 137 """Yield node for every successor of <nodes>.
129 138
130 139 Some successors may be unknown locally.
131 140
132 141 This is a linear yield unsuited to detecting split changesets. It includes
133 142 initial nodes too."""
134 143 remaining = set(nodes)
135 144 seen = set(remaining)
136 145 while remaining:
137 146 current = remaining.pop()
138 147 yield current
139 148 for mark in obsstore.successors.get(current, ()):
140 149 # ignore marker flagged with specified flag
141 150 if mark[2] & ignoreflags:
142 151 continue
143 152 for suc in mark[1]:
144 153 if suc not in seen:
145 154 seen.add(suc)
146 155 remaining.add(suc)
147 156
148 157 def _filterprunes(markers):
149 158 """return a set with no prune markers"""
150 159 return set(m for m in markers if m[1])
151 160
152 161 def exclusivemarkers(repo, nodes):
153 162 """set of markers relevant to "nodes" but no other locally-known nodes
154 163
155 164 This function compute the set of markers "exclusive" to a locally-known
156 165 node. This means we walk the markers starting from <nodes> until we reach a
157 166 locally-known precursors outside of <nodes>. Element of <nodes> with
158 167 locally-known successors outside of <nodes> are ignored (since their
159 168 precursors markers are also relevant to these successors).
160 169
161 170 For example:
162 171
163 172 # (A0 rewritten as A1)
164 173 #
165 174 # A0 <-1- A1 # Marker "1" is exclusive to A1
166 175
167 176 or
168 177
169 178 # (A0 rewritten as AX; AX rewritten as A1; AX is unkown locally)
170 179 #
171 180 # <-1- A0 <-2- AX <-3- A1 # Marker "2,3" are exclusive to A1
172 181
173 182 or
174 183
175 184 # (A0 has unknown precursors, A0 rewritten as A1 and A2 (divergence))
176 185 #
177 186 # <-2- A1 # Marker "2" is exclusive to A0,A1
178 187 # /
179 188 # <-1- A0
180 189 # \
181 190 # <-3- A2 # Marker "3" is exclusive to A0,A2
182 191 #
183 192 # in addition:
184 193 #
185 194 # Markers "2,3" are exclusive to A1,A2
186 195 # Markers "1,2,3" are exclusive to A0,A1,A2
187 196
188 197 See test/test-obsolete-bundle-strip.t for more examples.
189 198
190 199 An example usage is strip. When stripping a changeset, we also want to
191 200 strip the markers exclusive to this changeset. Otherwise we would have
192 201 "dangling"" obsolescence markers from its precursors: Obsolescence markers
193 202 marking a node as obsolete without any successors available locally.
194 203
195 204 As for relevant markers, the prune markers for children will be followed.
196 205 Of course, they will only be followed if the pruned children is
197 206 locally-known. Since the prune markers are relevant to the pruned node.
198 207 However, while prune markers are considered relevant to the parent of the
199 208 pruned changesets, prune markers for locally-known changeset (with no
200 209 successors) are considered exclusive to the pruned nodes. This allows
201 210 to strip the prune markers (with the rest of the exclusive chain) alongside
202 211 the pruned changesets.
203 212 """
204 213 # running on a filtered repository would be dangerous as markers could be
205 214 # reported as exclusive when they are relevant for other filtered nodes.
206 215 unfi = repo.unfiltered()
207 216
208 217 # shortcut to various useful item
209 218 nm = unfi.changelog.nodemap
210 219 precursorsmarkers = unfi.obsstore.predecessors
211 220 successormarkers = unfi.obsstore.successors
212 221 childrenmarkers = unfi.obsstore.children
213 222
214 223 # exclusive markers (return of the function)
215 224 exclmarkers = set()
216 225 # we need fast membership testing
217 226 nodes = set(nodes)
218 227 # looking for head in the obshistory
219 228 #
220 229 # XXX we are ignoring all issues in regard with cycle for now.
221 230 stack = [n for n in nodes if not _filterprunes(successormarkers.get(n, ()))]
222 231 stack.sort()
223 232 # nodes already stacked
224 233 seennodes = set(stack)
225 234 while stack:
226 235 current = stack.pop()
227 236 # fetch precursors markers
228 237 markers = list(precursorsmarkers.get(current, ()))
229 238 # extend the list with prune markers
230 239 for mark in successormarkers.get(current, ()):
231 240 if not mark[1]:
232 241 markers.append(mark)
233 242 # and markers from children (looking for prune)
234 243 for mark in childrenmarkers.get(current, ()):
235 244 if not mark[1]:
236 245 markers.append(mark)
237 246 # traverse the markers
238 247 for mark in markers:
239 248 if mark in exclmarkers:
240 249 # markers already selected
241 250 continue
242 251
243 252 # If the markers is about the current node, select it
244 253 #
245 254 # (this delay the addition of markers from children)
246 255 if mark[1] or mark[0] == current:
247 256 exclmarkers.add(mark)
248 257
249 258 # should we keep traversing through the precursors?
250 259 prec = mark[0]
251 260
252 261 # nodes in the stack or already processed
253 262 if prec in seennodes:
254 263 continue
255 264
256 265 # is this a locally known node ?
257 266 known = prec in nm
258 267 # if locally-known and not in the <nodes> set the traversal
259 268 # stop here.
260 269 if known and prec not in nodes:
261 270 continue
262 271
263 272 # do not keep going if there are unselected markers pointing to this
264 273 # nodes. If we end up traversing these unselected markers later the
265 274 # node will be taken care of at that point.
266 275 precmarkers = _filterprunes(successormarkers.get(prec))
267 276 if precmarkers.issubset(exclmarkers):
268 277 seennodes.add(prec)
269 278 stack.append(prec)
270 279
271 280 return exclmarkers
272 281
273 282 def foreground(repo, nodes):
274 283 """return all nodes in the "foreground" of other node
275 284
276 285 The foreground of a revision is anything reachable using parent -> children
277 286 or precursor -> successor relation. It is very similar to "descendant" but
278 287 augmented with obsolescence information.
279 288
280 289 Beware that possible obsolescence cycle may result if complex situation.
281 290 """
282 291 repo = repo.unfiltered()
283 292 foreground = set(repo.set('%ln::', nodes))
284 293 if repo.obsstore:
285 294 # We only need this complicated logic if there is obsolescence
286 295 # XXX will probably deserve an optimised revset.
287 296 nm = repo.changelog.nodemap
288 297 plen = -1
289 298 # compute the whole set of successors or descendants
290 299 while len(foreground) != plen:
291 300 plen = len(foreground)
292 301 succs = set(c.node() for c in foreground)
293 302 mutable = [c.node() for c in foreground if c.mutable()]
294 303 succs.update(allsuccessors(repo.obsstore, mutable))
295 304 known = (n for n in succs if n in nm)
296 305 foreground = set(repo.set('%ln::', known))
297 306 return set(c.node() for c in foreground)
298 307
299 308 def getobsoleted(repo, tr):
300 309 """return the set of pre-existing revisions obsoleted by a transaction"""
301 310 torev = repo.unfiltered().changelog.nodemap.get
302 311 phase = repo._phasecache.phase
303 312 succsmarkers = repo.obsstore.successors.get
304 313 public = phases.public
305 314 addedmarkers = tr.changes.get('obsmarkers')
306 315 addedrevs = tr.changes.get('revs')
307 316 seenrevs = set(addedrevs)
308 317 obsoleted = set()
309 318 for mark in addedmarkers:
310 319 node = mark[0]
311 320 rev = torev(node)
312 321 if rev is None or rev in seenrevs:
313 322 continue
314 323 seenrevs.add(rev)
315 324 if phase(repo, rev) == public:
316 325 continue
317 326 if set(succsmarkers(node) or []).issubset(addedmarkers):
318 327 obsoleted.add(rev)
319 328 return obsoleted
320 329
321 330 def successorssets(repo, initialnode, closest=False, cache=None):
322 331 """Return set of all latest successors of initial nodes
323 332
324 333 The successors set of a changeset A are the group of revisions that succeed
325 334 A. It succeeds A as a consistent whole, each revision being only a partial
326 335 replacement. By default, the successors set contains non-obsolete
327 336 changesets only, walking the obsolescence graph until reaching a leaf. If
328 337 'closest' is set to True, closest successors-sets are return (the
329 338 obsolescence walk stops on known changesets).
330 339
331 340 This function returns the full list of successor sets which is why it
332 341 returns a list of tuples and not just a single tuple. Each tuple is a valid
333 342 successors set. Note that (A,) may be a valid successors set for changeset A
334 343 (see below).
335 344
336 345 In most cases, a changeset A will have a single element (e.g. the changeset
337 346 A is replaced by A') in its successors set. Though, it is also common for a
338 347 changeset A to have no elements in its successor set (e.g. the changeset
339 348 has been pruned). Therefore, the returned list of successors sets will be
340 349 [(A',)] or [], respectively.
341 350
342 351 When a changeset A is split into A' and B', however, it will result in a
343 352 successors set containing more than a single element, i.e. [(A',B')].
344 353 Divergent changesets will result in multiple successors sets, i.e. [(A',),
345 354 (A'')].
346 355
347 356 If a changeset A is not obsolete, then it will conceptually have no
348 357 successors set. To distinguish this from a pruned changeset, the successor
349 358 set will contain itself only, i.e. [(A,)].
350 359
351 360 Finally, final successors unknown locally are considered to be pruned
352 361 (pruned: obsoleted without any successors). (Final: successors not affected
353 362 by markers).
354 363
355 364 The 'closest' mode respect the repoview filtering. For example, without
356 365 filter it will stop at the first locally known changeset, with 'visible'
357 366 filter it will stop on visible changesets).
358 367
359 368 The optional `cache` parameter is a dictionary that may contains
360 369 precomputed successors sets. It is meant to reuse the computation of a
361 370 previous call to `successorssets` when multiple calls are made at the same
362 371 time. The cache dictionary is updated in place. The caller is responsible
363 372 for its life span. Code that makes multiple calls to `successorssets`
364 373 *should* use this cache mechanism or risk a performance hit.
365 374
366 375 Since results are different depending of the 'closest' most, the same cache
367 376 cannot be reused for both mode.
368 377 """
369 378
370 379 succmarkers = repo.obsstore.successors
371 380
372 381 # Stack of nodes we search successors sets for
373 382 toproceed = [initialnode]
374 383 # set version of above list for fast loop detection
375 384 # element added to "toproceed" must be added here
376 385 stackedset = set(toproceed)
377 386 if cache is None:
378 387 cache = {}
379 388
380 389 # This while loop is the flattened version of a recursive search for
381 390 # successors sets
382 391 #
383 392 # def successorssets(x):
384 393 # successors = directsuccessors(x)
385 394 # ss = [[]]
386 395 # for succ in directsuccessors(x):
387 396 # # product as in itertools cartesian product
388 397 # ss = product(ss, successorssets(succ))
389 398 # return ss
390 399 #
391 400 # But we can not use plain recursive calls here:
392 401 # - that would blow the python call stack
393 402 # - obsolescence markers may have cycles, we need to handle them.
394 403 #
395 404 # The `toproceed` list act as our call stack. Every node we search
396 405 # successors set for are stacked there.
397 406 #
398 407 # The `stackedset` is set version of this stack used to check if a node is
399 408 # already stacked. This check is used to detect cycles and prevent infinite
400 409 # loop.
401 410 #
402 411 # successors set of all nodes are stored in the `cache` dictionary.
403 412 #
404 413 # After this while loop ends we use the cache to return the successors sets
405 414 # for the node requested by the caller.
406 415 while toproceed:
407 416 # Every iteration tries to compute the successors sets of the topmost
408 417 # node of the stack: CURRENT.
409 418 #
410 419 # There are four possible outcomes:
411 420 #
412 421 # 1) We already know the successors sets of CURRENT:
413 422 # -> mission accomplished, pop it from the stack.
414 423 # 2) Stop the walk:
415 424 # default case: Node is not obsolete
416 425 # closest case: Node is known at this repo filter level
417 426 # -> the node is its own successors sets. Add it to the cache.
418 427 # 3) We do not know successors set of direct successors of CURRENT:
419 428 # -> We add those successors to the stack.
420 429 # 4) We know successors sets of all direct successors of CURRENT:
421 430 # -> We can compute CURRENT successors set and add it to the
422 431 # cache.
423 432 #
424 433 current = toproceed[-1]
425 434
426 435 # case 2 condition is a bit hairy because of closest,
427 436 # we compute it on its own
428 437 case2condition = ((current not in succmarkers)
429 438 or (closest and current != initialnode
430 439 and current in repo))
431 440
432 441 if current in cache:
433 442 # case (1): We already know the successors sets
434 443 stackedset.remove(toproceed.pop())
435 444 elif case2condition:
436 445 # case (2): end of walk.
437 446 if current in repo:
438 447 # We have a valid successors.
439 448 cache[current] = [(current,)]
440 449 else:
441 450 # Final obsolete version is unknown locally.
442 451 # Do not count that as a valid successors
443 452 cache[current] = []
444 453 else:
445 454 # cases (3) and (4)
446 455 #
447 456 # We proceed in two phases. Phase 1 aims to distinguish case (3)
448 457 # from case (4):
449 458 #
450 459 # For each direct successors of CURRENT, we check whether its
451 460 # successors sets are known. If they are not, we stack the
452 461 # unknown node and proceed to the next iteration of the while
453 462 # loop. (case 3)
454 463 #
455 464 # During this step, we may detect obsolescence cycles: a node
456 465 # with unknown successors sets but already in the call stack.
457 466 # In such a situation, we arbitrary set the successors sets of
458 467 # the node to nothing (node pruned) to break the cycle.
459 468 #
460 469 # If no break was encountered we proceed to phase 2.
461 470 #
462 471 # Phase 2 computes successors sets of CURRENT (case 4); see details
463 472 # in phase 2 itself.
464 473 #
465 474 # Note the two levels of iteration in each phase.
466 475 # - The first one handles obsolescence markers using CURRENT as
467 476 # precursor (successors markers of CURRENT).
468 477 #
469 478 # Having multiple entry here means divergence.
470 479 #
471 480 # - The second one handles successors defined in each marker.
472 481 #
473 482 # Having none means pruned node, multiple successors means split,
474 483 # single successors are standard replacement.
475 484 #
476 485 for mark in sorted(succmarkers[current]):
477 486 for suc in mark[1]:
478 487 if suc not in cache:
479 488 if suc in stackedset:
480 489 # cycle breaking
481 490 cache[suc] = []
482 491 else:
483 492 # case (3) If we have not computed successors sets
484 493 # of one of those successors we add it to the
485 494 # `toproceed` stack and stop all work for this
486 495 # iteration.
487 496 toproceed.append(suc)
488 497 stackedset.add(suc)
489 498 break
490 499 else:
491 500 continue
492 501 break
493 502 else:
494 503 # case (4): we know all successors sets of all direct
495 504 # successors
496 505 #
497 506 # Successors set contributed by each marker depends on the
498 507 # successors sets of all its "successors" node.
499 508 #
500 509 # Each different marker is a divergence in the obsolescence
501 510 # history. It contributes successors sets distinct from other
502 511 # markers.
503 512 #
504 513 # Within a marker, a successor may have divergent successors
505 514 # sets. In such a case, the marker will contribute multiple
506 515 # divergent successors sets. If multiple successors have
507 516 # divergent successors sets, a Cartesian product is used.
508 517 #
509 518 # At the end we post-process successors sets to remove
510 519 # duplicated entry and successors set that are strict subset of
511 520 # another one.
512 521 succssets = []
513 522 for mark in sorted(succmarkers[current]):
514 523 # successors sets contributed by this marker
515 524 markss = [[]]
516 525 for suc in mark[1]:
517 526 # cardinal product with previous successors
518 527 productresult = []
519 528 for prefix in markss:
520 529 for suffix in cache[suc]:
521 530 newss = list(prefix)
522 531 for part in suffix:
523 532 # do not duplicated entry in successors set
524 533 # first entry wins.
525 534 if part not in newss:
526 535 newss.append(part)
527 536 productresult.append(newss)
528 537 markss = productresult
529 538 succssets.extend(markss)
530 539 # remove duplicated and subset
531 540 seen = []
532 541 final = []
533 542 candidate = sorted(((set(s), s) for s in succssets if s),
534 543 key=lambda x: len(x[1]), reverse=True)
535 544 for setversion, listversion in candidate:
536 545 for seenset in seen:
537 546 if setversion.issubset(seenset):
538 547 break
539 548 else:
540 549 final.append(listversion)
541 550 seen.append(setversion)
542 551 final.reverse() # put small successors set first
543 552 cache[current] = final
544 553 return cache[initialnode]
General Comments 0
You need to be logged in to leave comments. Login now