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