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