PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/phases.py

https://bitbucket.org/mirror/mercurial/
Python | 410 lines | 350 code | 6 blank | 54 comment | 18 complexity | fe57d541be0d550d19cbc0ccbe8fecf9 MD5 | raw file
Possible License(s): GPL-2.0
  1. """ Mercurial phases support code
  2. ---
  3. Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
  4. Logilab SA <contact@logilab.fr>
  5. Augie Fackler <durin42@gmail.com>
  6. This software may be used and distributed according to the terms
  7. of the GNU General Public License version 2 or any later version.
  8. ---
  9. This module implements most phase logic in mercurial.
  10. Basic Concept
  11. =============
  12. A 'changeset phase' is an indicator that tells us how a changeset is
  13. manipulated and communicated. The details of each phase is described
  14. below, here we describe the properties they have in common.
  15. Like bookmarks, phases are not stored in history and thus are not
  16. permanent and leave no audit trail.
  17. First, no changeset can be in two phases at once. Phases are ordered,
  18. so they can be considered from lowest to highest. The default, lowest
  19. phase is 'public' - this is the normal phase of existing changesets. A
  20. child changeset can not be in a lower phase than its parents.
  21. These phases share a hierarchy of traits:
  22. immutable shared
  23. public: X X
  24. draft: X
  25. secret:
  26. Local commits are draft by default.
  27. Phase Movement and Exchange
  28. ===========================
  29. Phase data is exchanged by pushkey on pull and push. Some servers have
  30. a publish option set, we call such a server a "publishing server".
  31. Pushing a draft changeset to a publishing server changes the phase to
  32. public.
  33. A small list of fact/rules define the exchange of phase:
  34. * old client never changes server states
  35. * pull never changes server states
  36. * publish and old server changesets are seen as public by client
  37. * any secret changeset seen in another repository is lowered to at
  38. least draft
  39. Here is the final table summing up the 49 possible use cases of phase
  40. exchange:
  41. server
  42. old publish non-publish
  43. N X N D P N D P
  44. old client
  45. pull
  46. N - X/X - X/D X/P - X/D X/P
  47. X - X/X - X/D X/P - X/D X/P
  48. push
  49. X X/X X/X X/P X/P X/P X/D X/D X/P
  50. new client
  51. pull
  52. N - P/X - P/D P/P - D/D P/P
  53. D - P/X - P/D P/P - D/D P/P
  54. P - P/X - P/D P/P - P/D P/P
  55. push
  56. D P/X P/X P/P P/P P/P D/D D/D P/P
  57. P P/X P/X P/P P/P P/P P/P P/P P/P
  58. Legend:
  59. A/B = final state on client / state on server
  60. * N = new/not present,
  61. * P = public,
  62. * D = draft,
  63. * X = not tracked (i.e., the old client or server has no internal
  64. way of recording the phase.)
  65. passive = only pushes
  66. A cell here can be read like this:
  67. "When a new client pushes a draft changeset (D) to a publishing
  68. server where it's not present (N), it's marked public on both
  69. sides (P/P)."
  70. Note: old client behave as a publishing server with draft only content
  71. - other people see it as public
  72. - content is pushed as draft
  73. """
  74. import errno
  75. from node import nullid, nullrev, bin, hex, short
  76. from i18n import _
  77. import util, error
  78. allphases = public, draft, secret = range(3)
  79. trackedphases = allphases[1:]
  80. phasenames = ['public', 'draft', 'secret']
  81. def _readroots(repo, phasedefaults=None):
  82. """Read phase roots from disk
  83. phasedefaults is a list of fn(repo, roots) callable, which are
  84. executed if the phase roots file does not exist. When phases are
  85. being initialized on an existing repository, this could be used to
  86. set selected changesets phase to something else than public.
  87. Return (roots, dirty) where dirty is true if roots differ from
  88. what is being stored.
  89. """
  90. repo = repo.unfiltered()
  91. dirty = False
  92. roots = [set() for i in allphases]
  93. try:
  94. f = repo.sopener('phaseroots')
  95. try:
  96. for line in f:
  97. phase, nh = line.split()
  98. roots[int(phase)].add(bin(nh))
  99. finally:
  100. f.close()
  101. except IOError, inst:
  102. if inst.errno != errno.ENOENT:
  103. raise
  104. if phasedefaults:
  105. for f in phasedefaults:
  106. roots = f(repo, roots)
  107. dirty = True
  108. return roots, dirty
  109. class phasecache(object):
  110. def __init__(self, repo, phasedefaults, _load=True):
  111. if _load:
  112. # Cheap trick to allow shallow-copy without copy module
  113. self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
  114. self._phaserevs = None
  115. self.filterunknown(repo)
  116. self.opener = repo.sopener
  117. def copy(self):
  118. # Shallow copy meant to ensure isolation in
  119. # advance/retractboundary(), nothing more.
  120. ph = phasecache(None, None, _load=False)
  121. ph.phaseroots = self.phaseroots[:]
  122. ph.dirty = self.dirty
  123. ph.opener = self.opener
  124. ph._phaserevs = self._phaserevs
  125. return ph
  126. def replace(self, phcache):
  127. for a in 'phaseroots dirty opener _phaserevs'.split():
  128. setattr(self, a, getattr(phcache, a))
  129. def getphaserevs(self, repo, rebuild=False):
  130. if rebuild or self._phaserevs is None:
  131. repo = repo.unfiltered()
  132. revs = [public] * len(repo.changelog)
  133. for phase in trackedphases:
  134. roots = map(repo.changelog.rev, self.phaseroots[phase])
  135. if roots:
  136. for rev in roots:
  137. revs[rev] = phase
  138. for rev in repo.changelog.descendants(roots):
  139. revs[rev] = phase
  140. self._phaserevs = revs
  141. return self._phaserevs
  142. def phase(self, repo, rev):
  143. # We need a repo argument here to be able to build _phaserevs
  144. # if necessary. The repository instance is not stored in
  145. # phasecache to avoid reference cycles. The changelog instance
  146. # is not stored because it is a filecache() property and can
  147. # be replaced without us being notified.
  148. if rev == nullrev:
  149. return public
  150. if rev < nullrev:
  151. raise ValueError(_('cannot lookup negative revision'))
  152. if self._phaserevs is None or rev >= len(self._phaserevs):
  153. self._phaserevs = self.getphaserevs(repo, rebuild=True)
  154. return self._phaserevs[rev]
  155. def write(self):
  156. if not self.dirty:
  157. return
  158. f = self.opener('phaseroots', 'w', atomictemp=True)
  159. try:
  160. for phase, roots in enumerate(self.phaseroots):
  161. for h in roots:
  162. f.write('%i %s\n' % (phase, hex(h)))
  163. finally:
  164. f.close()
  165. self.dirty = False
  166. def _updateroots(self, phase, newroots):
  167. self.phaseroots[phase] = newroots
  168. self._phaserevs = None
  169. self.dirty = True
  170. def advanceboundary(self, repo, targetphase, nodes):
  171. # Be careful to preserve shallow-copied values: do not update
  172. # phaseroots values, replace them.
  173. repo = repo.unfiltered()
  174. delroots = [] # set of root deleted by this path
  175. for phase in xrange(targetphase + 1, len(allphases)):
  176. # filter nodes that are not in a compatible phase already
  177. nodes = [n for n in nodes
  178. if self.phase(repo, repo[n].rev()) >= phase]
  179. if not nodes:
  180. break # no roots to move anymore
  181. olds = self.phaseroots[phase]
  182. roots = set(ctx.node() for ctx in repo.set(
  183. 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
  184. if olds != roots:
  185. self._updateroots(phase, roots)
  186. # some roots may need to be declared for lower phases
  187. delroots.extend(olds - roots)
  188. # declare deleted root in the target phase
  189. if targetphase != 0:
  190. self.retractboundary(repo, targetphase, delroots)
  191. repo.invalidatevolatilesets()
  192. def retractboundary(self, repo, targetphase, nodes):
  193. # Be careful to preserve shallow-copied values: do not update
  194. # phaseroots values, replace them.
  195. repo = repo.unfiltered()
  196. currentroots = self.phaseroots[targetphase]
  197. newroots = [n for n in nodes
  198. if self.phase(repo, repo[n].rev()) < targetphase]
  199. if newroots:
  200. if nullid in newroots:
  201. raise util.Abort(_('cannot change null revision phase'))
  202. currentroots = currentroots.copy()
  203. currentroots.update(newroots)
  204. ctxs = repo.set('roots(%ln::)', currentroots)
  205. currentroots.intersection_update(ctx.node() for ctx in ctxs)
  206. self._updateroots(targetphase, currentroots)
  207. repo.invalidatevolatilesets()
  208. def filterunknown(self, repo):
  209. """remove unknown nodes from the phase boundary
  210. Nothing is lost as unknown nodes only hold data for their descendants.
  211. """
  212. filtered = False
  213. nodemap = repo.changelog.nodemap # to filter unknown nodes
  214. for phase, nodes in enumerate(self.phaseroots):
  215. missing = sorted(node for node in nodes if node not in nodemap)
  216. if missing:
  217. for mnode in missing:
  218. repo.ui.debug(
  219. 'removing unknown node %s from %i-phase boundary\n'
  220. % (short(mnode), phase))
  221. nodes.symmetric_difference_update(missing)
  222. filtered = True
  223. if filtered:
  224. self.dirty = True
  225. # filterunknown is called by repo.destroyed, we may have no changes in
  226. # root but phaserevs contents is certainly invalid (or at least we
  227. # have not proper way to check that). related to issue 3858.
  228. #
  229. # The other caller is __init__ that have no _phaserevs initialized
  230. # anyway. If this change we should consider adding a dedicated
  231. # "destroyed" function to phasecache or a proper cache key mechanism
  232. # (see branchmap one)
  233. self._phaserevs = None
  234. def advanceboundary(repo, targetphase, nodes):
  235. """Add nodes to a phase changing other nodes phases if necessary.
  236. This function move boundary *forward* this means that all nodes
  237. are set in the target phase or kept in a *lower* phase.
  238. Simplify boundary to contains phase roots only."""
  239. phcache = repo._phasecache.copy()
  240. phcache.advanceboundary(repo, targetphase, nodes)
  241. repo._phasecache.replace(phcache)
  242. def retractboundary(repo, targetphase, nodes):
  243. """Set nodes back to a phase changing other nodes phases if
  244. necessary.
  245. This function move boundary *backward* this means that all nodes
  246. are set in the target phase or kept in a *higher* phase.
  247. Simplify boundary to contains phase roots only."""
  248. phcache = repo._phasecache.copy()
  249. phcache.retractboundary(repo, targetphase, nodes)
  250. repo._phasecache.replace(phcache)
  251. def listphases(repo):
  252. """List phases root for serialization over pushkey"""
  253. keys = {}
  254. value = '%i' % draft
  255. for root in repo._phasecache.phaseroots[draft]:
  256. keys[hex(root)] = value
  257. if repo.ui.configbool('phases', 'publish', True):
  258. # Add an extra data to let remote know we are a publishing
  259. # repo. Publishing repo can't just pretend they are old repo.
  260. # When pushing to a publishing repo, the client still need to
  261. # push phase boundary
  262. #
  263. # Push do not only push changeset. It also push phase data.
  264. # New phase data may apply to common changeset which won't be
  265. # push (as they are common). Here is a very simple example:
  266. #
  267. # 1) repo A push changeset X as draft to repo B
  268. # 2) repo B make changeset X public
  269. # 3) repo B push to repo A. X is not pushed but the data that
  270. # X as now public should
  271. #
  272. # The server can't handle it on it's own as it has no idea of
  273. # client phase data.
  274. keys['publishing'] = 'True'
  275. return keys
  276. def pushphase(repo, nhex, oldphasestr, newphasestr):
  277. """List phases root for serialization over pushkey"""
  278. repo = repo.unfiltered()
  279. lock = repo.lock()
  280. try:
  281. currentphase = repo[nhex].phase()
  282. newphase = abs(int(newphasestr)) # let's avoid negative index surprise
  283. oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
  284. if currentphase == oldphase and newphase < oldphase:
  285. advanceboundary(repo, newphase, [bin(nhex)])
  286. return 1
  287. elif currentphase == newphase:
  288. # raced, but got correct result
  289. return 1
  290. else:
  291. return 0
  292. finally:
  293. lock.release()
  294. def analyzeremotephases(repo, subset, roots):
  295. """Compute phases heads and root in a subset of node from root dict
  296. * subset is heads of the subset
  297. * roots is {<nodeid> => phase} mapping. key and value are string.
  298. Accept unknown element input
  299. """
  300. repo = repo.unfiltered()
  301. # build list from dictionary
  302. draftroots = []
  303. nodemap = repo.changelog.nodemap # to filter unknown nodes
  304. for nhex, phase in roots.iteritems():
  305. if nhex == 'publishing': # ignore data related to publish option
  306. continue
  307. node = bin(nhex)
  308. phase = int(phase)
  309. if phase == 0:
  310. if node != nullid:
  311. repo.ui.warn(_('ignoring inconsistent public root'
  312. ' from remote: %s\n') % nhex)
  313. elif phase == 1:
  314. if node in nodemap:
  315. draftroots.append(node)
  316. else:
  317. repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
  318. % (phase, nhex))
  319. # compute heads
  320. publicheads = newheads(repo, subset, draftroots)
  321. return publicheads, draftroots
  322. def newheads(repo, heads, roots):
  323. """compute new head of a subset minus another
  324. * `heads`: define the first subset
  325. * `roots`: define the second we subtract from the first"""
  326. repo = repo.unfiltered()
  327. revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
  328. heads, roots, roots, heads)
  329. return [c.node() for c in revset]
  330. def newcommitphase(ui):
  331. """helper to get the target phase of new commit
  332. Handle all possible values for the phases.new-commit options.
  333. """
  334. v = ui.config('phases', 'new-commit', draft)
  335. try:
  336. return phasenames.index(v)
  337. except ValueError:
  338. try:
  339. return int(v)
  340. except ValueError:
  341. msg = _("phases.new-commit: not a valid phase name ('%s')")
  342. raise error.ConfigError(msg % v)
  343. def hassecret(repo):
  344. """utility function that check if a repo have any secret changeset."""
  345. return bool(repo._phasecache.phaseroots[2])