comparison mercurial/obsutil.py @ 33142:4f49810a1011

obsutil: move 'successorssets' to the new modules We have a new 'obsutil' module now. We move this high level utility there to bring 'obsolete.py' back to a more reasonable size.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 27 Jun 2017 01:03:01 +0200
parents 1858fc2327ef
children d09ae850296d
comparison
equal deleted inserted replaced
33140:f458a6701983 33142:4f49810a1011
32 32
33 if precnodeid in repo: 33 if precnodeid in repo:
34 yield precnodeid 34 yield precnodeid
35 else: 35 else:
36 stack.append(precnodeid) 36 stack.append(precnodeid)
37
38 def successorssets(repo, initialnode, cache=None):
39 """Return set of all latest successors of initial nodes
40
41 The successors set of a changeset A are the group of revisions that succeed
42 A. It succeeds A as a consistent whole, each revision being only a partial
43 replacement. The successors set contains non-obsolete changesets only.
44
45 This function returns the full list of successor sets which is why it
46 returns a list of tuples and not just a single tuple. Each tuple is a valid
47 successors set. Note that (A,) may be a valid successors set for changeset A
48 (see below).
49
50 In most cases, a changeset A will have a single element (e.g. the changeset
51 A is replaced by A') in its successors set. Though, it is also common for a
52 changeset A to have no elements in its successor set (e.g. the changeset
53 has been pruned). Therefore, the returned list of successors sets will be
54 [(A',)] or [], respectively.
55
56 When a changeset A is split into A' and B', however, it will result in a
57 successors set containing more than a single element, i.e. [(A',B')].
58 Divergent changesets will result in multiple successors sets, i.e. [(A',),
59 (A'')].
60
61 If a changeset A is not obsolete, then it will conceptually have no
62 successors set. To distinguish this from a pruned changeset, the successor
63 set will contain itself only, i.e. [(A,)].
64
65 Finally, successors unknown locally are considered to be pruned (obsoleted
66 without any successors).
67
68 The optional `cache` parameter is a dictionary that may contain precomputed
69 successors sets. It is meant to reuse the computation of a previous call to
70 `successorssets` when multiple calls are made at the same time. The cache
71 dictionary is updated in place. The caller is responsible for its life
72 span. Code that makes multiple calls to `successorssets` *must* use this
73 cache mechanism or suffer terrible performance.
74 """
75
76 succmarkers = repo.obsstore.successors
77
78 # Stack of nodes we search successors sets for
79 toproceed = [initialnode]
80 # set version of above list for fast loop detection
81 # element added to "toproceed" must be added here
82 stackedset = set(toproceed)
83 if cache is None:
84 cache = {}
85
86 # This while loop is the flattened version of a recursive search for
87 # successors sets
88 #
89 # def successorssets(x):
90 # successors = directsuccessors(x)
91 # ss = [[]]
92 # for succ in directsuccessors(x):
93 # # product as in itertools cartesian product
94 # ss = product(ss, successorssets(succ))
95 # return ss
96 #
97 # But we can not use plain recursive calls here:
98 # - that would blow the python call stack
99 # - obsolescence markers may have cycles, we need to handle them.
100 #
101 # The `toproceed` list act as our call stack. Every node we search
102 # successors set for are stacked there.
103 #
104 # The `stackedset` is set version of this stack used to check if a node is
105 # already stacked. This check is used to detect cycles and prevent infinite
106 # loop.
107 #
108 # successors set of all nodes are stored in the `cache` dictionary.
109 #
110 # After this while loop ends we use the cache to return the successors sets
111 # for the node requested by the caller.
112 while toproceed:
113 # Every iteration tries to compute the successors sets of the topmost
114 # node of the stack: CURRENT.
115 #
116 # There are four possible outcomes:
117 #
118 # 1) We already know the successors sets of CURRENT:
119 # -> mission accomplished, pop it from the stack.
120 # 2) Node is not obsolete:
121 # -> the node is its own successors sets. Add it to the cache.
122 # 3) We do not know successors set of direct successors of CURRENT:
123 # -> We add those successors to the stack.
124 # 4) We know successors sets of all direct successors of CURRENT:
125 # -> We can compute CURRENT successors set and add it to the
126 # cache.
127 #
128 current = toproceed[-1]
129 if current in cache:
130 # case (1): We already know the successors sets
131 stackedset.remove(toproceed.pop())
132 elif current not in succmarkers:
133 # case (2): The node is not obsolete.
134 if current in repo:
135 # We have a valid last successors.
136 cache[current] = [(current,)]
137 else:
138 # Final obsolete version is unknown locally.
139 # Do not count that as a valid successors
140 cache[current] = []
141 else:
142 # cases (3) and (4)
143 #
144 # We proceed in two phases. Phase 1 aims to distinguish case (3)
145 # from case (4):
146 #
147 # For each direct successors of CURRENT, we check whether its
148 # successors sets are known. If they are not, we stack the
149 # unknown node and proceed to the next iteration of the while
150 # loop. (case 3)
151 #
152 # During this step, we may detect obsolescence cycles: a node
153 # with unknown successors sets but already in the call stack.
154 # In such a situation, we arbitrary set the successors sets of
155 # the node to nothing (node pruned) to break the cycle.
156 #
157 # If no break was encountered we proceed to phase 2.
158 #
159 # Phase 2 computes successors sets of CURRENT (case 4); see details
160 # in phase 2 itself.
161 #
162 # Note the two levels of iteration in each phase.
163 # - The first one handles obsolescence markers using CURRENT as
164 # precursor (successors markers of CURRENT).
165 #
166 # Having multiple entry here means divergence.
167 #
168 # - The second one handles successors defined in each marker.
169 #
170 # Having none means pruned node, multiple successors means split,
171 # single successors are standard replacement.
172 #
173 for mark in sorted(succmarkers[current]):
174 for suc in mark[1]:
175 if suc not in cache:
176 if suc in stackedset:
177 # cycle breaking
178 cache[suc] = []
179 else:
180 # case (3) If we have not computed successors sets
181 # of one of those successors we add it to the
182 # `toproceed` stack and stop all work for this
183 # iteration.
184 toproceed.append(suc)
185 stackedset.add(suc)
186 break
187 else:
188 continue
189 break
190 else:
191 # case (4): we know all successors sets of all direct
192 # successors
193 #
194 # Successors set contributed by each marker depends on the
195 # successors sets of all its "successors" node.
196 #
197 # Each different marker is a divergence in the obsolescence
198 # history. It contributes successors sets distinct from other
199 # markers.
200 #
201 # Within a marker, a successor may have divergent successors
202 # sets. In such a case, the marker will contribute multiple
203 # divergent successors sets. If multiple successors have
204 # divergent successors sets, a Cartesian product is used.
205 #
206 # At the end we post-process successors sets to remove
207 # duplicated entry and successors set that are strict subset of
208 # another one.
209 succssets = []
210 for mark in sorted(succmarkers[current]):
211 # successors sets contributed by this marker
212 markss = [[]]
213 for suc in mark[1]:
214 # cardinal product with previous successors
215 productresult = []
216 for prefix in markss:
217 for suffix in cache[suc]:
218 newss = list(prefix)
219 for part in suffix:
220 # do not duplicated entry in successors set
221 # first entry wins.
222 if part not in newss:
223 newss.append(part)
224 productresult.append(newss)
225 markss = productresult
226 succssets.extend(markss)
227 # remove duplicated and subset
228 seen = []
229 final = []
230 candidate = sorted(((set(s), s) for s in succssets if s),
231 key=lambda x: len(x[1]), reverse=True)
232 for setversion, listversion in candidate:
233 for seenset in seen:
234 if setversion.issubset(seenset):
235 break
236 else:
237 final.append(listversion)
238 seen.append(setversion)
239 final.reverse() # put small successors set first
240 cache[current] = final
241 return cache[initialnode]