File indexing completed on 2024-12-08 13:32:00

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] &lt; 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 (Часлав Илић) &lt;%(email)s&gt;",
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)