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