|
1 # states.py - introduce the state concept for mercurial changeset |
|
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 # |
|
7 # This software may be used and distributed according to the terms of the |
|
8 # GNU General Public License version 2 or any later version. |
|
9 |
|
10 '''introduce the state concept for mercurial changeset |
|
11 |
|
12 Change can be in the following state: |
|
13 |
|
14 0 immutable |
|
15 1 mutable |
|
16 2 private |
|
17 |
|
18 name are not fixed yet. |
|
19 ''' |
|
20 import os |
|
21 from functools import partial |
|
22 |
|
23 from mercurial.i18n import _ |
|
24 from mercurial import cmdutil |
|
25 from mercurial import scmutil |
|
26 from mercurial import context |
|
27 from mercurial import revset |
|
28 from mercurial import templatekw |
|
29 from mercurial import util |
|
30 from mercurial import node |
|
31 from mercurial.node import nullid, hex, short |
|
32 from mercurial import discovery |
|
33 from mercurial import extensions |
|
34 from mercurial import wireproto |
|
35 from mercurial import pushkey |
|
36 from mercurial.lock import release |
|
37 |
|
38 |
|
39 _NOSHARE=2 |
|
40 _MUTABLE=1 |
|
41 |
|
42 class state(object): |
|
43 |
|
44 def __init__(self, name, properties=0, next=None): |
|
45 self.name = name |
|
46 self.properties = properties |
|
47 assert next is None or self < next |
|
48 self.next = next |
|
49 |
|
50 def __repr__(self): |
|
51 return 'state(%s)' % self.name |
|
52 |
|
53 def __str__(self): |
|
54 return self.name |
|
55 |
|
56 @util.propertycache |
|
57 def trackheads(self): |
|
58 """Do we need to track heads of changeset in this state ? |
|
59 |
|
60 We don't need to track heads for the last state as this is repos heads""" |
|
61 return self.next is not None |
|
62 |
|
63 def __cmp__(self, other): |
|
64 return cmp(self.properties, other.properties) |
|
65 |
|
66 @util.propertycache |
|
67 def _revsetheads(self): |
|
68 """function to be used by revset to finds heads of this states""" |
|
69 assert self.trackheads |
|
70 def revsetheads(repo, subset, x): |
|
71 args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') |
|
72 heads = map(repo.changelog.rev, repo._statesheads[self]) |
|
73 heads.sort() |
|
74 return heads |
|
75 return revsetheads |
|
76 |
|
77 @util.propertycache |
|
78 def headssymbol(self): |
|
79 """name of the revset symbols""" |
|
80 if self.trackheads: |
|
81 return "%sheads" % self.name |
|
82 else: |
|
83 return 'heads' |
|
84 |
|
85 ST2 = state('draft', _NOSHARE | _MUTABLE) |
|
86 ST1 = state('ready', _MUTABLE, next=ST2) |
|
87 ST0 = state('published', next=ST1) |
|
88 |
|
89 STATES = (ST0, ST1, ST2) |
|
90 |
|
91 @util.cachefunc |
|
92 def laststatewithout(prop): |
|
93 for state in STATES: |
|
94 if not state.properties & prop: |
|
95 candidate = state |
|
96 else: |
|
97 return candidate |
|
98 |
|
99 # util function |
|
100 ############################# |
|
101 def noderange(repo, revsets): |
|
102 return map(repo.changelog.node, |
|
103 scmutil.revrange(repo, revsets)) |
|
104 |
|
105 # Patch changectx |
|
106 ############################# |
|
107 |
|
108 def state(ctx): |
|
109 if ctx.node()is None: |
|
110 return STATES[-1] |
|
111 return ctx._repo.nodestate(ctx.node()) |
|
112 context.changectx.state = state |
|
113 |
|
114 # improve template |
|
115 ############################# |
|
116 |
|
117 def showstate(ctx, **args): |
|
118 return ctx.state() |
|
119 |
|
120 |
|
121 # New commands |
|
122 ############################# |
|
123 |
|
124 |
|
125 def cmdstates(ui, repo, *states, **opt): |
|
126 """view and modify activated states. |
|
127 |
|
128 With no argument, list activated state. |
|
129 |
|
130 With argument, activate the state in argument. |
|
131 |
|
132 With argument plus the --off switch, deactivate the state in argument. |
|
133 |
|
134 note: published state are alway activated.""" |
|
135 |
|
136 if not states: |
|
137 for st in sorted(repo._enabledstates): |
|
138 ui.write('%s\n' % st) |
|
139 else: |
|
140 off = opt.get('off', False) |
|
141 for state_name in states: |
|
142 for st in STATES: |
|
143 if st.name == state_name: |
|
144 break |
|
145 else: |
|
146 ui.write_err(_('no state named %s\n') % state_name) |
|
147 return 1 |
|
148 if off and st in repo._enabledstates: |
|
149 repo._enabledstates.remove(st) |
|
150 else: |
|
151 repo._enabledstates.add(st) |
|
152 repo._writeenabledstates() |
|
153 return 0 |
|
154 |
|
155 cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')} |
|
156 #cmdtable = {'states': (cmdstates, [], '<state>')} |
|
157 |
|
158 def makecmd(state): |
|
159 def cmdmoveheads(ui, repo, *changesets): |
|
160 """set a revision in %s state""" % state |
|
161 revs = scmutil.revrange(repo, changesets) |
|
162 repo.setstate(state, [repo.changelog.node(rev) for rev in revs]) |
|
163 return 0 |
|
164 return cmdmoveheads |
|
165 |
|
166 for state in STATES: |
|
167 if state.trackheads: |
|
168 cmdmoveheads = makecmd(state) |
|
169 cmdtable[state.name] = (cmdmoveheads, [], '<revset>') |
|
170 |
|
171 # Pushkey mechanism for mutable |
|
172 ######################################### |
|
173 |
|
174 def pushimmutableheads(repo, key, old, new): |
|
175 st = ST0 |
|
176 w = repo.wlock() |
|
177 try: |
|
178 #print 'pushing', key |
|
179 repo.setstate(ST0, [node.bin(key)]) |
|
180 finally: |
|
181 w.release() |
|
182 |
|
183 def listimmutableheads(repo): |
|
184 return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1') |
|
185 |
|
186 pushkey.register('immutableheads', pushimmutableheads, listimmutableheads) |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 def uisetup(ui): |
|
193 def filterprivateout(orig, repo, *args,**kwargs): |
|
194 common, heads = orig(repo, *args, **kwargs) |
|
195 return common, repo._reducehead(heads) |
|
196 def filterprivatein(orig, repo, remote, *args, **kwargs): |
|
197 common, anyinc, heads = orig(repo, remote, *args, **kwargs) |
|
198 heads = remote._reducehead(heads) |
|
199 return common, anyinc, heads |
|
200 |
|
201 extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout) |
|
202 extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein) |
|
203 |
|
204 # Write protocols |
|
205 #################### |
|
206 def heads(repo, proto): |
|
207 st = laststatewithout(_NOSHARE) |
|
208 h = repo.stateheads(st) |
|
209 return wireproto.encodelist(h) + "\n" |
|
210 |
|
211 def _reducehead(wirerepo, heads): |
|
212 """heads filtering is done repo side""" |
|
213 return heads |
|
214 |
|
215 wireproto.wirerepository._reducehead = _reducehead |
|
216 wireproto.commands['heads'] = (heads, '') |
|
217 |
|
218 templatekw.keywords['state'] = showstate |
|
219 |
|
220 def extsetup(ui): |
|
221 for state in STATES: |
|
222 if state.trackheads: |
|
223 revset.symbols[state.headssymbol] = state._revsetheads |
|
224 |
|
225 def reposetup(ui, repo): |
|
226 |
|
227 if not repo.local(): |
|
228 return |
|
229 |
|
230 ocancopy =repo.cancopy |
|
231 opull = repo.pull |
|
232 opush = repo.push |
|
233 o_tag = repo._tag |
|
234 orollback = repo.rollback |
|
235 o_writejournal = repo._writejournal |
|
236 class statefulrepo(repo.__class__): |
|
237 |
|
238 def nodestate(self, node): |
|
239 rev = self.changelog.rev(node) |
|
240 |
|
241 for state in STATES: |
|
242 # XXX avoid for untracked heads |
|
243 if state.next is not None: |
|
244 ancestors = map(self.changelog.rev, self.stateheads(state)) |
|
245 ancestors.extend(self.changelog.ancestors(*ancestors)) |
|
246 if rev in ancestors: |
|
247 break |
|
248 return state |
|
249 |
|
250 |
|
251 |
|
252 def stateheads(self, state): |
|
253 # look for a relevant state |
|
254 while state.trackheads and state.next not in self._enabledstates: |
|
255 state = state.next |
|
256 # last state have no cached head. |
|
257 if state.trackheads: |
|
258 return self._statesheads[state] |
|
259 return self.heads() |
|
260 |
|
261 @util.propertycache |
|
262 def _statesheads(self): |
|
263 return self._readstatesheads() |
|
264 |
|
265 |
|
266 def _readheadsfile(self, filename): |
|
267 heads = [nullid] |
|
268 try: |
|
269 f = self.opener(filename) |
|
270 try: |
|
271 heads = sorted([node.bin(n) for n in f.read().split() if n]) |
|
272 finally: |
|
273 f.close() |
|
274 except IOError: |
|
275 pass |
|
276 return heads |
|
277 |
|
278 def _readstatesheads(self, undo=False): |
|
279 statesheads = {} |
|
280 for state in STATES: |
|
281 if state.trackheads: |
|
282 filemask = 'states/%s-heads' |
|
283 filename = filemask % state.name |
|
284 statesheads[state] = self._readheadsfile(filename) |
|
285 return statesheads |
|
286 |
|
287 def _writeheadsfile(self, filename, heads): |
|
288 f = self.opener(filename, 'w', atomictemp=True) |
|
289 try: |
|
290 for h in heads: |
|
291 f.write(hex(h) + '\n') |
|
292 f.rename() |
|
293 finally: |
|
294 f.close() |
|
295 |
|
296 def _writestateshead(self): |
|
297 # transaction! |
|
298 for state in STATES: |
|
299 if state.trackheads: |
|
300 filename = 'states/%s-heads' % state.name |
|
301 self._writeheadsfile(filename, self._statesheads[state]) |
|
302 |
|
303 def setstate(self, state, nodes): |
|
304 """change state of targets changeset and it's ancestors. |
|
305 |
|
306 Simplify the list of head.""" |
|
307 assert not isinstance(nodes, basestring) |
|
308 heads = self._statesheads[state] |
|
309 olds = heads[:] |
|
310 heads.extend(nodes) |
|
311 heads[:] = set(heads) |
|
312 heads.sort() |
|
313 if olds != heads: |
|
314 heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol]) |
|
315 heads.sort() |
|
316 if olds != heads: |
|
317 self._writestateshead() |
|
318 if state.next is not None and state.next.trackheads: |
|
319 self.setstate(state.next, nodes) # cascading |
|
320 |
|
321 def _reducehead(self, candidates): |
|
322 selected = set() |
|
323 st = laststatewithout(_NOSHARE) |
|
324 candidates = set(map(self.changelog.rev, candidates)) |
|
325 heads = set(map(self.changelog.rev, self.stateheads(st))) |
|
326 shareable = set(self.changelog.ancestors(*heads)) |
|
327 shareable.update(heads) |
|
328 selected = candidates & shareable |
|
329 unselected = candidates - shareable |
|
330 for rev in unselected: |
|
331 for revh in heads: |
|
332 if self.changelog.descendant(revh, rev): |
|
333 selected.add(revh) |
|
334 return sorted(map(self.changelog.node, selected)) |
|
335 |
|
336 ### enable // disable logic |
|
337 |
|
338 @util.propertycache |
|
339 def _enabledstates(self): |
|
340 return self._readenabledstates() |
|
341 |
|
342 def _readenabledstates(self): |
|
343 states = set() |
|
344 states.add(ST0) |
|
345 mapping = dict([(st.name, st) for st in STATES]) |
|
346 try: |
|
347 f = self.opener('states/Enabled') |
|
348 for line in f: |
|
349 st = mapping.get(line.strip()) |
|
350 if st is not None: |
|
351 states.add(st) |
|
352 finally: |
|
353 return states |
|
354 |
|
355 def _writeenabledstates(self): |
|
356 f = self.opener('states/Enabled', 'w', atomictemp=True) |
|
357 try: |
|
358 for st in self._enabledstates: |
|
359 f.write(st.name + '\n') |
|
360 f.rename() |
|
361 finally: |
|
362 f.close() |
|
363 |
|
364 ### local clone support |
|
365 |
|
366 def cancopy(self): |
|
367 st = laststatewithout(_NOSHARE) |
|
368 return ocancopy() and (self.stateheads(st) == self.heads()) |
|
369 |
|
370 ### pull // push support |
|
371 |
|
372 def pull(self, remote, *args, **kwargs): |
|
373 result = opull(remote, *args, **kwargs) |
|
374 remoteheads = self._pullimmutableheads(remote) |
|
375 #print [node.short(h) for h in remoteheads] |
|
376 self.setstate(ST0, remoteheads) |
|
377 return result |
|
378 |
|
379 def push(self, remote, *args, **opts): |
|
380 result = opush(remote, *args, **opts) |
|
381 remoteheads = self._pullimmutableheads(remote) |
|
382 self.setstate(ST0, remoteheads) |
|
383 if remoteheads != self.stateheads(ST0): |
|
384 #print 'stuff to push' |
|
385 #print 'remote', [node.short(h) for h in remoteheads] |
|
386 #print 'local', [node.short(h) for h in self._statesheads[ST0]] |
|
387 self._pushimmutableheads(remote, remoteheads) |
|
388 return result |
|
389 |
|
390 def _pushimmutableheads(self, remote, remoteheads): |
|
391 missing = set(self.stateheads(ST0)) - set(remoteheads) |
|
392 for h in missing: |
|
393 #print 'missing', node.short(h) |
|
394 remote.pushkey('immutableheads', node.hex(h), '', '1') |
|
395 |
|
396 |
|
397 def _pullimmutableheads(self, remote): |
|
398 self.ui.debug('checking for immutableheadshg on server') |
|
399 if 'immutableheads' not in remote.listkeys('namespaces'): |
|
400 self.ui.debug('immutableheads not enabled on the remote server, ' |
|
401 'marking everything as frozen') |
|
402 remote = remote.heads() |
|
403 else: |
|
404 self.ui.debug('server has immutableheads enabled, merging lists') |
|
405 remote = map(node.bin, remote.listkeys('immutableheads')) |
|
406 return remote |
|
407 |
|
408 ### Tag support |
|
409 |
|
410 def _tag(self, names, node, *args, **kwargs): |
|
411 tagnode = o_tag(names, node, *args, **kwargs) |
|
412 if tagnode is not None: # do nothing for local one |
|
413 self.setstate(ST0, [node, tagnode]) |
|
414 return tagnode |
|
415 |
|
416 ### rollback support |
|
417 |
|
418 def _writejournal(self, desc): |
|
419 entries = list(o_writejournal(desc)) |
|
420 for state in STATES: |
|
421 if state.trackheads: |
|
422 filename = 'states/%s-heads' % state.name |
|
423 filepath = self.join(filename) |
|
424 if os.path.exists(filepath): |
|
425 journalname = 'states/journal.%s-heads' % state.name |
|
426 journalpath = self.join(journalname) |
|
427 util.copyfile(filepath, journalpath) |
|
428 entries.append(journalpath) |
|
429 return tuple(entries) |
|
430 |
|
431 def rollback(self, dryrun=False): |
|
432 wlock = lock = None |
|
433 try: |
|
434 wlock = self.wlock() |
|
435 lock = self.lock() |
|
436 ret = orollback(dryrun) |
|
437 if not (ret or dryrun): #rollback did not failed |
|
438 for state in STATES: |
|
439 if state.trackheads: |
|
440 src = self.join('states/undo.%s-heads') % state.name |
|
441 dest = self.join('states/%s-heads') % state.name |
|
442 if os.path.exists(src): |
|
443 util.rename(src, dest) |
|
444 elif os.path.exists(dest): #unlink in any case |
|
445 os.unlink(dest) |
|
446 self.__dict__.pop('_statesheads', None) |
|
447 return ret |
|
448 finally: |
|
449 release(lock, wlock) |
|
450 |
|
451 repo.__class__ = statefulrepo |
|
452 |