1 # obsolete.py - introduce the obsolete concept in mercurial. |
|
2 # |
|
3 # Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> |
|
4 # Logilab SA <contact@logilab.fr> |
|
5 # |
|
6 # This software may be used and distributed according to the terms of the |
|
7 # GNU General Public License version 2 or any later version. |
|
8 |
|
9 from mercurial import util |
|
10 from mercurial import context |
|
11 from mercurial import revset |
|
12 from mercurial import scmutil |
|
13 from mercurial import extensions |
|
14 from mercurial import pushkey |
|
15 from mercurial import discovery |
|
16 from mercurial import error |
|
17 from mercurial.node import hex, bin |
|
18 |
|
19 # Patch changectx |
|
20 ############################# |
|
21 |
|
22 def obsolete(ctx): |
|
23 """is the changeset obsolete by other""" |
|
24 if ctx.node()is None: |
|
25 return False |
|
26 return bool(ctx._repo.obsoletedby(ctx.node())) |
|
27 |
|
28 context.changectx.obsolete = obsolete |
|
29 |
|
30 ohidden = context.changectx.hidden |
|
31 def hidden(ctx): |
|
32 # hack to fill hiddenrevs |
|
33 # compute hidden (XXX should move elsewhere) |
|
34 if not getattr(ctx._repo.changelog, 'hiddeninit', False): |
|
35 basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))' |
|
36 for rev in scmutil.revrange(ctx._repo, [basicquery]): |
|
37 ctx._repo.changelog.hiddenrevs.add(rev) |
|
38 ctx._repo.changelog.hiddeninit = True |
|
39 |
|
40 return ohidden(ctx) |
|
41 context.changectx.hidden = hidden |
|
42 |
|
43 # revset |
|
44 ############################# |
|
45 |
|
46 def revsetobsolete(repo, subset, x): |
|
47 args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') |
|
48 return [r for r in subset if repo[r].obsolete()] # XXX slow |
|
49 |
|
50 def extsetup(ui): |
|
51 revset.symbols["obsolete"] = revsetobsolete |
|
52 |
|
53 def filterobsoleteout(orig, repo, remote, *args,**kwargs): |
|
54 common, heads = orig(repo, remote, *args, **kwargs) |
|
55 |
|
56 # filter obsolete |
|
57 heads = set(map(repo.changelog.rev, heads)) |
|
58 obsoletes = set() |
|
59 for obj in repo._obsobjrels: |
|
60 try: |
|
61 obsoletes.add(repo.changelog.rev(obj)) |
|
62 except error.LookupError: |
|
63 pass # we don't have this node locally |
|
64 |
|
65 outgoing = set(repo.changelog.ancestors(*heads)) |
|
66 outgoing.update(heads) |
|
67 |
|
68 selected = outgoing - obsoletes |
|
69 heads = sorted(map(repo.changelog.node, selected)) |
|
70 |
|
71 return common, heads |
|
72 |
|
73 extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout) |
|
74 |
|
75 try: |
|
76 rebase = extensions.find('rebase') |
|
77 if rebase: |
|
78 extensions.wrapfunction(rebase, 'concludenode', concludenode) |
|
79 except KeyError: |
|
80 pass # rebase not found |
|
81 |
|
82 # Pushkey mechanism for mutable |
|
83 ######################################### |
|
84 |
|
85 def pushobsolete(repo, key, old, relations): |
|
86 assert key == "relations" |
|
87 w = repo.wlock() |
|
88 try: |
|
89 for sub, objs in relations.iteritems(): |
|
90 for obj in objs: |
|
91 repo.addobsolete(sub, obj) |
|
92 finally: |
|
93 w.release() |
|
94 |
|
95 def listobsolete(repo): |
|
96 return {'relations': repo._obssubrels} |
|
97 |
|
98 pushkey.register('obsolete', pushobsolete, listobsolete) |
|
99 |
|
100 # New commands |
|
101 ############################# |
|
102 |
|
103 |
|
104 def cmddebugobsolete(ui, repo, subject, object): |
|
105 """Add an obsolete relation between a too node |
|
106 |
|
107 The subject is expected to be a newer version of the object""" |
|
108 sub = repo[subject] |
|
109 obj = repo[object] |
|
110 repo.addobsolete(sub.node(), obj.node()) |
|
111 return 0 |
|
112 |
|
113 cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')} |
|
114 |
|
115 def reposetup(ui, repo): |
|
116 |
|
117 if not repo.local(): |
|
118 return |
|
119 |
|
120 opull = repo.pull |
|
121 opush = repo.push |
|
122 |
|
123 class obsoletingrepo(repo.__class__): |
|
124 |
|
125 |
|
126 ### Hidden revision support |
|
127 @util.propertycache |
|
128 def hiddenrevs(self): |
|
129 # It's a property because It simpler that to handle the __init__ |
|
130 revs = set() |
|
131 return revs |
|
132 |
|
133 ### obsolete storage |
|
134 @util.propertycache |
|
135 def _obsobjrels(self): |
|
136 """{<old-node> -> set(<new-node>)} |
|
137 |
|
138 also compute hidden revision""" |
|
139 #reverse sub -> objs mapping |
|
140 objrels = {} |
|
141 for sub, objs in self._obssubrels.iteritems(): |
|
142 for obj in objs: |
|
143 objrels.setdefault(obj, set()).add(sub) |
|
144 return objrels |
|
145 |
|
146 @util.propertycache |
|
147 def _obssubrels(self): |
|
148 """{<new-node> -> set(<old-node>)}""" |
|
149 return self._readobsrels() |
|
150 |
|
151 |
|
152 ### Disk IO |
|
153 def _readobsrels(self): |
|
154 """Write obsolete relation on disk""" |
|
155 # XXX handle lock |
|
156 rels = {} |
|
157 try: |
|
158 f = self.opener('obsolete-relations') |
|
159 try: |
|
160 for line in f: |
|
161 subhex, objhex = line.split() |
|
162 rels.setdefault(bin(subhex), set()).add(bin(objhex)) |
|
163 finally: |
|
164 f.close() |
|
165 except IOError: |
|
166 pass |
|
167 return rels |
|
168 |
|
169 def _writeobsrels(self): |
|
170 """Write obsolete relation on disk""" |
|
171 # XXX handle lock |
|
172 f = self.opener('obsolete-relations', 'w', atomictemp=True) |
|
173 try: |
|
174 for sub, objs in self._obssubrels.iteritems(): |
|
175 for obj in objs: |
|
176 f.write('%s %s\n' % (hex(sub), hex(obj))) |
|
177 f.rename() |
|
178 finally: |
|
179 f.close() |
|
180 |
|
181 ### local clone support |
|
182 |
|
183 def cancopy(self): |
|
184 return not bool(self._obsobjrels) # you can't copy if there is obsolete |
|
185 |
|
186 ### pull // push support |
|
187 |
|
188 def pull(self, remote, *args, **kwargs): |
|
189 obskey = remote.listkeys('obsolete') |
|
190 obsrels = obskey.get('relations', {}) |
|
191 result = opull(remote, *args, **kwargs) |
|
192 for sub, objs in obsrels.iteritems(): |
|
193 for obj in objs: |
|
194 self.addobsolete(sub, obj) |
|
195 return result |
|
196 |
|
197 def push(self, remote, *args, **opts): |
|
198 obskey = remote.listkeys('obsolete') |
|
199 obssupport = 'relations' in obskey |
|
200 result = opush(remote, *args, **opts) |
|
201 if obssupport: |
|
202 remote.pushkey('obsolete', 'relations', {}, self._obssubrels) |
|
203 return result |
|
204 |
|
205 |
|
206 ### Public method |
|
207 def obsoletedby(self, node): |
|
208 """return the set of node that make <node> obsolete (obj)""" |
|
209 return self._obsobjrels.get(node, set()) |
|
210 |
|
211 def obsolete(self, node): |
|
212 """return the set of node that <node> make obsolete (sub)""" |
|
213 return self._obssubrels.get(node, set()) |
|
214 |
|
215 def addobsolete(self, sub, obj): |
|
216 """Add a relation marking that node <sub> is a new version of <obj>""" |
|
217 self._obssubrels.setdefault(sub, set()).add(obj) |
|
218 self._obsobjrels.setdefault(obj, set()).add(sub) |
|
219 try: |
|
220 self.changelog.hiddenrevs.add(repo[obj].rev()) |
|
221 except error.RepoLookupError: |
|
222 pass #unknow revision (but keep propagating the data |
|
223 self._writeobsrels() |
|
224 |
|
225 repo.__class__ = obsoletingrepo |
|
226 |
|
227 |
|
228 ### Other Extension compat |
|
229 ############################ |
|
230 |
|
231 def concludenode(orig, repo, rev, *args, **kwargs): |
|
232 newrev = orig(repo, rev, *args, **kwargs) |
|
233 oldnode = repo[rev].node() |
|
234 newnode = repo[newrev].node() |
|
235 repo.addobsolete(newnode, oldnode) |
|
236 return newrev |
|
237 |
|