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