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 (see http://mercurial.selenic.com/wiki/StatesPlan) |
|
13 |
|
14 General concept |
|
15 =============== |
|
16 |
|
17 This extension adds the state concept. A changeset are now in a specific state |
|
18 that control they mutability and they exchange. |
|
19 |
|
20 States properties |
|
21 ................. |
|
22 |
|
23 The states extension currently alter two property for changeset |
|
24 |
|
25 :mutability: history rewritten tool should refuse to work on immutable changeset |
|
26 :sharing: shared changeset are exchanged during pull and push. other are not |
|
27 |
|
28 Here is a small summary of the current property of state existing state:: |
|
29 |
|
30 || || mutable || shared || |
|
31 || published || || x || |
|
32 || ready || x || x || |
|
33 || draft || x || || |
|
34 |
|
35 States consistency and ordering |
|
36 ............................... |
|
37 |
|
38 States of changesets have to be consistent with each other. A changeset can only have ancestors of it's state (or a compatible states) |
|
39 |
|
40 Example: |
|
41 |
|
42 A ``published`` changeset can't have a ``draft`` parent. |
|
43 |
|
44 a state is compatible with itself and all "smaller" states. Order is as follow:: |
|
45 |
|
46 published < ready < draft |
|
47 |
|
48 |
|
49 .. note: |
|
50 |
|
51 This section if probably far too conceptual for people. The result is just |
|
52 that: A ``published`` changeset can only have ``published`` ancestors. A |
|
53 ``ready`` changeset can only have ``published`` or ``ready`` ancestors. |
|
54 |
|
55 Moreover There is a need for a nice word to refer to "a state smaller than another" |
|
56 |
|
57 |
|
58 States details |
|
59 ============== |
|
60 |
|
61 |
|
62 published |
|
63 Changesets in the ``published`` state are the core of the history. They are |
|
64 changesets that you published to the world. People can expect them to always |
|
65 exist. They are changesets as you know them. **By default all changesets |
|
66 are published** |
|
67 |
|
68 - They are exchanged with other repositories (included in pull//push). |
|
69 |
|
70 - They are not mutable, extensions rewriting history should refuse to |
|
71 rewrite them. |
|
72 |
|
73 ready |
|
74 Changesets in the ``ready`` state have not yet been accepted in the |
|
75 immutable history. You can share them with others for review, testing or |
|
76 improvement. Any ``ready`` changeset can either be included in the |
|
77 published history (and become immutable) or be rewritten and never make it |
|
78 to the published history. |
|
79 |
|
80 - They are exchanged with other repositories (included in pull//push). |
|
81 |
|
82 - They are mutable, extensions rewriting history accept to work on them. |
|
83 |
|
84 draft |
|
85 |
|
86 Changesets in the ``draft`` state are heavy work in progress you are not |
|
87 yet willing to share with others. |
|
88 |
|
89 - They are not exchanged with other repositories. pull//push do not see them. |
|
90 - They are mutable, extensions rewriting history accept to work on them. |
|
91 |
|
92 -- |
|
93 |
|
94 .. note: |
|
95 |
|
96 The Dead states mentionned in on the wiki page are missing. There is two main reason for it: |
|
97 |
|
98 1. The ``dead`` state has a different behaviour that requires more work to be |
|
99 implemented. |
|
100 |
|
101 2. I believe that the use cases of ``dead changeset`` are better covered by |
|
102 the ``obsolete`` extension. |
|
103 |
|
104 -- |
|
105 |
|
106 .. note: |
|
107 |
|
108 I'm tempted to add a state with the same property that ``ready`` for review |
|
109 workflow.:: |
|
110 |
|
111 || || mutable || shared || |
|
112 || published || || x || |
|
113 || ready || x || x || |
|
114 || inprogress|| x || x || |
|
115 || draft || x || || |
|
116 |
|
117 The ``ready`` state would be for changeset that wait review of someone that |
|
118 can "publish" them. |
|
119 |
|
120 |
|
121 |
|
122 Current Feature and usage |
|
123 ========================= |
|
124 |
|
125 |
|
126 Enabling states |
|
127 ............... |
|
128 |
|
129 The extension adds a :hg:`hg states` command to display and choose which states |
|
130 are used by a repository, see :hg:`hg states` for details. |
|
131 |
|
132 By default all changesets in the repository are ``published``. Other states |
|
133 must be explicitly activated. Changeset in a remote repository that doesn't |
|
134 support states are all seen as ``published``. |
|
135 |
|
136 .. note: |
|
137 |
|
138 When a state is not activated, changesets in this state are handled as |
|
139 changesets of the previous state it (``draft`` are handled as ``ready``, |
|
140 ``ready`` are handled as ``published``). |
|
141 |
|
142 TODO: |
|
143 |
|
144 - have a configuration in hgrc:: |
|
145 |
|
146 [states] |
|
147 ready=(off|on)(-inherit)? |
|
148 <state>=(off|on)(-inherit)? |
|
149 |
|
150 :off: state disabled for new repo |
|
151 :on: state enabled for new repo |
|
152 :inherit: if present, inherit states of source on :hg:`clone`. |
|
153 |
|
154 - display the number of changesets that change state when activating a state. |
|
155 |
|
156 |
|
157 |
|
158 State transition |
|
159 ................ |
|
160 |
|
161 Changeset you create locally will be in the ``draft`` state. (or any previous |
|
162 state if draft isn't enabled) |
|
163 |
|
164 There is some situation where the state of a changeset will change |
|
165 automatically. Automatic movement always go in the same direction.: ``draft -> |
|
166 ``ready`` -> ``published`` |
|
167 |
|
168 1. When you pull or push boundary move. Common changeset that are ``published`` in |
|
169 one of the two repository are set to ``published``. Same goes for ``ready`` etc |
|
170 (states are evaluated from in increasing order XXX I bet no one understand this |
|
171 parenthesis. Pull operation alter the local repository. push alter both local |
|
172 and remote repository. |
|
173 |
|
174 .. note: |
|
175 |
|
176 As Repository without any specific state have all their changeset |
|
177 ``published``, Pushing to such repo will ``publish`` all common changeset. |
|
178 |
|
179 2. Tagged changeset get automatically Published. The tagging changeset is |
|
180 tagged too... This doesn't apply to local tag. |
|
181 |
|
182 |
|
183 You can also manually change changeset state with a dedicated command for each |
|
184 state. See :hg:`published`, :hg:`ready` and :hg:`draft` for details. |
|
185 |
|
186 XXX maybe we can details the general behaviour here |
|
187 |
|
188 :hg <state> revs: move boundary of state so it includes revs |
|
189 ( revs included in ::<state>heads()) |
|
190 :hg --exact <state> revs: move boundary so that revs are exactly in state |
|
191 <state> ( all([rev.state == <state> for rev in |
|
192 revs])) |
|
193 :hg --exact --force <state> revs: move boundary event if it create inconsistency |
|
194 (with tag for example) |
|
195 |
|
196 TODO: |
|
197 |
|
198 - implement consistency check |
|
199 |
|
200 - implement --force |
|
201 |
|
202 |
|
203 Existing command change |
|
204 ....................... |
|
205 |
|
206 As said in the previous section: |
|
207 |
|
208 :commit: Create draft changeset (or the first enabled previous changeset). |
|
209 :tag: Move tagged and tagging changeset in the ``published`` state. |
|
210 :incoming: Exclude ``draft`` changeset of remote repository. |
|
211 :outgoing: Exclude ``draft`` changeset of local repository. |
|
212 :pull: As :hg:`in` + change state of local changeset according to remote side. |
|
213 :push: As :hg:`out` + sync state of common changeset on both side |
|
214 :rollback: rollback restore states heads as before the last transaction (see bookmark) |
|
215 |
|
216 State Transition control |
|
217 ......................... |
|
218 |
|
219 There is currently no way to control who can alter boundary (The most notable |
|
220 usecase is about the published one). |
|
221 |
|
222 This is probably needed quickly |
|
223 |
|
224 XXX TODO: Proper behaviour when heads file are chmoded whould be a first step. |
|
225 |
|
226 XXX We are going to need hooks (pre and post) hook on state transition too. |
|
227 |
|
228 Template |
|
229 ........ |
|
230 |
|
231 A new template keyword ``{state}`` has been added. |
|
232 |
|
233 Revset |
|
234 ...... |
|
235 |
|
236 We add new ``readyheads()`` and ``publishedheads()`` revset directives. This |
|
237 returns the heads of each state **as if all of them were activated**. |
|
238 |
|
239 XXX TODO - I would like to |
|
240 |
|
241 - move the current ``<state>heads()`` directives to |
|
242 _``<state>heads()`` |
|
243 |
|
244 - add ``<state>heads()`` directives to that return the currently in used heads |
|
245 |
|
246 - add ``<state>()`` directives that match all node in a state. |
|
247 |
|
248 Context |
|
249 ....... |
|
250 |
|
251 The ``context`` class gain a new method ``states()`` that return a ``state`` object. The |
|
252 most notable property of this states object are ```name`` and ``mutable``. |
|
253 |
|
254 Other extensions |
|
255 ................ |
|
256 |
|
257 :rebase: can't rebase immutable changeset. |
|
258 :mq: can't qimport immutable changeset. |
|
259 |
|
260 TODO: publishing a changeset should qfinish mq patches. |
|
261 |
|
262 |
|
263 |
|
264 Implementation |
|
265 ============== |
|
266 |
|
267 State definition |
|
268 ................ |
|
269 |
|
270 Conceptually: |
|
271 |
|
272 The set of node in the states are defined by the set of the state heads. This allow |
|
273 easy storage, exchange and consistency. |
|
274 |
|
275 .. note: A cache of the complete set of node that belong to a states will |
|
276 probably be need for performance. |
|
277 |
|
278 Code wise: |
|
279 |
|
280 There is a ``state`` class that hold the state property and several useful |
|
281 logic (name, revset entry etc). |
|
282 |
|
283 All defined states are accessible thought the STATES tuple at the ROOT of the |
|
284 module. Or the STATESMAP dictionary that allow to fetch a state from it's |
|
285 name. |
|
286 |
|
287 You can get and edit the list head node that define a state with two methods on |
|
288 repo. |
|
289 |
|
290 :stateheads(<state>): Returns the list of heads node that define a states |
|
291 :setstate(<state>, [nodes]): Move states boundary forward to include the given |
|
292 nodes in the given states. |
|
293 |
|
294 Those methods handle ``node`` and not rev as it seems more resilient to me that |
|
295 rev in a mutable world. Maybe it' would make more sens to have ``node`` store |
|
296 on disk but revision in the code. |
|
297 |
|
298 Storage |
|
299 ....... |
|
300 |
|
301 States related data are stored in the ``.hg/states/`` directory. |
|
302 |
|
303 The ``.hg/states/Enabled`` file list the states enabled in this |
|
304 repository. This data is *not* stored in the .hg/hgrc because the .hg/hgrc |
|
305 might be ignored for trust reason. As missing und with states can be pretty |
|
306 annoying. (publishing unfinalized changeset, pulling draft one etc) we don't |
|
307 want trust issue to interfer with enabled states information. |
|
308 |
|
309 ``.hg/states/<state>-heads`` file list the nodes that define a states. |
|
310 |
|
311 _NOSHARE filtering |
|
312 .................. |
|
313 |
|
314 Any changeset in a state with a _NOSHARE property will be exclude from pull, |
|
315 push, clone, incoming, outgoing and bundle. It is done through three mechanism: |
|
316 |
|
317 1. Wrapping the findcommonincoming and findcommonoutgoing code with (not very |
|
318 efficient) logic that recompute the exchanged heads. |
|
319 |
|
320 2. Altering ``heads`` wireprotocol command to return sharead heads. |
|
321 |
|
322 3. Disabling hardlink cloning when there is _NOSHARE changeset available. |
|
323 |
|
324 Internal plumbery |
|
325 ----------------- |
|
326 |
|
327 sum up of what we do: |
|
328 |
|
329 * state are object |
|
330 |
|
331 * repo.__class__ is extended |
|
332 |
|
333 * discovery is wrapped up |
|
334 |
|
335 * wire protocol is patched |
|
336 |
|
337 * transaction and rollback mechanism are wrapped up. |
|
338 |
|
339 * XXX we write new version of the boundard whenever something happen. We need a |
|
340 smarter and faster way to do this. |
|
341 |
|
342 |
|
343 ''' |
|
344 import os |
|
345 from functools import partial |
|
346 from operator import or_ |
|
347 |
|
348 from mercurial.i18n import _ |
|
349 from mercurial import cmdutil |
|
350 from mercurial import scmutil |
|
351 from mercurial import context |
|
352 from mercurial import revset |
|
353 from mercurial import templatekw |
|
354 from mercurial import util |
|
355 from mercurial import node |
|
356 from mercurial.node import nullid, hex, short |
|
357 from mercurial import discovery |
|
358 from mercurial import extensions |
|
359 from mercurial import wireproto |
|
360 from mercurial import pushkey |
|
361 from mercurial import error |
|
362 from mercurial import repair |
|
363 from mercurial.lock import release |
|
364 |
|
365 |
|
366 |
|
367 # states property constante |
|
368 _NOSHARE=2 |
|
369 _MUTABLE=1 |
|
370 |
|
371 class state(object): |
|
372 """State of changeset |
|
373 |
|
374 An utility object that handle several behaviour and containts useful code |
|
375 |
|
376 A state is defined by: |
|
377 - It's name |
|
378 - It's property (defined right above) |
|
379 |
|
380 - It's next state. |
|
381 |
|
382 XXX maybe we could stick description of the state semantic here. |
|
383 """ |
|
384 |
|
385 # plumbery utily |
|
386 def __init__(self, name, properties=0, next=None): |
|
387 self.name = name |
|
388 self.properties = properties |
|
389 assert next is None or self < next |
|
390 self.next = next |
|
391 @util.propertycache |
|
392 def trackheads(self): |
|
393 """Do we need to track heads of changeset in this state ? |
|
394 |
|
395 We don't need to track heads for the last state as this is repo heads""" |
|
396 return self.next is not None |
|
397 |
|
398 # public utility |
|
399 def __cmp__(self, other): |
|
400 """Use property to compare states. |
|
401 |
|
402 This is a naiv approach that assume the the next state are strictly |
|
403 more property than the one before |
|
404 # assert min(self, other).properties = self.properties & other.properties |
|
405 """ |
|
406 return cmp(self.properties, other.properties) |
|
407 |
|
408 @property |
|
409 def mutable(self): |
|
410 return bool(self.properties & _MUTABLE) |
|
411 |
|
412 # display code |
|
413 def __repr__(self): |
|
414 return 'state(%s)' % self.name |
|
415 |
|
416 def __str__(self): |
|
417 return self.name |
|
418 |
|
419 |
|
420 # revset utility |
|
421 @util.propertycache |
|
422 def _revsetheads(self): |
|
423 """function to be used by revset to finds heads of this states""" |
|
424 assert self.trackheads |
|
425 def revsetheads(repo, subset, x): |
|
426 args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') |
|
427 heads = [] |
|
428 for h in repo._statesheads[self]: |
|
429 try: |
|
430 heads.append(repo.changelog.rev(h)) |
|
431 except error.LookupError: |
|
432 pass |
|
433 heads.sort() |
|
434 return heads |
|
435 return revsetheads |
|
436 |
|
437 @util.propertycache |
|
438 def headssymbol(self): |
|
439 """name of the revset symbols""" |
|
440 if self.trackheads: |
|
441 return "%sheads" % self.name |
|
442 else: |
|
443 return 'heads' |
|
444 |
|
445 # Actual state definition |
|
446 |
|
447 ST2 = state('draft', _NOSHARE | _MUTABLE) |
|
448 ST1 = state('ready', _MUTABLE, next=ST2) |
|
449 ST0 = state('published', next=ST1) |
|
450 |
|
451 # all available state |
|
452 STATES = (ST0, ST1, ST2) |
|
453 # all available state by name |
|
454 STATESMAP =dict([(st.name, st) for st in STATES]) |
|
455 |
|
456 @util.cachefunc |
|
457 def laststatewithout(prop): |
|
458 """Find the states with the most property but <prop> |
|
459 |
|
460 (This function is necessary because the whole state stuff are abstracted)""" |
|
461 for state in STATES: |
|
462 if not state.properties & prop: |
|
463 candidate = state |
|
464 else: |
|
465 return candidate |
|
466 |
|
467 # util function |
|
468 ############################# |
|
469 def noderange(repo, revsets): |
|
470 """The same as revrange but return node""" |
|
471 return map(repo.changelog.node, |
|
472 scmutil.revrange(repo, revsets)) |
|
473 |
|
474 # Patch changectx |
|
475 ############################# |
|
476 |
|
477 def state(ctx): |
|
478 """return the state objet associated to the context""" |
|
479 if ctx.node()is None: |
|
480 return STATES[-1] |
|
481 return ctx._repo.nodestate(ctx.node()) |
|
482 context.changectx.state = state |
|
483 |
|
484 # improve template |
|
485 ############################# |
|
486 |
|
487 def showstate(ctx, **args): |
|
488 """Show the name of the state associated with the context""" |
|
489 return ctx.state() |
|
490 |
|
491 |
|
492 # New commands |
|
493 ############################# |
|
494 |
|
495 |
|
496 def cmdstates(ui, repo, *states, **opt): |
|
497 """view and modify activated states. |
|
498 |
|
499 With no argument, list activated state. |
|
500 |
|
501 With argument, activate the state in argument. |
|
502 |
|
503 With argument plus the --off switch, deactivate the state in argument. |
|
504 |
|
505 note: published state are alway activated.""" |
|
506 |
|
507 if not states: |
|
508 for st in sorted(repo._enabledstates): |
|
509 ui.write('%s\n' % st) |
|
510 else: |
|
511 off = opt.get('off', False) |
|
512 for state_name in states: |
|
513 for st in STATES: |
|
514 if st.name == state_name: |
|
515 break |
|
516 else: |
|
517 ui.write_err(_('no state named %s\n') % state_name) |
|
518 return 1 |
|
519 if off: |
|
520 if st in repo._enabledstates: |
|
521 repo.disablestate(st) |
|
522 else: |
|
523 ui.write_err(_('state %s already deactivated\n') % |
|
524 state_name) |
|
525 |
|
526 else: |
|
527 repo.enablestate(st, not opt.get('clever')) |
|
528 repo._writeenabledstates() |
|
529 return 0 |
|
530 |
|
531 cmdtable = {'states': (cmdstates, [ |
|
532 ('', 'off', False, _('desactivate the state') ), |
|
533 ('', 'clever', False, _('do not fix lower when activating the state') )], |
|
534 '<state>')} |
|
535 |
|
536 # automatic generation of command that set state |
|
537 def makecmd(state): |
|
538 def cmdmoveheads(ui, repo, *changesets, **opts): |
|
539 """set revisions in %s state |
|
540 |
|
541 This command also alter state of ancestors if necessary. |
|
542 """ % state |
|
543 if not state in repo._enabledstates: |
|
544 raise error.Abort( |
|
545 _('state %s is not activated' % state), |
|
546 hint=_('try ``hg states %s`` before' % state)) |
|
547 if opts.get('exact'): |
|
548 repo.setstate_unsafe(state, changesets) |
|
549 return 0 |
|
550 revs = scmutil.revrange(repo, changesets) |
|
551 repo.setstate(state, [repo.changelog.node(rev) for rev in revs]) |
|
552 return 0 |
|
553 return cmdmoveheads |
|
554 |
|
555 for state in STATES: |
|
556 cmdmoveheads = makecmd(state) |
|
557 cmdtable[state.name] = (cmdmoveheads, [ |
|
558 ('e', 'exact', False, _('move boundary so that revs are exactly in ' |
|
559 'state <state> ( all([rev.state == <state> for ' |
|
560 'rev in revs]))')) |
|
561 ], '<revset>') |
|
562 |
|
563 # Pushkey mechanism for mutable |
|
564 ######################################### |
|
565 |
|
566 def pushstatesheads(repo, key, old, new): |
|
567 """receive a new state for a revision via pushkey |
|
568 |
|
569 It only move revision from a state to a <= one |
|
570 |
|
571 Return True if the <key> revision exist in the repository |
|
572 Return False otherwise. (and doesn't alter any state)""" |
|
573 st = STATESMAP[new] |
|
574 w = repo.wlock() |
|
575 try: |
|
576 newhead = node.bin(key) |
|
577 try: |
|
578 repo[newhead] |
|
579 except error.RepoLookupError: |
|
580 return False |
|
581 repo.setstate(st, [newhead]) |
|
582 return True |
|
583 finally: |
|
584 w.release() |
|
585 |
|
586 def liststatesheads(repo): |
|
587 """List the boundary of all states. |
|
588 |
|
589 {"node-hex" -> "comma separated list of state",} |
|
590 """ |
|
591 keys = {} |
|
592 for state in [st for st in STATES if st.trackheads]: |
|
593 for head in repo.stateheads(state): |
|
594 head = node.hex(head) |
|
595 if head in keys: |
|
596 keys[head] += ',' + state.name |
|
597 else: |
|
598 keys[head] = state.name |
|
599 return keys |
|
600 |
|
601 pushkey.register('states-heads', pushstatesheads, liststatesheads) |
|
602 |
|
603 |
|
604 # Wrap discovery |
|
605 #################### |
|
606 def filterprivateout(orig, repo, *args,**kwargs): |
|
607 """wrapper for findcommonoutgoing that remove _NOSHARE""" |
|
608 common, heads = orig(repo, *args, **kwargs) |
|
609 if getattr(repo, '_reducehead', None) is not None: |
|
610 return common, repo._reducehead(heads) |
|
611 def filterprivatein(orig, repo, remote, *args, **kwargs): |
|
612 """wrapper for findcommonincoming that remove _NOSHARE""" |
|
613 common, anyinc, heads = orig(repo, remote, *args, **kwargs) |
|
614 if getattr(remote, '_reducehead', None) is not None: |
|
615 heads = remote._reducehead(heads) |
|
616 return common, anyinc, heads |
|
617 |
|
618 # states boundary IO |
|
619 ##################### |
|
620 |
|
621 def _readheadsfile(repo, filename): |
|
622 """read head from the given file |
|
623 |
|
624 XXX move me elsewhere""" |
|
625 heads = [nullid] |
|
626 try: |
|
627 f = repo.opener(filename) |
|
628 try: |
|
629 heads = sorted([node.bin(n) for n in f.read().split() if n]) |
|
630 finally: |
|
631 f.close() |
|
632 except IOError: |
|
633 pass |
|
634 return heads |
|
635 |
|
636 def _readstatesheads(repo, undo=False): |
|
637 """read all state heads |
|
638 |
|
639 XXX move me elsewhere""" |
|
640 statesheads = {} |
|
641 for state in STATES: |
|
642 if state.trackheads: |
|
643 filemask = 'states/%s-heads' |
|
644 filename = filemask % state.name |
|
645 statesheads[state] = _readheadsfile(repo, filename) |
|
646 return statesheads |
|
647 |
|
648 def _writeheadsfile(repo, filename, heads): |
|
649 """write given <heads> in the file with at <filename> |
|
650 |
|
651 XXX move me elsewhere""" |
|
652 f = repo.opener(filename, 'w', atomictemp=True) |
|
653 try: |
|
654 for h in heads: |
|
655 f.write(hex(h) + '\n') |
|
656 try: |
|
657 f.rename() |
|
658 except AttributeError: # old version |
|
659 f.close() |
|
660 finally: |
|
661 f.close() |
|
662 |
|
663 def _writestateshead(repo): |
|
664 """write all heads |
|
665 |
|
666 XXX move me elsewhere""" |
|
667 # XXX transaction! |
|
668 for state in STATES: |
|
669 if state.trackheads: |
|
670 filename = 'states/%s-heads' % state.name |
|
671 _writeheadsfile(repo, filename, repo._statesheads[state]) |
|
672 |
|
673 # WireProtocols |
|
674 #################### |
|
675 def wireheads(repo, proto): |
|
676 """Altered head command that doesn't include _NOSHARE |
|
677 |
|
678 This is a write protocol command""" |
|
679 st = laststatewithout(_NOSHARE) |
|
680 h = repo.stateheads(st) |
|
681 return wireproto.encodelist(h) + "\n" |
|
682 |
|
683 # Other extension support |
|
684 ######################### |
|
685 |
|
686 def wraprebasebuildstate(orig, repo, *args, **kwargs): |
|
687 """Wrapped rebuild state that check for immutable changeset |
|
688 |
|
689 buildstate are the best place i found to hook :-/""" |
|
690 result = orig(repo, *args, **kwargs) |
|
691 if result is not None: |
|
692 # rebase.nullmerge is issued in the detach case |
|
693 rebase = extensions.find('rebase') |
|
694 rebased = [rev for rev, rbst in result[2].items() if rbst != rebase.nullmerge] |
|
695 base = repo.changelog.node(min(rebased)) |
|
696 state = repo.nodestate(base) |
|
697 if not state.mutable: |
|
698 raise util.Abort(_('can not rebase published changeset %s') |
|
699 % node.short(base), |
|
700 hint=_('see `hg help --extension states` for details')) |
|
701 return result |
|
702 |
|
703 def wrapmqqimport(orig, queue, repo, *args, **kwargs): |
|
704 """Wrapper for rebuild state that deny importing immutable changeset |
|
705 """ |
|
706 if 'rev' in kwargs: |
|
707 # we can take the min as non linear import will break |
|
708 # anyway |
|
709 revs = scmutil.revrange(repo, kwargs['rev']) |
|
710 if revs: |
|
711 base = min(revs) |
|
712 basenode = repo.changelog.node(base) |
|
713 state = repo.nodestate(basenode) |
|
714 if not state.mutable: |
|
715 raise util.Abort(_('can not qimport published changeset %s') |
|
716 % node.short(basenode), |
|
717 hint=_('see `hg help --extension states` for details')) |
|
718 return orig(queue, repo, *args, **kwargs) |
|
719 |
|
720 def strip(orig, ui, repo, node, backup="all"): |
|
721 cl = repo.changelog |
|
722 striprev = cl.rev(node) |
|
723 revstostrip = set(cl.descendants(striprev)) |
|
724 revstostrip.add(striprev) |
|
725 tostrip = set(map(cl.node, revstostrip)) |
|
726 # compute the potentially new created states bondaries which are any |
|
727 # parent of the stripped node that are not stripped (may not be heads) |
|
728 newbondaries = set(par for nod in tostrip for par in cl.parents(nod) |
|
729 if par not in tostrip) |
|
730 # save the current states of newbondaries in a chache as repo.nodestate |
|
731 # must work along the loop. We will use the next loop to add them. |
|
732 statesheads={} |
|
733 for nd in newbondaries: |
|
734 state = repo.nodestate(nd) |
|
735 if state.trackheads: |
|
736 statesheads.setdefault(state, set([])).add(nd) |
|
737 |
|
738 for state, heads in repo._statesheads.iteritems(): |
|
739 if not state.trackheads: |
|
740 continue |
|
741 heads = set(heads) - tostrip | statesheads.get(state, set([])) |
|
742 # reduce heads (make them really heads) |
|
743 revs = set(map(cl.rev, heads)) |
|
744 minrev = min(revs) |
|
745 for rev in cl.ancestors(*revs): |
|
746 if rev >= minrev: |
|
747 revs.discard(rev) |
|
748 repo._statesheads[state] = map(cl.node, revs) |
|
749 _writestateshead(repo) |
|
750 |
|
751 return orig(ui, repo, node, backup) |
|
752 |
|
753 |
|
754 def uisetup(ui): |
|
755 """ |
|
756 * patch stuff for the _NOSHARE property |
|
757 * add template keyword |
|
758 """ |
|
759 # patch discovery |
|
760 extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout) |
|
761 extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein) |
|
762 extensions.wrapfunction(repair, 'strip', strip) |
|
763 |
|
764 # patch wireprotocol |
|
765 wireproto.commands['heads'] = (wireheads, '') |
|
766 |
|
767 # add template keyword |
|
768 templatekw.keywords['state'] = showstate |
|
769 |
|
770 def extsetup(ui): |
|
771 """Extension setup |
|
772 |
|
773 * add revset entry""" |
|
774 for state in STATES: |
|
775 if state.trackheads: |
|
776 revset.symbols[state.headssymbol] = state._revsetheads |
|
777 # wrap rebase |
|
778 try: |
|
779 rebase = extensions.find('rebase') |
|
780 if rebase: |
|
781 extensions.wrapfunction(rebase, 'buildstate', wraprebasebuildstate) |
|
782 except KeyError: |
|
783 pass # rebase not found |
|
784 # wrap mq |
|
785 try: |
|
786 mq = extensions.find('mq') |
|
787 if mq: |
|
788 extensions.wrapfunction(mq.queue, 'qimport', wrapmqqimport) |
|
789 except KeyError: |
|
790 pass # mq not found |
|
791 |
|
792 |
|
793 |
|
794 def reposetup(ui, repo): |
|
795 """Repository setup |
|
796 |
|
797 * extend repo class with states logic""" |
|
798 |
|
799 if not repo.local(): |
|
800 return |
|
801 |
|
802 ocancopy =repo.cancopy |
|
803 opull = repo.pull |
|
804 opush = repo.push |
|
805 o_tag = repo._tag |
|
806 orollback = repo.rollback |
|
807 o_writejournal = repo._writejournal |
|
808 class statefulrepo(repo.__class__): |
|
809 """An extension of repo class that handle state logic |
|
810 |
|
811 - nodestate |
|
812 - stateheads |
|
813 """ |
|
814 |
|
815 def nodestate(self, node): |
|
816 """return the state object associated to the given node""" |
|
817 rev = self.changelog.rev(node) |
|
818 for state in STATES: |
|
819 # avoid for untracked heads |
|
820 if state.next is not None: |
|
821 ancestors = map(self.changelog.rev, self.stateheads(state)) |
|
822 ancestors.extend(self.changelog.ancestors(*ancestors)) |
|
823 if rev in ancestors: |
|
824 break |
|
825 return state |
|
826 |
|
827 def enablestate(self, state, fix_lower=True): |
|
828 if fix_lower: |
|
829 # at least published which is always activated |
|
830 lower = max(st for st in self._enabledstates if st < state) |
|
831 self.setstate(lower, self.stateheads(state)) |
|
832 self._enabledstates.add(state) |
|
833 |
|
834 def disablestate(self, state): |
|
835 """Disable empty state. |
|
836 Raise error.Abort if the state is not empty. |
|
837 """ |
|
838 # the lowest is mandatory |
|
839 if state == ST0: |
|
840 raise error.Abort(_('could not disable %s' % state.name)) |
|
841 enabled = self._enabledstates |
|
842 # look up for lower state that is enabled (at least published) |
|
843 lower = max(st for st in self._enabledstates if st < state) |
|
844 if repo.stateheads(state) != repo.stateheads(lower): |
|
845 raise error.Abort( |
|
846 _('could not disable non empty state %s' % state.name), |
|
847 hint=_("You may want to use `hg %s '%sheads()'`" |
|
848 % (lower.name, state.name)) |
|
849 ) |
|
850 else: |
|
851 enabled.remove(state) |
|
852 |
|
853 def stateheads(self, state): |
|
854 """Return the set of head that define the state""" |
|
855 # look for a relevant state |
|
856 while state.trackheads and state.next not in self._enabledstates: |
|
857 state = state.next |
|
858 # last state have no cached head. |
|
859 if state.trackheads: |
|
860 return self._statesheads[state] |
|
861 return self.heads() |
|
862 |
|
863 @util.propertycache |
|
864 def _statesheads(self): |
|
865 """{ state-object -> set(defining head)} mapping""" |
|
866 return _readstatesheads(self) |
|
867 |
|
868 def setstate_unsafe(self, state, changesets): |
|
869 """Change state of targets changesets and it's ancestors. |
|
870 |
|
871 Simplify the list of heads. |
|
872 |
|
873 Unlike ``setstate``, the "lower" states are also changed |
|
874 """ |
|
875 #modify "lower" states |
|
876 req_nodes_rst = '|'.join('((%s)::)' % rst for rst in changesets) |
|
877 for st in STATES: |
|
878 if st >= state: # only modify lower state heads for now |
|
879 continue |
|
880 try: |
|
881 heads = self._statesheads[st] |
|
882 except KeyError: # forget non-activated states |
|
883 continue |
|
884 olds = heads[:] |
|
885 rst = "heads((::%s()) - (%s))" % (st.headssymbol, req_nodes_rst) |
|
886 heads[:] = noderange(repo, [rst]) |
|
887 if olds != heads: |
|
888 _writestateshead(self) |
|
889 #modify the state |
|
890 if state in self._statesheads: |
|
891 revs = scmutil.revrange(repo, changesets) |
|
892 repo.setstate(state, [repo.changelog.node(rev) for rev in revs]) |
|
893 |
|
894 def setstate(self, state, nodes): |
|
895 """change state of targets changeset and it's ancestors. |
|
896 |
|
897 Simplify the list of head.""" |
|
898 assert not isinstance(nodes, basestring), repr(nodes) |
|
899 if not state.trackheads: |
|
900 return |
|
901 heads = self._statesheads[state] |
|
902 olds = heads[:] |
|
903 heads.extend(nodes) |
|
904 heads[:] = set(heads) |
|
905 heads.sort() |
|
906 if olds != heads: |
|
907 heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol]) |
|
908 heads.sort() |
|
909 if olds != heads: |
|
910 _writestateshead(self) |
|
911 if state.next is not None and state.next.trackheads: |
|
912 self.setstate(state.next, nodes) # cascading |
|
913 |
|
914 def _reducehead(self, candidates): |
|
915 """recompute a set of heads so it doesn't include _NOSHARE changeset |
|
916 |
|
917 This is basically a complicated method that compute |
|
918 heads(::candidates - _NOSHARE) |
|
919 """ |
|
920 selected = set() |
|
921 st = laststatewithout(_NOSHARE) |
|
922 candidates = set(map(self.changelog.rev, candidates)) |
|
923 heads = set(map(self.changelog.rev, self.stateheads(st))) |
|
924 shareable = set(self.changelog.ancestors(*heads)) |
|
925 shareable.update(heads) |
|
926 selected = candidates & shareable |
|
927 unselected = candidates - shareable |
|
928 for rev in unselected: |
|
929 for revh in heads: |
|
930 if self.changelog.descendant(revh, rev): |
|
931 selected.add(revh) |
|
932 return sorted(map(self.changelog.node, selected)) |
|
933 |
|
934 ### enable // disable logic |
|
935 |
|
936 @util.propertycache |
|
937 def _enabledstates(self): |
|
938 """The set of state enabled in this repository""" |
|
939 return self._readenabledstates() |
|
940 |
|
941 def _readenabledstates(self): |
|
942 """read enabled state from disk""" |
|
943 states = set() |
|
944 states.add(ST0) |
|
945 mapping = dict([(st.name, st) for st in STATES]) |
|
946 try: |
|
947 f = self.opener('states/Enabled') |
|
948 for line in f: |
|
949 st = mapping.get(line.strip()) |
|
950 if st is not None: |
|
951 states.add(st) |
|
952 finally: |
|
953 return states |
|
954 |
|
955 def _writeenabledstates(self): |
|
956 """read enabled state to disk""" |
|
957 f = self.opener('states/Enabled', 'w', atomictemp=True) |
|
958 try: |
|
959 for st in self._enabledstates: |
|
960 f.write(st.name + '\n') |
|
961 try: |
|
962 f.rename() |
|
963 except AttributeError: # old version |
|
964 f.close() |
|
965 finally: |
|
966 f.close() |
|
967 |
|
968 ### local clone support |
|
969 |
|
970 def cancopy(self): |
|
971 """deny copy if there is _NOSHARE changeset""" |
|
972 st = laststatewithout(_NOSHARE) |
|
973 return ocancopy() and (self.stateheads(st) == self.heads()) |
|
974 |
|
975 ### pull // push support |
|
976 |
|
977 def pull(self, remote, *args, **kwargs): |
|
978 """altered pull that also update states heads on local repo""" |
|
979 result = opull(remote, *args, **kwargs) |
|
980 remoteheads = self._pullstatesheads(remote) |
|
981 for st, heads in remoteheads.iteritems(): |
|
982 self.setstate(st, heads) |
|
983 return result |
|
984 |
|
985 def push(self, remote, *args, **opts): |
|
986 """altered push that also update states heads on local and remote""" |
|
987 result = opush(remote, *args, **opts) |
|
988 if not self.ui.configbool('states', 'bypass', False): |
|
989 remoteheads = self._pullstatesheads(remote) |
|
990 for st, heads in remoteheads.iteritems(): |
|
991 self.setstate(st, heads) |
|
992 if heads != self.stateheads(st): |
|
993 self._pushstatesheads(remote, st, heads) |
|
994 return result |
|
995 |
|
996 def _pushstatesheads(self, remote, state, remoteheads): |
|
997 """push head of a given state for remote |
|
998 |
|
999 This handle pushing boundary that does exist on remote host |
|
1000 This is done a very naive way""" |
|
1001 local = set(self.stateheads(state)) |
|
1002 missing = local - set(remoteheads) |
|
1003 while missing: |
|
1004 h = missing.pop() |
|
1005 ok = remote.pushkey('states-heads', node.hex(h), '', state.name) |
|
1006 if not ok: |
|
1007 missing.update(p.node() for p in repo[h].parents()) |
|
1008 |
|
1009 |
|
1010 def _pullstatesheads(self, remote): |
|
1011 """pull all remote states boundary locally |
|
1012 |
|
1013 This can only make the boundary move on a newer changeset""" |
|
1014 remoteheads = {} |
|
1015 self.ui.debug('checking for states-heads on remote server') |
|
1016 if 'states-heads' not in remote.listkeys('namespaces'): |
|
1017 self.ui.debug('states-heads not enabled on the remote server, ' |
|
1018 'marking everything as published\n') |
|
1019 remoteheads[ST0] = remote.heads() |
|
1020 else: |
|
1021 self.ui.debug('server has states-heads enabled, merging lists') |
|
1022 for hex, statenames in remote.listkeys('states-heads').iteritems(): |
|
1023 for stn in statenames.split(','): |
|
1024 remoteheads.setdefault(STATESMAP[stn], []).append(node.bin(hex)) |
|
1025 return remoteheads |
|
1026 |
|
1027 ### Tag support |
|
1028 |
|
1029 def _tag(self, names, node, *args, **kwargs): |
|
1030 """Altered version of _tag that make tag (and tagging) published""" |
|
1031 tagnode = o_tag(names, node, *args, **kwargs) |
|
1032 if tagnode is not None: # do nothing for local one |
|
1033 self.setstate(ST0, [node, tagnode]) |
|
1034 return tagnode |
|
1035 |
|
1036 ### rollback support |
|
1037 |
|
1038 def _writejournal(self, desc): |
|
1039 """extended _writejournal that also save states""" |
|
1040 entries = list(o_writejournal(desc)) |
|
1041 for state in STATES: |
|
1042 if state.trackheads: |
|
1043 filename = 'states/%s-heads' % state.name |
|
1044 filepath = self.join(filename) |
|
1045 if os.path.exists(filepath): |
|
1046 journalname = 'states/journal.%s-heads' % state.name |
|
1047 journalpath = self.join(journalname) |
|
1048 util.copyfile(filepath, journalpath) |
|
1049 entries.append(journalpath) |
|
1050 return tuple(entries) |
|
1051 |
|
1052 def rollback(self, dryrun=False): |
|
1053 """extended rollback that also restore states""" |
|
1054 wlock = lock = None |
|
1055 try: |
|
1056 wlock = self.wlock() |
|
1057 lock = self.lock() |
|
1058 ret = orollback(dryrun) |
|
1059 if not (ret or dryrun): #rollback did not failed |
|
1060 for state in STATES: |
|
1061 if state.trackheads: |
|
1062 src = self.join('states/undo.%s-heads') % state.name |
|
1063 dest = self.join('states/%s-heads') % state.name |
|
1064 if os.path.exists(src): |
|
1065 util.rename(src, dest) |
|
1066 elif os.path.exists(dest): #unlink in any case |
|
1067 os.unlink(dest) |
|
1068 self.__dict__.pop('_statesheads', None) |
|
1069 return ret |
|
1070 finally: |
|
1071 release(lock, wlock) |
|
1072 |
|
1073 repo.__class__ = statefulrepo |
|
1074 |
|