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