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