#!/usr/bin/perl -w =head1 NAME project_diff - show differences between two projects =head1 SYNOPSIS project_diff [options] old_project new_project old_project, new_project project specs, either in the form "proj_vers" or as four part objectname Options: -t | --terse terse diff listing (default) -d | --diff produce diff listing like "diff -ur" -p | --patch produce patch listing like "diff -urN" -h | --hide hide contents of added/deleted subtrees (for --terse) -r | --recursive recurse into sub projects Synergy options: -D PATH | --database PATH database path -H HOST | --host HOST engine host -U NAME | --user NAME user name -P STRING | --password STRING user's password --ui_database_dir PATH path to copy database information to =head1 DESCRIPTION C shows the differences between two projects in terms of the workarea paths of the projects' members. It does I need maintained workareas, though. =head2 C<--terse> listing (default) C traverses both projects and outputs the differences in the following form: a toolkit-1.0:project:1 2002-11-26 23:26:36 released b toolkit-darcy:project:1 2002-12-17 01:46:21 working ! toolkit/editor/sources/main.c main.c-1:csrc:2 main.c-2:csrc:2 + toolkit/guilib/includes/fonts.h fonts.h-1:incl:1 ! toolkit/guilib/makefile makefile-1:makefile:3 makefile-2:makefile:3 + toolkit/guilib/sources/fonts.c fonts.c-1:csrc:1 ! toolkit/misc/readme readme-1:ascii:1 readme-2:ascii:1 ... After the two header lines showing information about the projects, lines start with one of C<+>, C<->, C, or C<~> followed by the workarea pathname of the object in the project, followed by additional information (separated by tabs): =over 4 =item C<+> marks a path added in new_project; the correponding objectname is given as additional info =item C<-> marks a path deleted from old_project; the correponding objectname is given as additional info =item C marks a path where the corresponding objects have different versions in old_project and new_project; the objectnames are given as additional info =item C<~> marks a path where the corresponding objects are of different "lineage": either different instances (e.g. object was deleted and a new object created with the same name) or a different cvtype (e.g. a directory was replaced by a regular file); the objectnames are given as additional info =back All objects in an added/deleted subtree are shown. This can be suppressed with option C<--hide> which will only show the root of such a subtree. =head2 C<--diff> listing With this option the output resembles that of B: synergy a: toolkit-1.0:project:1 Tue Nov 26 23:26:36 2002 synergy b: toolkit-darcy:project:1 Tue Dec 17 01:46:21 2002 Only in b: toolkit/guilib/includes/fonts.h synergy b: fonts.h-1:incl:1 Wed Nov 17 04:23:11 1999 diff -u a/toolkit/guilib/makefile b/toolkit/guilib/makefile synergy a: makefile-1:makefile:3 Wed Nov 17 04:23:10 1999 synergy b: makefile-2:makefile:3 Wed Nov 17 04:23:11 1999 --- a/toolkit/guilib/makefile +++ b/toolkit/guilib/makefile @@ -2,7 +2,8 @@ INCLUDE_DIR = includes OBJ_FILES = $(SOURCE_DIR)/main.o \ - $(SOURCE_DIR)/controls.o + $(SOURCE_DIR)/controls.o \ + $(SOURCE_DIR)/fonts.o INCLUDES = -I$(INCLUDE_DIR) OPT_CFLAGS = -g ... Files that are only present in one project are indicated by "Only in a: ..." or "Only in b: ..." lines. An object that changes cvtype from "dir" to non-"dir" or vice versa is indicated by a line of the form: File a/foo is a regular file while file b/foo is a directory Differences in directories (i.e. both objects are of cvtype "dir", but different versions) are not shown. Additional CM Synergy information (objectname, modified_time) is given by lines starting with "synergy a:" or "synergy b:" immediately following any of the above "header" lines. Note that C<--diff> implies C<--hide> (the top of a added/deleted subtree is still indicated by a "Only in ..." line). =head2 C<--patch> listing With this option the output resembles that of B. This is the same as C<--diff> output except that "absent" objects are treated as empty files and added/deleted subtrees are not hidden. This listing is suitable to "patch up" a checked out I to a copy of I with the command: patch -p1 -E < project.patch =head1 OPTIONS =head2 C<-r>, C<--recursive> Traverse also subprojects. =head2 C<-h>, C<--hide> Hide added/deleted subtrees in C<--terse> output format. =head2 SYNERGY OPTIONS See L. =head1 EXIT STATUS Exit status is 0 if the projects are identical, 1 if some differences were found, 2 if some error occurred. =head1 AUTHORS Roderich Schupp, argumentum GmbH =cut use Getopt::Long qw(:config bundling); use Pod::Usage; use VCS::CMSynergy 1.29 qw(:cached_attributes :tied_objects); use VCS::CMSynergy::Helper; use strict; { package Terse; use constant OLD => 0; use constant NEW => 1; sub new { my ($class, $ccm) = @_; return bless {}, $class; } sub changed { my ($self, $path, $old, $new) = @_; my $indicator = ($old->cvtype eq $new->cvtype && $old->instance eq $new->instance) ? "!" : "~"; print "$indicator $path\t$old\t$new\n"; } sub only { my ($self, $in, $path, $obj) = @_; my $indicator = ("-", "+")[$in]; print "$indicator $path\t$obj\n"; } sub meta { } } { package Diff; use base qw/Terse/; use File::Temp qw/tempfile/; # diff program including options my @diff_prog = qw/diff -u/; sub new { my ($class, $ccm) = @_; my $self = { ccm => $ccm }; (undef, $self->{temp}) = tempfile; # NOTE: On Windows, CM Synergy executes cli_compare_cmd without # using the command interpreter, hence redirections don't work. # If we do not redirect it, the output of cli_compare_cmd # goes into the bit-bucket (i.e. can't be captured from $ccm->diff). # Hence, force the use of cmd.exe (this causes annoying # "flashing" command windows, though). $self->{saved_cli_compare_cmd} = $self->{ccm}->set( cli_compare_cmd => $^O eq "MSWin32" ? "cmd /c @diff_prog %file1 %file2 > $self->{temp}" : # FIXME maybe better - no flicker? # qq[$^X -e "open STDOUT, '>', pop \@ARGV; system \@ARGV;" @diff_prog %file1 %file2 $self->{temp_diff}] qq[@diff_prog %file1 %file2 > $self->{temp}]); return bless $self, $class; } sub DESTROY { my ($self) = @_; $self->{ccm}->set(cli_compare_cmd => $self->{saved_cli_compare_cmd}); unlink($self->{temp}) if $self->{temp}; } my ($a, $b) = qw(a b); sub changed { my ($self, $path, $old, $new) = @_; unless ($old->is_dir || $new->is_dir) { print "diff -u $a/$path $b/$path\n"; $self->meta(Terse::OLD, $old); $self->meta(Terse::NEW, $new); # generate diff $self->{ccm}->diff($old, $new); open(my $fh, "<$self->{temp}"); # eat and fake the two header lines (because they contain # pathnames that point either into the CM Synergy database's # cache area, the work area or a temp file) <$fh>; print "--- $a/$path\n"; <$fh>; print "+++ $b/$path\n"; # copy through the rest print while <$fh>; close($fh); return; } # one or both of $old and $new are dir's # NOTE: suppress output if both are dir's unless ($old->is_dir && $new->is_dir) { print $old->is_dir ? "File $a/$path is a directory while file $b/$path is a regular file\n" : "File $a/$path is a regular file while file $b/$path is a directory\n"; } } sub only { my ($self, $in, $path, $obj) = @_; print( ("Only in $a: ", "Only in $b: ")[$in], $path, "\n" ); $self->meta($in, $obj); } sub meta { my ($self, $in, $obj) = @_; print( ("synergy $a: ", "synergy $b: ")[$in], "$obj\t$obj->{modify_time}\n" ); } } { package Patch; use base qw/Diff/; my ($a, $b) = qw(a b); sub only { my ($self, $in, $path, $obj) = @_; print "diff -u $a/$path $b/$path\n"; $self->meta($in, $obj); my @lines; $self->{ccm}->cat_object($obj, \@lines); # fake a diff with /dev/null my $sign = ("-", "+")[$in]; my @range = ("0,0", "0,0"); my $nlines = @lines; $range[$in] = $nlines == 1 ? "1" : "1,$nlines"; print "--- $a/$path\n", "+++ $b/$path\n", "\@\@ -$range[0] +$range[1] \@\@\n"; print $sign, $_ foreach @lines; } } # extract CCM start options first... my $ccm_opts = VCS::CMSynergy::Helper::GetOptions or pod2usage(2) ; # ...then script-specific options my $Diff = "Terse"; my $recursive = 0; my $hide_sub_trees = 0; (GetOptions( 'r|recursive' => \$recursive, # include subprojects 'd|diff' => sub { $Diff = "Diff"; $hide_sub_trees = 1; }, 'p|patch' => sub { $Diff = "Patch"; $hide_sub_trees = 0; }, 't|terse' => sub { $Diff = "Terse"; $hide_sub_trees = 0; }, 'h|hide' => \$hide_sub_trees, # hide deleted/added sub trees ) && @ARGV == 2) or pod2usage(2); my $ccm = VCS::CMSynergy->new( %$ccm_opts, RaiseError => 1, PrintError => 0); my ($old_project, $new_project) = map { $ccm->project_object($_) } @ARGV; my $diff_status; END { $? = defined $diff_status ? $diff_status : 2; } my $tree = $ccm->project_tree( { subprojects => $recursive, attributes => [ qw/modify_time/ ], pathsep => "/" }, $old_project, $new_project); my $diff = $Diff->new($ccm); # print project header lines $diff->meta(Terse::OLD, $old_project); $diff->meta(Terse::NEW, $new_project); # NOTE: the hiding of subtrees depends on an ordering of keys %tree # that sorts "foo/bar/quux" _after_ "foo/bar" my %hide; # paths of deleted/added dirs my $ndiffs = 0; # number of differences found foreach my $path (sort keys %$tree) { my ($old, $new) = @{ $tree->{$path} }; unless (defined $new) # deleted object { $ndiffs++; # only show deleted directories once? if ($hide_sub_trees) { $hide{$path}++ if $old->is_dir; (my $dirname = $path) =~ s:/[^/]*$::; next if $hide{$dirname}; } $diff->only(Terse::OLD, $path, $old); next; } unless (defined $old) # added object { $ndiffs++; # only show added directories once? if ($hide_sub_trees) { $hide{$path}++ if $new->is_dir; (my $dirname = $path) =~ s:/[^/]*$::; next if $hide{$dirname}; } $diff->only(Terse::NEW, $path, $new); next; } next if $old eq $new; # same object $ndiffs++; $diff->changed($path, $old, $new); } $diff_status = $ndiffs ? 1 : 0; # FIXME patch: always exit 0