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