localization - Where's the 3-way Git merge driver for .PO (gettext) files? -
i have following
[attr]pofile merge=merge-po-files locale/*.po pofile
in .gitattributes
, i'd merging of branches work correctly when same localization file (e.g. locale/en.po
) has been modified in paraller branches. i'm using following merge driver:
#!/bin/bash # git merge driver .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 bit more meaningful filenames better conflict results cp "${1}" "$local" cp "${2}" "$base" cp "${3}" "$remote" # merge files , overwrite local file 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 here, merge successful exit 0
however, msgcat
dumb , this not true 3 way merge. example, if have
base version
msgid "foo" msgstr "foo"
local version
msgid "foo" msgstr "bar"
remote version
msgid "foo" msgstr "foo"
i'll end conflict. however, a true 3 way merge driver output correct merge:
msgid "foo" msgstr "bar"
note cannot add --use-first
msgcat
because remote contain updated translation. in addition, if base, local , remote unique, still want conflict, because conflict.
what need change make work? bonus points less insane conflict marker '#-#-#-#-#', if possible.
taking inspiration mikko's answer, we've added full-fledged 3-way merger git-whistles
ruby gem.
it doesn't rely of git-merge
or rewriting string perl, , manipulates po files gettext tools.
here's code (mit licensed):
#!/bin/sh # # three-way merge driver po files # set -e # failure handler on_error() { local parent_lineno="$1" local message="$2" local code="${3:-1}" if [[ -n "$message" ]] ; echo "error on or near line ${parent_lineno}: ${message}; exiting status ${code}" else echo "error on or near line ${parent_lineno}; exiting status ${code}" fi exit 255 } trap 'on_error ${lineno}' err # given file, find path matches contents show_file() { hash=`git hash-object "${1}"` git ls-tree -r head | fgrep "$hash" | cut -b54- } # wraps msgmerge default options function m_msgmerge() { msgmerge --force-po --quiet --no-fuzzy-matching $@ } # wraps msgcat default options function m_msgcat() { msgcat --force-po $@ } # removes "graveyard strings" input function strip_graveyard() { sed -e '/^#~/d' } # select messages conflict marker # pass -v inverse selection function grep_conflicts() { msggrep $@ --msgstr -f -e '#-#-#' - } # select messages $1 in $2 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 po header 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 , remote (conflicts) m_msgcat -o - ${temp}.remote-changes ${temp}.local-changes \ | grep_conflicts \ > ${temp}.conflicts # messages changed on local, not on remote; , 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 # big merge m_msgcat -o ${temp}.merge1 ${temp}.unchanged ${temp}.conflicts ${temp}.local-only ${temp}.remote-only # create template filter messages needed (those on local , 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 conflicts if grep '#-#' $output > /dev/null ; echo "conflict(s) detected" echo " between ${temp}.local , ${temp}.remote" exit 1 fi rm -f ${temp}* exit 0