##// END OF EJS Templates
merge with stable
Martin von Zweigbergk -
r39298:5b9f1161 merge default
parent child Browse files
Show More
@@ -1,96 +1,96
1 1 # -*- coding: UTF-8 -*-
2 2 # beautifygraph.py - improve graph output by using Unicode characters
3 3 #
4 4 # Copyright 2018 John Stiles <johnstiles@gmail.com>
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 '''beautify log -G output by using Unicode characters (EXPERIMENTAL)
10 10
11 11 A terminal with UTF-8 support and monospace narrow text are required.
12 12 '''
13 13
14 14 from __future__ import absolute_import
15 15
16 16 from mercurial.i18n import _
17 17 from mercurial import (
18 18 encoding,
19 19 extensions,
20 20 graphmod,
21 21 pycompat,
22 22 templatekw,
23 23 )
24 24
25 25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
26 26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 27 # be specifying the version(s) of Mercurial they are tested with, or
28 28 # leave the attribute unspecified.
29 29 testedwith = 'ships-with-hg-core'
30 30
31 31 def prettyedge(before, edge, after):
32 32 if edge == '~':
33 33 return '\xE2\x95\xA7' # U+2567 ╧
34 34 if edge == 'X':
35 35 return '\xE2\x95\xB3' # U+2573 β•³
36 36 if edge == '/':
37 37 return '\xE2\x95\xB1' # U+2571 β•±
38 38 if edge == '-':
39 39 return '\xE2\x94\x80' # U+2500 ─
40 40 if edge == '|':
41 41 return '\xE2\x94\x82' # U+2502 β”‚
42 42 if edge == ':':
43 43 return '\xE2\x94\x86' # U+2506 ┆
44 44 if edge == '\\':
45 45 return '\xE2\x95\xB2' # U+2572 β•²
46 46 if edge == '+':
47 47 if before == ' ' and not after == ' ':
48 48 return '\xE2\x94\x9C' # U+251C β”œ
49 49 if after == ' ' and not before == ' ':
50 50 return '\xE2\x94\xA4' # U+2524 ─
51 51 return '\xE2\x94\xBC' # U+253C β”Ό
52 52 return edge
53 53
54 54 def convertedges(line):
55 55 line = ' %s ' % line
56 56 pretty = []
57 57 for idx in pycompat.xrange(len(line) - 2):
58 58 pretty.append(prettyedge(line[idx:idx + 1],
59 59 line[idx + 1:idx + 2],
60 60 line[idx + 2:idx + 3]))
61 61 return ''.join(pretty)
62 62
63 63 def getprettygraphnode(orig, *args, **kwargs):
64 64 node = orig(*args, **kwargs)
65 65 if node == 'o':
66 66 return '\xE2\x97\x8B' # U+25CB β—‹
67 67 if node == '@':
68 68 return '\xE2\x97\x8D' # U+25CD ◍
69 69 if node == '*':
70 70 return '\xE2\x88\x97' # U+2217 βˆ—
71 71 if node == 'x':
72 72 return '\xE2\x97\x8C' # U+25CC β—Œ
73 73 if node == '_':
74 74 return '\xE2\x95\xA4' # U+2564 β•€
75 75 return node
76 76
77 77 def outputprettygraph(orig, ui, graph, *args, **kwargs):
78 78 (edges, text) = zip(*graph)
79 79 graph = zip([convertedges(e) for e in edges], text)
80 80 return orig(ui, graph, *args, **kwargs)
81 81
82 82 def extsetup(ui):
83 if ui.plain('graph'):
84 return
85
83 86 if encoding.encoding != 'UTF-8':
84 87 ui.warn(_('beautifygraph: unsupported encoding, UTF-8 required\n'))
85 88 return
86 89
87 90 if r'A' in encoding._wide:
88 91 ui.warn(_('beautifygraph: unsupported terminal settings, '
89 92 'monospace narrow text required\n'))
90 93 return
91 94
92 if ui.plain('graph'):
93 return
94
95 95 extensions.wrapfunction(graphmod, 'outputgraph', outputprettygraph)
96 96 extensions.wrapfunction(templatekw, 'getgraphnode', getprettygraphnode)
@@ -1,728 +1,726
1 1 """ Mercurial phases support code
2 2
3 3 ---
4 4
5 5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 6 Logilab SA <contact@logilab.fr>
7 7 Augie Fackler <durin42@gmail.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License version 2 or any later version.
11 11
12 12 ---
13 13
14 14 This module implements most phase logic in mercurial.
15 15
16 16
17 17 Basic Concept
18 18 =============
19 19
20 20 A 'changeset phase' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described
22 22 below, here we describe the properties they have in common.
23 23
24 24 Like bookmarks, phases are not stored in history and thus are not
25 25 permanent and leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered,
28 28 so they can be considered from lowest to highest. The default, lowest
29 29 phase is 'public' - this is the normal phase of existing changesets. A
30 30 child changeset can not be in a lower phase than its parents.
31 31
32 32 These phases share a hierarchy of traits:
33 33
34 34 immutable shared
35 35 public: X X
36 36 draft: X
37 37 secret:
38 38
39 39 Local commits are draft by default.
40 40
41 41 Phase Movement and Exchange
42 42 ===========================
43 43
44 44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 45 a publish option set, we call such a server a "publishing server".
46 46 Pushing a draft changeset to a publishing server changes the phase to
47 47 public.
48 48
49 49 A small list of fact/rules define the exchange of phase:
50 50
51 51 * old client never changes server states
52 52 * pull never changes server states
53 53 * publish and old server changesets are seen as public by client
54 54 * any secret changeset seen in another repository is lowered to at
55 55 least draft
56 56
57 57 Here is the final table summing up the 49 possible use cases of phase
58 58 exchange:
59 59
60 60 server
61 61 old publish non-publish
62 62 N X N D P N D P
63 63 old client
64 64 pull
65 65 N - X/X - X/D X/P - X/D X/P
66 66 X - X/X - X/D X/P - X/D X/P
67 67 push
68 68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 69 new client
70 70 pull
71 71 N - P/X - P/D P/P - D/D P/P
72 72 D - P/X - P/D P/P - D/D P/P
73 73 P - P/X - P/D P/P - P/D P/P
74 74 push
75 75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 76 P P/X P/X P/P P/P P/P P/P P/P P/P
77 77
78 78 Legend:
79 79
80 80 A/B = final state on client / state on server
81 81
82 82 * N = new/not present,
83 83 * P = public,
84 84 * D = draft,
85 85 * X = not tracked (i.e., the old client or server has no internal
86 86 way of recording the phase.)
87 87
88 88 passive = only pushes
89 89
90 90
91 91 A cell here can be read like this:
92 92
93 93 "When a new client pushes a draft changeset (D) to a publishing
94 94 server where it's not present (N), it's marked public on both
95 95 sides (P/P)."
96 96
97 97 Note: old client behave as a publishing server with draft only content
98 98 - other people see it as public
99 99 - content is pushed as draft
100 100
101 101 """
102 102
103 103 from __future__ import absolute_import
104 104
105 105 import errno
106 106 import struct
107 107
108 108 from .i18n import _
109 109 from .node import (
110 110 bin,
111 111 hex,
112 112 nullid,
113 113 nullrev,
114 114 short,
115 115 )
116 116 from . import (
117 117 error,
118 118 pycompat,
119 119 smartset,
120 120 txnutil,
121 121 util,
122 122 )
123 123
124 124 _fphasesentry = struct.Struct('>i20s')
125 125
126 126 allphases = public, draft, secret = range(3)
127 127 trackedphases = allphases[1:]
128 128 phasenames = ['public', 'draft', 'secret']
129 129 mutablephases = tuple(allphases[1:])
130 130 remotehiddenphases = tuple(allphases[2:])
131 131
132 132 def _readroots(repo, phasedefaults=None):
133 133 """Read phase roots from disk
134 134
135 135 phasedefaults is a list of fn(repo, roots) callable, which are
136 136 executed if the phase roots file does not exist. When phases are
137 137 being initialized on an existing repository, this could be used to
138 138 set selected changesets phase to something else than public.
139 139
140 140 Return (roots, dirty) where dirty is true if roots differ from
141 141 what is being stored.
142 142 """
143 143 repo = repo.unfiltered()
144 144 dirty = False
145 145 roots = [set() for i in allphases]
146 146 try:
147 147 f, pending = txnutil.trypending(repo.root, repo.svfs, 'phaseroots')
148 148 try:
149 149 for line in f:
150 150 phase, nh = line.split()
151 151 roots[int(phase)].add(bin(nh))
152 152 finally:
153 153 f.close()
154 154 except IOError as inst:
155 155 if inst.errno != errno.ENOENT:
156 156 raise
157 157 if phasedefaults:
158 158 for f in phasedefaults:
159 159 roots = f(repo, roots)
160 160 dirty = True
161 161 return roots, dirty
162 162
163 163 def binaryencode(phasemapping):
164 164 """encode a 'phase -> nodes' mapping into a binary stream
165 165
166 166 Since phases are integer the mapping is actually a python list:
167 167 [[PUBLIC_HEADS], [DRAFTS_HEADS], [SECRET_HEADS]]
168 168 """
169 169 binarydata = []
170 170 for phase, nodes in enumerate(phasemapping):
171 171 for head in nodes:
172 172 binarydata.append(_fphasesentry.pack(phase, head))
173 173 return ''.join(binarydata)
174 174
175 175 def binarydecode(stream):
176 176 """decode a binary stream into a 'phase -> nodes' mapping
177 177
178 178 Since phases are integer the mapping is actually a python list."""
179 179 headsbyphase = [[] for i in allphases]
180 180 entrysize = _fphasesentry.size
181 181 while True:
182 182 entry = stream.read(entrysize)
183 183 if len(entry) < entrysize:
184 184 if entry:
185 185 raise error.Abort(_('bad phase-heads stream'))
186 186 break
187 187 phase, node = _fphasesentry.unpack(entry)
188 188 headsbyphase[phase].append(node)
189 189 return headsbyphase
190 190
191 191 def _trackphasechange(data, rev, old, new):
192 192 """add a phase move the <data> dictionnary
193 193
194 194 If data is None, nothing happens.
195 195 """
196 196 if data is None:
197 197 return
198 198 existing = data.get(rev)
199 199 if existing is not None:
200 200 old = existing[0]
201 201 data[rev] = (old, new)
202 202
203 203 class phasecache(object):
204 204 def __init__(self, repo, phasedefaults, _load=True):
205 205 if _load:
206 206 # Cheap trick to allow shallow-copy without copy module
207 207 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
208 208 self._loadedrevslen = 0
209 209 self._phasesets = None
210 210 self.filterunknown(repo)
211 211 self.opener = repo.svfs
212 212
213 213 def getrevset(self, repo, phases, subset=None):
214 214 """return a smartset for the given phases"""
215 215 self.loadphaserevs(repo) # ensure phase's sets are loaded
216 216 phases = set(phases)
217 217 if public not in phases:
218 218 # fast path: _phasesets contains the interesting sets,
219 219 # might only need a union and post-filtering.
220 220 if len(phases) == 1:
221 221 [p] = phases
222 222 revs = self._phasesets[p]
223 223 else:
224 224 revs = set.union(*[self._phasesets[p] for p in phases])
225 225 if repo.changelog.filteredrevs:
226 226 revs = revs - repo.changelog.filteredrevs
227 227 if subset is None:
228 228 return smartset.baseset(revs)
229 229 else:
230 230 return subset & smartset.baseset(revs)
231 231 else:
232 232 phases = set(allphases).difference(phases)
233 233 if not phases:
234 234 return smartset.fullreposet(repo)
235 235 if len(phases) == 1:
236 236 [p] = phases
237 237 revs = self._phasesets[p]
238 238 else:
239 239 revs = set.union(*[self._phasesets[p] for p in phases])
240 240 if subset is None:
241 241 subset = smartset.fullreposet(repo)
242 242 if not revs:
243 243 return subset
244 244 return subset.filter(lambda r: r not in revs)
245 245
246 246 def copy(self):
247 247 # Shallow copy meant to ensure isolation in
248 248 # advance/retractboundary(), nothing more.
249 249 ph = self.__class__(None, None, _load=False)
250 250 ph.phaseroots = self.phaseroots[:]
251 251 ph.dirty = self.dirty
252 252 ph.opener = self.opener
253 253 ph._loadedrevslen = self._loadedrevslen
254 254 ph._phasesets = self._phasesets
255 255 return ph
256 256
257 257 def replace(self, phcache):
258 258 """replace all values in 'self' with content of phcache"""
259 259 for a in ('phaseroots', 'dirty', 'opener', '_loadedrevslen',
260 260 '_phasesets'):
261 261 setattr(self, a, getattr(phcache, a))
262 262
263 263 def _getphaserevsnative(self, repo):
264 264 repo = repo.unfiltered()
265 265 nativeroots = []
266 266 for phase in trackedphases:
267 267 nativeroots.append(pycompat.maplist(repo.changelog.rev,
268 268 self.phaseroots[phase]))
269 269 return repo.changelog.computephases(nativeroots)
270 270
271 271 def _computephaserevspure(self, repo):
272 272 repo = repo.unfiltered()
273 273 cl = repo.changelog
274 274 self._phasesets = [set() for phase in allphases]
275 275 roots = pycompat.maplist(cl.rev, self.phaseroots[secret])
276 276 if roots:
277 277 ps = set(cl.descendants(roots))
278 278 for root in roots:
279 279 ps.add(root)
280 280 self._phasesets[secret] = ps
281 281 roots = pycompat.maplist(cl.rev, self.phaseroots[draft])
282 282 if roots:
283 283 ps = set(cl.descendants(roots))
284 284 for root in roots:
285 285 ps.add(root)
286 286 ps.difference_update(self._phasesets[secret])
287 287 self._phasesets[draft] = ps
288 288 self._loadedrevslen = len(cl)
289 289
290 290 def loadphaserevs(self, repo):
291 291 """ensure phase information is loaded in the object"""
292 292 if self._phasesets is None:
293 293 try:
294 294 res = self._getphaserevsnative(repo)
295 295 self._loadedrevslen, self._phasesets = res
296 296 except AttributeError:
297 297 self._computephaserevspure(repo)
298 298
299 299 def invalidate(self):
300 300 self._loadedrevslen = 0
301 301 self._phasesets = None
302 302
303 303 def phase(self, repo, rev):
304 304 # We need a repo argument here to be able to build _phasesets
305 305 # if necessary. The repository instance is not stored in
306 306 # phasecache to avoid reference cycles. The changelog instance
307 307 # is not stored because it is a filecache() property and can
308 308 # be replaced without us being notified.
309 309 if rev == nullrev:
310 310 return public
311 311 if rev < nullrev:
312 312 raise ValueError(_('cannot lookup negative revision'))
313 313 if rev >= self._loadedrevslen:
314 314 self.invalidate()
315 315 self.loadphaserevs(repo)
316 316 for phase in trackedphases:
317 317 if rev in self._phasesets[phase]:
318 318 return phase
319 319 return public
320 320
321 321 def write(self):
322 322 if not self.dirty:
323 323 return
324 324 f = self.opener('phaseroots', 'w', atomictemp=True, checkambig=True)
325 325 try:
326 326 self._write(f)
327 327 finally:
328 328 f.close()
329 329
330 330 def _write(self, fp):
331 331 for phase, roots in enumerate(self.phaseroots):
332 332 for h in sorted(roots):
333 333 fp.write('%i %s\n' % (phase, hex(h)))
334 334 self.dirty = False
335 335
336 336 def _updateroots(self, phase, newroots, tr):
337 337 self.phaseroots[phase] = newroots
338 338 self.invalidate()
339 339 self.dirty = True
340 340
341 341 tr.addfilegenerator('phase', ('phaseroots',), self._write)
342 342 tr.hookargs['phases_moved'] = '1'
343 343
344 344 def registernew(self, repo, tr, targetphase, nodes):
345 345 repo = repo.unfiltered()
346 346 self._retractboundary(repo, tr, targetphase, nodes)
347 347 if tr is not None and 'phases' in tr.changes:
348 348 phasetracking = tr.changes['phases']
349 349 torev = repo.changelog.rev
350 350 phase = self.phase
351 351 for n in nodes:
352 352 rev = torev(n)
353 353 revphase = phase(repo, rev)
354 354 _trackphasechange(phasetracking, rev, None, revphase)
355 355 repo.invalidatevolatilesets()
356 356
357 357 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
358 358 """Set all 'nodes' to phase 'targetphase'
359 359
360 360 Nodes with a phase lower than 'targetphase' are not affected.
361 361
362 362 If dryrun is True, no actions will be performed
363 363
364 364 Returns a set of revs whose phase is changed or should be changed
365 365 """
366 366 # Be careful to preserve shallow-copied values: do not update
367 367 # phaseroots values, replace them.
368 368 if tr is None:
369 369 phasetracking = None
370 370 else:
371 371 phasetracking = tr.changes.get('phases')
372 372
373 373 repo = repo.unfiltered()
374 374
375 375 changes = set() # set of revisions to be changed
376 376 delroots = [] # set of root deleted by this path
377 377 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
378 378 # filter nodes that are not in a compatible phase already
379 379 nodes = [n for n in nodes
380 380 if self.phase(repo, repo[n].rev()) >= phase]
381 381 if not nodes:
382 382 break # no roots to move anymore
383 383
384 384 olds = self.phaseroots[phase]
385 385
386 386 affected = repo.revs('%ln::%ln', olds, nodes)
387 387 changes.update(affected)
388 388 if dryrun:
389 389 continue
390 390 for r in affected:
391 391 _trackphasechange(phasetracking, r, self.phase(repo, r),
392 392 targetphase)
393 393
394 394 roots = set(ctx.node() for ctx in repo.set(
395 395 'roots((%ln::) - %ld)', olds, affected))
396 396 if olds != roots:
397 397 self._updateroots(phase, roots, tr)
398 398 # some roots may need to be declared for lower phases
399 399 delroots.extend(olds - roots)
400 400 if not dryrun:
401 401 # declare deleted root in the target phase
402 402 if targetphase != 0:
403 403 self._retractboundary(repo, tr, targetphase, delroots)
404 404 repo.invalidatevolatilesets()
405 405 return changes
406 406
407 407 def retractboundary(self, repo, tr, targetphase, nodes):
408 408 oldroots = self.phaseroots[:targetphase + 1]
409 409 if tr is None:
410 410 phasetracking = None
411 411 else:
412 412 phasetracking = tr.changes.get('phases')
413 413 repo = repo.unfiltered()
414 414 if (self._retractboundary(repo, tr, targetphase, nodes)
415 415 and phasetracking is not None):
416 416
417 417 # find the affected revisions
418 418 new = self.phaseroots[targetphase]
419 419 old = oldroots[targetphase]
420 420 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
421 421
422 422 # find the phase of the affected revision
423 423 for phase in pycompat.xrange(targetphase, -1, -1):
424 424 if phase:
425 425 roots = oldroots[phase]
426 426 revs = set(repo.revs('%ln::%ld', roots, affected))
427 427 affected -= revs
428 428 else: # public phase
429 429 revs = affected
430 430 for r in revs:
431 431 _trackphasechange(phasetracking, r, phase, targetphase)
432 432 repo.invalidatevolatilesets()
433 433
434 434 def _retractboundary(self, repo, tr, targetphase, nodes):
435 435 # Be careful to preserve shallow-copied values: do not update
436 436 # phaseroots values, replace them.
437 437
438 438 repo = repo.unfiltered()
439 439 currentroots = self.phaseroots[targetphase]
440 440 finalroots = oldroots = set(currentroots)
441 441 newroots = [n for n in nodes
442 442 if self.phase(repo, repo[n].rev()) < targetphase]
443 443 if newroots:
444 444
445 445 if nullid in newroots:
446 446 raise error.Abort(_('cannot change null revision phase'))
447 447 currentroots = currentroots.copy()
448 448 currentroots.update(newroots)
449 449
450 450 # Only compute new roots for revs above the roots that are being
451 451 # retracted.
452 452 minnewroot = min(repo[n].rev() for n in newroots)
453 453 aboveroots = [n for n in currentroots
454 454 if repo[n].rev() >= minnewroot]
455 455 updatedroots = repo.set('roots(%ln::)', aboveroots)
456 456
457 457 finalroots = set(n for n in currentroots if repo[n].rev() <
458 458 minnewroot)
459 459 finalroots.update(ctx.node() for ctx in updatedroots)
460 460 if finalroots != oldroots:
461 461 self._updateroots(targetphase, finalroots, tr)
462 462 return True
463 463 return False
464 464
465 465 def filterunknown(self, repo):
466 466 """remove unknown nodes from the phase boundary
467 467
468 468 Nothing is lost as unknown nodes only hold data for their descendants.
469 469 """
470 470 filtered = False
471 471 nodemap = repo.changelog.nodemap # to filter unknown nodes
472 472 for phase, nodes in enumerate(self.phaseroots):
473 473 missing = sorted(node for node in nodes if node not in nodemap)
474 474 if missing:
475 475 for mnode in missing:
476 476 repo.ui.debug(
477 477 'removing unknown node %s from %i-phase boundary\n'
478 478 % (short(mnode), phase))
479 479 nodes.symmetric_difference_update(missing)
480 480 filtered = True
481 481 if filtered:
482 482 self.dirty = True
483 483 # filterunknown is called by repo.destroyed, we may have no changes in
484 484 # root but _phasesets contents is certainly invalid (or at least we
485 485 # have not proper way to check that). related to issue 3858.
486 486 #
487 487 # The other caller is __init__ that have no _phasesets initialized
488 488 # anyway. If this change we should consider adding a dedicated
489 489 # "destroyed" function to phasecache or a proper cache key mechanism
490 490 # (see branchmap one)
491 491 self.invalidate()
492 492
493 493 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
494 494 """Add nodes to a phase changing other nodes phases if necessary.
495 495
496 496 This function move boundary *forward* this means that all nodes
497 497 are set in the target phase or kept in a *lower* phase.
498 498
499 499 Simplify boundary to contains phase roots only.
500 500
501 501 If dryrun is True, no actions will be performed
502 502
503 503 Returns a set of revs whose phase is changed or should be changed
504 504 """
505 505 phcache = repo._phasecache.copy()
506 506 changes = phcache.advanceboundary(repo, tr, targetphase, nodes,
507 507 dryrun=dryrun)
508 508 if not dryrun:
509 509 repo._phasecache.replace(phcache)
510 510 return changes
511 511
512 512 def retractboundary(repo, tr, targetphase, nodes):
513 513 """Set nodes back to a phase changing other nodes phases if
514 514 necessary.
515 515
516 516 This function move boundary *backward* this means that all nodes
517 517 are set in the target phase or kept in a *higher* phase.
518 518
519 519 Simplify boundary to contains phase roots only."""
520 520 phcache = repo._phasecache.copy()
521 521 phcache.retractboundary(repo, tr, targetphase, nodes)
522 522 repo._phasecache.replace(phcache)
523 523
524 524 def registernew(repo, tr, targetphase, nodes):
525 525 """register a new revision and its phase
526 526
527 527 Code adding revisions to the repository should use this function to
528 528 set new changeset in their target phase (or higher).
529 529 """
530 530 phcache = repo._phasecache.copy()
531 531 phcache.registernew(repo, tr, targetphase, nodes)
532 532 repo._phasecache.replace(phcache)
533 533
534 534 def listphases(repo):
535 535 """List phases root for serialization over pushkey"""
536 536 # Use ordered dictionary so behavior is deterministic.
537 537 keys = util.sortdict()
538 538 value = '%i' % draft
539 539 cl = repo.unfiltered().changelog
540 540 for root in repo._phasecache.phaseroots[draft]:
541 541 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
542 542 keys[hex(root)] = value
543 543
544 544 if repo.publishing():
545 545 # Add an extra data to let remote know we are a publishing
546 546 # repo. Publishing repo can't just pretend they are old repo.
547 547 # When pushing to a publishing repo, the client still need to
548 548 # push phase boundary
549 549 #
550 550 # Push do not only push changeset. It also push phase data.
551 551 # New phase data may apply to common changeset which won't be
552 552 # push (as they are common). Here is a very simple example:
553 553 #
554 554 # 1) repo A push changeset X as draft to repo B
555 555 # 2) repo B make changeset X public
556 556 # 3) repo B push to repo A. X is not pushed but the data that
557 557 # X as now public should
558 558 #
559 559 # The server can't handle it on it's own as it has no idea of
560 560 # client phase data.
561 561 keys['publishing'] = 'True'
562 562 return keys
563 563
564 564 def pushphase(repo, nhex, oldphasestr, newphasestr):
565 565 """List phases root for serialization over pushkey"""
566 566 repo = repo.unfiltered()
567 567 with repo.lock():
568 568 currentphase = repo[nhex].phase()
569 569 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
570 570 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
571 571 if currentphase == oldphase and newphase < oldphase:
572 572 with repo.transaction('pushkey-phase') as tr:
573 573 advanceboundary(repo, tr, newphase, [bin(nhex)])
574 574 return True
575 575 elif currentphase == newphase:
576 576 # raced, but got correct result
577 577 return True
578 578 else:
579 579 return False
580 580
581 581 def subsetphaseheads(repo, subset):
582 582 """Finds the phase heads for a subset of a history
583 583
584 584 Returns a list indexed by phase number where each item is a list of phase
585 585 head nodes.
586 586 """
587 587 cl = repo.changelog
588 588
589 589 headsbyphase = [[] for i in allphases]
590 590 # No need to keep track of secret phase; any heads in the subset that
591 591 # are not mentioned are implicitly secret.
592 592 for phase in allphases[:-1]:
593 593 revset = "heads(%%ln & %s())" % phasenames[phase]
594 594 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
595 595 return headsbyphase
596 596
597 597 def updatephases(repo, trgetter, headsbyphase):
598 598 """Updates the repo with the given phase heads"""
599 599 # Now advance phase boundaries of all but secret phase
600 600 #
601 601 # run the update (and fetch transaction) only if there are actually things
602 602 # to update. This avoid creating empty transaction during no-op operation.
603 603
604 604 for phase in allphases[:-1]:
605 605 revset = '%%ln - %s()' % phasenames[phase]
606 606 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
607 607 if heads:
608 608 advanceboundary(repo, trgetter(), phase, heads)
609 609
610 610 def analyzeremotephases(repo, subset, roots):
611 611 """Compute phases heads and root in a subset of node from root dict
612 612
613 613 * subset is heads of the subset
614 614 * roots is {<nodeid> => phase} mapping. key and value are string.
615 615
616 616 Accept unknown element input
617 617 """
618 618 repo = repo.unfiltered()
619 619 # build list from dictionary
620 620 draftroots = []
621 621 nodemap = repo.changelog.nodemap # to filter unknown nodes
622 622 for nhex, phase in roots.iteritems():
623 623 if nhex == 'publishing': # ignore data related to publish option
624 624 continue
625 625 node = bin(nhex)
626 626 phase = int(phase)
627 627 if phase == public:
628 628 if node != nullid:
629 629 repo.ui.warn(_('ignoring inconsistent public root'
630 630 ' from remote: %s\n') % nhex)
631 631 elif phase == draft:
632 632 if node in nodemap:
633 633 draftroots.append(node)
634 634 else:
635 635 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
636 636 % (phase, nhex))
637 637 # compute heads
638 638 publicheads = newheads(repo, subset, draftroots)
639 639 return publicheads, draftroots
640 640
641 641 class remotephasessummary(object):
642 642 """summarize phase information on the remote side
643 643
644 644 :publishing: True is the remote is publishing
645 645 :publicheads: list of remote public phase heads (nodes)
646 646 :draftheads: list of remote draft phase heads (nodes)
647 647 :draftroots: list of remote draft phase root (nodes)
648 648 """
649 649
650 650 def __init__(self, repo, remotesubset, remoteroots):
651 651 unfi = repo.unfiltered()
652 652 self._allremoteroots = remoteroots
653 653
654 654 self.publishing = remoteroots.get('publishing', False)
655 655
656 656 ana = analyzeremotephases(repo, remotesubset, remoteroots)
657 657 self.publicheads, self.draftroots = ana
658 658 # Get the list of all "heads" revs draft on remote
659 659 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
660 660 self.draftheads = [c.node() for c in dheads]
661 661
662 662 def newheads(repo, heads, roots):
663 663 """compute new head of a subset minus another
664 664
665 665 * `heads`: define the first subset
666 666 * `roots`: define the second we subtract from the first"""
667 667 # prevent an import cycle
668 668 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
669 669 from . import dagop
670 670
671 671 repo = repo.unfiltered()
672 672 cl = repo.changelog
673 673 rev = cl.nodemap.get
674 674 if not roots:
675 675 return heads
676 if not heads or heads == [nullrev]:
676 if not heads or heads == [nullid]:
677 677 return []
678 678 # The logic operated on revisions, convert arguments early for convenience
679 679 new_heads = set(rev(n) for n in heads if n != nullid)
680 680 roots = [rev(n) for n in roots]
681 if not heads or not roots:
682 return heads
683 681 # compute the area we need to remove
684 682 affected_zone = repo.revs("(%ld::%ld)", roots, new_heads)
685 683 # heads in the area are no longer heads
686 684 new_heads.difference_update(affected_zone)
687 685 # revisions in the area have children outside of it,
688 686 # They might be new heads
689 687 candidates = repo.revs("parents(%ld + (%ld and merge())) and not null",
690 688 roots, affected_zone)
691 689 candidates -= affected_zone
692 690 if new_heads or candidates:
693 691 # remove candidate that are ancestors of other heads
694 692 new_heads.update(candidates)
695 693 prunestart = repo.revs("parents(%ld) and not null", new_heads)
696 694 pruned = dagop.reachableroots(repo, candidates, prunestart)
697 695 new_heads.difference_update(pruned)
698 696
699 697 return pycompat.maplist(cl.node, sorted(new_heads))
700 698
701 699 def newcommitphase(ui):
702 700 """helper to get the target phase of new commit
703 701
704 702 Handle all possible values for the phases.new-commit options.
705 703
706 704 """
707 705 v = ui.config('phases', 'new-commit')
708 706 try:
709 707 return phasenames.index(v)
710 708 except ValueError:
711 709 try:
712 710 return int(v)
713 711 except ValueError:
714 712 msg = _("phases.new-commit: not a valid phase name ('%s')")
715 713 raise error.ConfigError(msg % v)
716 714
717 715 def hassecret(repo):
718 716 """utility function that check if a repo have any secret changeset."""
719 717 return bool(repo._phasecache.phaseroots[2])
720 718
721 719 def preparehookargs(node, old, new):
722 720 if old is None:
723 721 old = ''
724 722 else:
725 723 old = phasenames[old]
726 724 return {'node': node,
727 725 'oldphase': old,
728 726 'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now