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