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