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