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