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