¿Dónde está el controller de combinación Git de 3 vías para files .PO (gettext)?

Ya tengo lo siguiente

[attr]POFILE merge=merge-po-files locale/*.po POFILE 

en .gitattributes y me gustaría fusionar twigs para que funcionen correctamente cuando el mismo file de localización (ej. locale/en.po ) ha sido modificado en las twigs del paraller. Actualmente estoy usando el siguiente controller de combinación:

 #!/bin/bash # git merge driver for .PO files (gettext localizations) # Install: # git config merge.merge-po-files.driver "./bin/merge-po-files %A %O %B" LOCAL="${1}._LOCAL_" BASE="${2}._BASE_" REMOTE="${3}._REMOTE_" # rename to bit more meaningful filenames to get better conflict results cp "${1}" "$LOCAL" cp "${2}" "$BASE" cp "${3}" "$REMOTE" # merge files and overwrite local file with the result msgcat "$LOCAL" "$BASE" "$REMOTE" -o "${1}" || exit 1 # cleanup rm -f "$LOCAL" "$BASE" "$REMOTE" # check if merge has conflicts fgrep -q '#-#-#-#-#' "${1}" && exit 1 # if we get here, merge is successful exit 0 

Sin embargo, el msgcat es demasiado tonto y esta no es una verdadera combinación de tres vías . Por ejemplo, si tengo

  1. Versión BASE

     msgid "foo" msgstr "foo" 
  2. Versión LOCAL

     msgid "foo" msgstr "bar" 
  3. Versión REMOTA

     msgid "foo" msgstr "foo" 

Terminaré con un conflicto. Sin embargo, un verdadero controller de combinación de tres vías produciría la fusión correcta :

 msgid "foo" msgstr "bar" 

Tenga en count que no puedo simplemente agregar --use-first a msgcat porque REMOTE podría contener la traducción actualizada. Además, si BASE, LOCAL y REMOTO son únicos, todavía quiero un conflicto, porque eso realmente sería un conflicto.

¿Qué necesito cambiar para que esto funcione? Puntos de bonificación por marcador de conflicto less insano que '# – # – # – # – #', si es posible.

Tomando algo de inspiración de la respuesta de Mikko, hemos agregado una fusión completa de 3 vías a la gem de Ruby de git-whistles .

No depende de git-merge o reescritura de cadenas con Perl, y solo manipula files PO con herramientas de Gettext.

Aquí está el código (licencia MIT):

 #!/bin/sh # # Three-way merge driver for PO files # set -e # failure handler on_error() { local parent_lineno="$1" local message="$2" local code="${3:-1}" if [[ -n "$message" ]] ; then echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}" else echo "Error on or near line ${parent_lineno}; exiting with status ${code}" fi exit 255 } trap 'on_error ${LINENO}' ERR # given a file, find the path that matches its contents show_file() { hash=`git hash-object "${1}"` git ls-tree -r HEAD | fgrep "$hash" | cut -b54- } # wraps msgmerge with default options function m_msgmerge() { msgmerge --force-po --quiet --no-fuzzy-matching $@ } # wraps msgcat with default options function m_msgcat() { msgcat --force-po $@ } # removes the "graveyard strings" from the input function strip_graveyard() { sed -e '/^#~/d' } # select messages with a conflict marker # pass -v to inverse selection function grep_conflicts() { msggrep $@ --msgstr -F -e '#-#-#' - } # select messages from $1 that are also in $2 but whose contents have changed function extract_changes() { msgcat -o - $1 $2 \ | grep_conflicts \ | m_msgmerge -o - $1 - \ | strip_graveyard } BASE=$1 LOCAL=$2 REMOTE=$3 OUTPUT=$LOCAL TEMP=`mktemp /tmp/merge-po.XXXX` echo "Using custom PO merge driver (`show_file ${LOCAL}`; $TEMP)" # Extract the PO header from the current branch (top of file until first empty line) sed -e '/^$/q' < $LOCAL > ${TEMP}.header # clean input files msguniq --force-po -o ${TEMP}.base --unique ${BASE} msguniq --force-po -o ${TEMP}.local --unique ${LOCAL} msguniq --force-po -o ${TEMP}.remote --unique ${REMOTE} # messages changed on local extract_changes ${TEMP}.local ${TEMP}.base > ${TEMP}.local-changes # messages changed on remote extract_changes ${TEMP}.remote ${TEMP}.base > ${TEMP}.remote-changes # unchanged messages m_msgcat -o - ${TEMP}.base ${TEMP}.local ${TEMP}.remote \ | grep_conflicts -v \ > ${TEMP}.unchanged # messages changed on both local and remote (conflicts) m_msgcat -o - ${TEMP}.remote-changes ${TEMP}.local-changes \ | grep_conflicts \ > ${TEMP}.conflicts # messages changed on local, not on remote; and vice-versa m_msgcat -o ${TEMP}.local-only --unique ${TEMP}.local-changes ${TEMP}.conflicts m_msgcat -o ${TEMP}.remote-only --unique ${TEMP}.remote-changes ${TEMP}.conflicts # the big merge m_msgcat -o ${TEMP}.merge1 ${TEMP}.unchanged ${TEMP}.conflicts ${TEMP}.local-only ${TEMP}.remote-only # create a template to filter messages actually needed (those on local and remote) m_msgcat -o - ${TEMP}.local ${TEMP}.remote \ | m_msgmerge -o ${TEMP}.merge2 ${TEMP}.merge1 - # final merge, adds saved header m_msgcat -o ${TEMP}.merge3 --use-first ${TEMP}.header ${TEMP}.merge2 # produce output file (overwrites input LOCAL file) cat ${TEMP}.merge3 > $OUTPUT # check for conflicts if grep '#-#' $OUTPUT > /dev/null ; then echo "Conflict(s) detected" echo " between ${TEMP}.local and ${TEMP}.remote" exit 1 fi rm -f ${TEMP}* exit 0 

Aquí hay un controller de ejemplo un poco complejo que parece producir fusión correcta que puede contener algunas traducciones que deberían haber sido eliminadas por versión local o remota.
No debería faltar nada por lo que este controller solo agrega algo de desorder extra en algunos casos.

Esta versión usa el marcador de conflicto nativo gettext que se ve como #-#-#-#-# combinado con el indicador fuzzy lugar de los marcadores de conflicto git normales.
El controller es un poco feo para solucionar errores (o funciones ) en msgcat y msguniq :

 #!/bin/bash # git merge driver for .PO files # Copyright (c) Mikko Rantalainen <mikko.rantalainen@peda.net>, 2013 # License: MIT ORIG_HASH=$(git hash-object "${1}") WORKFILE=$(git ls-tree -r HEAD | fgrep "$ORIG_HASH" | cut -b54-) echo "Using custom merge driver for $WORKFILE..." LOCAL="${1}._LOCAL_" BASE="${2}._BASE_" REMOTE="${3}._REMOTE_" LOCAL_ONELINE="$LOCAL""ONELINE_" BASE_ONELINE="$BASE""ONELINE_" REMOTE_ONELINE="$REMOTE""ONELINE_" OUTPUT="$LOCAL""OUTPUT_" MERGED="$LOCAL""MERGED_" MERGED2="$LOCAL""MERGED2_" TEMPLATE1="$LOCAL""TEMPLATE1_" TEMPLATE2="$LOCAL""TEMPLATE2_" FALLBACK_OBSOLETE="$LOCAL""FALLBACK_OBSOLETE_" # standardize the input files for regexping # default to UTF-8 in case charset is still the placeholder "CHARSET" cat "${1}" | perl -npe 's!(^"Content-Type: text/plain; charset=)(CHARSET)(\\n"$)!$1UTF-8$3!' | msgcat --no-wrap --sort-output - > "$LOCAL" cat "${2}" | perl -npe 's!(^"Content-Type: text/plain; charset=)(CHARSET)(\\n"$)!$1UTF-8$3!' | msgcat --no-wrap --sort-output - > "$BASE" cat "${3}" | perl -npe 's!(^"Content-Type: text/plain; charset=)(CHARSET)(\\n"$)!$1UTF-8$3!' | msgcat --no-wrap --sort-output - > "$REMOTE" # convert each definition to single line presentation # extra fill is requinetworking to make sure that git separates each conflict perl -npe 'BEGIN {$/ = "\n\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$LOCAL" > "$LOCAL_ONELINE" perl -npe 'BEGIN {$/ = "\n\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$BASE" > "$BASE_ONELINE" perl -npe 'BEGIN {$/ = "\n\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$REMOTE" > "$REMOTE_ONELINE" # merge files using normal git merge machinery git merge-file -p --union -L "Current (working directory)" -L "Base (common ancestor)" -L "Incoming (applied changeset)" "$LOCAL_ONELINE" "$BASE_ONELINE" "$REMOTE_ONELINE" > "$MERGED" MERGESTATUS=$? # remove possibly duplicated headers (workaround msguniq bug http://comments.gmane.org/gmane.comp.gnu.gettext.bugs/96) cat "$MERGED" | perl -npe 'BEGIN {$/ = "\n\n"}; s/^([^\n]+#nmsgid ""#nmsgstr ""#n.*?\n)([^\n]+#nmsgid ""#nmsgstr ""#n.*?\n)+/$1/gs' > "$MERGED2" # remove lines that have totally empty msgstr # and convert back to normal PO file representation cat "$MERGED2" | grep -v '#nmsgstr ""$' | grep -v '^#fill#$' | perl -npe 's/#n/\n/g; s/##/#/g' > "$MERGED" # run the output through msguniq to merge conflicts gettext style # msguniq seems to have a bug that causes empty output if zero msgids # are found after the header. Expected output would be the header... # Workaround the bug by adding an empty obsolete fallback msgid # that will be automatically removed by msguniq cat > "$FALLBACK_OBSOLETE" << 'EOF' #~ msgid "obsolete fallback" #~ msgstr "" EOF cat "$MERGED" "$FALLBACK_OBSOLETE" | msguniq --no-wrap --sort-output > "$MERGED2" # create a hacked template from default merge between 3 versions # we do this to try to preserve original file ordering msgcat --use-first "$LOCAL" "$REMOTE" "$BASE" > "$TEMPLATE1" msghack --empty "$TEMPLATE1" > "$TEMPLATE2" msgmerge --silent --no-wrap --no-fuzzy-matching "$MERGED2" "$TEMPLATE2" > "$OUTPUT" # show some results to stdout if grep -q '#-#-#-#-#' "$OUTPUT" then FUZZY=$(cat "$OUTPUT" | msgattrib --only-fuzzy --no-obsolete --color | perl -npe 'BEGIN{ undef $/; }; s/^.*?msgid "".*?\n\n//s') if test -n "$FUZZY" then echo "-------------------------------" echo "Fuzzy translations after merge:" echo "-------------------------------" echo "$FUZZY" echo "-------------------------------" fi fi # git merge driver must overwrite the first parameter with output mv "$OUTPUT" "${1}" # cleanup rm -f "$LOCAL" "$BASE" "$REMOTE" "$LOCAL_ONELINE" "$BASE_ONELINE" "$REMOTE_ONELINE" "$MERGED" "$MERGED2" "$TEMPLATE1" "$TEMPLATE2" "$FALLBACK_OBSOLETE" # return conflict if merge has conflicts according to msgcat/msguniq grep -q '#-#-#-#-#' "${1}" && exit 1 # otherwise, return git merge status exit $MERGESTATUS # Steps to install this driver: # (1) Edit ".git/config" in your repository directory # (2) Add following section: # # [merge "merge-po-files"] # name = merge po-files driver # driver = ./bin/merge-po-files %A %O %B # recursive = binary # # or # # git config merge.merge-po-files.driver "./bin/merge-po-files %A %O %B" # # The file ".gitattributes" will point git to use this merge driver. 

Breve explicación sobre este controller:

  • Convierte el formatting de file PO ordinario a formatting de línea única donde cada línea es una input de traducción.
  • Luego utiliza el git merge-file --union regular git merge-file --union para hacer la fusión y después de la fusión el formatting de línea único resultante se convierte de nuevo al formatting de file PO ordinario.
    La resolución real del conflicto se realiza después de esto usando msguniq ,
  • y luego finalmente fusiona el file resultante con la plantilla generada por msgcat regular combinando files de input originales para restaurar metadatos posiblemente perdidos.

Advertencia: este controller usará msgcat --no-wrap en el file .PO y forzará la UTF-8 si no se especifica la encoding real.
Si desea utilizar este controller de fusión pero inspeccione los resultados siempre, cambie la exit $MERGESTATUS final exit $MERGESTATUS para que parezca exit 1 .

Después de combinar el conflicto de este controller, el mejor método para solucionar el conflicto es abrir el file en conflicto con virtaal y seleccionar Navigation: Incomplete .
Encuentro que esta interfaz de usuario es una herramienta muy buena para solucionar el conflicto.

Aquí hay un controller de ejemplo que corrige la diferencia basada en text con marcadores de conflicto en lugares correctos. Sin embargo, en caso de conflicto, git mergetool seguramente arruinará los resultados, por lo que no es realmente bueno. Si quieres arreglar fusiones conflictivas usando solo un editor de text, entonces esto debería estar bien:

 #!/bin/bash # git merge driver for .PO files # Copyright (c) Mikko Rantalainen <mikko.rantalainen@peda.net>, 2013 # License: MIT LOCAL="${1}._LOCAL_" BASE="${2}._BASE_" REMOTE="${3}._REMOTE_" MERGED="${1}._MERGED_" OUTPUT="$LOCAL""OUTPUT_" LOCAL_ONELINE="$LOCAL""ONELINE_" BASE_ONELINE="$BASE""ONELINE_" REMOTE_ONELINE="$REMOTE""ONELINE_" # standardize the input files for regexping msgcat --no-wrap --strict --sort-output "${1}" > "$LOCAL" msgcat --no-wrap --strict --sort-output "${2}" > "$BASE" msgcat --no-wrap --strict --sort-output "${3}" > "$REMOTE" # convert each definition to single line presentation # extra fill is requinetworking to make sure that git separates each conflict perl -npe 'BEGIN {$/ = "#\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$LOCAL" > "$LOCAL_ONELINE" perl -npe 'BEGIN {$/ = "#\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$BASE" > "$BASE_ONELINE" perl -npe 'BEGIN {$/ = "#\n"}; s/#\n$/\n/s; s/#/##/sg; s/\n/#n/sg; s/#n$/\n/sg; s/#n$/\n/sg; $_.="#fill#\n" x 4' "$REMOTE" > "$REMOTE_ONELINE" # merge files using normal git merge machinery git merge-file -p -L "Current (working directory)" -L "Base (common ancestor)" -L "Incoming (another change)" "$LOCAL_ONELINE" "$BASE_ONELINE" "$REMOTE_ONELINE" > "$MERGED" MERGESTATUS=$? # convert back to normal PO file representation cat "$MERGED" | grep -v '^#fill#$' | perl -npe 's/#n/\n/g; s/##/#/g' > "$OUTPUT" # git merge driver must overwrite the first parameter with output mv "$OUTPUT" "${1}" # cleanup rm -f "$LOCAL" "$BASE" "$REMOTE" "$LOCAL_ONELINE" "$BASE_ONELINE" "$REMOTE_ONELINE" "$MERGED" exit $MERGESTATUS # Steps to install this driver: # (1) Edit ".git/config" in your repository directory # (2) Add following section: # # [merge "merge-po-files"] # name = merge po-files driver # driver = ./bin/merge-po-files %A %O %B # recursive = binary # # or # # git config merge.merge-po-files.driver "./bin/merge-po-files %A %O %B" # # The file ".gitattributes" will point git to use this merge driver. 

Breve explicación sobre este controller: convierte el formatting de file PO ordinario a formatting de línea única donde cada línea es una input de traducción. Luego usa el git merge-file para hacer la fusión y luego de la fusión, el formatting de una sola línea resultante se convierte de nuevo al formatting de file PO ordinario. Advertencia: este controller usará msgcat --sort-output en el file .PO por lo que si desea que sus files PO se encuentren en algún order específico, puede que esta no sea la herramienta adecuada para usted.