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