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