##// END OF EJS Templates
hooklib: fix detection of successors for changeset_obsoleted...
Joerg Sonnenberger -
r46020:04ef3810 default
parent child Browse files
Show More
@@ -1,131 +1,139 b''
1 1 # Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5 """changeset_obsoleted is a hook to send a mail when an
6 6 existing draft changeset is obsoleted by an obsmarker without successor.
7 7
8 8 Correct message threading requires the same messageidseed to be used for both
9 9 the original notification and the new mail.
10 10
11 11 Usage:
12 12 [notify]
13 13 messageidseed = myseed
14 14
15 15 [hooks]
16 16 pretxnclose.changeset_obsoleted = \
17 17 python:hgext.hooklib.changeset_obsoleted.hook
18 18 """
19 19
20 20 from __future__ import absolute_import
21 21
22 22 import email.errors as emailerrors
23 23 import email.utils as emailutils
24 24
25 25 from mercurial.i18n import _
26 26 from mercurial import (
27 27 encoding,
28 28 error,
29 29 logcmdutil,
30 30 mail,
31 31 obsutil,
32 32 pycompat,
33 33 registrar,
34 34 )
35 35 from mercurial.utils import dateutil
36 36 from .. import notify
37 37
38 38 configtable = {}
39 39 configitem = registrar.configitem(configtable)
40 40
41 41 configitem(
42 42 b'notify_obsoleted', b'domain', default=None,
43 43 )
44 44 configitem(
45 45 b'notify_obsoleted', b'messageidseed', default=None,
46 46 )
47 47 configitem(
48 48 b'notify_obsoleted',
49 49 b'template',
50 50 default=b'''Subject: changeset abandoned
51 51
52 52 This changeset has been abandoned.
53 53 ''',
54 54 )
55 55
56 56
57 57 def _report_commit(ui, repo, ctx):
58 58 domain = ui.config(b'notify_obsoleted', b'domain') or ui.config(
59 59 b'notify', b'domain'
60 60 )
61 61 messageidseed = ui.config(
62 62 b'notify_obsoleted', b'messageidseed'
63 63 ) or ui.config(b'notify', b'messageidseed')
64 64 template = ui.config(b'notify_obsoleted', b'template')
65 65 spec = logcmdutil.templatespec(template, None)
66 66 templater = logcmdutil.changesettemplater(ui, repo, spec)
67 67 ui.pushbuffer()
68 68 n = notify.notifier(ui, repo, b'incoming')
69 69
70 70 subs = set()
71 71 for sub, spec in n.subs:
72 72 if spec is None:
73 73 subs.add(sub)
74 74 continue
75 75 revs = repo.revs(b'%r and %d:', spec, ctx.rev())
76 76 if len(revs):
77 77 subs.add(sub)
78 78 continue
79 79 if len(subs) == 0:
80 80 ui.debug(
81 81 b'notify_obsoleted: no subscribers to selected repo and revset\n'
82 82 )
83 83 return
84 84
85 85 templater.show(
86 86 ctx,
87 87 changes=ctx.changeset(),
88 88 baseurl=ui.config(b'web', b'baseurl'),
89 89 root=repo.root,
90 90 webroot=n.root,
91 91 )
92 92 data = ui.popbuffer()
93 93
94 94 try:
95 95 msg = mail.parsebytes(data)
96 96 except emailerrors.MessageParseError as inst:
97 97 raise error.Abort(inst)
98 98
99 99 msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
100 100 msg['Message-Id'] = notify.messageid(
101 101 ctx, domain, messageidseed + b'-obsoleted'
102 102 )
103 103 msg['Date'] = encoding.strfromlocal(
104 104 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
105 105 )
106 106 if not msg['From']:
107 107 sender = ui.config(b'email', b'from') or ui.username()
108 108 if b'@' not in sender or b'@localhost' in sender:
109 109 sender = n.fixmail(sender)
110 110 msg['From'] = mail.addressencode(ui, sender, n.charsets, n.test)
111 111 msg['To'] = ', '.join(sorted(subs))
112 112
113 113 msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
114 114 if ui.configbool(b'notify', b'test'):
115 115 ui.write(msgtext)
116 116 if not msgtext.endswith(b'\n'):
117 117 ui.write(b'\n')
118 118 else:
119 119 ui.status(_(b'notify_obsoleted: sending mail for %d\n') % ctx.rev())
120 120 mail.sendmail(
121 121 ui, emailutils.parseaddr(msg['From'])[1], subs, msgtext, mbox=n.mbox
122 122 )
123 123
124 124
125 def has_successor(repo, rev):
126 return any(
127 r for r in obsutil.allsuccessors(repo.obsstore, [rev]) if r != rev
128 )
129
130
125 131 def hook(ui, repo, hooktype, node=None, **kwargs):
126 if hooktype != b"pretxnclose":
132 if hooktype != b"txnclose":
127 133 raise error.Abort(
128 134 _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
129 135 )
130 for rev in obsutil.getobsoleted(repo, repo.currenttransaction()):
131 _report_commit(ui, repo, repo.unfiltered()[rev])
136 for rev in obsutil.getobsoleted(repo, changes=kwargs['changes']):
137 ctx = repo.unfiltered()[rev]
138 if not has_successor(repo, ctx.node()):
139 _report_commit(ui, repo, ctx)
@@ -1,1040 +1,1050 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 import re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 diffutil,
15 15 encoding,
16 error,
16 17 node as nodemod,
17 18 phases,
18 19 pycompat,
19 20 util,
20 21 )
21 22 from .utils import dateutil
22 23
23 24 ### obsolescence marker flag
24 25
25 26 ## bumpedfix flag
26 27 #
27 28 # When a changeset A' succeed to a changeset A which became public, we call A'
28 29 # "bumped" because it's a successors of a public changesets
29 30 #
30 31 # o A' (bumped)
31 32 # |`:
32 33 # | o A
33 34 # |/
34 35 # o Z
35 36 #
36 37 # The way to solve this situation is to create a new changeset Ad as children
37 38 # of A. This changeset have the same content than A'. So the diff from A to A'
38 39 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
39 40 #
40 41 # o Ad
41 42 # |`:
42 43 # | x A'
43 44 # |'|
44 45 # o | A
45 46 # |/
46 47 # o Z
47 48 #
48 49 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
49 50 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
50 51 # This flag mean that the successors express the changes between the public and
51 52 # bumped version and fix the situation, breaking the transitivity of
52 53 # "bumped" here.
53 54 bumpedfix = 1
54 55 usingsha256 = 2
55 56
56 57
57 58 class marker(object):
58 59 """Wrap obsolete marker raw data"""
59 60
60 61 def __init__(self, repo, data):
61 62 # the repo argument will be used to create changectx in later version
62 63 self._repo = repo
63 64 self._data = data
64 65 self._decodedmeta = None
65 66
66 67 def __hash__(self):
67 68 return hash(self._data)
68 69
69 70 def __eq__(self, other):
70 71 if type(other) != type(self):
71 72 return False
72 73 return self._data == other._data
73 74
74 75 def prednode(self):
75 76 """Predecessor changeset node identifier"""
76 77 return self._data[0]
77 78
78 79 def succnodes(self):
79 80 """List of successor changesets node identifiers"""
80 81 return self._data[1]
81 82
82 83 def parentnodes(self):
83 84 """Parents of the predecessors (None if not recorded)"""
84 85 return self._data[5]
85 86
86 87 def metadata(self):
87 88 """Decoded metadata dictionary"""
88 89 return dict(self._data[3])
89 90
90 91 def date(self):
91 92 """Creation date as (unixtime, offset)"""
92 93 return self._data[4]
93 94
94 95 def flags(self):
95 96 """The flags field of the marker"""
96 97 return self._data[2]
97 98
98 99
99 100 def getmarkers(repo, nodes=None, exclusive=False):
100 101 """returns markers known in a repository
101 102
102 103 If <nodes> is specified, only markers "relevant" to those nodes are are
103 104 returned"""
104 105 if nodes is None:
105 106 rawmarkers = repo.obsstore
106 107 elif exclusive:
107 108 rawmarkers = exclusivemarkers(repo, nodes)
108 109 else:
109 110 rawmarkers = repo.obsstore.relevantmarkers(nodes)
110 111
111 112 for markerdata in rawmarkers:
112 113 yield marker(repo, markerdata)
113 114
114 115
115 116 def sortedmarkers(markers):
116 117 # last item of marker tuple ('parents') may be None or a tuple
117 118 return sorted(markers, key=lambda m: m[:-1] + (m[-1] or (),))
118 119
119 120
120 121 def closestpredecessors(repo, nodeid):
121 122 """yield the list of next predecessors pointing on visible changectx nodes
122 123
123 124 This function respect the repoview filtering, filtered revision will be
124 125 considered missing.
125 126 """
126 127
127 128 precursors = repo.obsstore.predecessors
128 129 stack = [nodeid]
129 130 seen = set(stack)
130 131
131 132 while stack:
132 133 current = stack.pop()
133 134 currentpreccs = precursors.get(current, ())
134 135
135 136 for prec in currentpreccs:
136 137 precnodeid = prec[0]
137 138
138 139 # Basic cycle protection
139 140 if precnodeid in seen:
140 141 continue
141 142 seen.add(precnodeid)
142 143
143 144 if precnodeid in repo:
144 145 yield precnodeid
145 146 else:
146 147 stack.append(precnodeid)
147 148
148 149
149 150 def allpredecessors(obsstore, nodes, ignoreflags=0):
150 151 """Yield node for every precursors of <nodes>.
151 152
152 153 Some precursors may be unknown locally.
153 154
154 155 This is a linear yield unsuited to detecting folded changesets. It includes
155 156 initial nodes too."""
156 157
157 158 remaining = set(nodes)
158 159 seen = set(remaining)
159 160 prec = obsstore.predecessors.get
160 161 while remaining:
161 162 current = remaining.pop()
162 163 yield current
163 164 for mark in prec(current, ()):
164 165 # ignore marker flagged with specified flag
165 166 if mark[2] & ignoreflags:
166 167 continue
167 168 suc = mark[0]
168 169 if suc not in seen:
169 170 seen.add(suc)
170 171 remaining.add(suc)
171 172
172 173
173 174 def allsuccessors(obsstore, nodes, ignoreflags=0):
174 175 """Yield node for every successor of <nodes>.
175 176
176 177 Some successors may be unknown locally.
177 178
178 179 This is a linear yield unsuited to detecting split changesets. It includes
179 180 initial nodes too."""
180 181 remaining = set(nodes)
181 182 seen = set(remaining)
182 183 while remaining:
183 184 current = remaining.pop()
184 185 yield current
185 186 for mark in obsstore.successors.get(current, ()):
186 187 # ignore marker flagged with specified flag
187 188 if mark[2] & ignoreflags:
188 189 continue
189 190 for suc in mark[1]:
190 191 if suc not in seen:
191 192 seen.add(suc)
192 193 remaining.add(suc)
193 194
194 195
195 196 def _filterprunes(markers):
196 197 """return a set with no prune markers"""
197 198 return {m for m in markers if m[1]}
198 199
199 200
200 201 def exclusivemarkers(repo, nodes):
201 202 """set of markers relevant to "nodes" but no other locally-known nodes
202 203
203 204 This function compute the set of markers "exclusive" to a locally-known
204 205 node. This means we walk the markers starting from <nodes> until we reach a
205 206 locally-known precursors outside of <nodes>. Element of <nodes> with
206 207 locally-known successors outside of <nodes> are ignored (since their
207 208 precursors markers are also relevant to these successors).
208 209
209 210 For example:
210 211
211 212 # (A0 rewritten as A1)
212 213 #
213 214 # A0 <-1- A1 # Marker "1" is exclusive to A1
214 215
215 216 or
216 217
217 218 # (A0 rewritten as AX; AX rewritten as A1; AX is unkown locally)
218 219 #
219 220 # <-1- A0 <-2- AX <-3- A1 # Marker "2,3" are exclusive to A1
220 221
221 222 or
222 223
223 224 # (A0 has unknown precursors, A0 rewritten as A1 and A2 (divergence))
224 225 #
225 226 # <-2- A1 # Marker "2" is exclusive to A0,A1
226 227 # /
227 228 # <-1- A0
228 229 # \
229 230 # <-3- A2 # Marker "3" is exclusive to A0,A2
230 231 #
231 232 # in addition:
232 233 #
233 234 # Markers "2,3" are exclusive to A1,A2
234 235 # Markers "1,2,3" are exclusive to A0,A1,A2
235 236
236 237 See test/test-obsolete-bundle-strip.t for more examples.
237 238
238 239 An example usage is strip. When stripping a changeset, we also want to
239 240 strip the markers exclusive to this changeset. Otherwise we would have
240 241 "dangling"" obsolescence markers from its precursors: Obsolescence markers
241 242 marking a node as obsolete without any successors available locally.
242 243
243 244 As for relevant markers, the prune markers for children will be followed.
244 245 Of course, they will only be followed if the pruned children is
245 246 locally-known. Since the prune markers are relevant to the pruned node.
246 247 However, while prune markers are considered relevant to the parent of the
247 248 pruned changesets, prune markers for locally-known changeset (with no
248 249 successors) are considered exclusive to the pruned nodes. This allows
249 250 to strip the prune markers (with the rest of the exclusive chain) alongside
250 251 the pruned changesets.
251 252 """
252 253 # running on a filtered repository would be dangerous as markers could be
253 254 # reported as exclusive when they are relevant for other filtered nodes.
254 255 unfi = repo.unfiltered()
255 256
256 257 # shortcut to various useful item
257 258 has_node = unfi.changelog.index.has_node
258 259 precursorsmarkers = unfi.obsstore.predecessors
259 260 successormarkers = unfi.obsstore.successors
260 261 childrenmarkers = unfi.obsstore.children
261 262
262 263 # exclusive markers (return of the function)
263 264 exclmarkers = set()
264 265 # we need fast membership testing
265 266 nodes = set(nodes)
266 267 # looking for head in the obshistory
267 268 #
268 269 # XXX we are ignoring all issues in regard with cycle for now.
269 270 stack = [n for n in nodes if not _filterprunes(successormarkers.get(n, ()))]
270 271 stack.sort()
271 272 # nodes already stacked
272 273 seennodes = set(stack)
273 274 while stack:
274 275 current = stack.pop()
275 276 # fetch precursors markers
276 277 markers = list(precursorsmarkers.get(current, ()))
277 278 # extend the list with prune markers
278 279 for mark in successormarkers.get(current, ()):
279 280 if not mark[1]:
280 281 markers.append(mark)
281 282 # and markers from children (looking for prune)
282 283 for mark in childrenmarkers.get(current, ()):
283 284 if not mark[1]:
284 285 markers.append(mark)
285 286 # traverse the markers
286 287 for mark in markers:
287 288 if mark in exclmarkers:
288 289 # markers already selected
289 290 continue
290 291
291 292 # If the markers is about the current node, select it
292 293 #
293 294 # (this delay the addition of markers from children)
294 295 if mark[1] or mark[0] == current:
295 296 exclmarkers.add(mark)
296 297
297 298 # should we keep traversing through the precursors?
298 299 prec = mark[0]
299 300
300 301 # nodes in the stack or already processed
301 302 if prec in seennodes:
302 303 continue
303 304
304 305 # is this a locally known node ?
305 306 known = has_node(prec)
306 307 # if locally-known and not in the <nodes> set the traversal
307 308 # stop here.
308 309 if known and prec not in nodes:
309 310 continue
310 311
311 312 # do not keep going if there are unselected markers pointing to this
312 313 # nodes. If we end up traversing these unselected markers later the
313 314 # node will be taken care of at that point.
314 315 precmarkers = _filterprunes(successormarkers.get(prec))
315 316 if precmarkers.issubset(exclmarkers):
316 317 seennodes.add(prec)
317 318 stack.append(prec)
318 319
319 320 return exclmarkers
320 321
321 322
322 323 def foreground(repo, nodes):
323 324 """return all nodes in the "foreground" of other node
324 325
325 326 The foreground of a revision is anything reachable using parent -> children
326 327 or precursor -> successor relation. It is very similar to "descendant" but
327 328 augmented with obsolescence information.
328 329
329 330 Beware that possible obsolescence cycle may result if complex situation.
330 331 """
331 332 repo = repo.unfiltered()
332 333 foreground = set(repo.set(b'%ln::', nodes))
333 334 if repo.obsstore:
334 335 # We only need this complicated logic if there is obsolescence
335 336 # XXX will probably deserve an optimised revset.
336 337 has_node = repo.changelog.index.has_node
337 338 plen = -1
338 339 # compute the whole set of successors or descendants
339 340 while len(foreground) != plen:
340 341 plen = len(foreground)
341 342 succs = {c.node() for c in foreground}
342 343 mutable = [c.node() for c in foreground if c.mutable()]
343 344 succs.update(allsuccessors(repo.obsstore, mutable))
344 345 known = (n for n in succs if has_node(n))
345 346 foreground = set(repo.set(b'%ln::', known))
346 347 return {c.node() for c in foreground}
347 348
348 349
349 350 # effectflag field
350 351 #
351 352 # Effect-flag is a 1-byte bit field used to store what changed between a
352 353 # changeset and its successor(s).
353 354 #
354 355 # The effect flag is stored in obs-markers metadata while we iterate on the
355 356 # information design. That's why we have the EFFECTFLAGFIELD. If we come up
356 357 # with an incompatible design for effect flag, we can store a new design under
357 358 # another field name so we don't break readers. We plan to extend the existing
358 359 # obsmarkers bit-field when the effect flag design will be stabilized.
359 360 #
360 361 # The effect-flag is placed behind an experimental flag
361 362 # `effect-flags` set to off by default.
362 363 #
363 364
364 365 EFFECTFLAGFIELD = b"ef1"
365 366
366 367 DESCCHANGED = 1 << 0 # action changed the description
367 368 METACHANGED = 1 << 1 # action change the meta
368 369 DIFFCHANGED = 1 << 3 # action change diff introduced by the changeset
369 370 PARENTCHANGED = 1 << 2 # action change the parent
370 371 USERCHANGED = 1 << 4 # the user changed
371 372 DATECHANGED = 1 << 5 # the date changed
372 373 BRANCHCHANGED = 1 << 6 # the branch changed
373 374
374 375 METABLACKLIST = [
375 376 re.compile(b'^branch$'),
376 377 re.compile(b'^.*-source$'),
377 378 re.compile(b'^.*_source$'),
378 379 re.compile(b'^source$'),
379 380 ]
380 381
381 382
382 383 def metanotblacklisted(metaitem):
383 384 """ Check that the key of a meta item (extrakey, extravalue) does not
384 385 match at least one of the blacklist pattern
385 386 """
386 387 metakey = metaitem[0]
387 388
388 389 return not any(pattern.match(metakey) for pattern in METABLACKLIST)
389 390
390 391
391 392 def _prepare_hunk(hunk):
392 393 """Drop all information but the username and patch"""
393 394 cleanhunk = []
394 395 for line in hunk.splitlines():
395 396 if line.startswith(b'# User') or not line.startswith(b'#'):
396 397 if line.startswith(b'@@'):
397 398 line = b'@@\n'
398 399 cleanhunk.append(line)
399 400 return cleanhunk
400 401
401 402
402 403 def _getdifflines(iterdiff):
403 404 """return a cleaned up lines"""
404 405 lines = next(iterdiff, None)
405 406
406 407 if lines is None:
407 408 return lines
408 409
409 410 return _prepare_hunk(lines)
410 411
411 412
412 413 def _cmpdiff(leftctx, rightctx):
413 414 """return True if both ctx introduce the "same diff"
414 415
415 416 This is a first and basic implementation, with many shortcoming.
416 417 """
417 418 diffopts = diffutil.diffallopts(leftctx.repo().ui, {b'git': True})
418 419
419 420 # Leftctx or right ctx might be filtered, so we need to use the contexts
420 421 # with an unfiltered repository to safely compute the diff
421 422
422 423 # leftctx and rightctx can be from different repository views in case of
423 424 # hgsubversion, do don't try to access them from same repository
424 425 # rightctx.repo() and leftctx.repo() are not always the same
425 426 leftunfi = leftctx._repo.unfiltered()[leftctx.rev()]
426 427 leftdiff = leftunfi.diff(opts=diffopts)
427 428 rightunfi = rightctx._repo.unfiltered()[rightctx.rev()]
428 429 rightdiff = rightunfi.diff(opts=diffopts)
429 430
430 431 left, right = (0, 0)
431 432 while None not in (left, right):
432 433 left = _getdifflines(leftdiff)
433 434 right = _getdifflines(rightdiff)
434 435
435 436 if left != right:
436 437 return False
437 438 return True
438 439
439 440
440 441 def geteffectflag(source, successors):
441 442 """ From an obs-marker relation, compute what changed between the
442 443 predecessor and the successor.
443 444 """
444 445 effects = 0
445 446
446 447 for changectx in successors:
447 448 # Check if description has changed
448 449 if changectx.description() != source.description():
449 450 effects |= DESCCHANGED
450 451
451 452 # Check if user has changed
452 453 if changectx.user() != source.user():
453 454 effects |= USERCHANGED
454 455
455 456 # Check if date has changed
456 457 if changectx.date() != source.date():
457 458 effects |= DATECHANGED
458 459
459 460 # Check if branch has changed
460 461 if changectx.branch() != source.branch():
461 462 effects |= BRANCHCHANGED
462 463
463 464 # Check if at least one of the parent has changed
464 465 if changectx.parents() != source.parents():
465 466 effects |= PARENTCHANGED
466 467
467 468 # Check if other meta has changed
468 469 changeextra = changectx.extra().items()
469 470 ctxmeta = list(filter(metanotblacklisted, changeextra))
470 471
471 472 sourceextra = source.extra().items()
472 473 srcmeta = list(filter(metanotblacklisted, sourceextra))
473 474
474 475 if ctxmeta != srcmeta:
475 476 effects |= METACHANGED
476 477
477 478 # Check if the diff has changed
478 479 if not _cmpdiff(source, changectx):
479 480 effects |= DIFFCHANGED
480 481
481 482 return effects
482 483
483 484
484 def getobsoleted(repo, tr):
485 """return the set of pre-existing revisions obsoleted by a transaction"""
485 def getobsoleted(repo, tr=None, changes=None):
486 """return the set of pre-existing revisions obsoleted by a transaction
487
488 Either the transaction or changes item of the transaction (for hooks)
489 must be provided, but not both.
490 """
491 if (tr is None) == (changes is None):
492 e = b"exactly one of tr and changes must be provided"
493 raise error.ProgrammingError(e)
486 494 torev = repo.unfiltered().changelog.index.get_rev
487 495 phase = repo._phasecache.phase
488 496 succsmarkers = repo.obsstore.successors.get
489 497 public = phases.public
490 addedmarkers = tr.changes[b'obsmarkers']
491 origrepolen = tr.changes[b'origrepolen']
498 if changes is None:
499 changes = tr.changes
500 addedmarkers = changes[b'obsmarkers']
501 origrepolen = changes[b'origrepolen']
492 502 seenrevs = set()
493 503 obsoleted = set()
494 504 for mark in addedmarkers:
495 505 node = mark[0]
496 506 rev = torev(node)
497 507 if rev is None or rev in seenrevs or rev >= origrepolen:
498 508 continue
499 509 seenrevs.add(rev)
500 510 if phase(repo, rev) == public:
501 511 continue
502 512 if set(succsmarkers(node) or []).issubset(addedmarkers):
503 513 obsoleted.add(rev)
504 514 return obsoleted
505 515
506 516
507 517 class _succs(list):
508 518 """small class to represent a successors with some metadata about it"""
509 519
510 520 def __init__(self, *args, **kwargs):
511 521 super(_succs, self).__init__(*args, **kwargs)
512 522 self.markers = set()
513 523
514 524 def copy(self):
515 525 new = _succs(self)
516 526 new.markers = self.markers.copy()
517 527 return new
518 528
519 529 @util.propertycache
520 530 def _set(self):
521 531 # immutable
522 532 return set(self)
523 533
524 534 def canmerge(self, other):
525 535 return self._set.issubset(other._set)
526 536
527 537
528 538 def successorssets(repo, initialnode, closest=False, cache=None):
529 539 """Return set of all latest successors of initial nodes
530 540
531 541 The successors set of a changeset A are the group of revisions that succeed
532 542 A. It succeeds A as a consistent whole, each revision being only a partial
533 543 replacement. By default, the successors set contains non-obsolete
534 544 changesets only, walking the obsolescence graph until reaching a leaf. If
535 545 'closest' is set to True, closest successors-sets are return (the
536 546 obsolescence walk stops on known changesets).
537 547
538 548 This function returns the full list of successor sets which is why it
539 549 returns a list of tuples and not just a single tuple. Each tuple is a valid
540 550 successors set. Note that (A,) may be a valid successors set for changeset A
541 551 (see below).
542 552
543 553 In most cases, a changeset A will have a single element (e.g. the changeset
544 554 A is replaced by A') in its successors set. Though, it is also common for a
545 555 changeset A to have no elements in its successor set (e.g. the changeset
546 556 has been pruned). Therefore, the returned list of successors sets will be
547 557 [(A',)] or [], respectively.
548 558
549 559 When a changeset A is split into A' and B', however, it will result in a
550 560 successors set containing more than a single element, i.e. [(A',B')].
551 561 Divergent changesets will result in multiple successors sets, i.e. [(A',),
552 562 (A'')].
553 563
554 564 If a changeset A is not obsolete, then it will conceptually have no
555 565 successors set. To distinguish this from a pruned changeset, the successor
556 566 set will contain itself only, i.e. [(A,)].
557 567
558 568 Finally, final successors unknown locally are considered to be pruned
559 569 (pruned: obsoleted without any successors). (Final: successors not affected
560 570 by markers).
561 571
562 572 The 'closest' mode respect the repoview filtering. For example, without
563 573 filter it will stop at the first locally known changeset, with 'visible'
564 574 filter it will stop on visible changesets).
565 575
566 576 The optional `cache` parameter is a dictionary that may contains
567 577 precomputed successors sets. It is meant to reuse the computation of a
568 578 previous call to `successorssets` when multiple calls are made at the same
569 579 time. The cache dictionary is updated in place. The caller is responsible
570 580 for its life span. Code that makes multiple calls to `successorssets`
571 581 *should* use this cache mechanism or risk a performance hit.
572 582
573 583 Since results are different depending of the 'closest' most, the same cache
574 584 cannot be reused for both mode.
575 585 """
576 586
577 587 succmarkers = repo.obsstore.successors
578 588
579 589 # Stack of nodes we search successors sets for
580 590 toproceed = [initialnode]
581 591 # set version of above list for fast loop detection
582 592 # element added to "toproceed" must be added here
583 593 stackedset = set(toproceed)
584 594 if cache is None:
585 595 cache = {}
586 596
587 597 # This while loop is the flattened version of a recursive search for
588 598 # successors sets
589 599 #
590 600 # def successorssets(x):
591 601 # successors = directsuccessors(x)
592 602 # ss = [[]]
593 603 # for succ in directsuccessors(x):
594 604 # # product as in itertools cartesian product
595 605 # ss = product(ss, successorssets(succ))
596 606 # return ss
597 607 #
598 608 # But we can not use plain recursive calls here:
599 609 # - that would blow the python call stack
600 610 # - obsolescence markers may have cycles, we need to handle them.
601 611 #
602 612 # The `toproceed` list act as our call stack. Every node we search
603 613 # successors set for are stacked there.
604 614 #
605 615 # The `stackedset` is set version of this stack used to check if a node is
606 616 # already stacked. This check is used to detect cycles and prevent infinite
607 617 # loop.
608 618 #
609 619 # successors set of all nodes are stored in the `cache` dictionary.
610 620 #
611 621 # After this while loop ends we use the cache to return the successors sets
612 622 # for the node requested by the caller.
613 623 while toproceed:
614 624 # Every iteration tries to compute the successors sets of the topmost
615 625 # node of the stack: CURRENT.
616 626 #
617 627 # There are four possible outcomes:
618 628 #
619 629 # 1) We already know the successors sets of CURRENT:
620 630 # -> mission accomplished, pop it from the stack.
621 631 # 2) Stop the walk:
622 632 # default case: Node is not obsolete
623 633 # closest case: Node is known at this repo filter level
624 634 # -> the node is its own successors sets. Add it to the cache.
625 635 # 3) We do not know successors set of direct successors of CURRENT:
626 636 # -> We add those successors to the stack.
627 637 # 4) We know successors sets of all direct successors of CURRENT:
628 638 # -> We can compute CURRENT successors set and add it to the
629 639 # cache.
630 640 #
631 641 current = toproceed[-1]
632 642
633 643 # case 2 condition is a bit hairy because of closest,
634 644 # we compute it on its own
635 645 case2condition = (current not in succmarkers) or (
636 646 closest and current != initialnode and current in repo
637 647 )
638 648
639 649 if current in cache:
640 650 # case (1): We already know the successors sets
641 651 stackedset.remove(toproceed.pop())
642 652 elif case2condition:
643 653 # case (2): end of walk.
644 654 if current in repo:
645 655 # We have a valid successors.
646 656 cache[current] = [_succs((current,))]
647 657 else:
648 658 # Final obsolete version is unknown locally.
649 659 # Do not count that as a valid successors
650 660 cache[current] = []
651 661 else:
652 662 # cases (3) and (4)
653 663 #
654 664 # We proceed in two phases. Phase 1 aims to distinguish case (3)
655 665 # from case (4):
656 666 #
657 667 # For each direct successors of CURRENT, we check whether its
658 668 # successors sets are known. If they are not, we stack the
659 669 # unknown node and proceed to the next iteration of the while
660 670 # loop. (case 3)
661 671 #
662 672 # During this step, we may detect obsolescence cycles: a node
663 673 # with unknown successors sets but already in the call stack.
664 674 # In such a situation, we arbitrary set the successors sets of
665 675 # the node to nothing (node pruned) to break the cycle.
666 676 #
667 677 # If no break was encountered we proceed to phase 2.
668 678 #
669 679 # Phase 2 computes successors sets of CURRENT (case 4); see details
670 680 # in phase 2 itself.
671 681 #
672 682 # Note the two levels of iteration in each phase.
673 683 # - The first one handles obsolescence markers using CURRENT as
674 684 # precursor (successors markers of CURRENT).
675 685 #
676 686 # Having multiple entry here means divergence.
677 687 #
678 688 # - The second one handles successors defined in each marker.
679 689 #
680 690 # Having none means pruned node, multiple successors means split,
681 691 # single successors are standard replacement.
682 692 #
683 693 for mark in sortedmarkers(succmarkers[current]):
684 694 for suc in mark[1]:
685 695 if suc not in cache:
686 696 if suc in stackedset:
687 697 # cycle breaking
688 698 cache[suc] = []
689 699 else:
690 700 # case (3) If we have not computed successors sets
691 701 # of one of those successors we add it to the
692 702 # `toproceed` stack and stop all work for this
693 703 # iteration.
694 704 toproceed.append(suc)
695 705 stackedset.add(suc)
696 706 break
697 707 else:
698 708 continue
699 709 break
700 710 else:
701 711 # case (4): we know all successors sets of all direct
702 712 # successors
703 713 #
704 714 # Successors set contributed by each marker depends on the
705 715 # successors sets of all its "successors" node.
706 716 #
707 717 # Each different marker is a divergence in the obsolescence
708 718 # history. It contributes successors sets distinct from other
709 719 # markers.
710 720 #
711 721 # Within a marker, a successor may have divergent successors
712 722 # sets. In such a case, the marker will contribute multiple
713 723 # divergent successors sets. If multiple successors have
714 724 # divergent successors sets, a Cartesian product is used.
715 725 #
716 726 # At the end we post-process successors sets to remove
717 727 # duplicated entry and successors set that are strict subset of
718 728 # another one.
719 729 succssets = []
720 730 for mark in sortedmarkers(succmarkers[current]):
721 731 # successors sets contributed by this marker
722 732 base = _succs()
723 733 base.markers.add(mark)
724 734 markss = [base]
725 735 for suc in mark[1]:
726 736 # cardinal product with previous successors
727 737 productresult = []
728 738 for prefix in markss:
729 739 for suffix in cache[suc]:
730 740 newss = prefix.copy()
731 741 newss.markers.update(suffix.markers)
732 742 for part in suffix:
733 743 # do not duplicated entry in successors set
734 744 # first entry wins.
735 745 if part not in newss:
736 746 newss.append(part)
737 747 productresult.append(newss)
738 748 if productresult:
739 749 markss = productresult
740 750 succssets.extend(markss)
741 751 # remove duplicated and subset
742 752 seen = []
743 753 final = []
744 754 candidates = sorted(
745 755 (s for s in succssets if s), key=len, reverse=True
746 756 )
747 757 for cand in candidates:
748 758 for seensuccs in seen:
749 759 if cand.canmerge(seensuccs):
750 760 seensuccs.markers.update(cand.markers)
751 761 break
752 762 else:
753 763 final.append(cand)
754 764 seen.append(cand)
755 765 final.reverse() # put small successors set first
756 766 cache[current] = final
757 767 return cache[initialnode]
758 768
759 769
760 770 def successorsandmarkers(repo, ctx):
761 771 """compute the raw data needed for computing obsfate
762 772 Returns a list of dict, one dict per successors set
763 773 """
764 774 if not ctx.obsolete():
765 775 return None
766 776
767 777 ssets = successorssets(repo, ctx.node(), closest=True)
768 778
769 779 # closestsuccessors returns an empty list for pruned revisions, remap it
770 780 # into a list containing an empty list for future processing
771 781 if ssets == []:
772 782 ssets = [[]]
773 783
774 784 # Try to recover pruned markers
775 785 succsmap = repo.obsstore.successors
776 786 fullsuccessorsets = [] # successor set + markers
777 787 for sset in ssets:
778 788 if sset:
779 789 fullsuccessorsets.append(sset)
780 790 else:
781 791 # successorsset return an empty set() when ctx or one of its
782 792 # successors is pruned.
783 793 # In this case, walk the obs-markers tree again starting with ctx
784 794 # and find the relevant pruning obs-makers, the ones without
785 795 # successors.
786 796 # Having these markers allow us to compute some information about
787 797 # its fate, like who pruned this changeset and when.
788 798
789 799 # XXX we do not catch all prune markers (eg rewritten then pruned)
790 800 # (fix me later)
791 801 foundany = False
792 802 for mark in succsmap.get(ctx.node(), ()):
793 803 if not mark[1]:
794 804 foundany = True
795 805 sset = _succs()
796 806 sset.markers.add(mark)
797 807 fullsuccessorsets.append(sset)
798 808 if not foundany:
799 809 fullsuccessorsets.append(_succs())
800 810
801 811 values = []
802 812 for sset in fullsuccessorsets:
803 813 values.append({b'successors': sset, b'markers': sset.markers})
804 814
805 815 return values
806 816
807 817
808 818 def _getobsfate(successorssets):
809 819 """ Compute a changeset obsolescence fate based on its successorssets.
810 820 Successors can be the tipmost ones or the immediate ones. This function
811 821 return values are not meant to be shown directly to users, it is meant to
812 822 be used by internal functions only.
813 823 Returns one fate from the following values:
814 824 - pruned
815 825 - diverged
816 826 - superseded
817 827 - superseded_split
818 828 """
819 829
820 830 if len(successorssets) == 0:
821 831 # The commit has been pruned
822 832 return b'pruned'
823 833 elif len(successorssets) > 1:
824 834 return b'diverged'
825 835 else:
826 836 # No divergence, only one set of successors
827 837 successors = successorssets[0]
828 838
829 839 if len(successors) == 1:
830 840 return b'superseded'
831 841 else:
832 842 return b'superseded_split'
833 843
834 844
835 845 def obsfateverb(successorset, markers):
836 846 """ Return the verb summarizing the successorset and potentially using
837 847 information from the markers
838 848 """
839 849 if not successorset:
840 850 verb = b'pruned'
841 851 elif len(successorset) == 1:
842 852 verb = b'rewritten'
843 853 else:
844 854 verb = b'split'
845 855 return verb
846 856
847 857
848 858 def markersdates(markers):
849 859 """returns the list of dates for a list of markers
850 860 """
851 861 return [m[4] for m in markers]
852 862
853 863
854 864 def markersusers(markers):
855 865 """ Returns a sorted list of markers users without duplicates
856 866 """
857 867 markersmeta = [dict(m[3]) for m in markers]
858 868 users = {
859 869 encoding.tolocal(meta[b'user'])
860 870 for meta in markersmeta
861 871 if meta.get(b'user')
862 872 }
863 873
864 874 return sorted(users)
865 875
866 876
867 877 def markersoperations(markers):
868 878 """ Returns a sorted list of markers operations without duplicates
869 879 """
870 880 markersmeta = [dict(m[3]) for m in markers]
871 881 operations = {
872 882 meta.get(b'operation') for meta in markersmeta if meta.get(b'operation')
873 883 }
874 884
875 885 return sorted(operations)
876 886
877 887
878 888 def obsfateprinter(ui, repo, successors, markers, formatctx):
879 889 """ Build a obsfate string for a single successorset using all obsfate
880 890 related function defined in obsutil
881 891 """
882 892 quiet = ui.quiet
883 893 verbose = ui.verbose
884 894 normal = not verbose and not quiet
885 895
886 896 line = []
887 897
888 898 # Verb
889 899 line.append(obsfateverb(successors, markers))
890 900
891 901 # Operations
892 902 operations = markersoperations(markers)
893 903 if operations:
894 904 line.append(b" using %s" % b", ".join(operations))
895 905
896 906 # Successors
897 907 if successors:
898 908 fmtsuccessors = [formatctx(repo[succ]) for succ in successors]
899 909 line.append(b" as %s" % b", ".join(fmtsuccessors))
900 910
901 911 # Users
902 912 users = markersusers(markers)
903 913 # Filter out current user in not verbose mode to reduce amount of
904 914 # information
905 915 if not verbose:
906 916 currentuser = ui.username(acceptempty=True)
907 917 if len(users) == 1 and currentuser in users:
908 918 users = None
909 919
910 920 if (verbose or normal) and users:
911 921 line.append(b" by %s" % b", ".join(users))
912 922
913 923 # Date
914 924 dates = markersdates(markers)
915 925
916 926 if dates and verbose:
917 927 min_date = min(dates)
918 928 max_date = max(dates)
919 929
920 930 if min_date == max_date:
921 931 fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
922 932 line.append(b" (at %s)" % fmtmin_date)
923 933 else:
924 934 fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
925 935 fmtmax_date = dateutil.datestr(max_date, b'%Y-%m-%d %H:%M %1%2')
926 936 line.append(b" (between %s and %s)" % (fmtmin_date, fmtmax_date))
927 937
928 938 return b"".join(line)
929 939
930 940
931 941 filteredmsgtable = {
932 942 b"pruned": _(b"hidden revision '%s' is pruned"),
933 943 b"diverged": _(b"hidden revision '%s' has diverged"),
934 944 b"superseded": _(b"hidden revision '%s' was rewritten as: %s"),
935 945 b"superseded_split": _(b"hidden revision '%s' was split as: %s"),
936 946 b"superseded_split_several": _(
937 947 b"hidden revision '%s' was split as: %s and %d more"
938 948 ),
939 949 }
940 950
941 951
942 952 def _getfilteredreason(repo, changeid, ctx):
943 953 """return a human-friendly string on why a obsolete changeset is hidden
944 954 """
945 955 successors = successorssets(repo, ctx.node())
946 956 fate = _getobsfate(successors)
947 957
948 958 # Be more precise in case the revision is superseded
949 959 if fate == b'pruned':
950 960 return filteredmsgtable[b'pruned'] % changeid
951 961 elif fate == b'diverged':
952 962 return filteredmsgtable[b'diverged'] % changeid
953 963 elif fate == b'superseded':
954 964 single_successor = nodemod.short(successors[0][0])
955 965 return filteredmsgtable[b'superseded'] % (changeid, single_successor)
956 966 elif fate == b'superseded_split':
957 967
958 968 succs = []
959 969 for node_id in successors[0]:
960 970 succs.append(nodemod.short(node_id))
961 971
962 972 if len(succs) <= 2:
963 973 fmtsuccs = b', '.join(succs)
964 974 return filteredmsgtable[b'superseded_split'] % (changeid, fmtsuccs)
965 975 else:
966 976 firstsuccessors = b', '.join(succs[:2])
967 977 remainingnumber = len(succs) - 2
968 978
969 979 args = (changeid, firstsuccessors, remainingnumber)
970 980 return filteredmsgtable[b'superseded_split_several'] % args
971 981
972 982
973 983 def divergentsets(repo, ctx):
974 984 """Compute sets of commits divergent with a given one"""
975 985 cache = {}
976 986 base = {}
977 987 for n in allpredecessors(repo.obsstore, [ctx.node()]):
978 988 if n == ctx.node():
979 989 # a node can't be a base for divergence with itself
980 990 continue
981 991 nsuccsets = successorssets(repo, n, cache)
982 992 for nsuccset in nsuccsets:
983 993 if ctx.node() in nsuccset:
984 994 # we are only interested in *other* successor sets
985 995 continue
986 996 if tuple(nsuccset) in base:
987 997 # we already know the latest base for this divergency
988 998 continue
989 999 base[tuple(nsuccset)] = n
990 1000 return [
991 1001 {b'divergentnodes': divset, b'commonpredecessor': b}
992 1002 for divset, b in pycompat.iteritems(base)
993 1003 ]
994 1004
995 1005
996 1006 def whyunstable(repo, ctx):
997 1007 result = []
998 1008 if ctx.orphan():
999 1009 for parent in ctx.parents():
1000 1010 kind = None
1001 1011 if parent.orphan():
1002 1012 kind = b'orphan'
1003 1013 elif parent.obsolete():
1004 1014 kind = b'obsolete'
1005 1015 if kind is not None:
1006 1016 result.append(
1007 1017 {
1008 1018 b'instability': b'orphan',
1009 1019 b'reason': b'%s parent' % kind,
1010 1020 b'node': parent.hex(),
1011 1021 }
1012 1022 )
1013 1023 if ctx.phasedivergent():
1014 1024 predecessors = allpredecessors(
1015 1025 repo.obsstore, [ctx.node()], ignoreflags=bumpedfix
1016 1026 )
1017 1027 immutable = [
1018 1028 repo[p] for p in predecessors if p in repo and not repo[p].mutable()
1019 1029 ]
1020 1030 for predecessor in immutable:
1021 1031 result.append(
1022 1032 {
1023 1033 b'instability': b'phase-divergent',
1024 1034 b'reason': b'immutable predecessor',
1025 1035 b'node': predecessor.hex(),
1026 1036 }
1027 1037 )
1028 1038 if ctx.contentdivergent():
1029 1039 dsets = divergentsets(repo, ctx)
1030 1040 for dset in dsets:
1031 1041 divnodes = [repo[n] for n in dset[b'divergentnodes']]
1032 1042 result.append(
1033 1043 {
1034 1044 b'instability': b'content-divergent',
1035 1045 b'divergentnodes': divnodes,
1036 1046 b'reason': b'predecessor',
1037 1047 b'node': nodemod.hex(dset[b'commonpredecessor']),
1038 1048 }
1039 1049 )
1040 1050 return result
@@ -1,84 +1,114 b''
1 1 $ cat <<EOF >> $HGRCPATH
2 2 > [experimental]
3 3 > evolution = true
4 4 >
5 5 > [extensions]
6 6 > notify =
7 7 > hooklib =
8 8 >
9 9 > [phases]
10 10 > publish = False
11 11 >
12 12 > [notify]
13 13 > sources = pull
14 14 > diffstat = False
15 15 > messageidseed = example
16 16 > domain = example.com
17 17 >
18 18 > [reposubs]
19 19 > * = baz
20 20 > EOF
21 21 $ hg init a
22 22 $ hg --cwd a debugbuilddag +2
23 23 $ hg init b
24 24 $ cat <<EOF >> b/.hg/hgrc
25 25 > [hooks]
26 26 > incoming.notify = python:hgext.notify.hook
27 > pretxnclose.changeset_obsoleted = python:hgext.hooklib.changeset_obsoleted.hook
27 > txnclose.changeset_obsoleted = python:hgext.hooklib.changeset_obsoleted.hook
28 28 > EOF
29 29 $ hg --cwd b pull ../a | "$PYTHON" $TESTDIR/unwrap-message-id.py
30 30 pulling from ../a
31 31 requesting all changes
32 32 adding changesets
33 33 adding manifests
34 34 adding file changes
35 35 added 2 changesets with 0 changes to 0 files
36 36 new changesets 1ea73414a91b:66f7d451a68b (2 drafts)
37 37 MIME-Version: 1.0
38 38 Content-Type: text/plain; charset="us-ascii"
39 39 Content-Transfer-Encoding: 7bit
40 40 Date: * (glob)
41 41 Subject: changeset in * (glob)
42 42 From: debugbuilddag@example.com
43 43 X-Hg-Notification: changeset 1ea73414a91b
44 44 Message-Id: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
45 45 To: baz@example.com
46 46
47 47 changeset 1ea73414a91b in $TESTTMP/b
48 48 details: $TESTTMP/b?cmd=changeset;node=1ea73414a91b
49 49 description:
50 50 r0
51 51 MIME-Version: 1.0
52 52 Content-Type: text/plain; charset="us-ascii"
53 53 Content-Transfer-Encoding: 7bit
54 54 Date: * (glob)
55 55 Subject: changeset in * (glob)
56 56 From: debugbuilddag@example.com
57 57 X-Hg-Notification: changeset 66f7d451a68b
58 58 Message-Id: <hg.364d03da7dc13829eb779a805be7e37f54f572e9afcea7d2626856a794d3e8f3@example.com>
59 59 To: baz@example.com
60 60
61 61 changeset 66f7d451a68b in $TESTTMP/b
62 62 details: $TESTTMP/b?cmd=changeset;node=66f7d451a68b
63 63 description:
64 64 r1
65 65 (run 'hg update' to get a working copy)
66 66 $ hg --cwd a debugobsolete 1ea73414a91b0920940797d8fc6a11e447f8ea1e
67 67 1 new obsolescence markers
68 68 obsoleted 1 changesets
69 69 1 new orphan changesets
70 70 $ hg --cwd a push ../b --hidden | "$PYTHON" $TESTDIR/unwrap-message-id.py
71 71 1 new orphan changesets
72 72 pushing to ../b
73 73 searching for changes
74 74 no changes found
75 1 new obsolescence markers
76 obsoleted 1 changesets
75 77 Subject: changeset abandoned
76 78 In-reply-to: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
77 79 Message-Id: <hg.d6329e9481594f0f3c8a84362b3511318bfbce50748ab1123f909eb6fbcab018@example.com>
78 80 Date: * (glob)
79 81 From: test@example.com
80 82 To: baz@example.com
81 83
82 84 This changeset has been abandoned.
85
86 Check that known changesets with known successors do not result in a mail.
87
88 $ hg init c
89 $ hg init d
90 $ cat <<EOF >> d/.hg/hgrc
91 > [hooks]
92 > incoming.notify = python:hgext.notify.hook
93 > txnclose.changeset_obsoleted = python:hgext.hooklib.changeset_obsoleted.hook
94 > EOF
95 $ hg --cwd c debugbuilddag '.:parent.*parent'
96 $ hg --cwd c push ../d -r 1
97 pushing to ../d
98 searching for changes
99 adding changesets
100 adding manifests
101 adding file changes
102 added 2 changesets with 0 changes to 0 files
103 $ hg --cwd c debugobsolete $(hg --cwd c log -T '{node}' -r 1) $(hg --cwd c log -T '{node}' -r 2)
83 104 1 new obsolescence markers
84 105 obsoleted 1 changesets
106 $ hg --cwd c push ../d | "$PYTHON" $TESTDIR/unwrap-message-id.py
107 pushing to ../d
108 searching for changes
109 adding changesets
110 adding manifests
111 adding file changes
112 added 1 changesets with 0 changes to 0 files (+1 heads)
113 1 new obsolescence markers
114 obsoleted 1 changesets
General Comments 0
You need to be logged in to leave comments. Login now