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