Mercurial > evolve
comparison hgext/evolve.py @ 133:aa182b912d62
rename evolution to evolve
too much confusion with the email client
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Fri, 17 Feb 2012 10:29:01 +0100 |
parents | hgext/evolution.py@3124889cad55 |
children | bbc653876876 |
comparison
equal
deleted
inserted
replaced
132:64d16f07d67f | 133:aa182b912d62 |
---|---|
1 # states.py - introduce the state concept for mercurial changeset | |
2 # | |
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com> | |
4 # Logilab SA <contact@logilab.fr> | |
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org> | |
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 '''A set of command to make changeset evolve.''' | |
11 | |
12 from mercurial import cmdutil | |
13 from mercurial import scmutil | |
14 from mercurial import node | |
15 from mercurial import error | |
16 from mercurial import extensions | |
17 from mercurial import commands | |
18 from mercurial import bookmarks | |
19 from mercurial import phases | |
20 from mercurial import context | |
21 from mercurial import commands | |
22 from mercurial import util | |
23 from mercurial.i18n import _ | |
24 from mercurial.commands import walkopts, commitopts, commitopts2, logopt | |
25 from mercurial import hg | |
26 | |
27 ### util function | |
28 ############################# | |
29 def noderange(repo, revsets): | |
30 """The same as revrange but return node""" | |
31 return map(repo.changelog.node, | |
32 scmutil.revrange(repo, revsets)) | |
33 | |
34 ### extension check | |
35 ############################# | |
36 | |
37 def extsetup(ui): | |
38 try: | |
39 obsolete = extensions.find('obsolete') | |
40 except KeyError: | |
41 raise error.Abort(_('evolution extension require obsolete extension.')) | |
42 try: | |
43 rebase = extensions.find('rebase') | |
44 except KeyError: | |
45 raise error.Abort(_('evolution extension require rebase extension.')) | |
46 | |
47 ### changeset rewriting logic | |
48 ############################# | |
49 | |
50 def rewrite(repo, old, updates, head, newbases, commitopts): | |
51 if len(old.parents()) > 1: #XXX remove this unecessary limitation. | |
52 raise error.Abort(_('cannot amend merge changesets')) | |
53 base = old.p1() | |
54 bm = bookmarks.readcurrent(repo) | |
55 | |
56 wlock = repo.wlock() | |
57 try: | |
58 | |
59 # commit a new version of the old changeset, including the update | |
60 # collect all files which might be affected | |
61 files = set(old.files()) | |
62 for u in updates: | |
63 files.update(u.files()) | |
64 # prune files which were reverted by the updates | |
65 def samefile(f): | |
66 if f in head.manifest(): | |
67 a = head.filectx(f) | |
68 if f in base.manifest(): | |
69 b = base.filectx(f) | |
70 return (a.data() == b.data() | |
71 and a.flags() == b.flags() | |
72 and a.renamed() == b.renamed()) | |
73 else: | |
74 return False | |
75 else: | |
76 return f not in base.manifest() | |
77 files = [f for f in files if not samefile(f)] | |
78 # commit version of these files as defined by head | |
79 headmf = head.manifest() | |
80 def filectxfn(repo, ctx, path): | |
81 if path in headmf: | |
82 return head.filectx(path) | |
83 raise IOError() | |
84 if commitopts.get('message') and commitopts.get('logfile'): | |
85 raise util.Abort(_('options --message and --logfile are mutually' | |
86 ' exclusive')) | |
87 if commitopts.get('logfile'): | |
88 message= open(commitopts['logfile']).read() | |
89 elif commitopts.get('message'): | |
90 message = commitopts['message'] | |
91 else: | |
92 message = old.description() | |
93 | |
94 | |
95 | |
96 new = context.memctx(repo, | |
97 parents=newbases, | |
98 text=message, | |
99 files=files, | |
100 filectxfn=filectxfn, | |
101 user=commitopts.get('user') or None, | |
102 date=commitopts.get('date') or None, | |
103 extra=commitopts.get('extra') or None) | |
104 | |
105 if commitopts.get('edit'): | |
106 new._text = cmdutil.commitforceeditor(repo, new, []) | |
107 newid = repo.commitctx(new) | |
108 new = repo[newid] | |
109 | |
110 # update the bookmark | |
111 if bm: | |
112 repo._bookmarks[bm] = newid | |
113 bookmarks.write(repo) | |
114 | |
115 # hide obsolete csets | |
116 repo.changelog.hiddeninit = False | |
117 | |
118 # add evolution metadata | |
119 repo.addobsolete(new.node(), old.node()) | |
120 for u in updates: | |
121 repo.addobsolete(u.node(), old.node()) | |
122 repo.addobsolete(new.node(), u.node()) | |
123 | |
124 finally: | |
125 wlock.release() | |
126 | |
127 return newid | |
128 | |
129 def relocate(repo, rev, dest): | |
130 """rewrite <rev> on dest""" | |
131 try: | |
132 rebase = extensions.find('rebase') | |
133 # dummy state to trick rebase node | |
134 assert repo[rev].p2().rev() == node.nullrev, 'no support yet' | |
135 cmdutil.duplicatecopies(repo, rev, repo[dest].node(), | |
136 repo[rev].p2().node()) | |
137 rebase.rebasenode(repo, rev, dest, {node.nullrev: node.nullrev}) | |
138 nodenew = rebase.concludenode(repo, rev, dest, node.nullid) | |
139 nodesrc = repo.changelog.node(rev) | |
140 repo.addobsolete(nodenew, nodesrc) | |
141 phases.retractboundary(repo, repo[nodesrc].phase(), [nodenew]) | |
142 oldbookmarks = repo.nodebookmarks(nodesrc) | |
143 for book in oldbookmarks: | |
144 repo._bookmarks[book] = nodenew | |
145 if oldbookmarks: | |
146 bookmarks.write(repo) | |
147 except util.Abort: | |
148 # Invalidate the previous setparents | |
149 repo.dirstate.invalidate() | |
150 raise | |
151 | |
152 | |
153 | |
154 ### new command | |
155 ############################# | |
156 cmdtable = {} | |
157 command = cmdutil.command(cmdtable) | |
158 | |
159 @command('^evolve', | |
160 [], | |
161 '') | |
162 def evolve(ui, repo): | |
163 """suggest the next evolution step""" | |
164 obsolete = extensions.find('obsolete') | |
165 next = min(obsolete.unstables(repo)) | |
166 obs = repo[next].parents()[0] | |
167 if not obs.obsolete(): | |
168 obs = next.parents()[1] | |
169 assert obs.obsolete() | |
170 newer = obsolete.newerversion(repo, obs.node()) | |
171 target = newer[-1] | |
172 repo.ui.status('hg relocate --rev %s %s\n' % (repo[next], repo[target])) | |
173 | |
174 shorttemplate = '[{rev}] {desc|firstline}\n' | |
175 | |
176 @command('^gdown', | |
177 [], | |
178 'update to working directory parent an display summary lines') | |
179 def cmdgdown(ui, repo): | |
180 wkctx = repo[None] | |
181 wparents = wkctx.parents() | |
182 if len(wparents) != 1: | |
183 raise util.Abort('merge in progress') | |
184 | |
185 parents = wparents[0].parents() | |
186 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) | |
187 if len(parents) == 1: | |
188 p = parents[0] | |
189 hg.update(repo, p.rev()) | |
190 displayer.show(p) | |
191 return 0 | |
192 else: | |
193 for p in parents: | |
194 displayer.show(p) | |
195 ui.warn(_('multiple parents, explicitly update to one\n')) | |
196 return 1 | |
197 | |
198 @command('^gup', | |
199 [], | |
200 'update to working directory children an display summary lines') | |
201 def cmdup(ui, repo): | |
202 wkctx = repo[None] | |
203 wparents = wkctx.parents() | |
204 if len(wparents) != 1: | |
205 raise util.Abort('merge in progress') | |
206 | |
207 children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] | |
208 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) | |
209 if not children: | |
210 ui.warn(_('No non-obsolete children\n')) | |
211 return 1 | |
212 if len(children) == 1: | |
213 c = children[0] | |
214 hg.update(repo, c.rev()) | |
215 displayer.show(c) | |
216 return 0 | |
217 else: | |
218 for c in children: | |
219 displayer.show(c) | |
220 ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) | |
221 return 1 | |
222 | |
223 | |
224 @command('^kill', | |
225 [ | |
226 ('n', 'new', [], _("New changeset that justify this one to be killed")) | |
227 ], | |
228 '<revs>') | |
229 def kill(ui, repo, *revs, **opts): | |
230 """mark a changeset as obsolete | |
231 | |
232 This update the parent directory to a not-killed parent if the current | |
233 working directory parent are killed. | |
234 | |
235 XXX bookmark support | |
236 XXX handle merge | |
237 XXX check immutable first | |
238 """ | |
239 wlock = repo.wlock() | |
240 try: | |
241 new = opts['new'] | |
242 targetnodes = set(noderange(repo, revs)) | |
243 if not new: | |
244 new = [node.nullid] | |
245 for n in targetnodes: | |
246 if not repo[n].mutable(): | |
247 ui.warn(_("Can't kill immutable changeset %s") % repo[n]) | |
248 else: | |
249 for ne in new: | |
250 repo.addobsolete(ne, n) | |
251 # update to an unkilled parent | |
252 wdp = repo['.'] | |
253 newnode = wdp | |
254 while newnode.obsolete(): | |
255 newnode = newnode.parents()[0] | |
256 if newnode.node() != wdp.node(): | |
257 commands.update(ui, repo, newnode.rev()) | |
258 ui.status(_('working directory now at %s\n') % newnode) | |
259 | |
260 finally: | |
261 wlock.release() | |
262 | |
263 @command('^amend', | |
264 [('A', 'addremove', None, | |
265 _('mark new/missing files as added/removed before committing')), | |
266 ('n', 'note', '', | |
267 _('use text as commit message for this update')), | |
268 ('c', 'change', '', | |
269 _('specifies the changeset to amend'), _('REV')), | |
270 ('b', 'branch', '', | |
271 _('specifies a branch for the new.'), _('REV')), | |
272 ('e', 'edit', False, | |
273 _('edit commit message.'), _('')), | |
274 ] + walkopts + commitopts + commitopts2, | |
275 _('[OPTION]... [FILE]...')) | |
276 | |
277 def amend(ui, repo, *pats, **opts): | |
278 """combine a changeset with updates and replace it with a new one | |
279 | |
280 Commits a new changeset incorporating both the changes to the given files | |
281 and all the changes from the current parent changeset into the repository. | |
282 | |
283 See :hg:`commit` for details about committing changes. | |
284 | |
285 If you don't specify -m, the parent's message will be reused. | |
286 | |
287 If you specify --change, amend additionally considers all changesets between | |
288 the indicated changeset and the working copy parent as updates to be subsumed. | |
289 This allows you to commit updates manually first. As a special shorthand you | |
290 can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest | |
291 commit as an update of its parent. | |
292 | |
293 Behind the scenes, Mercurial first commits the update as a regular child | |
294 of the current parent. Then it creates a new commit on the parent's parents | |
295 with the updated contents. Then it changes the working copy parent to this | |
296 new combined changeset. Finally, the old changeset and its update are hidden | |
297 from :hg:`log` (unless you use --hidden with log). | |
298 | |
299 Returns 0 on success, 1 if nothing changed. | |
300 """ | |
301 | |
302 # determine updates to subsume | |
303 change = opts.get('change') | |
304 if change == '.': | |
305 change = 'p1(p1())' | |
306 old = scmutil.revsingle(repo, change) | |
307 branch = opts.get('branch') | |
308 if branch: | |
309 opts.setdefault('extra', {})['branch'] = branch | |
310 else: | |
311 if old.branch() != 'default': | |
312 opts.setdefault('extra', {})['branch'] = old.branch() | |
313 | |
314 lock = repo.lock() | |
315 try: | |
316 wlock = repo.wlock() | |
317 try: | |
318 if not old.phase(): | |
319 raise util.Abort(_("can not rewrite immutable changeset %s") % old) | |
320 | |
321 # commit current changes as update | |
322 # code copied from commands.commit to avoid noisy messages | |
323 ciopts = dict(opts) | |
324 ciopts.pop('message', None) | |
325 ciopts.pop('logfile', None) | |
326 ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) | |
327 e = cmdutil.commiteditor | |
328 def commitfunc(ui, repo, message, match, opts): | |
329 return repo.commit(message, opts.get('user'), opts.get('date'), match, | |
330 editor=e) | |
331 cmdutil.commit(ui, repo, commitfunc, pats, ciopts) | |
332 | |
333 # find all changesets to be considered updates | |
334 cl = repo.changelog | |
335 head = repo['.'] | |
336 updatenodes = set(cl.nodesbetween(roots=[old.node()], | |
337 heads=[head.node()])[0]) | |
338 updatenodes.remove(old.node()) | |
339 if not updatenodes and not (opts.get('message') or opts.get('logfile') or opts.get('edit')): | |
340 raise error.Abort(_('no updates found')) | |
341 updates = [repo[n] for n in updatenodes] | |
342 | |
343 # perform amend | |
344 if opts.get('edit'): | |
345 opts['force_editor'] = True | |
346 newid = rewrite(repo, old, updates, head, | |
347 [old.p1().node(), old.p2().node()], opts) | |
348 | |
349 # reroute the working copy parent to the new changeset | |
350 phases.retractboundary(repo, old.phase(), [newid]) | |
351 repo.dirstate.setparents(newid, node.nullid) | |
352 finally: | |
353 wlock.release() | |
354 finally: | |
355 lock.release() | |
356 | |
357 def commitwrapper(orig, ui, repo, *arg, **kwargs): | |
358 obsoleted = kwargs.get('obsolete', []) | |
359 if obsoleted: | |
360 obsoleted = repo.set('%lr', obsoleted) | |
361 result = orig(ui, repo, *arg, **kwargs) | |
362 if not result: # commit successed | |
363 new = repo['-1'] | |
364 for old in obsoleted: | |
365 repo.addobsolete(new.node(), old.node()) | |
366 return result | |
367 | |
368 def graftwrapper(orig, ui, repo, *revs, **kwargs): | |
369 lock = repo.lock() | |
370 try: | |
371 if kwargs.get('old_obsolete'): | |
372 obsoleted = kwargs.setdefault('obsolete', []) | |
373 if kwargs['continue']: | |
374 obsoleted.extend(repo.opener.read('graftstate').splitlines()) | |
375 else: | |
376 obsoleted.extend(revs) | |
377 return commitwrapper(orig, ui, repo,*revs, **kwargs) | |
378 finally: | |
379 lock.release() | |
380 | |
381 def extsetup(ui): | |
382 entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) | |
383 entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision"))) | |
384 entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) | |
385 entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision"))) | |
386 entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source"))) |