##// END OF EJS Templates
transplant: mark some undocumented options deprecated
Matt Mackall -
r25828:5ae4b128 default
parent child Browse files
Show More
@@ -1,708 +1,710 b''
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to transplant changesets from another branch
8 '''command to transplant changesets from another branch
9
9
10 This extension allows you to transplant changes to another parent revision,
10 This extension allows you to transplant changes to another parent revision,
11 possibly in another repository. The transplant is done using 'diff' patches.
11 possibly in another repository. The transplant is done using 'diff' patches.
12
12
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
14 map from a changeset hash to its hash in the source repository.
14 map from a changeset hash to its hash in the source repository.
15 '''
15 '''
16
16
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 import os, tempfile
18 import os, tempfile
19 from mercurial.node import short
19 from mercurial.node import short
20 from mercurial import bundlerepo, hg, merge, match
20 from mercurial import bundlerepo, hg, merge, match
21 from mercurial import patch, revlog, scmutil, util, error, cmdutil
21 from mercurial import patch, revlog, scmutil, util, error, cmdutil
22 from mercurial import revset, templatekw, exchange
22 from mercurial import revset, templatekw, exchange
23
23
24 class TransplantError(error.Abort):
24 class TransplantError(error.Abort):
25 pass
25 pass
26
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29 # Note for extension authors: ONLY specify testedwith = 'internal' for
29 # Note for extension authors: ONLY specify testedwith = 'internal' for
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 # be specifying the version(s) of Mercurial they are tested with, or
31 # be specifying the version(s) of Mercurial they are tested with, or
32 # leave the attribute unspecified.
32 # leave the attribute unspecified.
33 testedwith = 'internal'
33 testedwith = 'internal'
34
34
35 class transplantentry(object):
35 class transplantentry(object):
36 def __init__(self, lnode, rnode):
36 def __init__(self, lnode, rnode):
37 self.lnode = lnode
37 self.lnode = lnode
38 self.rnode = rnode
38 self.rnode = rnode
39
39
40 class transplants(object):
40 class transplants(object):
41 def __init__(self, path=None, transplantfile=None, opener=None):
41 def __init__(self, path=None, transplantfile=None, opener=None):
42 self.path = path
42 self.path = path
43 self.transplantfile = transplantfile
43 self.transplantfile = transplantfile
44 self.opener = opener
44 self.opener = opener
45
45
46 if not opener:
46 if not opener:
47 self.opener = scmutil.opener(self.path)
47 self.opener = scmutil.opener(self.path)
48 self.transplants = {}
48 self.transplants = {}
49 self.dirty = False
49 self.dirty = False
50 self.read()
50 self.read()
51
51
52 def read(self):
52 def read(self):
53 abspath = os.path.join(self.path, self.transplantfile)
53 abspath = os.path.join(self.path, self.transplantfile)
54 if self.transplantfile and os.path.exists(abspath):
54 if self.transplantfile and os.path.exists(abspath):
55 for line in self.opener.read(self.transplantfile).splitlines():
55 for line in self.opener.read(self.transplantfile).splitlines():
56 lnode, rnode = map(revlog.bin, line.split(':'))
56 lnode, rnode = map(revlog.bin, line.split(':'))
57 list = self.transplants.setdefault(rnode, [])
57 list = self.transplants.setdefault(rnode, [])
58 list.append(transplantentry(lnode, rnode))
58 list.append(transplantentry(lnode, rnode))
59
59
60 def write(self):
60 def write(self):
61 if self.dirty and self.transplantfile:
61 if self.dirty and self.transplantfile:
62 if not os.path.isdir(self.path):
62 if not os.path.isdir(self.path):
63 os.mkdir(self.path)
63 os.mkdir(self.path)
64 fp = self.opener(self.transplantfile, 'w')
64 fp = self.opener(self.transplantfile, 'w')
65 for list in self.transplants.itervalues():
65 for list in self.transplants.itervalues():
66 for t in list:
66 for t in list:
67 l, r = map(revlog.hex, (t.lnode, t.rnode))
67 l, r = map(revlog.hex, (t.lnode, t.rnode))
68 fp.write(l + ':' + r + '\n')
68 fp.write(l + ':' + r + '\n')
69 fp.close()
69 fp.close()
70 self.dirty = False
70 self.dirty = False
71
71
72 def get(self, rnode):
72 def get(self, rnode):
73 return self.transplants.get(rnode) or []
73 return self.transplants.get(rnode) or []
74
74
75 def set(self, lnode, rnode):
75 def set(self, lnode, rnode):
76 list = self.transplants.setdefault(rnode, [])
76 list = self.transplants.setdefault(rnode, [])
77 list.append(transplantentry(lnode, rnode))
77 list.append(transplantentry(lnode, rnode))
78 self.dirty = True
78 self.dirty = True
79
79
80 def remove(self, transplant):
80 def remove(self, transplant):
81 list = self.transplants.get(transplant.rnode)
81 list = self.transplants.get(transplant.rnode)
82 if list:
82 if list:
83 del list[list.index(transplant)]
83 del list[list.index(transplant)]
84 self.dirty = True
84 self.dirty = True
85
85
86 class transplanter(object):
86 class transplanter(object):
87 def __init__(self, ui, repo, opts):
87 def __init__(self, ui, repo, opts):
88 self.ui = ui
88 self.ui = ui
89 self.path = repo.join('transplant')
89 self.path = repo.join('transplant')
90 self.opener = scmutil.opener(self.path)
90 self.opener = scmutil.opener(self.path)
91 self.transplants = transplants(self.path, 'transplants',
91 self.transplants = transplants(self.path, 'transplants',
92 opener=self.opener)
92 opener=self.opener)
93 def getcommiteditor():
93 def getcommiteditor():
94 editform = cmdutil.mergeeditform(repo[None], 'transplant')
94 editform = cmdutil.mergeeditform(repo[None], 'transplant')
95 return cmdutil.getcommiteditor(editform=editform, **opts)
95 return cmdutil.getcommiteditor(editform=editform, **opts)
96 self.getcommiteditor = getcommiteditor
96 self.getcommiteditor = getcommiteditor
97
97
98 def applied(self, repo, node, parent):
98 def applied(self, repo, node, parent):
99 '''returns True if a node is already an ancestor of parent
99 '''returns True if a node is already an ancestor of parent
100 or is parent or has already been transplanted'''
100 or is parent or has already been transplanted'''
101 if hasnode(repo, parent):
101 if hasnode(repo, parent):
102 parentrev = repo.changelog.rev(parent)
102 parentrev = repo.changelog.rev(parent)
103 if hasnode(repo, node):
103 if hasnode(repo, node):
104 rev = repo.changelog.rev(node)
104 rev = repo.changelog.rev(node)
105 reachable = repo.changelog.ancestors([parentrev], rev,
105 reachable = repo.changelog.ancestors([parentrev], rev,
106 inclusive=True)
106 inclusive=True)
107 if rev in reachable:
107 if rev in reachable:
108 return True
108 return True
109 for t in self.transplants.get(node):
109 for t in self.transplants.get(node):
110 # it might have been stripped
110 # it might have been stripped
111 if not hasnode(repo, t.lnode):
111 if not hasnode(repo, t.lnode):
112 self.transplants.remove(t)
112 self.transplants.remove(t)
113 return False
113 return False
114 lnoderev = repo.changelog.rev(t.lnode)
114 lnoderev = repo.changelog.rev(t.lnode)
115 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
115 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
116 inclusive=True):
116 inclusive=True):
117 return True
117 return True
118 return False
118 return False
119
119
120 def apply(self, repo, source, revmap, merges, opts={}):
120 def apply(self, repo, source, revmap, merges, opts={}):
121 '''apply the revisions in revmap one by one in revision order'''
121 '''apply the revisions in revmap one by one in revision order'''
122 revs = sorted(revmap)
122 revs = sorted(revmap)
123 p1, p2 = repo.dirstate.parents()
123 p1, p2 = repo.dirstate.parents()
124 pulls = []
124 pulls = []
125 diffopts = patch.difffeatureopts(self.ui, opts)
125 diffopts = patch.difffeatureopts(self.ui, opts)
126 diffopts.git = True
126 diffopts.git = True
127
127
128 lock = wlock = tr = None
128 lock = wlock = tr = None
129 try:
129 try:
130 wlock = repo.wlock()
130 wlock = repo.wlock()
131 lock = repo.lock()
131 lock = repo.lock()
132 tr = repo.transaction('transplant')
132 tr = repo.transaction('transplant')
133 for rev in revs:
133 for rev in revs:
134 node = revmap[rev]
134 node = revmap[rev]
135 revstr = '%s:%s' % (rev, short(node))
135 revstr = '%s:%s' % (rev, short(node))
136
136
137 if self.applied(repo, node, p1):
137 if self.applied(repo, node, p1):
138 self.ui.warn(_('skipping already applied revision %s\n') %
138 self.ui.warn(_('skipping already applied revision %s\n') %
139 revstr)
139 revstr)
140 continue
140 continue
141
141
142 parents = source.changelog.parents(node)
142 parents = source.changelog.parents(node)
143 if not (opts.get('filter') or opts.get('log')):
143 if not (opts.get('filter') or opts.get('log')):
144 # If the changeset parent is the same as the
144 # If the changeset parent is the same as the
145 # wdir's parent, just pull it.
145 # wdir's parent, just pull it.
146 if parents[0] == p1:
146 if parents[0] == p1:
147 pulls.append(node)
147 pulls.append(node)
148 p1 = node
148 p1 = node
149 continue
149 continue
150 if pulls:
150 if pulls:
151 if source != repo:
151 if source != repo:
152 exchange.pull(repo, source.peer(), heads=pulls)
152 exchange.pull(repo, source.peer(), heads=pulls)
153 merge.update(repo, pulls[-1], False, False, None)
153 merge.update(repo, pulls[-1], False, False, None)
154 p1, p2 = repo.dirstate.parents()
154 p1, p2 = repo.dirstate.parents()
155 pulls = []
155 pulls = []
156
156
157 domerge = False
157 domerge = False
158 if node in merges:
158 if node in merges:
159 # pulling all the merge revs at once would mean we
159 # pulling all the merge revs at once would mean we
160 # couldn't transplant after the latest even if
160 # couldn't transplant after the latest even if
161 # transplants before them fail.
161 # transplants before them fail.
162 domerge = True
162 domerge = True
163 if not hasnode(repo, node):
163 if not hasnode(repo, node):
164 exchange.pull(repo, source.peer(), heads=[node])
164 exchange.pull(repo, source.peer(), heads=[node])
165
165
166 skipmerge = False
166 skipmerge = False
167 if parents[1] != revlog.nullid:
167 if parents[1] != revlog.nullid:
168 if not opts.get('parent'):
168 if not opts.get('parent'):
169 self.ui.note(_('skipping merge changeset %s:%s\n')
169 self.ui.note(_('skipping merge changeset %s:%s\n')
170 % (rev, short(node)))
170 % (rev, short(node)))
171 skipmerge = True
171 skipmerge = True
172 else:
172 else:
173 parent = source.lookup(opts['parent'])
173 parent = source.lookup(opts['parent'])
174 if parent not in parents:
174 if parent not in parents:
175 raise util.Abort(_('%s is not a parent of %s') %
175 raise util.Abort(_('%s is not a parent of %s') %
176 (short(parent), short(node)))
176 (short(parent), short(node)))
177 else:
177 else:
178 parent = parents[0]
178 parent = parents[0]
179
179
180 if skipmerge:
180 if skipmerge:
181 patchfile = None
181 patchfile = None
182 else:
182 else:
183 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
183 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
184 fp = os.fdopen(fd, 'w')
184 fp = os.fdopen(fd, 'w')
185 gen = patch.diff(source, parent, node, opts=diffopts)
185 gen = patch.diff(source, parent, node, opts=diffopts)
186 for chunk in gen:
186 for chunk in gen:
187 fp.write(chunk)
187 fp.write(chunk)
188 fp.close()
188 fp.close()
189
189
190 del revmap[rev]
190 del revmap[rev]
191 if patchfile or domerge:
191 if patchfile or domerge:
192 try:
192 try:
193 try:
193 try:
194 n = self.applyone(repo, node,
194 n = self.applyone(repo, node,
195 source.changelog.read(node),
195 source.changelog.read(node),
196 patchfile, merge=domerge,
196 patchfile, merge=domerge,
197 log=opts.get('log'),
197 log=opts.get('log'),
198 filter=opts.get('filter'))
198 filter=opts.get('filter'))
199 except TransplantError:
199 except TransplantError:
200 # Do not rollback, it is up to the user to
200 # Do not rollback, it is up to the user to
201 # fix the merge or cancel everything
201 # fix the merge or cancel everything
202 tr.close()
202 tr.close()
203 raise
203 raise
204 if n and domerge:
204 if n and domerge:
205 self.ui.status(_('%s merged at %s\n') % (revstr,
205 self.ui.status(_('%s merged at %s\n') % (revstr,
206 short(n)))
206 short(n)))
207 elif n:
207 elif n:
208 self.ui.status(_('%s transplanted to %s\n')
208 self.ui.status(_('%s transplanted to %s\n')
209 % (short(node),
209 % (short(node),
210 short(n)))
210 short(n)))
211 finally:
211 finally:
212 if patchfile:
212 if patchfile:
213 os.unlink(patchfile)
213 os.unlink(patchfile)
214 tr.close()
214 tr.close()
215 if pulls:
215 if pulls:
216 exchange.pull(repo, source.peer(), heads=pulls)
216 exchange.pull(repo, source.peer(), heads=pulls)
217 merge.update(repo, pulls[-1], False, False, None)
217 merge.update(repo, pulls[-1], False, False, None)
218 finally:
218 finally:
219 self.saveseries(revmap, merges)
219 self.saveseries(revmap, merges)
220 self.transplants.write()
220 self.transplants.write()
221 if tr:
221 if tr:
222 tr.release()
222 tr.release()
223 lock.release()
223 lock.release()
224 wlock.release()
224 wlock.release()
225
225
226 def filter(self, filter, node, changelog, patchfile):
226 def filter(self, filter, node, changelog, patchfile):
227 '''arbitrarily rewrite changeset before applying it'''
227 '''arbitrarily rewrite changeset before applying it'''
228
228
229 self.ui.status(_('filtering %s\n') % patchfile)
229 self.ui.status(_('filtering %s\n') % patchfile)
230 user, date, msg = (changelog[1], changelog[2], changelog[4])
230 user, date, msg = (changelog[1], changelog[2], changelog[4])
231 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
231 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
232 fp = os.fdopen(fd, 'w')
232 fp = os.fdopen(fd, 'w')
233 fp.write("# HG changeset patch\n")
233 fp.write("# HG changeset patch\n")
234 fp.write("# User %s\n" % user)
234 fp.write("# User %s\n" % user)
235 fp.write("# Date %d %d\n" % date)
235 fp.write("# Date %d %d\n" % date)
236 fp.write(msg + '\n')
236 fp.write(msg + '\n')
237 fp.close()
237 fp.close()
238
238
239 try:
239 try:
240 self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile),
240 self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile),
241 util.shellquote(patchfile)),
241 util.shellquote(patchfile)),
242 environ={'HGUSER': changelog[1],
242 environ={'HGUSER': changelog[1],
243 'HGREVISION': revlog.hex(node),
243 'HGREVISION': revlog.hex(node),
244 },
244 },
245 onerr=util.Abort, errprefix=_('filter failed'))
245 onerr=util.Abort, errprefix=_('filter failed'))
246 user, date, msg = self.parselog(file(headerfile))[1:4]
246 user, date, msg = self.parselog(file(headerfile))[1:4]
247 finally:
247 finally:
248 os.unlink(headerfile)
248 os.unlink(headerfile)
249
249
250 return (user, date, msg)
250 return (user, date, msg)
251
251
252 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
252 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
253 filter=None):
253 filter=None):
254 '''apply the patch in patchfile to the repository as a transplant'''
254 '''apply the patch in patchfile to the repository as a transplant'''
255 (manifest, user, (time, timezone), files, message) = cl[:5]
255 (manifest, user, (time, timezone), files, message) = cl[:5]
256 date = "%d %d" % (time, timezone)
256 date = "%d %d" % (time, timezone)
257 extra = {'transplant_source': node}
257 extra = {'transplant_source': node}
258 if filter:
258 if filter:
259 (user, date, message) = self.filter(filter, node, cl, patchfile)
259 (user, date, message) = self.filter(filter, node, cl, patchfile)
260
260
261 if log:
261 if log:
262 # we don't translate messages inserted into commits
262 # we don't translate messages inserted into commits
263 message += '\n(transplanted from %s)' % revlog.hex(node)
263 message += '\n(transplanted from %s)' % revlog.hex(node)
264
264
265 self.ui.status(_('applying %s\n') % short(node))
265 self.ui.status(_('applying %s\n') % short(node))
266 self.ui.note('%s %s\n%s\n' % (user, date, message))
266 self.ui.note('%s %s\n%s\n' % (user, date, message))
267
267
268 if not patchfile and not merge:
268 if not patchfile and not merge:
269 raise util.Abort(_('can only omit patchfile if merging'))
269 raise util.Abort(_('can only omit patchfile if merging'))
270 if patchfile:
270 if patchfile:
271 try:
271 try:
272 files = set()
272 files = set()
273 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
273 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
274 files = list(files)
274 files = list(files)
275 except Exception as inst:
275 except Exception as inst:
276 seriespath = os.path.join(self.path, 'series')
276 seriespath = os.path.join(self.path, 'series')
277 if os.path.exists(seriespath):
277 if os.path.exists(seriespath):
278 os.unlink(seriespath)
278 os.unlink(seriespath)
279 p1 = repo.dirstate.p1()
279 p1 = repo.dirstate.p1()
280 p2 = node
280 p2 = node
281 self.log(user, date, message, p1, p2, merge=merge)
281 self.log(user, date, message, p1, p2, merge=merge)
282 self.ui.write(str(inst) + '\n')
282 self.ui.write(str(inst) + '\n')
283 raise TransplantError(_('fix up the merge and run '
283 raise TransplantError(_('fix up the merge and run '
284 'hg transplant --continue'))
284 'hg transplant --continue'))
285 else:
285 else:
286 files = None
286 files = None
287 if merge:
287 if merge:
288 p1, p2 = repo.dirstate.parents()
288 p1, p2 = repo.dirstate.parents()
289 repo.setparents(p1, node)
289 repo.setparents(p1, node)
290 m = match.always(repo.root, '')
290 m = match.always(repo.root, '')
291 else:
291 else:
292 m = match.exact(repo.root, '', files)
292 m = match.exact(repo.root, '', files)
293
293
294 n = repo.commit(message, user, date, extra=extra, match=m,
294 n = repo.commit(message, user, date, extra=extra, match=m,
295 editor=self.getcommiteditor())
295 editor=self.getcommiteditor())
296 if not n:
296 if not n:
297 self.ui.warn(_('skipping emptied changeset %s\n') % short(node))
297 self.ui.warn(_('skipping emptied changeset %s\n') % short(node))
298 return None
298 return None
299 if not merge:
299 if not merge:
300 self.transplants.set(n, node)
300 self.transplants.set(n, node)
301
301
302 return n
302 return n
303
303
304 def resume(self, repo, source, opts):
304 def resume(self, repo, source, opts):
305 '''recover last transaction and apply remaining changesets'''
305 '''recover last transaction and apply remaining changesets'''
306 if os.path.exists(os.path.join(self.path, 'journal')):
306 if os.path.exists(os.path.join(self.path, 'journal')):
307 n, node = self.recover(repo, source, opts)
307 n, node = self.recover(repo, source, opts)
308 if n:
308 if n:
309 self.ui.status(_('%s transplanted as %s\n') % (short(node),
309 self.ui.status(_('%s transplanted as %s\n') % (short(node),
310 short(n)))
310 short(n)))
311 else:
311 else:
312 self.ui.status(_('%s skipped due to empty diff\n')
312 self.ui.status(_('%s skipped due to empty diff\n')
313 % (short(node),))
313 % (short(node),))
314 seriespath = os.path.join(self.path, 'series')
314 seriespath = os.path.join(self.path, 'series')
315 if not os.path.exists(seriespath):
315 if not os.path.exists(seriespath):
316 self.transplants.write()
316 self.transplants.write()
317 return
317 return
318 nodes, merges = self.readseries()
318 nodes, merges = self.readseries()
319 revmap = {}
319 revmap = {}
320 for n in nodes:
320 for n in nodes:
321 revmap[source.changelog.rev(n)] = n
321 revmap[source.changelog.rev(n)] = n
322 os.unlink(seriespath)
322 os.unlink(seriespath)
323
323
324 self.apply(repo, source, revmap, merges, opts)
324 self.apply(repo, source, revmap, merges, opts)
325
325
326 def recover(self, repo, source, opts):
326 def recover(self, repo, source, opts):
327 '''commit working directory using journal metadata'''
327 '''commit working directory using journal metadata'''
328 node, user, date, message, parents = self.readlog()
328 node, user, date, message, parents = self.readlog()
329 merge = False
329 merge = False
330
330
331 if not user or not date or not message or not parents[0]:
331 if not user or not date or not message or not parents[0]:
332 raise util.Abort(_('transplant log file is corrupt'))
332 raise util.Abort(_('transplant log file is corrupt'))
333
333
334 parent = parents[0]
334 parent = parents[0]
335 if len(parents) > 1:
335 if len(parents) > 1:
336 if opts.get('parent'):
336 if opts.get('parent'):
337 parent = source.lookup(opts['parent'])
337 parent = source.lookup(opts['parent'])
338 if parent not in parents:
338 if parent not in parents:
339 raise util.Abort(_('%s is not a parent of %s') %
339 raise util.Abort(_('%s is not a parent of %s') %
340 (short(parent), short(node)))
340 (short(parent), short(node)))
341 else:
341 else:
342 merge = True
342 merge = True
343
343
344 extra = {'transplant_source': node}
344 extra = {'transplant_source': node}
345 wlock = repo.wlock()
345 wlock = repo.wlock()
346 try:
346 try:
347 p1, p2 = repo.dirstate.parents()
347 p1, p2 = repo.dirstate.parents()
348 if p1 != parent:
348 if p1 != parent:
349 raise util.Abort(_('working directory not at transplant '
349 raise util.Abort(_('working directory not at transplant '
350 'parent %s') % revlog.hex(parent))
350 'parent %s') % revlog.hex(parent))
351 if merge:
351 if merge:
352 repo.setparents(p1, parents[1])
352 repo.setparents(p1, parents[1])
353 modified, added, removed, deleted = repo.status()[:4]
353 modified, added, removed, deleted = repo.status()[:4]
354 if merge or modified or added or removed or deleted:
354 if merge or modified or added or removed or deleted:
355 n = repo.commit(message, user, date, extra=extra,
355 n = repo.commit(message, user, date, extra=extra,
356 editor=self.getcommiteditor())
356 editor=self.getcommiteditor())
357 if not n:
357 if not n:
358 raise util.Abort(_('commit failed'))
358 raise util.Abort(_('commit failed'))
359 if not merge:
359 if not merge:
360 self.transplants.set(n, node)
360 self.transplants.set(n, node)
361 else:
361 else:
362 n = None
362 n = None
363 self.unlog()
363 self.unlog()
364
364
365 return n, node
365 return n, node
366 finally:
366 finally:
367 wlock.release()
367 wlock.release()
368
368
369 def readseries(self):
369 def readseries(self):
370 nodes = []
370 nodes = []
371 merges = []
371 merges = []
372 cur = nodes
372 cur = nodes
373 for line in self.opener.read('series').splitlines():
373 for line in self.opener.read('series').splitlines():
374 if line.startswith('# Merges'):
374 if line.startswith('# Merges'):
375 cur = merges
375 cur = merges
376 continue
376 continue
377 cur.append(revlog.bin(line))
377 cur.append(revlog.bin(line))
378
378
379 return (nodes, merges)
379 return (nodes, merges)
380
380
381 def saveseries(self, revmap, merges):
381 def saveseries(self, revmap, merges):
382 if not revmap:
382 if not revmap:
383 return
383 return
384
384
385 if not os.path.isdir(self.path):
385 if not os.path.isdir(self.path):
386 os.mkdir(self.path)
386 os.mkdir(self.path)
387 series = self.opener('series', 'w')
387 series = self.opener('series', 'w')
388 for rev in sorted(revmap):
388 for rev in sorted(revmap):
389 series.write(revlog.hex(revmap[rev]) + '\n')
389 series.write(revlog.hex(revmap[rev]) + '\n')
390 if merges:
390 if merges:
391 series.write('# Merges\n')
391 series.write('# Merges\n')
392 for m in merges:
392 for m in merges:
393 series.write(revlog.hex(m) + '\n')
393 series.write(revlog.hex(m) + '\n')
394 series.close()
394 series.close()
395
395
396 def parselog(self, fp):
396 def parselog(self, fp):
397 parents = []
397 parents = []
398 message = []
398 message = []
399 node = revlog.nullid
399 node = revlog.nullid
400 inmsg = False
400 inmsg = False
401 user = None
401 user = None
402 date = None
402 date = None
403 for line in fp.read().splitlines():
403 for line in fp.read().splitlines():
404 if inmsg:
404 if inmsg:
405 message.append(line)
405 message.append(line)
406 elif line.startswith('# User '):
406 elif line.startswith('# User '):
407 user = line[7:]
407 user = line[7:]
408 elif line.startswith('# Date '):
408 elif line.startswith('# Date '):
409 date = line[7:]
409 date = line[7:]
410 elif line.startswith('# Node ID '):
410 elif line.startswith('# Node ID '):
411 node = revlog.bin(line[10:])
411 node = revlog.bin(line[10:])
412 elif line.startswith('# Parent '):
412 elif line.startswith('# Parent '):
413 parents.append(revlog.bin(line[9:]))
413 parents.append(revlog.bin(line[9:]))
414 elif not line.startswith('# '):
414 elif not line.startswith('# '):
415 inmsg = True
415 inmsg = True
416 message.append(line)
416 message.append(line)
417 if None in (user, date):
417 if None in (user, date):
418 raise util.Abort(_("filter corrupted changeset (no user or date)"))
418 raise util.Abort(_("filter corrupted changeset (no user or date)"))
419 return (node, user, date, '\n'.join(message), parents)
419 return (node, user, date, '\n'.join(message), parents)
420
420
421 def log(self, user, date, message, p1, p2, merge=False):
421 def log(self, user, date, message, p1, p2, merge=False):
422 '''journal changelog metadata for later recover'''
422 '''journal changelog metadata for later recover'''
423
423
424 if not os.path.isdir(self.path):
424 if not os.path.isdir(self.path):
425 os.mkdir(self.path)
425 os.mkdir(self.path)
426 fp = self.opener('journal', 'w')
426 fp = self.opener('journal', 'w')
427 fp.write('# User %s\n' % user)
427 fp.write('# User %s\n' % user)
428 fp.write('# Date %s\n' % date)
428 fp.write('# Date %s\n' % date)
429 fp.write('# Node ID %s\n' % revlog.hex(p2))
429 fp.write('# Node ID %s\n' % revlog.hex(p2))
430 fp.write('# Parent ' + revlog.hex(p1) + '\n')
430 fp.write('# Parent ' + revlog.hex(p1) + '\n')
431 if merge:
431 if merge:
432 fp.write('# Parent ' + revlog.hex(p2) + '\n')
432 fp.write('# Parent ' + revlog.hex(p2) + '\n')
433 fp.write(message.rstrip() + '\n')
433 fp.write(message.rstrip() + '\n')
434 fp.close()
434 fp.close()
435
435
436 def readlog(self):
436 def readlog(self):
437 return self.parselog(self.opener('journal'))
437 return self.parselog(self.opener('journal'))
438
438
439 def unlog(self):
439 def unlog(self):
440 '''remove changelog journal'''
440 '''remove changelog journal'''
441 absdst = os.path.join(self.path, 'journal')
441 absdst = os.path.join(self.path, 'journal')
442 if os.path.exists(absdst):
442 if os.path.exists(absdst):
443 os.unlink(absdst)
443 os.unlink(absdst)
444
444
445 def transplantfilter(self, repo, source, root):
445 def transplantfilter(self, repo, source, root):
446 def matchfn(node):
446 def matchfn(node):
447 if self.applied(repo, node, root):
447 if self.applied(repo, node, root):
448 return False
448 return False
449 if source.changelog.parents(node)[1] != revlog.nullid:
449 if source.changelog.parents(node)[1] != revlog.nullid:
450 return False
450 return False
451 extra = source.changelog.read(node)[5]
451 extra = source.changelog.read(node)[5]
452 cnode = extra.get('transplant_source')
452 cnode = extra.get('transplant_source')
453 if cnode and self.applied(repo, cnode, root):
453 if cnode and self.applied(repo, cnode, root):
454 return False
454 return False
455 return True
455 return True
456
456
457 return matchfn
457 return matchfn
458
458
459 def hasnode(repo, node):
459 def hasnode(repo, node):
460 try:
460 try:
461 return repo.changelog.rev(node) is not None
461 return repo.changelog.rev(node) is not None
462 except error.RevlogError:
462 except error.RevlogError:
463 return False
463 return False
464
464
465 def browserevs(ui, repo, nodes, opts):
465 def browserevs(ui, repo, nodes, opts):
466 '''interactively transplant changesets'''
466 '''interactively transplant changesets'''
467 displayer = cmdutil.show_changeset(ui, repo, opts)
467 displayer = cmdutil.show_changeset(ui, repo, opts)
468 transplants = []
468 transplants = []
469 merges = []
469 merges = []
470 prompt = _('apply changeset? [ynmpcq?]:'
470 prompt = _('apply changeset? [ynmpcq?]:'
471 '$$ &yes, transplant this changeset'
471 '$$ &yes, transplant this changeset'
472 '$$ &no, skip this changeset'
472 '$$ &no, skip this changeset'
473 '$$ &merge at this changeset'
473 '$$ &merge at this changeset'
474 '$$ show &patch'
474 '$$ show &patch'
475 '$$ &commit selected changesets'
475 '$$ &commit selected changesets'
476 '$$ &quit and cancel transplant'
476 '$$ &quit and cancel transplant'
477 '$$ &? (show this help)')
477 '$$ &? (show this help)')
478 for node in nodes:
478 for node in nodes:
479 displayer.show(repo[node])
479 displayer.show(repo[node])
480 action = None
480 action = None
481 while not action:
481 while not action:
482 action = 'ynmpcq?'[ui.promptchoice(prompt)]
482 action = 'ynmpcq?'[ui.promptchoice(prompt)]
483 if action == '?':
483 if action == '?':
484 for c, t in ui.extractchoices(prompt)[1]:
484 for c, t in ui.extractchoices(prompt)[1]:
485 ui.write('%s: %s\n' % (c, t))
485 ui.write('%s: %s\n' % (c, t))
486 action = None
486 action = None
487 elif action == 'p':
487 elif action == 'p':
488 parent = repo.changelog.parents(node)[0]
488 parent = repo.changelog.parents(node)[0]
489 for chunk in patch.diff(repo, parent, node):
489 for chunk in patch.diff(repo, parent, node):
490 ui.write(chunk)
490 ui.write(chunk)
491 action = None
491 action = None
492 if action == 'y':
492 if action == 'y':
493 transplants.append(node)
493 transplants.append(node)
494 elif action == 'm':
494 elif action == 'm':
495 merges.append(node)
495 merges.append(node)
496 elif action == 'c':
496 elif action == 'c':
497 break
497 break
498 elif action == 'q':
498 elif action == 'q':
499 transplants = ()
499 transplants = ()
500 merges = ()
500 merges = ()
501 break
501 break
502 displayer.close()
502 displayer.close()
503 return (transplants, merges)
503 return (transplants, merges)
504
504
505 @command('transplant',
505 @command('transplant',
506 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
506 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
507 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
507 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
508 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
508 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
509 ('p', 'prune', [], _('skip over REV'), _('REV')),
509 ('p', 'prune', [], _('skip over REV'), _('REV')),
510 ('m', 'merge', [], _('merge at REV'), _('REV')),
510 ('m', 'merge', [], _('merge at REV'), _('REV')),
511 ('', 'parent', '',
511 ('', 'parent', '',
512 _('parent to choose when transplanting merge'), _('REV')),
512 _('parent to choose when transplanting merge'), _('REV')),
513 ('e', 'edit', False, _('invoke editor on commit messages')),
513 ('e', 'edit', False, _('invoke editor on commit messages')),
514 ('', 'log', None, _('append transplant info to log message')),
514 ('', 'log', None, _('append transplant info to log message')),
515 ('c', 'continue', None, _('continue last transplant session '
515 ('c', 'continue', None, _('continue last transplant session '
516 'after fixing conflicts')),
516 'after fixing conflicts')),
517 ('', 'filter', '',
517 ('', 'filter', '',
518 _('filter changesets through command'), _('CMD'))],
518 _('filter changesets through command'), _('CMD'))],
519 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
519 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
520 '[-m REV] [REV]...'))
520 '[-m REV] [REV]...'))
521 def transplant(ui, repo, *revs, **opts):
521 def transplant(ui, repo, *revs, **opts):
522 '''transplant changesets from another branch
522 '''transplant changesets from another branch
523
523
524 Selected changesets will be applied on top of the current working
524 Selected changesets will be applied on top of the current working
525 directory with the log of the original changeset. The changesets
525 directory with the log of the original changeset. The changesets
526 are copied and will thus appear twice in the history with different
526 are copied and will thus appear twice in the history with different
527 identities.
527 identities.
528
528
529 Consider using the graft command if everything is inside the same
529 Consider using the graft command if everything is inside the same
530 repository - it will use merges and will usually give a better result.
530 repository - it will use merges and will usually give a better result.
531 Use the rebase extension if the changesets are unpublished and you want
531 Use the rebase extension if the changesets are unpublished and you want
532 to move them instead of copying them.
532 to move them instead of copying them.
533
533
534 If --log is specified, log messages will have a comment appended
534 If --log is specified, log messages will have a comment appended
535 of the form::
535 of the form::
536
536
537 (transplanted from CHANGESETHASH)
537 (transplanted from CHANGESETHASH)
538
538
539 You can rewrite the changelog message with the --filter option.
539 You can rewrite the changelog message with the --filter option.
540 Its argument will be invoked with the current changelog message as
540 Its argument will be invoked with the current changelog message as
541 $1 and the patch as $2.
541 $1 and the patch as $2.
542
542
543 --source/-s specifies another repository to use for selecting changesets,
543 --source/-s specifies another repository to use for selecting changesets,
544 just as if it temporarily had been pulled.
544 just as if it temporarily had been pulled.
545 If --branch/-b is specified, these revisions will be used as
545 If --branch/-b is specified, these revisions will be used as
546 heads when deciding which changesets to transplant, just as if only
546 heads when deciding which changesets to transplant, just as if only
547 these revisions had been pulled.
547 these revisions had been pulled.
548 If --all/-a is specified, all the revisions up to the heads specified
548 If --all/-a is specified, all the revisions up to the heads specified
549 with --branch will be transplanted.
549 with --branch will be transplanted.
550
550
551 Example:
551 Example:
552
552
553 - transplant all changes up to REV on top of your current revision::
553 - transplant all changes up to REV on top of your current revision::
554
554
555 hg transplant --branch REV --all
555 hg transplant --branch REV --all
556
556
557 You can optionally mark selected transplanted changesets as merge
557 You can optionally mark selected transplanted changesets as merge
558 changesets. You will not be prompted to transplant any ancestors
558 changesets. You will not be prompted to transplant any ancestors
559 of a merged transplant, and you can merge descendants of them
559 of a merged transplant, and you can merge descendants of them
560 normally instead of transplanting them.
560 normally instead of transplanting them.
561
561
562 Merge changesets may be transplanted directly by specifying the
562 Merge changesets may be transplanted directly by specifying the
563 proper parent changeset by calling :hg:`transplant --parent`.
563 proper parent changeset by calling :hg:`transplant --parent`.
564
564
565 If no merges or revisions are provided, :hg:`transplant` will
565 If no merges or revisions are provided, :hg:`transplant` will
566 start an interactive changeset browser.
566 start an interactive changeset browser.
567
567
568 If a changeset application fails, you can fix the merge by hand
568 If a changeset application fails, you can fix the merge by hand
569 and then resume where you left off by calling :hg:`transplant
569 and then resume where you left off by calling :hg:`transplant
570 --continue/-c`.
570 --continue/-c`.
571 '''
571 '''
572 def incwalk(repo, csets, match=util.always):
572 def incwalk(repo, csets, match=util.always):
573 for node in csets:
573 for node in csets:
574 if match(node):
574 if match(node):
575 yield node
575 yield node
576
576
577 def transplantwalk(repo, dest, heads, match=util.always):
577 def transplantwalk(repo, dest, heads, match=util.always):
578 '''Yield all nodes that are ancestors of a head but not ancestors
578 '''Yield all nodes that are ancestors of a head but not ancestors
579 of dest.
579 of dest.
580 If no heads are specified, the heads of repo will be used.'''
580 If no heads are specified, the heads of repo will be used.'''
581 if not heads:
581 if not heads:
582 heads = repo.heads()
582 heads = repo.heads()
583 ancestors = []
583 ancestors = []
584 ctx = repo[dest]
584 ctx = repo[dest]
585 for head in heads:
585 for head in heads:
586 ancestors.append(ctx.ancestor(repo[head]).node())
586 ancestors.append(ctx.ancestor(repo[head]).node())
587 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
587 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
588 if match(node):
588 if match(node):
589 yield node
589 yield node
590
590
591 def checkopts(opts, revs):
591 def checkopts(opts, revs):
592 if opts.get('continue'):
592 if opts.get('continue'):
593 if opts.get('branch') or opts.get('all') or opts.get('merge'):
593 if opts.get('branch') or opts.get('all') or opts.get('merge'):
594 raise util.Abort(_('--continue is incompatible with '
594 raise util.Abort(_('--continue is incompatible with '
595 '--branch, --all and --merge'))
595 '--branch, --all and --merge'))
596 return
596 return
597 if not (opts.get('source') or revs or
597 if not (opts.get('source') or revs or
598 opts.get('merge') or opts.get('branch')):
598 opts.get('merge') or opts.get('branch')):
599 raise util.Abort(_('no source URL, branch revision or revision '
599 raise util.Abort(_('no source URL, branch revision or revision '
600 'list provided'))
600 'list provided'))
601 if opts.get('all'):
601 if opts.get('all'):
602 if not opts.get('branch'):
602 if not opts.get('branch'):
603 raise util.Abort(_('--all requires a branch revision'))
603 raise util.Abort(_('--all requires a branch revision'))
604 if revs:
604 if revs:
605 raise util.Abort(_('--all is incompatible with a '
605 raise util.Abort(_('--all is incompatible with a '
606 'revision list'))
606 'revision list'))
607
607
608 checkopts(opts, revs)
608 checkopts(opts, revs)
609
609
610 if not opts.get('log'):
610 if not opts.get('log'):
611 # deprecated config: transplant.log
611 opts['log'] = ui.config('transplant', 'log')
612 opts['log'] = ui.config('transplant', 'log')
612 if not opts.get('filter'):
613 if not opts.get('filter'):
614 # deprecated config: transplant.filter
613 opts['filter'] = ui.config('transplant', 'filter')
615 opts['filter'] = ui.config('transplant', 'filter')
614
616
615 tp = transplanter(ui, repo, opts)
617 tp = transplanter(ui, repo, opts)
616
618
617 cmdutil.checkunfinished(repo)
619 cmdutil.checkunfinished(repo)
618 p1, p2 = repo.dirstate.parents()
620 p1, p2 = repo.dirstate.parents()
619 if len(repo) > 0 and p1 == revlog.nullid:
621 if len(repo) > 0 and p1 == revlog.nullid:
620 raise util.Abort(_('no revision checked out'))
622 raise util.Abort(_('no revision checked out'))
621 if not opts.get('continue'):
623 if not opts.get('continue'):
622 if p2 != revlog.nullid:
624 if p2 != revlog.nullid:
623 raise util.Abort(_('outstanding uncommitted merges'))
625 raise util.Abort(_('outstanding uncommitted merges'))
624 m, a, r, d = repo.status()[:4]
626 m, a, r, d = repo.status()[:4]
625 if m or a or r or d:
627 if m or a or r or d:
626 raise util.Abort(_('outstanding local changes'))
628 raise util.Abort(_('outstanding local changes'))
627
629
628 sourcerepo = opts.get('source')
630 sourcerepo = opts.get('source')
629 if sourcerepo:
631 if sourcerepo:
630 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
632 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
631 heads = map(peer.lookup, opts.get('branch', ()))
633 heads = map(peer.lookup, opts.get('branch', ()))
632 target = set(heads)
634 target = set(heads)
633 for r in revs:
635 for r in revs:
634 try:
636 try:
635 target.add(peer.lookup(r))
637 target.add(peer.lookup(r))
636 except error.RepoError:
638 except error.RepoError:
637 pass
639 pass
638 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
640 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
639 onlyheads=sorted(target), force=True)
641 onlyheads=sorted(target), force=True)
640 else:
642 else:
641 source = repo
643 source = repo
642 heads = map(source.lookup, opts.get('branch', ()))
644 heads = map(source.lookup, opts.get('branch', ()))
643 cleanupfn = None
645 cleanupfn = None
644
646
645 try:
647 try:
646 if opts.get('continue'):
648 if opts.get('continue'):
647 tp.resume(repo, source, opts)
649 tp.resume(repo, source, opts)
648 return
650 return
649
651
650 tf = tp.transplantfilter(repo, source, p1)
652 tf = tp.transplantfilter(repo, source, p1)
651 if opts.get('prune'):
653 if opts.get('prune'):
652 prune = set(source.lookup(r)
654 prune = set(source.lookup(r)
653 for r in scmutil.revrange(source, opts.get('prune')))
655 for r in scmutil.revrange(source, opts.get('prune')))
654 matchfn = lambda x: tf(x) and x not in prune
656 matchfn = lambda x: tf(x) and x not in prune
655 else:
657 else:
656 matchfn = tf
658 matchfn = tf
657 merges = map(source.lookup, opts.get('merge', ()))
659 merges = map(source.lookup, opts.get('merge', ()))
658 revmap = {}
660 revmap = {}
659 if revs:
661 if revs:
660 for r in scmutil.revrange(source, revs):
662 for r in scmutil.revrange(source, revs):
661 revmap[int(r)] = source.lookup(r)
663 revmap[int(r)] = source.lookup(r)
662 elif opts.get('all') or not merges:
664 elif opts.get('all') or not merges:
663 if source != repo:
665 if source != repo:
664 alltransplants = incwalk(source, csets, match=matchfn)
666 alltransplants = incwalk(source, csets, match=matchfn)
665 else:
667 else:
666 alltransplants = transplantwalk(source, p1, heads,
668 alltransplants = transplantwalk(source, p1, heads,
667 match=matchfn)
669 match=matchfn)
668 if opts.get('all'):
670 if opts.get('all'):
669 revs = alltransplants
671 revs = alltransplants
670 else:
672 else:
671 revs, newmerges = browserevs(ui, source, alltransplants, opts)
673 revs, newmerges = browserevs(ui, source, alltransplants, opts)
672 merges.extend(newmerges)
674 merges.extend(newmerges)
673 for r in revs:
675 for r in revs:
674 revmap[source.changelog.rev(r)] = r
676 revmap[source.changelog.rev(r)] = r
675 for r in merges:
677 for r in merges:
676 revmap[source.changelog.rev(r)] = r
678 revmap[source.changelog.rev(r)] = r
677
679
678 tp.apply(repo, source, revmap, merges, opts)
680 tp.apply(repo, source, revmap, merges, opts)
679 finally:
681 finally:
680 if cleanupfn:
682 if cleanupfn:
681 cleanupfn()
683 cleanupfn()
682
684
683 def revsettransplanted(repo, subset, x):
685 def revsettransplanted(repo, subset, x):
684 """``transplanted([set])``
686 """``transplanted([set])``
685 Transplanted changesets in set, or all transplanted changesets.
687 Transplanted changesets in set, or all transplanted changesets.
686 """
688 """
687 if x:
689 if x:
688 s = revset.getset(repo, subset, x)
690 s = revset.getset(repo, subset, x)
689 else:
691 else:
690 s = subset
692 s = subset
691 return revset.baseset([r for r in s if
693 return revset.baseset([r for r in s if
692 repo[r].extra().get('transplant_source')])
694 repo[r].extra().get('transplant_source')])
693
695
694 def kwtransplanted(repo, ctx, **args):
696 def kwtransplanted(repo, ctx, **args):
695 """:transplanted: String. The node identifier of the transplanted
697 """:transplanted: String. The node identifier of the transplanted
696 changeset if any."""
698 changeset if any."""
697 n = ctx.extra().get('transplant_source')
699 n = ctx.extra().get('transplant_source')
698 return n and revlog.hex(n) or ''
700 return n and revlog.hex(n) or ''
699
701
700 def extsetup(ui):
702 def extsetup(ui):
701 revset.symbols['transplanted'] = revsettransplanted
703 revset.symbols['transplanted'] = revsettransplanted
702 templatekw.keywords['transplanted'] = kwtransplanted
704 templatekw.keywords['transplanted'] = kwtransplanted
703 cmdutil.unfinishedstates.append(
705 cmdutil.unfinishedstates.append(
704 ['series', True, False, _('transplant in progress'),
706 ['series', True, False, _('transplant in progress'),
705 _("use 'hg transplant --continue' or 'hg update' to abort")])
707 _("use 'hg transplant --continue' or 'hg update' to abort")])
706
708
707 # tell hggettext to extract docstrings from these functions:
709 # tell hggettext to extract docstrings from these functions:
708 i18nfunctions = [revsettransplanted, kwtransplanted]
710 i18nfunctions = [revsettransplanted, kwtransplanted]
General Comments 0
You need to be logged in to leave comments. Login now