##// END OF EJS Templates
filemerge.prompt: separate out choice selection and action...
Siddharth Agarwal -
r26851:859f453e default
parent child Browse files
Show More
@@ -1,582 +1,585
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import filecmp
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import short
17 17
18 18 from . import (
19 19 error,
20 20 match,
21 21 simplemerge,
22 22 tagmerge,
23 23 templatekw,
24 24 templater,
25 25 util,
26 26 )
27 27
28 28 def _toolstr(ui, tool, part, default=""):
29 29 return ui.config("merge-tools", tool + "." + part, default)
30 30
31 31 def _toolbool(ui, tool, part, default=False):
32 32 return ui.configbool("merge-tools", tool + "." + part, default)
33 33
34 34 def _toollist(ui, tool, part, default=[]):
35 35 return ui.configlist("merge-tools", tool + "." + part, default)
36 36
37 37 internals = {}
38 38 # Merge tools to document.
39 39 internalsdoc = {}
40 40
41 41 # internal tool merge types
42 42 nomerge = None
43 43 mergeonly = 'mergeonly' # just the full merge, no premerge
44 44 fullmerge = 'fullmerge' # both premerge and merge
45 45
46 46 def internaltool(name, mergetype, onfailure=None, precheck=None):
47 47 '''return a decorator for populating internal merge tool table'''
48 48 def decorator(func):
49 49 fullname = ':' + name
50 50 func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip()
51 51 internals[fullname] = func
52 52 internals['internal:' + name] = func
53 53 internalsdoc[fullname] = func
54 54 func.mergetype = mergetype
55 55 func.onfailure = onfailure
56 56 func.precheck = precheck
57 57 return func
58 58 return decorator
59 59
60 60 def _findtool(ui, tool):
61 61 if tool in internals:
62 62 return tool
63 63 return findexternaltool(ui, tool)
64 64
65 65 def findexternaltool(ui, tool):
66 66 for kn in ("regkey", "regkeyalt"):
67 67 k = _toolstr(ui, tool, kn)
68 68 if not k:
69 69 continue
70 70 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
71 71 if p:
72 72 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
73 73 if p:
74 74 return p
75 75 exe = _toolstr(ui, tool, "executable", tool)
76 76 return util.findexe(util.expandpath(exe))
77 77
78 78 def _picktool(repo, ui, path, binary, symlink):
79 79 def check(tool, pat, symlink, binary):
80 80 tmsg = tool
81 81 if pat:
82 82 tmsg += " specified for " + pat
83 83 if not _findtool(ui, tool):
84 84 if pat: # explicitly requested tool deserves a warning
85 85 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
86 86 else: # configured but non-existing tools are more silent
87 87 ui.note(_("couldn't find merge tool %s\n") % tmsg)
88 88 elif symlink and not _toolbool(ui, tool, "symlink"):
89 89 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
90 90 elif binary and not _toolbool(ui, tool, "binary"):
91 91 ui.warn(_("tool %s can't handle binary\n") % tmsg)
92 92 elif not util.gui() and _toolbool(ui, tool, "gui"):
93 93 ui.warn(_("tool %s requires a GUI\n") % tmsg)
94 94 else:
95 95 return True
96 96 return False
97 97
98 98 # internal config: ui.forcemerge
99 99 # forcemerge comes from command line arguments, highest priority
100 100 force = ui.config('ui', 'forcemerge')
101 101 if force:
102 102 toolpath = _findtool(ui, force)
103 103 if toolpath:
104 104 return (force, util.shellquote(toolpath))
105 105 else:
106 106 # mimic HGMERGE if given tool not found
107 107 return (force, force)
108 108
109 109 # HGMERGE takes next precedence
110 110 hgmerge = os.environ.get("HGMERGE")
111 111 if hgmerge:
112 112 return (hgmerge, hgmerge)
113 113
114 114 # then patterns
115 115 for pat, tool in ui.configitems("merge-patterns"):
116 116 mf = match.match(repo.root, '', [pat])
117 117 if mf(path) and check(tool, pat, symlink, False):
118 118 toolpath = _findtool(ui, tool)
119 119 return (tool, util.shellquote(toolpath))
120 120
121 121 # then merge tools
122 122 tools = {}
123 123 disabled = set()
124 124 for k, v in ui.configitems("merge-tools"):
125 125 t = k.split('.')[0]
126 126 if t not in tools:
127 127 tools[t] = int(_toolstr(ui, t, "priority", "0"))
128 128 if _toolbool(ui, t, "disabled", False):
129 129 disabled.add(t)
130 130 names = tools.keys()
131 131 tools = sorted([(-p, t) for t, p in tools.items() if t not in disabled])
132 132 uimerge = ui.config("ui", "merge")
133 133 if uimerge:
134 134 if uimerge not in names:
135 135 return (uimerge, uimerge)
136 136 tools.insert(0, (None, uimerge)) # highest priority
137 137 tools.append((None, "hgmerge")) # the old default, if found
138 138 for p, t in tools:
139 139 if check(t, None, symlink, binary):
140 140 toolpath = _findtool(ui, t)
141 141 return (t, util.shellquote(toolpath))
142 142
143 143 # internal merge or prompt as last resort
144 144 if symlink or binary:
145 145 return ":prompt", None
146 146 return ":merge", None
147 147
148 148 def _eoltype(data):
149 149 "Guess the EOL type of a file"
150 150 if '\0' in data: # binary
151 151 return None
152 152 if '\r\n' in data: # Windows
153 153 return '\r\n'
154 154 if '\r' in data: # Old Mac
155 155 return '\r'
156 156 if '\n' in data: # UNIX
157 157 return '\n'
158 158 return None # unknown
159 159
160 160 def _matcheol(file, origfile):
161 161 "Convert EOL markers in a file to match origfile"
162 162 tostyle = _eoltype(util.readfile(origfile))
163 163 if tostyle:
164 164 data = util.readfile(file)
165 165 style = _eoltype(data)
166 166 if style:
167 167 newdata = data.replace(style, tostyle)
168 168 if newdata != data:
169 169 util.writefile(file, newdata)
170 170
171 171 @internaltool('prompt', nomerge)
172 172 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf):
173 173 """Asks the user which of the local or the other version to keep as
174 174 the merged version."""
175 175 ui = repo.ui
176 176 fd = fcd.path()
177 177
178 if ui.promptchoice(_(" no tool found to merge %s\n"
179 "keep (l)ocal or take (o)ther?"
180 "$$ &Local $$ &Other") % fd, 0):
178 index = ui.promptchoice(_(" no tool found to merge %s\n"
179 "keep (l)ocal or take (o)ther?"
180 "$$ &Local $$ &Other") % fd, 0)
181 choice = ['local', 'other'][index]
182
183 if choice == 'other':
181 184 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf)
182 185 else:
183 186 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf)
184 187
185 188 @internaltool('local', nomerge)
186 189 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf):
187 190 """Uses the local version of files as the merged version."""
188 191 return 0
189 192
190 193 @internaltool('other', nomerge)
191 194 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf):
192 195 """Uses the other version of files as the merged version."""
193 196 repo.wwrite(fcd.path(), fco.data(), fco.flags())
194 197 return 0
195 198
196 199 @internaltool('fail', nomerge)
197 200 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf):
198 201 """
199 202 Rather than attempting to merge files that were modified on both
200 203 branches, it marks them as unresolved. The resolve command must be
201 204 used to resolve these conflicts."""
202 205 return 1
203 206
204 207 def _premerge(repo, toolconf, files, labels=None):
205 208 tool, toolpath, binary, symlink = toolconf
206 209 if symlink:
207 210 return 1
208 211 a, b, c, back = files
209 212
210 213 ui = repo.ui
211 214
212 215 validkeep = ['keep', 'keep-merge3']
213 216
214 217 # do we attempt to simplemerge first?
215 218 try:
216 219 premerge = _toolbool(ui, tool, "premerge", not binary)
217 220 except error.ConfigError:
218 221 premerge = _toolstr(ui, tool, "premerge").lower()
219 222 if premerge not in validkeep:
220 223 _valid = ', '.join(["'" + v + "'" for v in validkeep])
221 224 raise error.ConfigError(_("%s.premerge not valid "
222 225 "('%s' is neither boolean nor %s)") %
223 226 (tool, premerge, _valid))
224 227
225 228 if premerge:
226 229 if premerge == 'keep-merge3':
227 230 if not labels:
228 231 labels = _defaultconflictlabels
229 232 if len(labels) < 3:
230 233 labels.append('base')
231 234 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
232 235 if not r:
233 236 ui.debug(" premerge successful\n")
234 237 return 0
235 238 if premerge not in validkeep:
236 239 util.copyfile(back, a) # restore from backup and try again
237 240 return 1 # continue merging
238 241
239 242 def _symlinkcheck(repo, mynode, orig, fcd, fco, fca, toolconf):
240 243 tool, toolpath, binary, symlink = toolconf
241 244 if symlink:
242 245 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
243 246 'for %s\n') % (tool, fcd.path()))
244 247 return False
245 248 return True
246 249
247 250 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
248 251 """
249 252 Uses the internal non-interactive simple merge algorithm for merging
250 253 files. It will fail if there are any conflicts and leave markers in
251 254 the partially merged file. Markers will have two sections, one for each side
252 255 of merge, unless mode equals 'union' which suppresses the markers."""
253 256 a, b, c, back = files
254 257
255 258 ui = repo.ui
256 259
257 260 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
258 261 return True, r
259 262
260 263 @internaltool('union', fullmerge,
261 264 _("warning: conflicts while merging %s! "
262 265 "(edit, then use 'hg resolve --mark')\n"),
263 266 precheck=_symlinkcheck)
264 267 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
265 268 """
266 269 Uses the internal non-interactive simple merge algorithm for merging
267 270 files. It will use both left and right sides for conflict regions.
268 271 No markers are inserted."""
269 272 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
270 273 files, labels, 'union')
271 274
272 275 @internaltool('merge', fullmerge,
273 276 _("warning: conflicts while merging %s! "
274 277 "(edit, then use 'hg resolve --mark')\n"),
275 278 precheck=_symlinkcheck)
276 279 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
277 280 """
278 281 Uses the internal non-interactive simple merge algorithm for merging
279 282 files. It will fail if there are any conflicts and leave markers in
280 283 the partially merged file. Markers will have two sections, one for each side
281 284 of merge."""
282 285 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
283 286 files, labels, 'merge')
284 287
285 288 @internaltool('merge3', fullmerge,
286 289 _("warning: conflicts while merging %s! "
287 290 "(edit, then use 'hg resolve --mark')\n"),
288 291 precheck=_symlinkcheck)
289 292 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
290 293 """
291 294 Uses the internal non-interactive simple merge algorithm for merging
292 295 files. It will fail if there are any conflicts and leave markers in
293 296 the partially merged file. Marker will have three sections, one from each
294 297 side of the merge and one for the base content."""
295 298 if not labels:
296 299 labels = _defaultconflictlabels
297 300 if len(labels) < 3:
298 301 labels.append('base')
299 302 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
300 303
301 304 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
302 305 labels=None, localorother=None):
303 306 """
304 307 Generic driver for _imergelocal and _imergeother
305 308 """
306 309 assert localorother is not None
307 310 tool, toolpath, binary, symlink = toolconf
308 311 if symlink:
309 312 repo.ui.warn(_('warning: :merge-%s cannot merge symlinks '
310 313 'for %s\n') % (localorother, fcd.path()))
311 314 return False, 1
312 315 a, b, c, back = files
313 316 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
314 317 localorother=localorother)
315 318 return True, r
316 319
317 320 @internaltool('merge-local', mergeonly)
318 321 def _imergelocal(*args, **kwargs):
319 322 """
320 323 Like :merge, but resolve all conflicts non-interactively in favor
321 324 of the local changes."""
322 325 success, status = _imergeauto(localorother='local', *args, **kwargs)
323 326 return success, status
324 327
325 328 @internaltool('merge-other', mergeonly)
326 329 def _imergeother(*args, **kwargs):
327 330 """
328 331 Like :merge, but resolve all conflicts non-interactively in favor
329 332 of the other changes."""
330 333 success, status = _imergeauto(localorother='other', *args, **kwargs)
331 334 return success, status
332 335
333 336 @internaltool('tagmerge', mergeonly,
334 337 _("automatic tag merging of %s failed! "
335 338 "(use 'hg resolve --tool :merge' or another merge "
336 339 "tool of your choice)\n"))
337 340 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
338 341 """
339 342 Uses the internal tag merge algorithm (experimental).
340 343 """
341 344 return tagmerge.merge(repo, fcd, fco, fca)
342 345
343 346 @internaltool('dump', fullmerge)
344 347 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
345 348 """
346 349 Creates three versions of the files to merge, containing the
347 350 contents of local, other and base. These files can then be used to
348 351 perform a merge manually. If the file to be merged is named
349 352 ``a.txt``, these files will accordingly be named ``a.txt.local``,
350 353 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
351 354 same directory as ``a.txt``."""
352 355 a, b, c, back = files
353 356
354 357 fd = fcd.path()
355 358
356 359 util.copyfile(a, a + ".local")
357 360 repo.wwrite(fd + ".other", fco.data(), fco.flags())
358 361 repo.wwrite(fd + ".base", fca.data(), fca.flags())
359 362 return False, 1
360 363
361 364 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
362 365 tool, toolpath, binary, symlink = toolconf
363 366 a, b, c, back = files
364 367 out = ""
365 368 env = {'HG_FILE': fcd.path(),
366 369 'HG_MY_NODE': short(mynode),
367 370 'HG_OTHER_NODE': str(fco.changectx()),
368 371 'HG_BASE_NODE': str(fca.changectx()),
369 372 'HG_MY_ISLINK': 'l' in fcd.flags(),
370 373 'HG_OTHER_ISLINK': 'l' in fco.flags(),
371 374 'HG_BASE_ISLINK': 'l' in fca.flags(),
372 375 }
373 376
374 377 ui = repo.ui
375 378
376 379 args = _toolstr(ui, tool, "args", '$local $base $other')
377 380 if "$output" in args:
378 381 out, a = a, back # read input from backup, write to original
379 382 replace = {'local': a, 'base': b, 'other': c, 'output': out}
380 383 args = util.interpolate(r'\$', replace, args,
381 384 lambda s: util.shellquote(util.localpath(s)))
382 385 cmd = toolpath + ' ' + args
383 386 repo.ui.debug('launching merge tool: %s\n' % cmd)
384 387 r = ui.system(cmd, cwd=repo.root, environ=env)
385 388 repo.ui.debug('merge tool returned: %s\n' % r)
386 389 return True, r
387 390
388 391 def _formatconflictmarker(repo, ctx, template, label, pad):
389 392 """Applies the given template to the ctx, prefixed by the label.
390 393
391 394 Pad is the minimum width of the label prefix, so that multiple markers
392 395 can have aligned templated parts.
393 396 """
394 397 if ctx.node() is None:
395 398 ctx = ctx.p1()
396 399
397 400 props = templatekw.keywords.copy()
398 401 props['templ'] = template
399 402 props['ctx'] = ctx
400 403 props['repo'] = repo
401 404 templateresult = template('conflictmarker', **props)
402 405
403 406 label = ('%s:' % label).ljust(pad + 1)
404 407 mark = '%s %s' % (label, templater.stringify(templateresult))
405 408
406 409 if mark:
407 410 mark = mark.splitlines()[0] # split for safety
408 411
409 412 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
410 413 return util.ellipsis(mark, 80 - 8)
411 414
412 415 _defaultconflictmarker = ('{node|short} ' +
413 416 '{ifeq(tags, "tip", "", "{tags} ")}' +
414 417 '{if(bookmarks, "{bookmarks} ")}' +
415 418 '{ifeq(branch, "default", "", "{branch} ")}' +
416 419 '- {author|user}: {desc|firstline}')
417 420
418 421 _defaultconflictlabels = ['local', 'other']
419 422
420 423 def _formatlabels(repo, fcd, fco, fca, labels):
421 424 """Formats the given labels using the conflict marker template.
422 425
423 426 Returns a list of formatted labels.
424 427 """
425 428 cd = fcd.changectx()
426 429 co = fco.changectx()
427 430 ca = fca.changectx()
428 431
429 432 ui = repo.ui
430 433 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
431 434 tmpl = templater.templater(None, cache={'conflictmarker': template})
432 435
433 436 pad = max(len(l) for l in labels)
434 437
435 438 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
436 439 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
437 440 if len(labels) > 2:
438 441 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
439 442 return newlabels
440 443
441 444 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
442 445 """perform a 3-way merge in the working directory
443 446
444 447 premerge = whether this is a premerge
445 448 mynode = parent node before merge
446 449 orig = original local filename before merge
447 450 fco = other file context
448 451 fca = ancestor file context
449 452 fcd = local file context for current/destination file
450 453
451 454 Returns whether the merge is complete, and the return value of the merge.
452 455 """
453 456
454 457 def temp(prefix, ctx):
455 458 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
456 459 (fd, name) = tempfile.mkstemp(prefix=pre)
457 460 data = repo.wwritedata(ctx.path(), ctx.data())
458 461 f = os.fdopen(fd, "wb")
459 462 f.write(data)
460 463 f.close()
461 464 return name
462 465
463 466 if not fco.cmp(fcd): # files identical?
464 467 return True, None
465 468
466 469 ui = repo.ui
467 470 fd = fcd.path()
468 471 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
469 472 symlink = 'l' in fcd.flags() + fco.flags()
470 473 tool, toolpath = _picktool(repo, ui, fd, binary, symlink)
471 474 if tool in internals and tool.startswith('internal:'):
472 475 # normalize to new-style names (':merge' etc)
473 476 tool = tool[len('internal'):]
474 477 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
475 478 (tool, fd, binary, symlink))
476 479
477 480 if tool in internals:
478 481 func = internals[tool]
479 482 mergetype = func.mergetype
480 483 onfailure = func.onfailure
481 484 precheck = func.precheck
482 485 else:
483 486 func = _xmerge
484 487 mergetype = fullmerge
485 488 onfailure = _("merging %s failed!\n")
486 489 precheck = None
487 490
488 491 toolconf = tool, toolpath, binary, symlink
489 492
490 493 if mergetype == nomerge:
491 494 return True, func(repo, mynode, orig, fcd, fco, fca, toolconf)
492 495
493 496 if premerge:
494 497 if orig != fco.path():
495 498 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
496 499 else:
497 500 ui.status(_("merging %s\n") % fd)
498 501
499 502 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
500 503
501 504 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
502 505 toolconf):
503 506 if onfailure:
504 507 ui.warn(onfailure % fd)
505 508 return True, 1
506 509
507 510 a = repo.wjoin(fd)
508 511 b = temp("base", fca)
509 512 c = temp("other", fco)
510 513 back = a + ".orig"
511 514 if premerge:
512 515 util.copyfile(a, back)
513 516 files = (a, b, c, back)
514 517
515 518 r = 1
516 519 try:
517 520 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
518 521 if not labels:
519 522 labels = _defaultconflictlabels
520 523 if markerstyle != 'basic':
521 524 labels = _formatlabels(repo, fcd, fco, fca, labels)
522 525
523 526 if premerge and mergetype == fullmerge:
524 527 r = _premerge(repo, toolconf, files, labels=labels)
525 528 # complete if premerge successful (r is 0)
526 529 return not r, r
527 530
528 531 needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf, files,
529 532 labels=labels)
530 533 if needcheck:
531 534 r = _check(r, ui, tool, fcd, files)
532 535
533 536 if r:
534 537 if onfailure:
535 538 ui.warn(onfailure % fd)
536 539
537 540 return True, r
538 541 finally:
539 542 if not r:
540 543 util.unlink(back)
541 544 util.unlink(b)
542 545 util.unlink(c)
543 546
544 547 def _check(r, ui, tool, fcd, files):
545 548 fd = fcd.path()
546 549 a, b, c, back = files
547 550
548 551 if not r and (_toolbool(ui, tool, "checkconflicts") or
549 552 'conflicts' in _toollist(ui, tool, "check")):
550 553 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
551 554 re.MULTILINE):
552 555 r = 1
553 556
554 557 checked = False
555 558 if 'prompt' in _toollist(ui, tool, "check"):
556 559 checked = True
557 560 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
558 561 "$$ &Yes $$ &No") % fd, 1):
559 562 r = 1
560 563
561 564 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
562 565 'changed' in
563 566 _toollist(ui, tool, "check")):
564 567 if filecmp.cmp(a, back):
565 568 if ui.promptchoice(_(" output file %s appears unchanged\n"
566 569 "was merge successful (yn)?"
567 570 "$$ &Yes $$ &No") % fd, 1):
568 571 r = 1
569 572
570 573 if _toolbool(ui, tool, "fixeol"):
571 574 _matcheol(a, back)
572 575
573 576 return r
574 577
575 578 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
576 579 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
577 580
578 581 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
579 582 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
580 583
581 584 # tell hggettext to extract docstrings from these functions:
582 585 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now