1
# See bottom of file for license and copyright information
5
---+ package Foswiki::Merge
7
Support for merging strings
11
package Foswiki::Merge;
20
---++ StaticMethod merge2( $arev, $a, $brev, $b, $sep, $session, $info )
22
* =$arev= - rev for $a (string)
23
* =$a= - first ('original') string
24
* =$brev= - rev for $b (string)
25
* =$b= - second ('new') string
26
* =$sep= = separator, string RE e.g. '.*?\n' for lines
27
* =$session= - Foswiki object
28
* =$info= - data block passed to plugins merge handler. Conventionally this will identify the source of the text being merged (the source form field, or undef for the body text)
30
Perform a merge of two versions of the same text, using
31
HTML tags to mark conflicts.
33
The granularity of the merge depends on the setting of $sep.
34
For example, if it is ="\\n"=, a line-by-line merge will be done.
36
Where conflicts exist, they are marked using HTML <del> and
37
<ins> tags. <del> marks content from $a while <ins>
38
marks content from $b.
40
Non-conflicting content (insertions from either set) are not
43
The plugins =mergeHandler= is called for each merge.
47
$newText = Foswiki::Merge::merge2(
48
$oldrev, $old, $newrev, $new, '.*?\n', $session, $info );
54
my ( $va, $ia, $vb, $ib, $sep, $session, $info ) = @_;
56
my @a = split( /($sep)/, $ia );
57
my @b = split( /($sep)/, $ib );
59
ASSERT( $session && $session->isa('Foswiki') ) if DEBUG;
62
require Algorithm::Diff;
63
Algorithm::Diff::traverse_balanced(
68
DISCARD_A => \&_acceptA,
69
DISCARD_B => \&_acceptB,
78
return join( '', @out );
82
my ( $a, $b, $out, $ai, $bi, $session, $info ) = @_;
84
ASSERT( $session->isa('Foswiki') ) if DEBUG;
86
#print STDERR "From A: '$ai->[$a]'\n";
87
# accept text from the old version without asking for resolution
90
->dispatch( 'mergeHandler', ' ', $ai->[$a], undef, $info );
91
if ( defined $merged ) {
92
push( @$out, $merged );
95
push( @$out, $ai->[$a] );
100
my ( $a, $b, $out, $ai, $bi, $session, $info ) = @_;
102
ASSERT( $session->isa('Foswiki') ) if DEBUG;
104
#print STDERR "From B: '$bi->[$b]'\n";
107
->dispatch( 'mergeHandler', ' ', $bi->[$b], undef, $info );
108
if ( defined $merged ) {
109
push( @$out, $merged );
112
push( @$out, $bi->[$b] );
117
my ( $a, $b, $out, $ai, $bi, $session, $info ) = @_;
119
ASSERT( $session->isa('Foswiki') ) if DEBUG;
121
# Diff isn't terribly smart sometimes; it will generate changes
122
# with a or b empty, which I would have thought should have
124
if ( $ai->[$a] =~ /\S/ ) {
126
# there is some non-white text to delete
127
if ( $bi->[$b] =~ /\S/ ) {
129
# this insert is replacing something with something
132
->dispatch( 'mergeHandler', 'c', $ai->[$a], $bi->[$b], $info );
133
if ( defined $merged ) {
134
push( @$out, $merged );
137
push( @$out, CGI::del( $ai->[$a] ) );
138
push( @$out, CGI::ins( $bi->[$b] ) );
144
->dispatch( 'mergeHandler', '-', $ai->[$a], $bi->[$b], $info );
145
if ( defined $merged ) {
146
push( @$out, $merged );
149
push( @$out, CGI::del( $ai->[$a] ) );
153
elsif ( $bi->[$b] =~ /\S/ ) {
158
->dispatch( 'mergeHandler', '+', $ai->[$a], $bi->[$b], $info );
160
#print STDERR "From B: '$bi->[$b]'\n";
161
if ( defined $merged ) {
162
push( @$out, $merged );
165
push( @$out, $bi->[$b] );
170
# otherwise this insert is not replacing anything
171
#print STDERR "From B: '$bi->[$b]'\n";
174
->dispatch( 'mergeHandler', ' ', $ai->[$a], $bi->[$b], $info );
175
if ( defined $merged ) {
176
push( @$out, $merged );
179
push( @$out, $bi->[$b] );
186
---++ StaticMethod simpleMerge( $a, $b, $sep ) -> \@arr
188
Perform a merge of two versions of the same text, returning
189
an array of strings representing the blocks in the merged context
190
where each string starts with one of "+", "-" or " " depending on
191
whether it is an insertion, a deletion, or just text. Insertions
192
and deletions alway happen in pairs, as text taken in from either
193
version that does not replace text in the other version will simply
196
The granularity of the merge depends on the setting of $sep.
197
For example, if it is ="\\n"=, a line-by-line merge will be done.
198
$sep characters are retained in the outout.
203
my ( $ia, $ib, $sep ) = @_;
205
my @a = split( /($sep)/, $ia );
206
my @b = split( /($sep)/, $ib );
209
require Algorithm::Diff;
210
Algorithm::Diff::traverse_balanced(
214
MATCH => \&_sAcceptA,
215
DISCARD_A => \&_sAcceptA,
216
DISCARD_B => \&_sAcceptB,
227
my ( $a, $b, $out, $ai, $bi ) = @_;
229
push( @$out, ' ' . $ai->[$a] );
233
my ( $a, $b, $out, $ai, $bi ) = @_;
235
push( @$out, ' ' . $bi->[$b] );
239
my ( $a, $b, $out, $ai, $bi ) = @_;
240
my $simpleInsert = 0;
242
if ( $ai->[$a] =~ /\S/ ) {
244
# there is some non-white text to delete
245
push( @$out, '-' . $ai->[$a] );
249
# otherwise this insert is not replacing anything
253
if ( !$simpleInsert && $bi->[$b] =~ /\S/ ) {
255
# this insert is replacing something with something
256
push( @$out, '+' . $bi->[$b] );
260
# otherwise it is replacing nothing, or is whitespace or null
261
push( @$out, ' ' . $bi->[$b] );
267
return 1 if ( !defined($a) && !defined($b) );
268
return 0 if ( !defined($a) || !defined($b) );
274
---++ StaticMethod merge3( $arev, $a, $brev, $b, $crev, $c, $sep,
277
* =$arev= - rev for common ancestor (id e.g. ver no)
278
* =$a= - common ancestor
279
* =$brev= - rev no for first derivative string (id)
280
* =$b= - first derivative string
281
* =$crev= - rev no for second derivative string (id)
282
* =$c= - second derivative string
283
* =$sep= = separator, string RE e.g. '.*?\n' for lines
284
* =$session= - Foswiki object
285
* =$info= - data block passed to plugins merge handler. Conventionally this will identify the source of the text being merged (the source form field, or undef for the body text)
287
Perform a merge of two versions (b and c) of the same text, using
288
HTML <div> tags to mark conflicts. a is the common ancestor.
290
The granularity of the merge depends on the setting of $sep.
291
For example, if it is =".*?\\n"=, a line-by-line merge will be done.
293
Where conflicts exist, they are labeled using the provided revision
296
The plugins =mergeHandler= is called for each merge.
298
Here's a little picture of a 3-way merge:
304
d <- merged result, returned.
308
my ( $ancestorMeta, $ancestorText ) =
309
$store->readTopic( undef, $webName, $topic, $originalrev );
310
$newText = Foswiki::Merge::merge3(
311
$ancestorText, $prevText, $newText,
312
$originalrev, $rev, "new",
319
my ( $arev, $ia, $brev, $ib, $crev, $ic, $sep, $session, $info ) = @_;
321
$sep = "\r?\n" if ( !defined($sep) );
323
my @a = split( /(.+?$sep)/, $ia );
324
my @b = split( /(.+?$sep)/, $ib );
325
my @c = split( /(.+?$sep)/, $ic );
326
require Algorithm::Diff;
327
my @bdiffs = Algorithm::Diff::sdiff( \@a, \@b );
328
my @cdiffs = Algorithm::Diff::sdiff( \@a, \@c );
330
my $ai = 0; # index into a
331
my $bdi = 0; # index into bdiffs
332
my $cdi = 0; # index into bdiffs
334
my $nbd = scalar(@bdiffs);
335
my $ncd = scalar(@cdiffs);
337
my ( @achunk, @bchunk, @cchunk );
338
my @diffs; # (a, b, c)
340
# diffs are of the form [ [ modifier, b_elem, c_elem ] ... ]
341
# where modifiers is one of:
342
# '+': element (b or c) added
343
# '-': element (from a) removed
344
# 'u': element unmodified
345
# 'c': element changed (a to b/c)
347
# first, collate the diffs.
350
my $bop = ( $bdi < $nbd ) ? $bdiffs[$bdi][0] : 'x';
352
push @bchunk, $bdiffs[ $bdi++ ][2];
355
my $cop = ( $cdi < $ncd ) ? $cdiffs[$cdi][0] : 'x';
357
push @cchunk, $cdiffs[ $cdi++ ][2];
360
while ( scalar(@bchunk) || scalar(@cchunk) ) {
361
push @diffs, [ shift @achunk, shift @bchunk, shift @cchunk ];
363
if ( scalar(@achunk) ) {
366
last if ( $bop eq 'x' || $cop eq 'x' );
368
# now that we've dealt with '+' and 'x', the only remaining
369
# operations are '-', 'u', and 'c', which all consume an
370
# element of a, so we should increment them together.
371
my $aline = $bdiffs[$bdi][1];
372
my $bline = $bdiffs[$bdi][2];
373
my $cline = $cdiffs[$cdi][2];
374
push @diffs, [ $aline, $bline, $cline ];
379
# at this point, both lists should be consumed, unless theres a bug in
380
# Algorithm::Diff. We'll consume whatevers left if necessary though.
382
while ( $bdi < $nbd ) {
383
push @diffs, [ $bdiffs[$bdi][1], undef, $bdiffs[$bdi][2] ];
386
while ( $cdi < $ncd ) {
387
push @diffs, [ $cdiffs[$cdi][1], undef, $cdiffs[$cdi][2] ];
391
my ( @aconf, @bconf, @cconf, @merged );
394
my ( $aline, $bline, $cline );
396
for my $diff (@diffs) {
397
( $aline, $bline, $cline ) = @$diff;
398
my $ab = _equal( $aline, $bline );
399
my $ac = _equal( $aline, $cline );
400
my $bc = _equal( $bline, $cline );
405
# same change (or no change) in b and c
410
# line did not change in b
415
# line did not change in c
420
# line changed in both b and c
426
# store up conflicting lines until we get a non-conflicting
432
if ( defined($dline) ) {
434
# we have a non-conflicting line
437
# flush any pending conflict if there is enough
438
# context (at least 3 lines)
439
push( @merged, $dline );
441
for my $i ( 0 .. $#merged ) {
447
\@out, \@aconf, \@bconf, \@cconf, $arev,
448
$brev, $crev, $sep, $session, $info
457
# the line is non-conflicting
460
->dispatch( 'mergeHandler', ' ', $dline, $dline, $info );
461
if ( defined $merged ) {
462
push( @out, $merged );
465
push( @out, $dline );
475
for my $i ( 0 .. $#merged ) {
482
\@out, \@aconf, \@bconf, \@cconf, $arev,
483
$brev, $crev, $sep, $session, $info
489
#foreach ( @out ) { print STDERR (defined($_) ? $_ : "undefined") . "\n"; }
491
return join( '', @out );
494
my $conflictAttrs = { class => 'twikiConflict' };
496
# SMELL: internationalisation?
497
my $conflictB = CGI::b('CONFLICT');
499
sub _handleConflict {
501
$out, $aconf, $bconf, $cconf, $arev,
502
$brev, $crev, $sep, $session, $info
506
@a = grep( $_, @$aconf );
507
@b = grep( $_, @$bconf );
508
@c = grep( $_, @$cconf );
511
->dispatch( 'mergeHandler', 'c', join( '', @b ), join( '', @c ), $info );
512
if ( defined $merged ) {
513
push( @$out, $merged );
518
CGI::div( $conflictAttrs, "$conflictB original $arev:" )
524
CGI::div( $conflictAttrs,, "$conflictB version $brev:" )
530
CGI::div( $conflictAttrs,, "$conflictB version $crev:" )
534
push( @$out, CGI::div( $conflictAttrs,, "$conflictB end" ) . "\n" );
540
# Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/
542
# Copyright (C) 2008-2009 Foswiki Contributors. All Rights Reserved.
543
# Foswiki Contributors are listed in the AUTHORS file in the root
544
# of this distribution. NOTE: Please extend that file, not this notice.
546
# Additional copyrights apply to some or all of the code in this
549
# Copyright (C) 2004-2007 TWiki Contributors. All Rights Reserved.
550
# TWiki Contributors are listed in the AUTHORS file in the root
551
# of this distribution. NOTE: Please extend that file, not this notice.
553
# This program is free software; you can redistribute it and/or
554
# modify it under the terms of the GNU General Public License
555
# as published by the Free Software Foundation; either version 2
556
# of the License, or (at your option) any later version. For
557
# more details read LICENSE in the root of this distribution.
559
# This program is distributed in the hope that it will be useful,
560
# but WITHOUT ANY WARRANTY; without even the implied warranty of
561
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
563
# As per the GPL, removal of this notice is prohibited.