Warning, /sdk/pology/bin/poepatch is written in an unsupported language. File is not indexed.
0001 #!/usr/bin/env python3
0002 # -*- coding: UTF-8 -*-
0003
0004 """
0005 Patch PO files from an embedded diff.
0006
0007 Documented in C{doc/user/diffpatch.docbook#sec-dpdiff}.
0008
0009 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0010 @license: GPLv3
0011 """
0012
0013 try:
0014 import fallback_import_paths
0015 except:
0016 pass
0017
0018 import sys
0019 import os
0020 import locale
0021 import re
0022 from tempfile import NamedTemporaryFile
0023
0024 from pology import version, _, n_
0025 from pology.colors import ColorOptionParser
0026 from pology.report import error, warning, report
0027 from pology.msgreport import error_on_msg, warning_on_msg
0028 import pology.config as pology_config
0029 from pology.fsops import str_to_unicode, mkdirpath, collect_catalogs
0030 from pology.fsops import exit_on_exception
0031 from pology.catalog import Catalog
0032 from pology.message import Message, MessageUnsafe
0033 from pology.header import Header
0034 from pology.diff import msg_ediff, msg_ediff_to_new, msg_ediff_to_old
0035
0036 from pology.internal.poediffpatch import MPC, EDST
0037 from pology.internal.poediffpatch import msg_eq_fields, msg_copy_fields
0038 from pology.internal.poediffpatch import msg_clear_prev_fields
0039 from pology.internal.poediffpatch import diff_cats
0040 from pology.internal.poediffpatch import init_ediff_header
0041 from pology.internal.poediffpatch import get_msgctxt_for_headers
0042 from functools import reduce
0043
0044
0045 _flag_ediff = "ediff"
0046 _flag_ediff_to_cur = "%s-to-cur" % _flag_ediff
0047 _flag_ediff_to_new = "%s-to-new" % _flag_ediff
0048 _flag_ediff_no_match = "%s-no-match" % _flag_ediff
0049 _flags_all = (
0050 _flag_ediff,
0051 _flag_ediff_to_cur, _flag_ediff_to_new,
0052 _flag_ediff_no_match,
0053 )
0054
0055
0056 def main ():
0057
0058 locale.setlocale(locale.LC_ALL, "")
0059
0060 # Get defaults for command line options from global config.
0061 cfgsec = pology_config.section("poepatch")
0062 def_do_merge = cfgsec.boolean("merge", True)
0063
0064 # Setup options and parse the command line.
0065 usage = _("@info command usage",
0066 "%(cmd)s [OPTIONS] [OPTIONS] < EDIFF\n"
0067 "%(cmd)s -u [OPTIONS] PATHS...",
0068 cmd="%prog")
0069 desc = _("@info command description",
0070 "Apply embedded diff of PO files as patch.")
0071 ver = _("@info command version",
0072 "%(cmd)s (Pology) %(version)s\n"
0073 "Copyright © 2009, 2010 "
0074 "Chusslove Illich (Часлав Илић) <%(email)s>",
0075 cmd="%prog", version=version(), email="caslav.ilic@gmx.net")
0076
0077 opars = ColorOptionParser(usage=usage, description=desc, version=ver)
0078 opars.add_option(
0079 "-a", "--aggressive",
0080 action="store_true", dest="aggressive", default=False,
0081 help=_("@info command line option description",
0082 "Apply every message to its paired message in the target file, "
0083 "irrespective of whether its non-pairing parts match too."))
0084 opars.add_option(
0085 "-d", "--directory",
0086 metavar=_("@info command line value placeholder", "DIR"),
0087 dest="directory",
0088 help=_("@info command line option description",
0089 "Prepend this directory path to any resolved target file path."))
0090 opars.add_option(
0091 "-e", "--embed",
0092 action="store_true", dest="embed", default=False,
0093 help=_("@info command line option description",
0094 "Instead of applying resolved newer version of the message, "
0095 "add the full embedded diff into the target file."))
0096 opars.add_option(
0097 "-i", "--input",
0098 metavar=_("@info command line value placeholder", "FILE"),
0099 dest="input",
0100 help=_("@info command line option description",
0101 "Read the patch from the given file instead of standard input."))
0102 opars.add_option(
0103 "-n", "--no-merge",
0104 action="store_false", dest="do_merge", default=def_do_merge,
0105 help=_("@info command line option description",
0106 "Do not try to indirectly pair messages by merging catalogs."))
0107 opars.add_option(
0108 "-p", "--strip",
0109 metavar=_("@info command line value placeholder", "NUM"),
0110 dest="strip",
0111 help=_("@info command line option description",
0112 "Strip the smallest prefix containing NUM leading slashes from "
0113 "each file name found in the ediff file (like in patch(1)). "
0114 "If not given, only the base name of each file is taken."))
0115 opars.add_option(
0116 "-u", "--unembed",
0117 action="store_true", dest="unembed", default=False,
0118 help=_("@info command line option description",
0119 "Instead of applying a patch, resolve all embedded differences "
0120 "in given paths to newer versions of messages."))
0121
0122 (op, free_args) = opars.parse_args(str_to_unicode(sys.argv[1:]))
0123
0124 # Could use some speedup.
0125 try:
0126 import psyco
0127 psyco.full()
0128 except ImportError:
0129 pass
0130
0131 if not op.unembed:
0132 if free_args:
0133 error(_("@info",
0134 "Too many arguments in command line: %(argspec)s",
0135 argspec=" ".join(free_args)))
0136 if op.strip and not op.strip.isdigit():
0137 error(_("@info",
0138 "Option %(opt)s expects a positive integer value.",
0139 opt="--strip"))
0140 apply_ediff(op)
0141 else:
0142 paths = []
0143 for path in free_args:
0144 if not os.path.exists(path):
0145 warning(_("@info",
0146 "Path '%(path)s' does not exist.",
0147 path=path))
0148 if os.path.isdir(path):
0149 paths.extend(collect_catalogs(path))
0150 else:
0151 paths.append(path)
0152 for path in paths:
0153 unembed_ediff(path)
0154
0155
0156 def apply_ediff (op):
0157
0158 # Read the ediff PO.
0159 dummy_stream_path = "<stdin>"
0160 if op.input:
0161 if not os.path.isfile(op.input):
0162 error(_("@info",
0163 "Path '%(path)s' is not a file or does not exist.",
0164 path=op.input))
0165 edfpath = op.input
0166 readfh = None
0167 else:
0168 edfpath = dummy_stream_path
0169 readfh = sys.stdin
0170 try:
0171 ecat = Catalog(edfpath, monitored=False, readfh=readfh)
0172 except:
0173 error(_("@info ediff is shorthand for \"embedded difference\"",
0174 "Error reading ediff '%(file)s'.",
0175 file=edfpath))
0176
0177 # Split ediff by diffed catalog into original and new file paths,
0178 # header message, and ordinary messages.
0179 hmsgctxt = ecat.header.get_field_value(EDST.hmsgctxt_field)
0180 if hmsgctxt is None:
0181 error(_("@info",
0182 "Header field '%(field)s' is missing in the ediff.",
0183 field=EDST.hmsgctxt_field))
0184 edsplits = []
0185 cehmsg = None
0186 smsgid = "\x00"
0187 ecat.add_last(MessageUnsafe(dict(msgctxt=hmsgctxt, msgid=smsgid))) # sentry
0188 for emsg in ecat:
0189 if emsg.msgctxt == hmsgctxt:
0190 if cehmsg:
0191 # Record previous section.
0192 edsplits.append((fpaths, cehmsg, cemsgs))
0193 if emsg.msgid == smsgid: # end sentry, avoid parsing below
0194 break
0195
0196 # Mine original and new file paths out of header.
0197 fpaths = []
0198 for fpath in emsg.msgid.split("\n")[:2]:
0199 # Strip leading "+ "/"- "
0200 fpath = fpath[2:]
0201 # Convert to planform path separators.
0202 fpath = re.sub(r"/+", os.path.sep, fpath)
0203 # Remove revision indicator.
0204 p = fpath.find(EDST.filerev_sep)
0205 if p >= 0:
0206 fpath = fpath[:p]
0207 # Strip path and append directory as requested.
0208 if op.strip:
0209 preflen = int(op.strip)
0210 lst = fpath.split(os.path.sep, preflen)
0211 if preflen + 1 == len(lst):
0212 fpath = lst[preflen]
0213 else:
0214 fpath = os.path.basename(fpath)
0215 else:
0216 fpath = os.path.basename(fpath)
0217 if op.directory and fpath:
0218 fpath = os.path.join(op.directory, fpath)
0219 # All done.
0220 fpaths.append(fpath)
0221
0222 cehmsg = emsg
0223 cemsgs = []
0224 else:
0225 cemsgs.append(emsg)
0226
0227 # Prepare catalog for rejects and merges.
0228 rcat = Catalog("", create=True, monitored=False, wrapping=ecat.wrapping())
0229 init_ediff_header(rcat.header, hmsgctxt=hmsgctxt, extitle="rejects")
0230
0231 # Apply diff to catalogs.
0232 for fpaths, ehmsg, emsgs in edsplits:
0233 # Open catalog for patching.
0234 fpath1, fpath2 = fpaths
0235 if fpath1:
0236 # Diff from an existing catalog, open it.
0237 if not os.path.isfile(fpath1):
0238 warning(_("@info",
0239 "Path '%(path)s' is not a file or does not exist, "
0240 "skipping it.",
0241 path=fpath1))
0242 continue
0243 try:
0244 cat = Catalog(fpath1)
0245 except:
0246 warning(_("@info",
0247 "Error reading catalog '%(file)s', skipping it.",
0248 file=fpath1))
0249 continue
0250 elif fpath2:
0251 # New catalog added in diff, create it (or open if it exists).
0252 try:
0253 mkdirpath(os.path.dirname(fpath2))
0254 cat = Catalog(fpath2, create=True)
0255 if cat.created():
0256 cat.set_wrapping(ecat.wrapping())
0257 except:
0258 if os.path.isfile(fpath2):
0259 warning(_("@info",
0260 "Error reading catalog '%(file)s', skipping it.",
0261 file=fpath1))
0262 else:
0263 warning(_("@info",
0264 "Cannot create catalog '%(file)s', skipping it.",
0265 file=fpath2))
0266 continue
0267 else:
0268 error(_("@info",
0269 "Both catalogs in ediff indicated not to exist."))
0270
0271 # Do not try to patch catalog with embedded differences
0272 # (i.e. previously patched using -e).
0273 if cat.header.get_field_value(EDST.hmsgctxt_field) is not None:
0274 warning(_("@info",
0275 "Catalog '%(file)s' already contains "
0276 "embedded differences, skipping it.",
0277 file=cat.filename))
0278 continue
0279
0280 # Do not try to patch catalog if the patch contains
0281 # unresolved split differences.
0282 if reduce(lambda r, x: r or _flag_ediff_to_new in x.flag,
0283 emsgs, False):
0284 warning(_("@info",
0285 "Patch for catalog '%(file)s' contains unresolved "
0286 "split differences, skipping it.",
0287 file=cat.filename))
0288 continue
0289
0290 # Patch the catalog.
0291 rejected_ehmsg = patch_header(cat, ehmsg, ecat, op)
0292 rejected_emsgs_flags = patch_messages(cat, emsgs, ecat, op)
0293 any_rejected = rejected_ehmsg or rejected_emsgs_flags
0294 if fpath2 or any_rejected:
0295 created = cat.created()
0296 if cat.sync():
0297 if not created:
0298 if any_rejected and op.embed:
0299 report(_("@info:progress E is for \"with embedding\"",
0300 "Partially patched (E): %(file)s",
0301 file=cat.filename))
0302 elif any_rejected:
0303 report(_("@info:progress",
0304 "Partially patched: %(file)s",
0305 file=cat.filename))
0306 elif op.embed:
0307 report(_("@info:progress E is for \"with embedding\"",
0308 "Patched (E): %(file)s",
0309 file=cat.filename))
0310 else:
0311 report(_("@info:progress",
0312 "Patched: %(file)s",
0313 file=cat.filename))
0314 else:
0315 if op.embed:
0316 report(_("@info:progress E is for \"with embedding\"",
0317 "Created (E): %(file)s",
0318 file=cat.filename))
0319 else:
0320 report(_("@info:progress",
0321 "Created: %(file)s",
0322 file=cat.filename))
0323 else:
0324 pass #report("unchanged: %s" % cat.filename)
0325 else:
0326 os.unlink(fpath1)
0327 report(_("@info:progress",
0328 "Removed: %(file)s",
0329 file=fpath1))
0330
0331 # If there were any rejects and reembedding is not in effect,
0332 # record the necessary to present them.
0333 if any_rejected and not op.embed:
0334 if not rejected_ehmsg:
0335 # Clean header diff.
0336 ehmsg.manual_comment = ehmsg.manual_comment[:1]
0337 ehmsg.msgstr[0] = ""
0338 rcat.add_last(ehmsg)
0339 for emsg, flag in rejected_emsgs_flags:
0340 # Reembed to avoid any conflicts.
0341 msg1, msg2, msg1_s, msg2_s = resolve_diff_pair(emsg)
0342 emsg = msg_ediff(msg1_s, msg2_s,
0343 emsg=msg2_s, ecat=rcat, enoctxt=hmsgctxt)
0344 if flag:
0345 emsg.flag.add(flag)
0346 rcat.add_last(emsg)
0347
0348 # If there were any rejects, write them out.
0349 if len(rcat) > 0:
0350 # Construct paths for embedded diffs of rejects.
0351 rsuff = "rej"
0352 if ecat.filename != dummy_stream_path:
0353 rpath = ecat.filename
0354 p = rpath.rfind(".")
0355 if p < 0:
0356 p = len(rpath)
0357 rpath = rpath[:p] + (".%s" % rsuff) + rpath[p:]
0358 else:
0359 rpath = "stdin.%s.po" % rsuff
0360
0361 rcat.filename = rpath
0362 rcat.sync(force=True, noobsend=True)
0363 report(_("@info:progress file to which rejected parts of the patch "
0364 "have been written to",
0365 "*** Rejects: %(file)s",
0366 file=rcat.filename))
0367
0368
0369 # Patch application types.
0370 _pt_merge, _pt_insert, _pt_remove = list(range(3))
0371
0372 def patch_messages (cat, emsgs, ecat, options):
0373
0374 # It may happen that a single message from original catalog
0375 # is paired with more than one from the diff
0376 # (e.g. single old translated message going into two new fuzzy).
0377 # Therefore paired messages must be tracked, to know if patched
0378 # message can be merged into the existing, or it must be inserted.
0379 pmsgkeys = set()
0380
0381 # Triplets for splitting directly unapplicable patches into two.
0382 # Delay building of triplets until needed for the first time.
0383 striplets_pack = [None]
0384 def striplets ():
0385 if striplets_pack[0] is None:
0386 striplets_pack[0] = build_splitting_triplets(emsgs, cat, options)
0387 return striplets_pack[0]
0388
0389 # Check whether diffs apply, and where and how if they do.
0390 rejected_emsgs_flags = []
0391 patch_specs = []
0392 for emsg in emsgs:
0393 pspecs = msg_apply_diff(cat, emsg, ecat, pmsgkeys, striplets)
0394 for pspec in pspecs:
0395 emsg_m, flag = pspec[:2]
0396 if flag == _flag_ediff or options.embed:
0397 patch_specs.append(pspec)
0398 if flag != _flag_ediff:
0399 rejected_emsgs_flags.append((emsg_m, flag))
0400
0401 # Sort accepted patches by position of application.
0402 patch_specs.sort(key=lambda x: x[3])
0403
0404 # Add accepted patches to catalog.
0405 incpos = 0
0406 for emsg, flag, typ, pos, msg1, msg2, msg1_s, msg2_s in patch_specs:
0407 if pos is not None:
0408 pos += incpos
0409
0410 if options.embed:
0411 # Embedded diff may conflict one of the messages in catalog.
0412 # Make a new diff of special messages,
0413 # and embed them either into existing message in catalog,
0414 # or into new message.
0415 if typ == _pt_merge:
0416 tmsg = cat[pos]
0417 tpos = pos
0418 else:
0419 tmsg = MessageUnsafe(msg2 or {})
0420 tpos = None
0421 emsg = msg_ediff(msg1_s, msg2_s, emsg=tmsg, ecat=cat, eokpos=tpos)
0422
0423 if 0:pass
0424 elif typ == _pt_merge:
0425 if not options.embed:
0426 cat[pos].set_inv(msg2)
0427 else:
0428 cat[pos].flag.add(flag)
0429 elif typ == _pt_insert:
0430 if not options.embed:
0431 cat.add(Message(msg2), pos)
0432 else:
0433 cat.add(Message(emsg), pos)
0434 cat[pos].flag.add(flag)
0435 incpos += 1
0436 elif typ == _pt_remove:
0437 if pos is None:
0438 continue
0439 if not options.embed:
0440 cat.remove(pos)
0441 incpos -= 1
0442 else:
0443 cat[pos].flag.add(flag)
0444 else:
0445 error_on_msg(_("@info",
0446 "Unknown patch type %(type)s.",
0447 type=typ), emsg, ecat)
0448
0449 return rejected_emsgs_flags
0450
0451
0452 def msg_apply_diff (cat, emsg, ecat, pmsgkeys, striplets):
0453
0454 msg1, msg2, msg1_s, msg2_s = resolve_diff_pair(emsg)
0455
0456 # Try to select existing message from the original messages.
0457 # Order is important, should try first new, then old
0458 # (e.g. if an old fuzzy was resolved to new after diff was made).
0459 msg = None
0460 if msg2 and msg2 in cat:
0461 msg = cat[msg2]
0462 elif msg1 and msg1 in cat:
0463 msg = cat[msg1]
0464
0465 patch_specs = []
0466
0467 # Try to apply the patch.
0468 if msg_patchable(msg, msg1, msg2):
0469 # Patch can be directly applied.
0470 if msg1 and msg2:
0471 if msg.key not in pmsgkeys:
0472 typ = _pt_merge
0473 pos = cat.find(msg)
0474 pmsgkeys.add(msg.key)
0475 else:
0476 typ = _pt_insert
0477 pos, weight = cat.insertion_inquiry(msg2)
0478 elif msg2: # patch adds a message
0479 if msg:
0480 typ = _pt_merge
0481 pos = cat.find(msg)
0482 pmsgkeys.add(msg.key)
0483 else:
0484 typ = _pt_insert
0485 pos, weight = cat.insertion_inquiry(msg2)
0486 elif msg1: # patch removes a message
0487 if msg:
0488 typ = _pt_remove
0489 pos = cat.find(msg)
0490 pmsgkeys.add(msg.key)
0491 else:
0492 typ = _pt_remove
0493 pos = None # no position to remove from
0494 else:
0495 # Cannot happen.
0496 error_on_msg(_("@info",
0497 "Neither the old nor the new message "
0498 "in the diff is indicated to exist."),
0499 emsg, ecat)
0500 patch_specs.append((emsg, _flag_ediff, typ, pos,
0501 msg1, msg2, msg1_s, msg2_s))
0502 else:
0503 # Patch cannot be applied directly,
0504 # try to split into old-to-current and current-to-new diffs.
0505 split_found = False
0506 if callable(striplets):
0507 striplets = striplets() # delayed creation of splitting triplets
0508 for i in range(len(striplets)):
0509 m1_t, m1_ts, m2_t, m2_ts, m_t, m_ts1, m_ts2 = striplets[i]
0510 if msg1.inv == m1_t.inv and msg2.inv == m2_t.inv:
0511 striplets.pop(i) # remove to not slow further searches
0512 split_found = True
0513 break
0514 if split_found:
0515 # Construct new corresponding diffs.
0516 em_1c = msg_ediff(m1_ts, m_ts1, emsg=MessageUnsafe(m_t))
0517 em_c2 = msg_ediff(m_ts2, m2_ts, emsg=MessageUnsafe(m2_t))
0518 # Current-to-new can be merged or inserted,
0519 # and old-to-current is then inserted just before it.
0520 if m_t.key not in pmsgkeys:
0521 typ = _pt_merge
0522 pos = cat.find(m_t)
0523 pmsgkeys.add(m_t.key)
0524 else:
0525 typ = _pt_insert
0526 pos, weight = cat.insertion_inquiry(m2_t)
0527 # Order of adding patch specs here important for rejects file.
0528 patch_specs.append((em_1c, _flag_ediff_to_cur, _pt_insert, pos,
0529 m1_t, m_t, m1_ts, m_ts1))
0530 patch_specs.append((em_c2, _flag_ediff_to_new, typ, pos,
0531 m_t, m2_t, m_ts2, m2_ts))
0532
0533 # The patch is totally rejected.
0534 # Will be inserted if reembedding requested, so compute insertion.
0535 if not patch_specs:
0536 typ = _pt_insert
0537 if msg2 is not None:
0538 pos, weight = cat.insertion_inquiry(msg2)
0539 else:
0540 pos = len(cat)
0541 patch_specs.append((emsg, _flag_ediff_no_match, typ, pos,
0542 msg1, msg2, msg1_s, msg2_s))
0543
0544 return patch_specs
0545
0546
0547 def msg_patchable (msg, msg1, msg2):
0548
0549 # Check for cases where current message does not match old or new,
0550 # but there is a transformation that can also be cleanly merged.
0551 msg_m = msg
0552 if 0: pass
0553
0554 # Old and new are translated, but current is fuzzy and has previous fields.
0555 # Transform current to its previous state, from which it may have became
0556 # fuzzy by merging with templates.
0557 elif ( msg and msg.fuzzy and msg.key_previous is not None
0558 and msg1 and not msg1.fuzzy and msg2 and not msg2.fuzzy
0559 ):
0560 msg_m = MessageUnsafe(msg)
0561 msg_copy_fields(msg, msg_m, MPC.prevcurr_fields)
0562 msg_clear_prev_fields(msg_m)
0563 msg_m.fuzzy = False
0564
0565 # Old is None, new is translated, and current is untranslated.
0566 # Add translation of new to current, since it may have been added as
0567 # untranslated after merging with templates.
0568 elif msg and msg.untranslated and not msg1 and msg2 and msg2.translated:
0569 msg_m = MessageUnsafe(msg)
0570 msg_copy_fields(msg2, msg_m, ["msgstr"])
0571
0572 if msg1 and msg2:
0573 return msg and msg_m.inv in (msg1.inv, msg2.inv)
0574 elif msg2:
0575 return not msg or msg_m.inv == msg2.inv
0576 elif msg1:
0577 return not msg or msg_m.inv == msg1.inv
0578 else:
0579 return not msg
0580
0581
0582 def resolve_diff_pair (emsg):
0583
0584 # Recover old and new message according to diff.
0585 # Resolve into copies of ediff message, to preserve non-inv parts.
0586 emsg1 = MessageUnsafe(emsg)
0587 msg1_s = msg_ediff_to_old(emsg1, rmsg=emsg1)
0588 emsg2 = MessageUnsafe(emsg)
0589 msg2_s = msg_ediff_to_new(emsg2, rmsg=emsg2)
0590
0591 # Resolve any special pairings.
0592 msg1, msg2 = msg1_s, msg2_s
0593 if not msg1_s or not msg2_s:
0594 # No special cases if either message non-existant.
0595 pass
0596
0597 # Cases f-nf-*.
0598 elif msg1_s.fuzzy and not msg2_s.fuzzy:
0599 # Case f-nf-ecc.
0600 if ( msg2_s.key_previous is None
0601 and not msg_eq_fields(msg1_s, msg2_s, MPC.curr_fields)
0602 ):
0603 msg1 = MessageUnsafe(msg1_s)
0604 msg_copy_fields(msg1_s, msg1, MPC.currprev_fields)
0605 msg_copy_fields(msg2_s, msg1, MPC.curr_fields)
0606 # Case f-nf-necc.
0607 elif msg2_s.key_previous is not None:
0608 msg1 = MessageUnsafe(msg1_s)
0609 msg2 = MessageUnsafe(msg2_s)
0610 msg_copy_fields(msg2_s, msg1, MPC.prevcurr_fields)
0611 msg_clear_prev_fields(msg2)
0612
0613 # Cases nf-f-*.
0614 elif not msg1_s.fuzzy and msg2_s.fuzzy:
0615 # Case nf-f-ecp.
0616 if ( msg1_s.key_previous is None
0617 and not msg_eq_fields(msg1_s, msg2_s, MPC.curr_fields)
0618 ):
0619 msg2 = MessageUnsafe(msg2_s)
0620 msg_copy_fields(msg1_s, msg2, MPC.currprev_fields)
0621 # Case nf-f-necp.
0622 elif msg1_s.key_previous is not None:
0623 msg1 = MessageUnsafe(msg1_s)
0624 msg2 = MessageUnsafe(msg2_s)
0625 msg_copy_fields(msg1_s, msg2, MPC.prev_fields)
0626 msg_clear_prev_fields(msg1)
0627
0628 return msg1, msg2, msg1_s, msg2_s
0629
0630
0631 def build_splitting_triplets (emsgs, cat, options):
0632
0633 # Create catalogs of old and new messages.
0634 cat1 = Catalog("", create=True, monitored=False)
0635 cat2 = Catalog("", create=True, monitored=False)
0636 for emsg in emsgs:
0637 msg1, msg2, msg1_s, msg2_s = resolve_diff_pair(emsg)
0638 if msg1:
0639 cat1.add_last(msg1)
0640 if msg2:
0641 cat2.add_last(msg2)
0642 # Make headers same, to avoid any diffs there.
0643 cat1.header = cat.header
0644 cat2.header = cat.header
0645
0646 # Write created catalogs to disk if
0647 # msgmerge may be used on files during diffing.
0648 if options.do_merge:
0649 tmpfs = [] # to avoid garbage collection until the function returns
0650 for tcat, tsuff in ((cat1, "1"), (cat2, "2")):
0651 tmpf = NamedTemporaryFile(prefix="poepatch-split-%s-" % tsuff,
0652 suffix=".po")
0653 tmpfs.append(tmpf)
0654 tcat.filename = tmpf.name
0655 tcat.sync(force=True)
0656
0657 # Create the old-to-current and current-to-new diffs.
0658 ecat_1c = Catalog("", create=True, monitored=False)
0659 diff_cats(cat1, cat, ecat_1c, options.do_merge, wadd=False, wrem=False)
0660 ecat_c2 = Catalog("", create=True, monitored=False)
0661 diff_cats(cat, cat2, ecat_c2, options.do_merge, wadd=False, wrem=False)
0662
0663 # Mine splitting triplets out of diffs.
0664 sdoublets_1c = {}
0665 for emsg in ecat_1c:
0666 m1_t, m_t, m1_ts, m_ts1 = resolve_diff_pair(emsg)
0667 sdoublets_1c[m_t.key] = [m1_t, m1_ts, m_t, m_ts1]
0668 sdoublets_c2 = {}
0669 for emsg in ecat_c2:
0670 m_t, m2_t, m_ts2, m2_ts = resolve_diff_pair(emsg)
0671 sdoublets_c2[m_t.key] = [m_t, m_ts2, m2_t, m2_ts]
0672 common_keys = set(sdoublets_1c).intersection(sdoublets_c2)
0673 striplets = []
0674 for key in common_keys:
0675 m1_t, m1_ts, m_t, m_ts1 = sdoublets_1c[key]
0676 m_t, m_ts2, m2_t, m2_ts = sdoublets_c2[key]
0677 striplets.append((m1_t, m1_ts, m2_t, m2_ts, m_t, m_ts1, m_ts2))
0678
0679 return striplets
0680
0681
0682 def patch_header (cat, ehmsg, ecat, options):
0683
0684 if not ehmsg.msgstr[0]: # no header diff, only metadata
0685 return None
0686
0687 ehmsg_clean = clear_header_metadata(ehmsg)
0688
0689 # Create reduced headers.
0690 hmsg1 = msg_ediff_to_old(ehmsg_clean)
0691 hmsg2 = msg_ediff_to_new(ehmsg_clean)
0692 hmsg = not cat.created() and cat.header.to_msg() or None
0693 hdrs = []
0694 for m in (hmsg, hmsg1, hmsg2):
0695 h = m is not None and reduce_header_fields(Header(m)) or None
0696 hdrs.append(h)
0697 rhdr, rhdr1, rhdr2 = hdrs
0698
0699 # Decide if the header can be cleanly patched.
0700 clean = False
0701 if not rhdr:
0702 clean = rhdr1 or rhdr2
0703 else:
0704 clean = (rhdr1 and rhdr == rhdr1) or (rhdr2 and rhdr == rhdr2)
0705
0706 if clean:
0707 if not options.embed:
0708 if hmsg2:
0709 cat.header = Header(hmsg2)
0710 else:
0711 # Catalog will be removed if no messages are rejected,
0712 # and otherwise the header should stay as-is.
0713 pass
0714 else:
0715 if cat.created():
0716 cat.header = Header(hmsg2)
0717 ehmsg = MessageUnsafe(ehmsg)
0718 ehmsg.flag.add(_flag_ediff)
0719 hmsgctxt = get_msgctxt_for_headers(cat)
0720 ehmsg.msgctxt = hmsgctxt
0721 cat.header.set_field(EDST.hmsgctxt_field, hmsgctxt)
0722 cat.add(Message(ehmsg), 0)
0723 return None
0724 else:
0725 return ehmsg
0726
0727
0728 # Clear header diff message of metadata.
0729 # A copy of the message is returned.
0730 def clear_header_metadata (ehmsg):
0731
0732 ehmsg = MessageUnsafe(ehmsg)
0733 ehmsg.manual_comment.pop(0)
0734 ehmsg.msgctxt = None
0735 ehmsg.msgid = ""
0736
0737 return ehmsg
0738
0739
0740 # Remove known unimportant fields from the header,
0741 # to ignore them on comparisons.
0742 def reduce_header_fields (hdr):
0743
0744 rhdr = Header(hdr)
0745 for field in (
0746 "POT-Creation-Date",
0747 "PO-Revision-Date",
0748 "Last-Translator",
0749 "X-Generator",
0750 ):
0751 rhdr.remove_field(field)
0752
0753 return rhdr
0754
0755
0756 def unembed_ediff (path, all=False, old=False):
0757
0758 try:
0759 cat = Catalog(path)
0760 except:
0761 warning(_("@info",
0762 "Error reading catalog '%(file)s', skipping it.",
0763 file=path))
0764 return
0765
0766 hmsgctxt = cat.header.get_field_value(EDST.hmsgctxt_field)
0767 if hmsgctxt is not None:
0768 cat.header.remove_field(EDST.hmsgctxt_field)
0769
0770 uehmsg = None
0771 unembedded = {}
0772 for msg in cat:
0773 ediff_flag = None
0774 for flag in _flags_all:
0775 if flag in msg.flag:
0776 ediff_flag = flag
0777 msg.flag.remove(flag)
0778 if not ediff_flag and not all:
0779 continue
0780 if ediff_flag in (_flag_ediff_no_match, _flag_ediff_to_new):
0781 # Throw away fully rejected embeddings, i.e. reject the patch.
0782 # For split-difference embeddings, throw away the current-to-new;
0783 # this effectively rejects the patch, which is safest thing to do.
0784 cat.remove_on_sync(msg)
0785 elif hmsgctxt is not None and msg.msgctxt == hmsgctxt:
0786 if uehmsg:
0787 warning_on_msg(_("@info",
0788 "Unembedding results in duplicate header, "
0789 "previous header at %(line)d(#%(entry)d); "
0790 "skipping it.",
0791 line=uehmsg.refline, entry=uehmsg.refentry),
0792 msg, cat)
0793 return
0794 msg_ediff_to_x = not old and msg_ediff_to_new or msg_ediff_to_old
0795 hmsg = msg_ediff_to_x(clear_header_metadata(msg))
0796 if hmsg.msgstr and hmsg.msgstr[0]:
0797 cat.header = Header(hmsg)
0798 cat.remove_on_sync(msg)
0799 uehmsg = msg
0800 else:
0801 msg1, msg2, msg1_s, msg2_s = resolve_diff_pair(msg)
0802 tmsg = (not old and (msg2,) or (msg1,))[0]
0803 if tmsg is not None:
0804 if tmsg.key in unembedded:
0805 msg_p = unembedded[tmsg.key]
0806 warning_on_msg(_("@info",
0807 "Unembedding results in "
0808 "duplicate message, previous message "
0809 "at %(line)d(#%(entry)d); skipping it.",
0810 line=msg_p.refline, entry=msg_p.refentry),
0811 msg, cat)
0812 return
0813 msg.set(Message(msg2))
0814 unembedded[tmsg.key] = msg
0815 else:
0816 cat.remove_on_sync(msg)
0817
0818 if cat.sync():
0819 report(_("@info:progress",
0820 "Unembedded: %(file)s",
0821 file=cat.filename))
0822
0823
0824 if __name__ == '__main__':
0825 exit_on_exception(main)