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")))