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