File: merge-po-files

package info (click to toggle)
debian-reference 2.138
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 30,016 kB
  • sloc: python: 586; makefile: 575; sh: 528; sed: 80; xml: 36
file content (218 lines) | stat: -rwxr-xr-x 7,956 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/bin/bash
#
# Three-way merge driver for PO files, runs on multiple CPUs where possible
#
# Copyright 2015-2016 Marco Ciampa
# Copyright 2021 Mikko Rantalainen <mikko.rantalainen@iki.fi>
# License: MIT (https://opensource.org/licenses/MIT)
#
# Original source:
# https://stackoverflow.com/a/29535676/334451
# https://github.com/mezis/git-whistles/blob/master/libexec/git-merge-po.sh
#
# Install with
# git config merge.merge-po-files.driver "./bin/merge-po-files %A %O %B %P"
#
# Note that you also need file `.gitattributes` with following lines:
# (for debian-reference)
#
# po/*.po merge=merge-po-files
# po/*.pot merge=merge-po-files
#
##########################################################################
# CONFIG:

# Formatting flags to be be used to produce merged .po files
# This can be set to match project needs for the .po files.
# NOTE: $MSGCAT_FINAL_FLAGS will be passed to msgcat without quotation
MSGCAT_FINAL_FLAGS="--no-wrap --sort-output"

# Verbosity level:
# 0: Silent except for real errors
# 1: Show simple header for each file processed
# 2: Also show all conflicts in merge result (both new and existing)
# 3: Also show all status messages with timestamps
VERBOSITY="${VERBOSITY:=2}"

##########################################################################
# Implementation:

# Use logical names for arguments:
LOCAL="$1"
BASE="$2"
OTHER="$3"
FILENAME="$4"
OUTPUT="$LOCAL"

# The temporary directory for all files we need - note that most files are
# created without extensions to emit nicer conflict messages where gettext
# likes to embed the basename of the file in the conflict message so we
# use names like "local" and "other" instead of e.g. "local.G2wZ.po".
TEMP="$(mktemp -d /tmp/merge-po.XXXXXX)"

# abort on any error and report the details if possible
set -E
set -e
on_error() {
  local parent_lineno="$1"
  local message="$3"
  local code="$2"
  if [[ -n $message ]]; then
    printf "### $0: error near line %d: status %d: %s\n" "${parent_lineno}" "${code}" "${message}" 1>&2
  else
    printf "### $0: error near line %d: status %d\n" "${parent_lineno}" "${code}" 1>&2
  fi
  exit 255
}
trap 'on_error ${LINENO} $?' ERR

# Maybe print message(s) to stdout with timestamps
function status() {
  if test "$VERBOSITY" -ge 3; then
    printf "%s %s\n" "$(date '+%Y-%m-%d %H:%M:%S.%3N')" "$@"
  fi
}

# Quietly take translations from $1 and apply those according to template $2
# (and do not use fuzzy-matching, always generate output)
# also supports all flags to msgmerge
function apply_po_template() {
  msgmerge --force-po --quiet --no-fuzzy-matching "$@"
}

# Take stdin, remove the "graveyard strings" and emit the result to stdout
function strip_graveyard() {
  msgattrib --no-obsolete
}

# Take stdin, keep only confict lines and emit the result to stdout
function only_conflicts() {
  msggrep --msgstr -F -e '#-#-#-#-#' -
  # alternative slightly worse implementation: msgattrib --only-fuzzy
}

# Take stdin, discard confict lines and emit the result to stdout
function without_conflicts() {
  msggrep -v --msgstr -F -e '#-#-#-#-#' -
  # alternative slightly worse implementation: msgattrib --no-fuzzy
}

# Select messages from $1 that are also in $2 but whose contents have changed
# and emit results to stdout
function extract_changes() {
  # Extract conflicting changes and discard any changes to graveyard area only
  msgcat -o - "$1" "$2" |
    only_conflicts |
    apply_po_template -o - "$1" - |
    strip_graveyard
}

# Emit only the header of $1, supports flags of msggrep
function extract_header() {
  # Unfortunately gettext really doesn't support extracting just header
  # so we have to get creative: extract only strings that originate
  # from file called "//" which should result to header only
  msggrep --force-po -N // "$@"

  # Logically msggrep --force-po -v -K -E -e '.' should return the header
  # only but msggrep seems be buggy with msgids with line feeds and output
  # those, too
}

# Take file in $1 and show conflicts with colors in the file to stdout
function show_conflicts() {
  OUTPUT="$1"
  shift
  # Count number of lines to remove from the output and output conflict lines without the header
  CONFLICT_HEADER_LINES=$(cat "$OUTPUT" | msggrep --force-po --color=never --msgstr -F -e '#-#-#-#-#' - | extract_header - | wc -l)
  # tail wants line number of the first displayed line so we want +1 here:
  CONFLICTS=$(cat "$OUTPUT" | msggrep --force-po --color --msgstr -F -e '#-#-#-#-#' - | tail -n "+$((CONFLICT_HEADER_LINES + 1))")
  if test -n "$CONFLICTS"; then
    #echo "----------------------------"
    #echo "Conflicts after merge:"
    echo "----------------------------"
    printf "%s\n" "$CONFLICTS"
    echo "----------------------------"
  fi
}

# Sanity check that we have a sensible temporary directory
test -n "$TEMP" || exit 125
test -d "$TEMP" || exit 126
test -w "$TEMP" || exit 127

if test "$VERBOSITY" -ge 1; then
  printf "Using gettext .PO merge driver: %s ...\n" "$FILENAME"
fi

# Extract the PO header from the current branch (top of file until first empty line)
extract_header -o "${TEMP}/header" "$LOCAL"

##########################################################################
# Following parts can be run partially parallel and "wait" is used to syncronize processing

# Clean input files and use logical filenames for possible conflict markers:
status "Canonicalizing input files ..."
msguniq --force-po -o "${TEMP}/base" --unique "${BASE}" &
msguniq --force-po -o "${TEMP}/local" --unique "${LOCAL}" &
msguniq --force-po -o "${TEMP}/other" --unique "${OTHER}" &
wait

status "Computing local-changes, other-changes and unchanged ..."
msgcat --force-po -o - "${TEMP}/base" "${TEMP}/local" "${TEMP}/other" | without_conflicts >"${TEMP}/unchanged" &
extract_changes "${TEMP}/local" "${TEMP}/base" >"${TEMP}/local-changes" &
extract_changes "${TEMP}/other" "${TEMP}/base" >"${TEMP}/other-changes" &
wait

# Messages changed on both local and other (conflicts):
status "Computing conflicts ..."
msgcat --force-po -o - "${TEMP}/other-changes" "${TEMP}/local-changes" | only_conflicts >"${TEMP}/conflicts"

# Messages changed on local, not on other; and vice-versa:
status "Computing local-only and other-only changes ..."
msgcat --force-po -o "${TEMP}/local-only" --unique "${TEMP}/local-changes" "${TEMP}/conflicts" &
msgcat --force-po -o "${TEMP}/other-only" --unique "${TEMP}/other-changes" "${TEMP}/conflicts" &
wait

# Note: following steps require sequential processing and cannot be run in parallel

status "Computing initial merge without template ..."
# Note that we may end up with some extra so we have to apply template later
msgcat --force-po -o "${TEMP}/merge1" "${TEMP}/unchanged" "${TEMP}/conflicts" "${TEMP}/local-only" "${TEMP}/other-only"

# Create a template to only output messages that are actually needed (union of messages on local and other create the template!)
status "Computing template and applying it to merge result ..."
msgcat --force-po -o - "${TEMP}/local" "${TEMP}/other" | apply_po_template -o "${TEMP}/merge2" "${TEMP}/merge1" -

# Final merge result is merge2 with original header
status "Fixing the header after merge ..."
msgcat --force-po $MSGCAT_FINAL_FLAGS -o "${TEMP}/merge3" --use-first "${TEMP}/header" "${TEMP}/merge2"

# Produce output file (overwrites input LOCAL file because git expects that for the results)
status "Saving output ..."
mv "${TEMP}/merge3" "$OUTPUT"

status "Cleaning up ..."

rm "${TEMP}"/*
rmdir "${TEMP}"

status "Checking for conflicts in the result ..."

# Check for conflicts in the final merge
if grep -q '#-#-#-#-#' "$OUTPUT"; then
  if test "$VERBOSITY" -ge 1; then
    printf "### Conflict(s) detected ###\n"
  fi

  if test "$VERBOSITY" -ge 2; then
    # Verbose diagnostics
    show_conflicts "$OUTPUT"
  fi

  status "Automatic merge failed, exiting with status 1."
  exit 1
fi

status "Automatic merge completed successfully, exiting with status 0."
exit 0