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