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