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