##// END OF EJS Templates
transplant: preserve filter changes in --continue log
Brendan Cully -
r3725:ccc7a9eb default
parent child Browse files
Show More
@@ -1,565 +1,563 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' % patchfile)
173 self.ui.status('filtering %s' % 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 cl = list(cl)
191 cl[4] = message
192
190
193 self.ui.status(_('applying %s\n') % revlog.short(node))
191 self.ui.status(_('applying %s\n') % revlog.short(node))
194 self.ui.note('%s %s\n%s\n' % (user, date, message))
192 self.ui.note('%s %s\n%s\n' % (user, date, message))
195
193
196 if not patchfile and not merge:
194 if not patchfile and not merge:
197 raise util.Abort(_('can only omit patchfile if merging'))
195 raise util.Abort(_('can only omit patchfile if merging'))
198 if patchfile:
196 if patchfile:
199 try:
197 try:
200 files = {}
198 files = {}
201 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
199 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
202 files=files)
200 files=files)
203 if not files:
201 if not files:
204 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
202 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
205 return
203 return
206 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
204 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
207 if filter:
205 if filter:
208 os.unlink(patchfile)
206 os.unlink(patchfile)
209 except Exception, inst:
207 except Exception, inst:
210 if filter:
208 if filter:
211 os.unlink(patchfile)
209 os.unlink(patchfile)
212 p1 = repo.dirstate.parents()[0]
210 p1 = repo.dirstate.parents()[0]
213 p2 = node
211 p2 = node
214 self.log(cl, p1, p2, merge=merge)
212 self.log(user, date, message, p1, p2, merge=merge)
215 self.ui.write(str(inst) + '\n')
213 self.ui.write(str(inst) + '\n')
216 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
214 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
217 else:
215 else:
218 files = None
216 files = None
219 if merge:
217 if merge:
220 p1, p2 = repo.dirstate.parents()
218 p1, p2 = repo.dirstate.parents()
221 repo.dirstate.setparents(p1, node)
219 repo.dirstate.setparents(p1, node)
222
220
223 n = repo.commit(files, message, user, date, lock=lock, wlock=wlock,
221 n = repo.commit(files, message, user, date, lock=lock, wlock=wlock,
224 extra=extra)
222 extra=extra)
225 if not merge:
223 if not merge:
226 self.transplants.set(n, node)
224 self.transplants.set(n, node)
227
225
228 return n
226 return n
229
227
230 def resume(self, repo, source, opts=None):
228 def resume(self, repo, source, opts=None):
231 '''recover last transaction and apply remaining changesets'''
229 '''recover last transaction and apply remaining changesets'''
232 if os.path.exists(os.path.join(self.path, 'journal')):
230 if os.path.exists(os.path.join(self.path, 'journal')):
233 n, node = self.recover(repo)
231 n, node = self.recover(repo)
234 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
232 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
235 revlog.short(n)))
233 revlog.short(n)))
236 seriespath = os.path.join(self.path, 'series')
234 seriespath = os.path.join(self.path, 'series')
237 if not os.path.exists(seriespath):
235 if not os.path.exists(seriespath):
238 return
236 return
239 nodes, merges = self.readseries()
237 nodes, merges = self.readseries()
240 revmap = {}
238 revmap = {}
241 for n in nodes:
239 for n in nodes:
242 revmap[source.changelog.rev(n)] = n
240 revmap[source.changelog.rev(n)] = n
243 os.unlink(seriespath)
241 os.unlink(seriespath)
244
242
245 self.apply(repo, source, revmap, merges, opts)
243 self.apply(repo, source, revmap, merges, opts)
246
244
247 def recover(self, repo):
245 def recover(self, repo):
248 '''commit working directory using journal metadata'''
246 '''commit working directory using journal metadata'''
249 node, user, date, message, parents = self.readlog()
247 node, user, date, message, parents = self.readlog()
250 merge = len(parents) == 2
248 merge = len(parents) == 2
251
249
252 if not user or not date or not message or not parents[0]:
250 if not user or not date or not message or not parents[0]:
253 raise util.Abort(_('transplant log file is corrupt'))
251 raise util.Abort(_('transplant log file is corrupt'))
254
252
255 wlock = repo.wlock()
253 wlock = repo.wlock()
256 p1, p2 = repo.dirstate.parents()
254 p1, p2 = repo.dirstate.parents()
257 if p1 != parents[0]:
255 if p1 != parents[0]:
258 raise util.Abort(_('working dir not at transplant parent %s') %
256 raise util.Abort(_('working dir not at transplant parent %s') %
259 revlog.hex(parents[0]))
257 revlog.hex(parents[0]))
260 if merge:
258 if merge:
261 repo.dirstate.setparents(p1, parents[1])
259 repo.dirstate.setparents(p1, parents[1])
262 n = repo.commit(None, message, user, date, wlock=wlock)
260 n = repo.commit(None, message, user, date, wlock=wlock)
263 if not n:
261 if not n:
264 raise util.Abort(_('commit failed'))
262 raise util.Abort(_('commit failed'))
265 if not merge:
263 if not merge:
266 self.transplants.set(n, node)
264 self.transplants.set(n, node)
267 self.unlog()
265 self.unlog()
268
266
269 return n, node
267 return n, node
270
268
271 def readseries(self):
269 def readseries(self):
272 nodes = []
270 nodes = []
273 merges = []
271 merges = []
274 cur = nodes
272 cur = nodes
275 for line in self.opener('series').read().splitlines():
273 for line in self.opener('series').read().splitlines():
276 if line.startswith('# Merges'):
274 if line.startswith('# Merges'):
277 cur = merges
275 cur = merges
278 continue
276 continue
279 cur.append(revlog.bin(line))
277 cur.append(revlog.bin(line))
280
278
281 return (nodes, merges)
279 return (nodes, merges)
282
280
283 def saveseries(self, revmap, merges):
281 def saveseries(self, revmap, merges):
284 if not revmap:
282 if not revmap:
285 return
283 return
286
284
287 if not os.path.isdir(self.path):
285 if not os.path.isdir(self.path):
288 os.mkdir(self.path)
286 os.mkdir(self.path)
289 series = self.opener('series', 'w')
287 series = self.opener('series', 'w')
290 revs = revmap.keys()
288 revs = revmap.keys()
291 revs.sort()
289 revs.sort()
292 for rev in revs:
290 for rev in revs:
293 series.write(revlog.hex(revmap[rev]) + '\n')
291 series.write(revlog.hex(revmap[rev]) + '\n')
294 if merges:
292 if merges:
295 series.write('# Merges\n')
293 series.write('# Merges\n')
296 for m in merges:
294 for m in merges:
297 series.write(revlog.hex(m) + '\n')
295 series.write(revlog.hex(m) + '\n')
298 series.close()
296 series.close()
299
297
300 def log(self, changelog, p1, p2, merge=False):
298 def log(self, user, date, message, p1, p2, merge=False):
301 '''journal changelog metadata for later recover'''
299 '''journal changelog metadata for later recover'''
302
300
303 if not os.path.isdir(self.path):
301 if not os.path.isdir(self.path):
304 os.mkdir(self.path)
302 os.mkdir(self.path)
305 fp = self.opener('journal', 'w')
303 fp = self.opener('journal', 'w')
306 fp.write('# User %s\n' % changelog[1])
304 fp.write('# User %s\n' % user)
307 fp.write('# Date %d %d\n' % changelog[2])
305 fp.write('# Date %s\n' % date)
308 fp.write('# Node ID %s\n' % revlog.hex(p2))
306 fp.write('# Node ID %s\n' % revlog.hex(p2))
309 fp.write('# Parent ' + revlog.hex(p1) + '\n')
307 fp.write('# Parent ' + revlog.hex(p1) + '\n')
310 if merge:
308 if merge:
311 fp.write('# Parent ' + revlog.hex(p2) + '\n')
309 fp.write('# Parent ' + revlog.hex(p2) + '\n')
312 fp.write(changelog[4].rstrip() + '\n')
310 fp.write(message.rstrip() + '\n')
313 fp.close()
311 fp.close()
314
312
315 def readlog(self):
313 def readlog(self):
316 parents = []
314 parents = []
317 message = []
315 message = []
318 for line in self.opener('journal').read().splitlines():
316 for line in self.opener('journal').read().splitlines():
319 if line.startswith('# User '):
317 if line.startswith('# User '):
320 user = line[7:]
318 user = line[7:]
321 elif line.startswith('# Date '):
319 elif line.startswith('# Date '):
322 date = line[7:]
320 date = line[7:]
323 elif line.startswith('# Node ID '):
321 elif line.startswith('# Node ID '):
324 node = revlog.bin(line[10:])
322 node = revlog.bin(line[10:])
325 elif line.startswith('# Parent '):
323 elif line.startswith('# Parent '):
326 parents.append(revlog.bin(line[9:]))
324 parents.append(revlog.bin(line[9:]))
327 else:
325 else:
328 message.append(line)
326 message.append(line)
329 return (node, user, date, '\n'.join(message), parents)
327 return (node, user, date, '\n'.join(message), parents)
330
328
331 def unlog(self):
329 def unlog(self):
332 '''remove changelog journal'''
330 '''remove changelog journal'''
333 absdst = os.path.join(self.path, 'journal')
331 absdst = os.path.join(self.path, 'journal')
334 if os.path.exists(absdst):
332 if os.path.exists(absdst):
335 os.unlink(absdst)
333 os.unlink(absdst)
336
334
337 def transplantfilter(self, repo, source, root):
335 def transplantfilter(self, repo, source, root):
338 def matchfn(node):
336 def matchfn(node):
339 if self.applied(repo, node, root):
337 if self.applied(repo, node, root):
340 return False
338 return False
341 if source.changelog.parents(node)[1] != revlog.nullid:
339 if source.changelog.parents(node)[1] != revlog.nullid:
342 return False
340 return False
343 extra = source.changelog.read(node)[5]
341 extra = source.changelog.read(node)[5]
344 cnode = extra.get('transplant_source')
342 cnode = extra.get('transplant_source')
345 if cnode and self.applied(repo, cnode, root):
343 if cnode and self.applied(repo, cnode, root):
346 return False
344 return False
347 return True
345 return True
348
346
349 return matchfn
347 return matchfn
350
348
351 def hasnode(repo, node):
349 def hasnode(repo, node):
352 try:
350 try:
353 return repo.changelog.rev(node) != None
351 return repo.changelog.rev(node) != None
354 except revlog.RevlogError:
352 except revlog.RevlogError:
355 return False
353 return False
356
354
357 def browserevs(ui, repo, nodes, opts):
355 def browserevs(ui, repo, nodes, opts):
358 '''interactively transplant changesets'''
356 '''interactively transplant changesets'''
359 def browsehelp(ui):
357 def browsehelp(ui):
360 ui.write('y: transplant this changeset\n'
358 ui.write('y: transplant this changeset\n'
361 'n: skip this changeset\n'
359 'n: skip this changeset\n'
362 'm: merge at this changeset\n'
360 'm: merge at this changeset\n'
363 'p: show patch\n'
361 'p: show patch\n'
364 'c: commit selected changesets\n'
362 'c: commit selected changesets\n'
365 'q: cancel transplant\n'
363 'q: cancel transplant\n'
366 '?: show this help\n')
364 '?: show this help\n')
367
365
368 displayer = cmdutil.show_changeset(ui, repo, opts)
366 displayer = cmdutil.show_changeset(ui, repo, opts)
369 transplants = []
367 transplants = []
370 merges = []
368 merges = []
371 for node in nodes:
369 for node in nodes:
372 displayer.show(changenode=node)
370 displayer.show(changenode=node)
373 action = None
371 action = None
374 while not action:
372 while not action:
375 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
373 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
376 if action == '?':
374 if action == '?':
377 browsehelp(ui)
375 browsehelp(ui)
378 action = None
376 action = None
379 elif action == 'p':
377 elif action == 'p':
380 parent = repo.changelog.parents(node)[0]
378 parent = repo.changelog.parents(node)[0]
381 patch.diff(repo, parent, node)
379 patch.diff(repo, parent, node)
382 action = None
380 action = None
383 elif action not in ('y', 'n', 'm', 'c', 'q'):
381 elif action not in ('y', 'n', 'm', 'c', 'q'):
384 ui.write('no such option\n')
382 ui.write('no such option\n')
385 action = None
383 action = None
386 if action == 'y':
384 if action == 'y':
387 transplants.append(node)
385 transplants.append(node)
388 elif action == 'm':
386 elif action == 'm':
389 merges.append(node)
387 merges.append(node)
390 elif action == 'c':
388 elif action == 'c':
391 break
389 break
392 elif action == 'q':
390 elif action == 'q':
393 transplants = ()
391 transplants = ()
394 merges = ()
392 merges = ()
395 break
393 break
396 return (transplants, merges)
394 return (transplants, merges)
397
395
398 def transplant(ui, repo, *revs, **opts):
396 def transplant(ui, repo, *revs, **opts):
399 '''transplant changesets from another branch
397 '''transplant changesets from another branch
400
398
401 Selected changesets will be applied on top of the current working
399 Selected changesets will be applied on top of the current working
402 directory with the log of the original changeset. If --log is
400 directory with the log of the original changeset. If --log is
403 specified, log messages will have a comment appended of the form:
401 specified, log messages will have a comment appended of the form:
404
402
405 (transplanted from CHANGESETHASH)
403 (transplanted from CHANGESETHASH)
406
404
407 You can rewrite the changelog message with the --filter option.
405 You can rewrite the changelog message with the --filter option.
408 Its argument will be invoked with the current changelog message
406 Its argument will be invoked with the current changelog message
409 as $1 and the patch as $2.
407 as $1 and the patch as $2.
410
408
411 If --source is specified, selects changesets from the named
409 If --source is specified, selects changesets from the named
412 repository. If --branch is specified, selects changesets from the
410 repository. If --branch is specified, selects changesets from the
413 branch holding the named revision, up to that revision. If --all
411 branch holding the named revision, up to that revision. If --all
414 is specified, all changesets on the branch will be transplanted,
412 is specified, all changesets on the branch will be transplanted,
415 otherwise you will be prompted to select the changesets you want.
413 otherwise you will be prompted to select the changesets you want.
416
414
417 hg transplant --branch REVISION --all will rebase the selected branch
415 hg transplant --branch REVISION --all will rebase the selected branch
418 (up to the named revision) onto your current working directory.
416 (up to the named revision) onto your current working directory.
419
417
420 You can optionally mark selected transplanted changesets as
418 You can optionally mark selected transplanted changesets as
421 merge changesets. You will not be prompted to transplant any
419 merge changesets. You will not be prompted to transplant any
422 ancestors of a merged transplant, and you can merge descendants
420 ancestors of a merged transplant, and you can merge descendants
423 of them normally instead of transplanting them.
421 of them normally instead of transplanting them.
424
422
425 If no merges or revisions are provided, hg transplant will start
423 If no merges or revisions are provided, hg transplant will start
426 an interactive changeset browser.
424 an interactive changeset browser.
427
425
428 If a changeset application fails, you can fix the merge by hand and
426 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.
427 then resume where you left off by calling hg transplant --continue.
430 '''
428 '''
431 def getoneitem(opts, item, errmsg):
429 def getoneitem(opts, item, errmsg):
432 val = opts.get(item)
430 val = opts.get(item)
433 if val:
431 if val:
434 if len(val) > 1:
432 if len(val) > 1:
435 raise util.Abort(errmsg)
433 raise util.Abort(errmsg)
436 else:
434 else:
437 return val[0]
435 return val[0]
438
436
439 def getremotechanges(repo, url):
437 def getremotechanges(repo, url):
440 sourcerepo = ui.expandpath(url)
438 sourcerepo = ui.expandpath(url)
441 source = hg.repository(ui, sourcerepo)
439 source = hg.repository(ui, sourcerepo)
442 incoming = repo.findincoming(source, force=True)
440 incoming = repo.findincoming(source, force=True)
443 if not incoming:
441 if not incoming:
444 return (source, None, None)
442 return (source, None, None)
445
443
446 bundle = None
444 bundle = None
447 if not source.local():
445 if not source.local():
448 cg = source.changegroup(incoming, 'incoming')
446 cg = source.changegroup(incoming, 'incoming')
449 bundle = commands.write_bundle(cg, compress=False)
447 bundle = commands.write_bundle(cg, compress=False)
450 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
448 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
451
449
452 return (source, incoming, bundle)
450 return (source, incoming, bundle)
453
451
454 def incwalk(repo, incoming, branches, match=util.always):
452 def incwalk(repo, incoming, branches, match=util.always):
455 if not branches:
453 if not branches:
456 branches=None
454 branches=None
457 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
455 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
458 if match(node):
456 if match(node):
459 yield node
457 yield node
460
458
461 def transplantwalk(repo, root, branches, match=util.always):
459 def transplantwalk(repo, root, branches, match=util.always):
462 if not branches:
460 if not branches:
463 branches = repo.heads()
461 branches = repo.heads()
464 ancestors = []
462 ancestors = []
465 for branch in branches:
463 for branch in branches:
466 ancestors.append(repo.changelog.ancestor(root, branch))
464 ancestors.append(repo.changelog.ancestor(root, branch))
467 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
465 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
468 if match(node):
466 if match(node):
469 yield node
467 yield node
470
468
471 def checkopts(opts, revs):
469 def checkopts(opts, revs):
472 if opts.get('continue'):
470 if opts.get('continue'):
473 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
471 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
474 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
472 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
475 return
473 return
476 if not (opts.get('source') or revs or
474 if not (opts.get('source') or revs or
477 opts.get('merge') or opts.get('branch')):
475 opts.get('merge') or opts.get('branch')):
478 raise util.Abort(_('no source URL, branch tag or revision list provided'))
476 raise util.Abort(_('no source URL, branch tag or revision list provided'))
479 if opts.get('all'):
477 if opts.get('all'):
480 if not opts.get('branch'):
478 if not opts.get('branch'):
481 raise util.Abort(_('--all requires a branch revision'))
479 raise util.Abort(_('--all requires a branch revision'))
482 if revs:
480 if revs:
483 raise util.Abort(_('--all is incompatible with a revision list'))
481 raise util.Abort(_('--all is incompatible with a revision list'))
484
482
485 checkopts(opts, revs)
483 checkopts(opts, revs)
486
484
487 if not opts.get('log'):
485 if not opts.get('log'):
488 opts['log'] = ui.config('transplant', 'log')
486 opts['log'] = ui.config('transplant', 'log')
489 if not opts.get('filter'):
487 if not opts.get('filter'):
490 opts['filter'] = ui.config('transplant', 'filter')
488 opts['filter'] = ui.config('transplant', 'filter')
491
489
492 tp = transplanter(ui, repo)
490 tp = transplanter(ui, repo)
493
491
494 p1, p2 = repo.dirstate.parents()
492 p1, p2 = repo.dirstate.parents()
495 if p1 == revlog.nullid:
493 if p1 == revlog.nullid:
496 raise util.Abort(_('no revision checked out'))
494 raise util.Abort(_('no revision checked out'))
497 if not opts.get('continue'):
495 if not opts.get('continue'):
498 if p2 != revlog.nullid:
496 if p2 != revlog.nullid:
499 raise util.Abort(_('outstanding uncommitted merges'))
497 raise util.Abort(_('outstanding uncommitted merges'))
500 m, a, r, d = repo.status()[:4]
498 m, a, r, d = repo.status()[:4]
501 if m or a or r or d:
499 if m or a or r or d:
502 raise util.Abort(_('outstanding local changes'))
500 raise util.Abort(_('outstanding local changes'))
503
501
504 bundle = None
502 bundle = None
505 source = opts.get('source')
503 source = opts.get('source')
506 if source:
504 if source:
507 (source, incoming, bundle) = getremotechanges(repo, source)
505 (source, incoming, bundle) = getremotechanges(repo, source)
508 else:
506 else:
509 source = repo
507 source = repo
510
508
511 try:
509 try:
512 if opts.get('continue'):
510 if opts.get('continue'):
513 tp.resume(repo, source, opts)
511 tp.resume(repo, source, opts)
514 return
512 return
515
513
516 tf=tp.transplantfilter(repo, source, p1)
514 tf=tp.transplantfilter(repo, source, p1)
517 if opts.get('prune'):
515 if opts.get('prune'):
518 prune = [source.lookup(r)
516 prune = [source.lookup(r)
519 for r in cmdutil.revrange(source, opts.get('prune'))]
517 for r in cmdutil.revrange(source, opts.get('prune'))]
520 matchfn = lambda x: tf(x) and x not in prune
518 matchfn = lambda x: tf(x) and x not in prune
521 else:
519 else:
522 matchfn = tf
520 matchfn = tf
523 branches = map(source.lookup, opts.get('branch', ()))
521 branches = map(source.lookup, opts.get('branch', ()))
524 merges = map(source.lookup, opts.get('merge', ()))
522 merges = map(source.lookup, opts.get('merge', ()))
525 revmap = {}
523 revmap = {}
526 if revs:
524 if revs:
527 for r in cmdutil.revrange(source, revs):
525 for r in cmdutil.revrange(source, revs):
528 revmap[int(r)] = source.lookup(r)
526 revmap[int(r)] = source.lookup(r)
529 elif opts.get('all') or not merges:
527 elif opts.get('all') or not merges:
530 if source != repo:
528 if source != repo:
531 alltransplants = incwalk(source, incoming, branches, match=matchfn)
529 alltransplants = incwalk(source, incoming, branches, match=matchfn)
532 else:
530 else:
533 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
531 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
534 if opts.get('all'):
532 if opts.get('all'):
535 revs = alltransplants
533 revs = alltransplants
536 else:
534 else:
537 revs, newmerges = browserevs(ui, source, alltransplants, opts)
535 revs, newmerges = browserevs(ui, source, alltransplants, opts)
538 merges.extend(newmerges)
536 merges.extend(newmerges)
539 for r in revs:
537 for r in revs:
540 revmap[source.changelog.rev(r)] = r
538 revmap[source.changelog.rev(r)] = r
541 for r in merges:
539 for r in merges:
542 revmap[source.changelog.rev(r)] = r
540 revmap[source.changelog.rev(r)] = r
543
541
544 revs = revmap.keys()
542 revs = revmap.keys()
545 revs.sort()
543 revs.sort()
546 pulls = []
544 pulls = []
547
545
548 tp.apply(repo, source, revmap, merges, opts)
546 tp.apply(repo, source, revmap, merges, opts)
549 finally:
547 finally:
550 if bundle:
548 if bundle:
551 os.unlink(bundle)
549 os.unlink(bundle)
552
550
553 cmdtable = {
551 cmdtable = {
554 "transplant":
552 "transplant":
555 (transplant,
553 (transplant,
556 [('s', 'source', '', _('pull patches from REPOSITORY')),
554 [('s', 'source', '', _('pull patches from REPOSITORY')),
557 ('b', 'branch', [], _('pull patches from branch BRANCH')),
555 ('b', 'branch', [], _('pull patches from branch BRANCH')),
558 ('a', 'all', None, _('pull all changesets up to BRANCH')),
556 ('a', 'all', None, _('pull all changesets up to BRANCH')),
559 ('p', 'prune', [], _('skip over REV')),
557 ('p', 'prune', [], _('skip over REV')),
560 ('m', 'merge', [], _('merge at REV')),
558 ('m', 'merge', [], _('merge at REV')),
561 ('', 'log', None, _('append transplant info to log message')),
559 ('', 'log', None, _('append transplant info to log message')),
562 ('c', 'continue', None, _('continue last transplant session after repair')),
560 ('c', 'continue', None, _('continue last transplant session after repair')),
563 ('', 'filter', '', _('filter changesets through FILTER'))],
561 ('', 'filter', '', _('filter changesets through FILTER'))],
564 _('hg transplant [-s REPOSITORY] [-b BRANCH] [-p REV] [-m REV] [-n] REV...'))
562 _('hg transplant [-s REPOSITORY] [-b BRANCH] [-p REV] [-m REV] [-n] REV...'))
565 }
563 }
General Comments 0
You need to be logged in to leave comments. Login now