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