5 # script to compare changes between translation files before merging them
8 # ./diff_po.pl cs.po.old cs.po
9 # svn diff -r38367 --diff-cmd ./diff_po.pl cs.po
10 # git difftool --extcmd=./diff_po.pl sk.po
11 # ./diff_po.pl -rHEAD~100 cs.po #fetch git revision and compare
12 # ./diff_po.pl -r39229 cs.po #fetch svn revision and compare
13 # ./diff_po.pl -r-1 cs.po #fetch the previous change of cs.po and compare
15 # This file is free software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public
17 # License as published by the Free Software Foundation; either
18 # version 2 of the License, or (at your option) any later version.
20 # This software is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23 # General Public License for more details.
25 # You should have received a copy of the GNU General Public
26 # License along with this software; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
29 # Copyright (c) 2010-2013 Kornel Benko, kornel@lyx.org
32 # 1.) Search for good correlations of deleted <==> inserted string
33 # using Text::Levenshtein or Algorithm::Diff
37 # fuzzyopt: '--display-fuzzy=' val ;
39 # untranslatedopt: '--display-untranslated=' val ;
44 # options: | options option
47 # revspec: revision-tag # e.g. 46c00bab7
48 # | 'HEAD' relative-rev # e.g. HEAD~3, HEAD-3
49 # | '-' number # -1 == previous commit of the following po-file
52 # revision: '-r' revspec ;
54 # filespecold: revision | filespec ;
56 # filespec: # path to existing po-file
58 # filespecnew: filespec ;
60 # files: filespecold filespecnew ;
62 # diff: 'diff_po.pl' ' ' options files
67 my $p = File::Spec->rel2abs( __FILE__ );
68 $p =~ s/[\/\\]?[^\/\\]+$//;
74 sub buildParentDir($$);
77 sub check_po_file_readable($$);
80 sub printExtraMessages($$$);
86 use Term::ANSIColor qw(:constants);
88 use Cwd qw(abs_path getcwd);
90 my ($status, $foundline, $msgid, $msgstr, $fuzzy);
92 my %Messages = (); # Used for original po-file
93 my %newMessages = (); # new po-file
94 my %Untranslated = (); # inside new po-file
95 my %Fuzzy = (); # inside new po-file
96 my $result = 0; # exit value
100 "--display-fuzzy" => 1,
101 "--display-untranslated" => 1,
110 return undef if ($e !~ s/^\-\-//);
113 return "DIFF_PO_" . $e;
116 # Set option-defaults from environment
117 # git: not needed, diff is not recursive here
118 # svn: needed to pass options through --diff-cmd parameter
119 # hg: needed to pass options through extdiff parameter
120 for my $opt (keys %options) {
121 my $e = get_env_name($opt);
123 if (defined($ENV{$e})) {
124 $options{$opt} = $ENV{$e};
129 while (($opt=$ARGV[0]) =~ s/=(\d+)$//) {
131 if (defined($options{$opt})) {
132 $options{$opt} = $val;
133 my $e = get_env_name($opt);
140 die("illegal option \"$opt\"\n");
143 # Check first, if called as standalone program for git
144 if ($ARGV[0] =~ /^-r(.*)/) {
150 # convert arguments to full path ...
151 for my $argf1 (@ARGV) {
152 $argf1 = abs_path($argf1);
154 for my $argf (@ARGV) {
155 #my $argf = abs_path($argf1);
158 if ($argf =~ /^(.*)\/([^\/]+)$/) {
161 chdir($filedir); # set working directory for the repo-command
168 my ($repo, $level) = searchRepo($filedir);
169 my $relargf = $baseargf; # argf relative to the top-most repo directory
171 if (defined($level)) {
172 my $abspathpo = $filedir; # directory of the po-file
173 $topdir = $abspathpo;
174 #print "Level = $level, abs path = $abspathpo\n";
176 $topdir =~ s/\/([^\/]+)$//;
177 $relargf = "$1/$relargf";
179 #print "Level = $level, topdir = $topdir, rel path = $relargf\n";
184 print "Could not find the repo-type\n";
188 check_po_file_readable($baseargf, $relargf);
189 if ($repo eq ".git") {
191 my $tmpfile = File::Temp->new();
192 $rev = getrev($repo, $rev, $argf);
193 push(@args, "-L", $argf . " (" . $rev . ")");
194 push(@args, "-L", $argf . " (local copy)");
195 print "git show $rev:$relargf\n";
196 open(FI, "git show $rev:$relargf|");
197 $tmpfile->unlink_on_destroy( 1 );
198 while(my $l = <FI>) {
202 $tmpfile->seek( 0, SEEK_END ); # Flush()
203 push(@args, $tmpfile->filename, $argf);
204 print "===================================================================\n";
207 elsif ($repo eq ".svn") {
208 # program svnversion needed here
209 $rev = getrev($repo, $rev, $argf);
210 # call it again indirectly
211 my @cmd = ("svn", "diff", "-r$rev", "--diff-cmd", $0, $relargf);
212 print "cmd = " . join(' ', @cmd) . "\n";
215 elsif ($repo eq ".hg") {
216 # for this to work, one has to edit ~/.hgrc
221 $rev = getrev($repo, $rev, $argf);
222 my @cmd = ("hg", "extdiff", "-r", "$rev", "-p", $0, $relargf);
223 print "cmd = " . join(' ', @cmd) . "\n";
233 #########################################################
235 # This routine builds n-th parent-path
236 # E.g. buildParentDir("abc", 1) --> "abc/.."
237 # buildParentDir("abc", 4) --> "abc/../../../.."
238 sub buildParentDir($$)
240 my ($dir, $par) = @_;
242 return buildParentDir("$dir/..", $par-1);
249 # Tries up to 10 parent levels to find the repo-type
250 # Returns the repo-type
254 for my $parent ( 0 .. 10 ) {
255 my $f = buildParentDir($dir, $parent);
256 for my $s (".git", ".svn", ".hg") {
258 #print "Found repo on level $parent\n";
259 return ($s, $parent);
263 return(""); # not found
275 while(defined($args[0])) {
276 last if ($args[0] !~ /^\-/);
277 my $param = shift(@args);
278 if ($param eq "-L") {
279 my $name = shift(@args);
283 # ignore other options
286 if (! defined($names[0])) {
287 push(@names, "original");
289 if (! defined($names[1])) {
294 die("names = \"", join('" "', @names) . "\"... args = \"" . join('" "', @args) . "\" Expected exactly 2 parameters");
297 check_po_file_readable($names[0], $args[0]);
298 check_po_file_readable($names[1], $args[1]);
300 parse_po_file($args[0], %Messages);
301 parse_po_file($args[1], %newMessages);
303 my @MsgKeys = getLineSortedKeys(%newMessages);
305 print RED "<<< \"$names[0]\"\n", RESET;
306 print GREEN ">>> \"$names[1]\"\n", RESET;
307 for my $k (@MsgKeys) {
308 if ($newMessages{$k}->{msgstr} eq "") {
309 # this is still untranslated string
310 $Untranslated{$newMessages{$k}->{line}} = $k;
312 elsif ($newMessages{$k}->{fuzzy}) {
314 # mark only, if not in alternative area
315 if (! $newMessages{$k}->{alternative}) {
316 $Fuzzy{$newMessages{$k}->{line}} = $k;
319 if (exists($Messages{$k})) {
320 printIfDiff($k, $Messages{$k}, $newMessages{$k});
321 delete($Messages{$k});
322 delete($newMessages{$k});
327 @MsgKeys = sort keys %Messages, keys %newMessages;
328 for my $k (@MsgKeys) {
329 if (defined($Messages{$k})) {
331 print "deleted message\n";
332 print "< line = " . $Messages{$k}->{line} . "\n" if ($printlines);
333 print RED "< fuzzy = " . $Messages{$k}->{fuzzy} . "\n", RESET;
334 print RED "< msgid = \"$k\"\n", RESET;
335 print RED "< msgstr = \"" . $Messages{$k}->{msgstr} . "\"\n", RESET;
337 if (defined($newMessages{$k})) {
339 print "new message\n";
340 print "> line = " . $newMessages{$k}->{line} . "\n" if ($printlines);
341 print GREEN "> fuzzy = " . $newMessages{$k}->{fuzzy} . "\n", RESET;
342 print GREEN "> msgid = \"$k\"\n", RESET;
343 print GREEN "> msgstr = \"" . $newMessages{$k}->{msgstr} . "\"\n", RESET;
348 @MsgKeys = getLineSortedKeys(%Messages);
349 for my $k (@MsgKeys) {
351 print "deleted message\n";
352 print "< line = " . $Messages{$k}->{line} . "\n" if ($printlines);
353 print RED "< fuzzy = " . $Messages{$k}->{fuzzy} . "\n", RESET;
354 print RED "< msgid = \"$k\"\n", RESET;
355 print RED "< msgstr = \"" . $Messages{$k}->{msgstr} . "\"\n", RESET;
358 @MsgKeys = getLineSortedKeys(%newMessages);
359 for my $k (@MsgKeys) {
361 print "new message\n";
362 print "> line = " . $newMessages{$k}->{line} . "\n" if ($printlines);
363 print GREEN "> fuzzy = " . $newMessages{$k}->{fuzzy} . "\n", RESET;
364 print GREEN "> msgid = \"$k\"\n", RESET;
365 print GREEN "> msgstr = \"" . $newMessages{$k}->{msgstr} . "\"\n", RESET;
368 if ($options{"--display-fuzzy"}) {
369 printExtraMessages("fuzzy", \%Fuzzy, \@names);
371 if ($options{"--display-untranslated"}) {
372 printExtraMessages("untranslated", \%Untranslated, \@names);
376 sub check_po_file_readable($$)
378 my ($spec, $filename) = @_;
380 if (! -e $filename ) {
381 die("$spec po file does not exist");
383 if ( ! -f $filename ) {
384 die("$spec po file is not regular");
386 if ( ! -r $filename ) {
387 die("$spec po file is not readable");
391 # Diff of one corresponding entry
394 my ($k, $nk, $rM, $rnM) = @_;
395 print "diffline = " . $rM->{line} . "," . $rnM->{line} . "\n" if ($printlines);
396 print " msgid = \"$k\"\n";
397 if ($rM->{fuzzy} eq $rnM->{fuzzy}) {
398 print " fuzzy = \"" . $rM->{fuzzy} . "\"\n" if ($printlines);
401 print RED "< fuzzy = \"" . $rM->{fuzzy} . "\"\n", RESET;
403 print RED "< msgstr = \"" . $rM->{msgstr} . "\"\n", RESET;
405 print GREEN "> msgid = \"$nk\"\n", RESET;
407 if ($rM->{fuzzy} ne $rnM->{fuzzy}) {
408 print GREEN "> fuzzy = \"" . $rnM->{fuzzy} . "\"\n", RESET;
410 print GREEN "> msgstr = \"" . $rnM->{msgstr} . "\"\n", RESET;
416 my ($k, $rM, $rnM) = @_;
418 $doprint = 1 if ($rM->{fuzzy} != $rnM->{fuzzy});
419 $doprint = 1 if ($rM->{msgstr} ne $rnM->{msgstr});
422 printDiff($k, $k, $rM, $rnM);
426 sub printExtraMessages($$$)
428 my ($type, $rExtra, $rNames) = @_;
429 #print "file1 = $rNames->[0], file2 = $rNames->[1]\n";
430 my @sortedExtraKeys = sort { $a <=> $b;} keys %{$rExtra};
432 if (@sortedExtraKeys > 0) {
433 print "Still " . 0 + @sortedExtraKeys . " $type messages found in $rNames->[1]\n";
434 for my $l (@sortedExtraKeys) {
435 print "> line $l: \"" . $rExtra->{$l} . "\"\n";
441 # get repository dependent revision representation
444 my ($repo, $rev, $argf) = @_;
447 if ($rev eq "HEAD") {
451 return $rev if ($rev !~ /^(-|HEAD[-~])(\d+)$/);
454 if ($repo eq ".hg") {
455 # try to get the revision of n-th previous change of the po-file
456 if (open(FIR, "hg log '$argf'|")) {
458 my $res = "-$revnum";
459 while (my $l = <FIR>) {
461 if ($l =~ /:\s+(\d+):([^\s]+)$/) {
463 last if ($count-- <= 0);
473 elsif ($repo eq ".git") {
474 # try to get the revision of n-th previous change of the po-file
475 if (open(FIR, "git log --skip=$revnum -1 '$argf'|")) {
476 my $res = "HEAD~$revnum";
477 while (my $l = <FIR>) {
479 if ($l =~ /^commit\s+([^\s]+)$/) {
488 return("HEAD~$revnum");
491 elsif ($repo eq ".svn") {
492 if (open(FIR, "svn log '$argf'|")) {
495 while (my $l = <FIR>) {
497 if ($l =~ /^r(\d+)\s+\|/) {
499 last if ($count-- <= 0);
506 if (open(VI, "svnversion |")) {
507 while (my $r1 = <VI>) {
509 if ($r1 =~ /^((\d+):)?(\d+)M?$/) {