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