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