#### Definition and mappings of functions ####
+# NOTE: Despite the existence of req_CATCHALL and req_EMPTY unimplemented
+# requests, this list is incomplete. It is missing many rarer/optional
+# requests. Perhaps some clients require a claim of support for
+# these specific requests for main functionality to work?
my $methods = {
'Root' => \&req_Root,
'Valid-responses' => \&req_Validresponses,
'noop' => \&req_EMPTY,
'annotate' => \&req_annotate,
'Global_option' => \&req_Globaloption,
- #'annotate' => \&req_CATCHALL,
};
##############################################
#$log->debug("req_Entry : $data");
- my @data = split(/\//, $data);
+ my @data = split(/\//, $data, -1);
$state->{entries}{$state->{directory}.$data[1]} = {
revision => $data[2],
my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
$updater->update();
- argsfromdir($updater);
-
my $addcount = 0;
foreach my $filename ( @{$state->{args}} )
my $meta = $updater->getmeta($filename);
my $wrev = revparse($filename);
- if ($wrev && $meta && ($wrev < 0))
+ if ($wrev && $meta && ($wrev=~/^-/))
{
# previously removed file, add back
- $log->info("added file $filename was previously removed, send 1.$meta->{revision}");
+ $log->info("added file $filename was previously removed, send $meta->{revision}");
print "MT +updated\n";
print "MT text U \n";
# this is an "entries" line
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
- $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
- print "/$filepart/1.$meta->{revision}//$kopts/\n";
+ $log->debug("/$filepart/$meta->{revision}//$kopts/");
+ print "/$filepart/$meta->{revision}//$kopts/\n";
# permissions
$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
next;
}
- if ( defined($wrev) and $wrev < 0 )
+ if ( defined($wrev) and ($wrev=~/^-/) )
{
print "E cvs remove: file `$filename' already scheduled for removal\n";
next;
}
- unless ( $wrev == $meta->{revision} )
+ unless ( $wrev eq $meta->{revision} )
{
# TODO : not sure if the format of this message is quite correct.
print "E cvs remove: Up to date check failed for `$filename'\n";
print "Checked-in $dirpart\n";
print "$filename\n";
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
- print "/$filepart/-1.$wrev//$kopts/\n";
+ print "/$filepart/-$wrev//$kopts/\n";
$rmcount++;
}
# this is an "entries" line
my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
- print "/$git->{name}/1.$git->{revision}//$kopts/\n";
+ print "/$git->{name}/$git->{revision}//$kopts/\n";
# permissions
print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
}
my $meta;
- if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ )
+ if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
{
$meta = $updater->getmeta($filename, $1);
} else {
{
$meta = {
name => $filename,
- revision => 0,
+ revision => '0',
filehash => 'added'
};
}
my $wrev = revparse($filename);
# If the working copy is an old revision, lets get that version too for comparison.
- if ( defined($wrev) and $wrev != $meta->{revision} )
+ if ( defined($wrev) and $wrev ne $meta->{revision} )
{
$oldmeta = $updater->getmeta($filename, $wrev);
}
# and the working copy is unmodified _and_ the user hasn't specified -C
next if ( defined ( $wrev )
and defined($meta->{revision})
- and $wrev == $meta->{revision}
+ and $wrev eq $meta->{revision}
and $state->{entries}{$filename}{unchanged}
and not exists ( $state->{opt}{C} ) );
# but the working copy is modified, tell the client it's modified
if ( defined ( $wrev )
and defined($meta->{revision})
- and $wrev == $meta->{revision}
+ and $wrev eq $meta->{revision}
and defined($state->{entries}{$filename}{modified_hash})
and not exists ( $state->{opt}{C} ) )
{
if ( $meta->{filehash} eq "deleted" )
{
+ # TODO: If it has been modified in the sandbox, error out
+ # with the appropriate message, rather than deleting a modified
+ # file.
+
my ( $filepart, $dirpart ) = filenamesplit($filename,1);
$log->info("Removing '$filename' from working copy (no longer in the repo)");
{
# normal update, just send the new revision (either U=Update,
# or A=Add, or R=Remove)
- if ( defined($wrev) && $wrev < 0 )
+ if ( defined($wrev) && ($wrev=~/^-/) )
{
$log->info("Tell the client the file is scheduled for removal");
print "MT text R \n";
print "MT newline\n";
next;
}
- elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) )
+ elsif ( (!defined($wrev) || $wrev eq '0') &&
+ (!defined($meta->{revision}) || $meta->{revision} eq '0') )
{
$log->info("Tell the client the file is scheduled for addition");
print "MT text A \n";
}
else {
- $log->info("Updating '$filename' to ".$meta->{revision});
+ $log->info("UpdatingX3 '$filename' to ".$meta->{revision});
print "MT +updated\n";
print "MT text U \n";
print "MT fname $filename\n";
# this is an "entries" line
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
- $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
- print "/$filepart/1.$meta->{revision}//$kopts/\n";
+ $log->debug("/$filepart/$meta->{revision}//$kopts/");
+ print "/$filepart/$meta->{revision}//$kopts/\n";
# permissions
$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
transmitfile($meta->{filehash});
}
} else {
- $log->info("Updating '$filename'");
my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1);
my $mergeDir = setupTmpDir();
# we need to merge with the local changes ( M=successful merge, C=conflict merge )
$log->info("Merging $file_local, $file_old, $file_new");
- print "M Merging differences between 1.$oldmeta->{revision} and 1.$meta->{revision} into $filename\n";
+ print "M Merging differences between $oldmeta->{revision} and $meta->{revision} into $filename\n";
$log->debug("Temporary directory for merge is $mergeDir");
print $state->{CVSROOT} . "/$state->{module}/$filename\n";
my $kopts = kopts_from_path("$dirpart/$filepart",
"file",$mergedFile);
- $log->debug("/$filepart/1.$meta->{revision}//$kopts/");
- print "/$filepart/1.$meta->{revision}//$kopts/\n";
+ $log->debug("/$filepart/$meta->{revision}//$kopts/");
+ print "/$filepart/$meta->{revision}//$kopts/\n";
}
}
elsif ( $return == 1 )
print $state->{CVSROOT} . "/$state->{module}/$filename\n";
my $kopts = kopts_from_path("$dirpart/$filepart",
"file",$mergedFile);
- print "/$filepart/1.$meta->{revision}/+/$kopts/\n";
+ print "/$filepart/$meta->{revision}/+/$kopts/\n";
}
}
else
my $addflag = 0;
my $rmflag = 0;
- $rmflag = 1 if ( defined($wrev) and $wrev < 0 );
+ $rmflag = 1 if ( defined($wrev) and ($wrev=~/^-/) );
$addflag = 1 unless ( -e $filename );
# Do up to date checking
- unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) )
+ unless ( $addflag or $wrev eq $meta->{revision} or
+ ( $rmflag and $wrev eq "-$meta->{revision}" ) )
{
# fail everything if an up to date check fails
print "error 1 Up to date check failed for $filename\n";
$log->info("Adding file '$filename'");
system("git", "update-index", "--add", $filename);
} else {
- $log->info("Updating file '$filename'");
+ $log->info("UpdatingX2 file '$filename'");
system("git", "update-index", $filename);
}
}
my $meta = $updater->getmeta($filename);
unless (defined $meta->{revision}) {
- $meta->{revision} = 1;
+ $meta->{revision} = "1.1";
}
my ( $filepart, $dirpart ) = filenamesplit($filename, 1);
print "M $state->{CVSROOT}/$state->{module}/$filename,v <-- $dirpart$filepart\n";
if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" )
{
- print "M new revision: delete; previous revision: 1.$oldmeta{$filename}{revision}\n";
+ print "M new revision: delete; previous revision: $oldmeta{$filename}{revision}\n";
print "Remove-entry $dirpart\n";
print "$filename\n";
} else {
- if ($meta->{revision} == 1) {
+ if ($meta->{revision} eq "1.1") {
print "M initial revision: 1.1\n";
} else {
- print "M new revision: 1.$meta->{revision}; previous revision: 1.$oldmeta{$filename}{revision}\n";
+ print "M new revision: $meta->{revision}; previous revision: $oldmeta{$filename}{revision}\n";
}
print "Checked-in $dirpart\n";
print "$filename\n";
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
- print "/$filepart/1.$meta->{revision}//$kopts/\n";
+ print "/$filepart/$meta->{revision}//$kopts/\n";
}
}
#$log->debug("status state : " . Dumper($state));
# Grab a handle to the SQLite db and do any necessary updates
- my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ my $updater;
+ $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
$updater->update();
- # if no files were specified, we need to work out what files we should be providing status on ...
+ # if no files were specified, we need to work out what files we should
+ # be providing status on ...
argsfromdir($updater);
# foreach file specified on the command line ...
{
$filename = filecleanup($filename);
- next if exists($state->{opt}{l}) && index($filename, '/', length($state->{prependdir})) >= 0;
+ if ( exists($state->{opt}{l}) &&
+ index($filename, '/', length($state->{prependdir})) >= 0 )
+ {
+ next;
+ }
my $meta = $updater->getmeta($filename);
my $oldmeta = $meta;
my $wrev = revparse($filename);
- # If the working copy is an old revision, lets get that version too for comparison.
- if ( defined($wrev) and $wrev != $meta->{revision} )
+ # If the working copy is an old revision, lets get that
+ # version too for comparison.
+ if ( defined($wrev) and $wrev ne $meta->{revision} )
{
$oldmeta = $updater->getmeta($filename, $wrev);
}
# TODO : All possible statuses aren't yet implemented
my $status;
- # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified
- $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision}
- and
- ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
- or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) )
- );
-
- # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified
- $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev
- and
- ( $state->{entries}{$filename}{unchanged}
- or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) )
- );
-
- # Need checkout if it exists in the repo but doesn't have a working copy
- $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) );
-
- # Locally modified if working copy and repo copy have the same revision but there are local changes
- $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} );
-
- # Needs Merge if working copy revision is less than repo copy and there are local changes
- $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} );
-
- $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) );
- $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} );
- $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ );
- $status ||= "File had conflicts on merge" if ( 0 );
+ # Files are up to date if the working copy and repo copy have
+ # the same revision, and the working copy is unmodified
+ if ( defined ( $wrev ) and defined($meta->{revision}) and
+ $wrev eq $meta->{revision} and
+ ( ( $state->{entries}{$filename}{unchanged} and
+ ( not defined ( $state->{entries}{$filename}{conflict} ) or
+ $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
+ ( defined($state->{entries}{$filename}{modified_hash}) and
+ $state->{entries}{$filename}{modified_hash} eq
+ $meta->{filehash} ) ) )
+ {
+ $status = "Up-to-date"
+ }
+
+ # Need checkout if the working copy has a different (usually
+ # older) revision than the repo copy, and the working copy is
+ # unmodified
+ if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+ $meta->{revision} ne $wrev and
+ ( $state->{entries}{$filename}{unchanged} or
+ ( defined($state->{entries}{$filename}{modified_hash}) and
+ $state->{entries}{$filename}{modified_hash} eq
+ $oldmeta->{filehash} ) ) )
+ {
+ $status ||= "Needs Checkout";
+ }
+
+ # Need checkout if it exists in the repo but doesn't have a working
+ # copy
+ if ( not defined ( $wrev ) and defined ( $meta->{revision} ) )
+ {
+ $status ||= "Needs Checkout";
+ }
+
+ # Locally modified if working copy and repo copy have the
+ # same revision but there are local changes
+ if ( defined ( $wrev ) and defined($meta->{revision}) and
+ $wrev eq $meta->{revision} and
+ $state->{entries}{$filename}{modified_filename} )
+ {
+ $status ||= "Locally Modified";
+ }
+
+ # Needs Merge if working copy revision is different
+ # (usually older) than repo copy and there are local changes
+ if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+ $meta->{revision} ne $wrev and
+ $state->{entries}{$filename}{modified_filename} )
+ {
+ $status ||= "Needs Merge";
+ }
+
+ if ( defined ( $state->{entries}{$filename}{revision} ) and
+ not defined ( $meta->{revision} ) )
+ {
+ $status ||= "Locally Added";
+ }
+ if ( defined ( $wrev ) and defined ( $meta->{revision} ) and
+ $wrev eq "-$meta->{revision}" )
+ {
+ $status ||= "Locally Removed";
+ }
+ if ( defined ( $state->{entries}{$filename}{conflict} ) and
+ $state->{entries}{$filename}{conflict} =~ /^\+=/ )
+ {
+ $status ||= "Unresolved Conflict";
+ }
+ if ( 0 )
+ {
+ $status ||= "File had conflicts on merge";
+ }
$status ||= "Unknown";
my ($filepart) = filenamesplit($filename);
- print "M ===================================================================\n";
+ print "M =======" . ( "=" x 60 ) . "\n";
print "M File: $filepart\tStatus: $status\n";
if ( defined($state->{entries}{$filename}{revision}) )
{
- print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n";
+ print "M Working revision:\t" .
+ $state->{entries}{$filename}{revision} . "\n";
} else {
print "M Working revision:\tNo entry for $filename\n";
}
if ( defined($meta->{revision}) )
{
- print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
- print "M Sticky Tag:\t\t(none)\n";
- print "M Sticky Date:\t\t(none)\n";
- print "M Sticky Options:\t\t(none)\n";
+ print "M Repository revision:\t" .
+ $meta->{revision} .
+ "\t$state->{CVSROOT}/$state->{module}/$filename,v\n";
+ my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
+ my($tag)=($tagOrDate=~m/^T(.+)$/);
+ if( !defined($tag) )
+ {
+ $tag="(none)";
+ }
+ print "M Sticky Tag:\t\t$tag\n";
+ my($date)=($tagOrDate=~m/^D(.+)$/);
+ if( !defined($date) )
+ {
+ $date="(none)";
+ }
+ print "M Sticky Date:\t\t$date\n";
+ my($options)=$state->{entries}{$filename}{options};
+ if( $options eq "" )
+ {
+ $options="(none)";
+ }
+ print "M Sticky Options:\t\t$options\n";
} else {
print "M Repository revision:\tNo revision control file\n";
}
$revision1 = $state->{opt}{r};
}
- $revision1 =~ s/^1\.// if ( defined ( $revision1 ) );
- $revision2 =~ s/^1\.// if ( defined ( $revision2 ) );
-
- $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
+ $log->debug("Diffing revisions " .
+ ( defined($revision1) ? $revision1 : "[NULL]" ) .
+ " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) );
# Grab a handle to the SQLite db and do any necessary updates
- my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ my $updater;
+ $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
$updater->update();
- # if no files were specified, we need to work out what files we should be providing status on ...
+ # if no files were specified, we need to work out what files we should
+ # be providing status on ...
argsfromdir($updater);
# foreach file specified on the command line ...
$meta1 = $updater->getmeta($filename, $revision1);
unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
{
- print "E File $filename at revision 1.$revision1 doesn't exist\n";
+ print "E File $filename at revision $revision1 doesn't exist\n";
next;
}
transmitfile($meta1->{filehash}, { targetfile => $file1 });
unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
{
- print "E File $filename at revision 1.$revision2 doesn't exist\n";
+ print "E File $filename at revision $revision2 doesn't exist\n";
next;
}
$file2 = $state->{entries}{$filename}{modified_filename};
}
- # if we have been given -r, and we don't have a $file2 yet, lets get one
+ # if we have been given -r, and we don't have a $file2 yet, lets
+ # get one
if ( defined ( $revision1 ) and not defined ( $file2 ) )
{
( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
# We need to have retrieved something useful
next unless ( defined ( $meta1 ) );
- # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified
- next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision}
- and
- ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) )
- or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) )
- );
+ # Files to date if the working copy and repo copy have the same
+ # revision, and the working copy is unmodified
+ if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
+ ( ( $state->{entries}{$filename}{unchanged} and
+ ( not defined ( $state->{entries}{$filename}{conflict} ) or
+ $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
+ ( defined($state->{entries}{$filename}{modified_hash}) and
+ $state->{entries}{$filename}{modified_hash} eq
+ $meta1->{filehash} ) ) )
+ {
+ next;
+ }
# Apparently we only show diffs for locally modified files
- next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) );
+ unless ( defined($meta2) or
+ defined ( $state->{entries}{$filename}{modified_filename} ) )
+ {
+ next;
+ }
print "M Index: $filename\n";
- print "M ===================================================================\n";
+ print "M =======" . ( "=" x 60 ) . "\n";
print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
- print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) );
- print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) );
+ if ( defined ( $meta1 ) )
+ {
+ print "M retrieving revision $meta1->{revision}\n"
+ }
+ if ( defined ( $meta2 ) )
+ {
+ print "M retrieving revision $meta2->{revision}\n"
+ }
print "M diff ";
foreach my $opt ( keys %{$state->{opt}} )
{
}
} else {
print "-$opt ";
- print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) );
+ if ( defined ( $state->{opt}{$opt} ) )
+ {
+ print "$state->{opt}{$opt} "
+ }
}
}
print "$filename\n";
- $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" ));
+ $log->info("Diffing $filename -r $meta1->{revision} -r " .
+ ( $meta2->{revision} or "workingcopy" ));
( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
if ( exists $state->{opt}{u} )
{
- system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff");
+ system("diff -u -L '$filename revision $meta1->{revision}'" .
+ " -L '$filename " .
+ ( defined($meta2->{revision}) ?
+ "revision $meta2->{revision}" :
+ "working copy" ) .
+ "' $file1 $file2 > $filediff" );
} else {
system("diff $file1 $file2 > $filediff");
}
$log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" ));
#$log->debug("log state : " . Dumper($state));
- my ( $minrev, $maxrev );
- if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ )
+ my ( $revFilter );
+ if ( defined ( $state->{opt}{r} ) )
{
- my $control = $2;
- $minrev = $1;
- $maxrev = $3;
- $minrev =~ s/^1\.// if ( defined ( $minrev ) );
- $maxrev =~ s/^1\.// if ( defined ( $maxrev ) );
- $minrev++ if ( defined($minrev) and $control eq "::" );
+ $revFilter = $state->{opt}{r};
}
# Grab a handle to the SQLite db and do any necessary updates
- my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
+ my $updater;
+ $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
$updater->update();
- # if no files were specified, we need to work out what files we should be providing status on ...
+ # if no files were specified, we need to work out what files we
+ # should be providing status on ...
argsfromdir($updater);
# foreach file specified on the command line ...
my $headmeta = $updater->getmeta($filename);
- my $revisions = $updater->getlog($filename);
- my $totalrevisions = scalar(@$revisions);
-
- if ( defined ( $minrev ) )
- {
- $log->debug("Removing revisions less than $minrev");
- while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev )
- {
- pop @$revisions;
- }
- }
- if ( defined ( $maxrev ) )
- {
- $log->debug("Removing revisions greater than $maxrev");
- while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev )
- {
- shift @$revisions;
- }
- }
+ my ($revisions,$totalrevisions) = $updater->getlog($filename,
+ $revFilter);
next unless ( scalar(@$revisions) );
print "M \n";
print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
print "M Working file: $filename\n";
- print "M head: 1.$headmeta->{revision}\n";
+ print "M head: $headmeta->{revision}\n";
print "M branch:\n";
print "M locks: strict\n";
print "M access list:\n";
print "M symbolic names:\n";
print "M keyword substitution: kv\n";
- print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n";
+ print "M total revisions: $totalrevisions;\tselected revisions: " .
+ scalar(@$revisions) . "\n";
print "M description:\n";
foreach my $revision ( @$revisions )
{
print "M ----------------------------\n";
- print "M revision 1.$revision->{revision}\n";
+ print "M revision $revision->{revision}\n";
# reformat the date for log output
- $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) );
+ if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and
+ defined($DATE_LIST->{$2}) )
+ {
+ $revision->{modified} = sprintf('%04d/%02d/%02d %s',
+ $3, $DATE_LIST->{$2}, $1, $4 );
+ }
$revision->{author} = cvs_author($revision->{author});
- print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n";
- my $commitmessage = $updater->commitmessage($revision->{commithash});
+ print "M date: $revision->{modified};" .
+ " author: $revision->{author}; state: " .
+ ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) .
+ "; lines: +2 -3\n";
+ my $commitmessage;
+ $commitmessage = $updater->commitmessage($revision->{commithash});
$commitmessage =~ s/^/M /mg;
print $commitmessage . "\n";
}
- print "M =============================================================================\n";
+ print "M =======" . ( "=" x 70 ) . "\n";
}
print "ok\n";
$metadata->{$commithash}{author} = cvs_author($metadata->{$commithash}{author});
$metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ );
}
- printf("M 1.%-5d (%-8s %10s): %s\n",
+ printf("M %-7s (%-8s %10s): %s\n",
$metadata->{$commithash}{revision},
$metadata->{$commithash}{author},
$metadata->{$commithash}{modified},
# push added files
foreach my $file (keys %{$state->{entries}}) {
if ( exists $state->{entries}{$file}{revision} &&
- $state->{entries}{$file}{revision} == 0 )
+ $state->{entries}{$file}{revision} eq '0' )
{
push @gethead, { name => $file, filehash => 'added' };
}
$state->{entries} = {};
}
+# Return working directory CVS revision "1.X" out
+# of the the working directory "entries" state, for the given filename.
+# This is prefixed with a dash if the file is scheduled for removal
+# when it is committed.
sub revparse
{
my $filename = shift;
- return undef unless ( defined ( $state->{entries}{$filename}{revision} ) );
-
- return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ );
- return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ );
-
- return undef;
+ return $state->{entries}{$filename}{revision};
}
# This method takes a file hash and does a CVS "file transfer". Its
return ( $filepart, $dirpart );
}
+# Cleanup various junk in filename (try to canonicalize it), and
+# add prependdir to accomodate running CVS client from a
+# subdirectory (so the output is relative to top directory of the project).
sub filecleanup
{
my $filename = shift;
return undef;
}
+ if($filename eq ".")
+ {
+ $filename="";
+ }
$filename =~ s/^\.\///g;
+ $filename =~ s%/+%/%g;
$filename = $state->{prependdir} . $filename;
+ $filename =~ s%/$%%;
return $filename;
}
+# Remove prependdir from the path, so that is is relative to the directory
+# the CVS client was started from, rather than the top of the project.
+# Essentially the inverse of filecleanup().
+sub remove_prependdir
+{
+ my($path) = @_;
+ if(defined($state->{prependdir}) && $state->{prependdir} ne "")
+ {
+ my($pre)=$state->{prependdir};
+ $pre=~s%/$%%;
+ if(!($path=~s%^\Q$pre\E/?%%))
+ {
+ $log->fatal("internal error missing prependdir");
+ die("internal error missing prependdir");
+ }
+ }
+ return $path;
+}
+
sub validateGitDir
{
if( !defined($state->{CVSROOT}) )
die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
+ # Stores full sha1's for various branch/tag names, abbreviations, etc:
+ $self->{commitRefCache} = {};
+
$self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
$cfg->{gitcvs}{dbdriver} || "SQLite";
$self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
}
# Construct the revision table if required
+ # The revision table stores an entry for each file, each time that file
+ # changes.
+ # numberOfRecords = O( numCommits * averageNumChangedFilesPerCommit )
+ # This is not sufficient to support "-r {commithash}" for any
+ # files except files that were modified by that commit (also,
+ # some places in the code ignore/effectively strip out -r in
+ # some cases, before it gets passed to getmeta()).
+ # The "filehash" field typically has a git blob hash, but can also
+ # be set to "dead" to indicate that the given version of the file
+ # should not exist in the sandbox.
unless ( $self->{tables}{$self->tablename("revision")} )
{
my $tablename = $self->tablename("revision");
}
# Construct the head table if required
+ # The head table (along with the "last_commit" entry in the property
+ # table) is the persisted working state of the "sub update" subroutine.
+ # All of it's data is read entirely first, and completely recreated
+ # last, every time "sub update" runs.
+ # This is also used by "sub getmeta" when it is asked for the latest
+ # version of a file (as opposed to some specific version).
+ # Another way of thinking about it is as a single slice out of
+ # "revisions", giving just the most recent revision information for
+ # each file.
unless ( $self->{tables}{$self->tablename("head")} )
{
my $tablename = $self->tablename("head");
}
# Construct the properties table if required
+ # - "last_commit" - Used by "sub update".
unless ( $self->{tables}{$self->tablename("properties")} )
{
my $tablename = $self->tablename("properties");
}
# Construct the commitmsgs table if required
+ # The commitmsgs table is only used for merge commits, since
+ # "sub update" will only keep one branch of parents. Shortlogs
+ # for ignored commits (i.e. not on the chosen branch) will be used
+ # to construct a replacement "collapsed" merge commit message,
+ # which will be stored in this table. See also "sub commitmessage".
unless ( $self->{tables}{$self->tablename("commitmsgs")} )
{
my $tablename = $self->tablename("commitmsgs");
=head2 update
+Bring the database up to date with the latest changes from
+the git repository.
+
+Internal working state is read out of the "head" table and the
+"last_commit" property, then it updates "revisions" based on that, and
+finally it writes the new internal state back to the "head" table
+so it can be used as a starting point the next time update is called.
+
=cut
sub update
{
push @git_log_params, $self->{module};
}
# git-rev-list is the backend / plumbing version of git-log
- open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
-
- my @commits;
-
- my %commit = ();
-
- while ( <GITLOG> )
- {
- chomp;
- if (m/^commit\s+(.*)$/) {
- # on ^commit lines put the just seen commit in the stack
- # and prime things for the next one
- if (keys %commit) {
- my %copy = %commit;
- unshift @commits, \%copy;
- %commit = ();
- }
- my @parents = split(m/\s+/, $1);
- $commit{hash} = shift @parents;
- $commit{parents} = \@parents;
- } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
- # on rfc822-like lines seen before we see any message,
- # lowercase the entry and put it in the hash as key-value
- $commit{lc($1)} = $2;
- } else {
- # message lines - skip initial empty line
- # and trim whitespace
- if (!exists($commit{message}) && m/^\s*$/) {
- # define it to mark the end of headers
- $commit{message} = '';
- next;
- }
- s/^\s+//; s/\s+$//; # trim ws
- $commit{message} .= $_ . "\n";
- }
- }
- close GITLOG;
-
- unshift @commits, \%commit if ( keys %commit );
+ open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
+ or die "Cannot call git-rev-list: $!";
+ my @commits=readCommits($gitLogPipe);
+ close $gitLogPipe;
# Now all the commits are in the @commits bucket
# ordered by time DESC. for each commit that needs processing,
my $commitcount = 0;
# Load the head table into $head (for cached lookups during the update process)
- foreach my $file ( @{$self->gethead()} )
+ foreach my $file ( @{$self->gethead(1)} )
{
$head->{$file->{name}} = $file;
}
next;
} elsif (@{$commit->{parents}} > 1) {
# it is a merge commit, for each parent that is
- # not $lastpicked, see if we can get a log
+ # not $lastpicked (not given a CVS revision number),
+ # see if we can get a log
# from the merge-base to that parent to put it
# in the message as a merge summary.
my @parents = @{$commit->{parents}};
foreach my $parent (@parents) {
- # git-merge-base can potentially (but rarely) throw
- # several candidate merge bases. let's assume
- # that the first one is the best one.
if ($parent eq $lastpicked) {
next;
}
+ # git-merge-base can potentially (but rarely) throw
+ # several candidate merge bases. let's assume
+ # that the first one is the best one.
my $base = eval {
safe_pipe_capture('git', 'merge-base',
$lastpicked, $parent);
}
# convert the date to CVS-happy format
- $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
+ my $cvsDate = convertToCvsDate($commit->{date});
if ( defined ( $lastpicked ) )
{
while ( <FILELIST> )
{
chomp;
- unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
+ unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
{
die("Couldn't process git-diff-tree line : $_");
}
# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
- my $git_perms = "";
- $git_perms .= "r" if ( $mode & 4 );
- $git_perms .= "w" if ( $mode & 2 );
- $git_perms .= "x" if ( $mode & 1 );
- $git_perms = "rw" if ( $git_perms eq "" );
+ my $dbMode = convertToDbMode($mode);
if ( $change eq "D" )
{
revision => $head->{$name}{revision} + 1,
filehash => "deleted",
commithash => $commit->{hash},
- modified => $commit->{date},
+ modified => $cvsDate,
author => $commit->{author},
- mode => $git_perms,
+ mode => $dbMode,
};
- $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
elsif ( $change eq "M" || $change eq "T" )
{
revision => $head->{$name}{revision} + 1,
filehash => $hash,
commithash => $commit->{hash},
- modified => $commit->{date},
+ modified => $cvsDate,
author => $commit->{author},
- mode => $git_perms,
+ mode => $dbMode,
};
- $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
elsif ( $change eq "A" )
{
revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
filehash => $hash,
commithash => $commit->{hash},
- modified => $commit->{date},
+ modified => $cvsDate,
author => $commit->{author},
- mode => $git_perms,
+ mode => $dbMode,
};
- $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
else
{
die("Couldn't process git-ls-tree line : $_");
}
- my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+ my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
$seen_files->{$git_filename} = 1;
$head->{$git_filename}{mode}
);
- if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
- {
- $git_perms = "";
- $git_perms .= "r" if ( $1 & 4 );
- $git_perms .= "w" if ( $1 & 2 );
- $git_perms .= "x" if ( $1 & 1 );
- } else {
- $git_perms = "rw";
- }
+ my $dbMode = convertToDbMode($mode);
# unless the file exists with the same hash, we need to update it ...
- unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
+ unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
{
my $newrevision = ( $oldrevision or 0 ) + 1;
revision => $newrevision,
filehash => $git_hash,
commithash => $commit->{hash},
- modified => $commit->{date},
+ modified => $cvsDate,
author => $commit->{author},
- mode => $git_perms,
+ mode => $dbMode,
};
- $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
+ $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
}
close FILELIST;
$head->{$file}{revision}++;
$head->{$file}{filehash} = "deleted";
$head->{$file}{commithash} = $commit->{hash};
- $head->{$file}{modified} = $commit->{date};
+ $head->{$file}{modified} = $cvsDate;
$head->{$file}{author} = $commit->{author};
- $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
+ $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
}
}
# END : "Detect deleted files"
);
}
# invalidate the gethead cache
- $self->{gethead_cache} = undef;
+ $self->clearCommitRefCaches();
# Ending exclusive lock here
$self->{dbh}->commit() or die "Failed to commit changes to SQLite";
}
+sub readCommits
+{
+ my $pipeHandle = shift;
+ my @commits;
+
+ my %commit = ();
+
+ while ( <$pipeHandle> )
+ {
+ chomp;
+ if (m/^commit\s+(.*)$/) {
+ # on ^commit lines put the just seen commit in the stack
+ # and prime things for the next one
+ if (keys %commit) {
+ my %copy = %commit;
+ unshift @commits, \%copy;
+ %commit = ();
+ }
+ my @parents = split(m/\s+/, $1);
+ $commit{hash} = shift @parents;
+ $commit{parents} = \@parents;
+ } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
+ # on rfc822-like lines seen before we see any message,
+ # lowercase the entry and put it in the hash as key-value
+ $commit{lc($1)} = $2;
+ } else {
+ # message lines - skip initial empty line
+ # and trim whitespace
+ if (!exists($commit{message}) && m/^\s*$/) {
+ # define it to mark the end of headers
+ $commit{message} = '';
+ next;
+ }
+ s/^\s+//; s/\s+$//; # trim ws
+ $commit{message} .= $_ . "\n";
+ }
+ }
+
+ unshift @commits, \%commit if ( keys %commit );
+
+ return @commits;
+}
+
+sub convertToCvsDate
+{
+ my $date = shift;
+ # Convert from: "git rev-list --pretty" formatted date
+ # Convert to: "the format specified by RFC822 as modified by RFC1123."
+ # Example: 26 May 1997 13:01:40 -0400
+ if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
+ {
+ $date = "$2 $1 $4 $3 $5";
+ }
+
+ return $date;
+}
+
+sub convertToDbMode
+{
+ my $mode = shift;
+
+ # NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
+ # but the database "mode" column historically (and currently)
+ # only stores the "rw" (for user) part of the string.
+ # FUTURE: It might make more sense to persist the raw
+ # octal mode (or perhaps the final full CVS form) instead of
+ # this half-converted form, but it isn't currently worth the
+ # backwards compatibility headaches.
+
+ $mode=~/^\d\d(\d)\d{3}$/;
+ my $userBits=$1;
+
+ my $dbMode = "";
+ $dbMode .= "r" if ( $userBits & 4 );
+ $dbMode .= "w" if ( $userBits & 2 );
+ $dbMode .= "x" if ( $userBits & 1 );
+ $dbMode = "rw" if ( $dbMode eq "" );
+
+ return $dbMode;
+}
+
sub insert_rev
{
my $self = shift;
$insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode);
}
-sub _headrev
-{
- my $self = shift;
- my $filename = shift;
- my $tablename = $self->tablename("head");
-
- my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM $tablename WHERE name=?",{},1);
- $db_query->execute($filename);
- my ( $hash, $revision, $mode ) = $db_query->fetchrow_array;
-
- return ( $hash, $revision, $mode );
-}
-
sub _get_prop
{
my $self = shift;
sub gethead
{
my $self = shift;
+ my $intRev = shift;
my $tablename = $self->tablename("head");
return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) );
my $tree = [];
while ( my $file = $db_query->fetchrow_hashref )
{
+ if(!$intRev)
+ {
+ $file->{revision} = "1.$file->{revision}"
+ }
push @$tree, $file;
}
return $tree;
}
+=head2 getAnyHead
+
+Returns a reference to an array of getmeta structures, one
+per file in the specified tree hash.
+
+=cut
+
+sub getAnyHead
+{
+ my ($self,$hash) = @_;
+
+ if(!defined($hash))
+ {
+ return $self->gethead();
+ }
+
+ my @files;
+ {
+ open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+ or die("Cannot call git-ls-tree : $!");
+ local $/ = "\0";
+ @files=<$filePipe>;
+ close $filePipe;
+ }
+
+ my $tree=[];
+ my($line);
+ foreach $line (@files)
+ {
+ $line=~s/\0$//;
+ unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+ {
+ die("Couldn't process git-ls-tree line : $_");
+ }
+
+ my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+ push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
+ }
+
+ return $tree;
+}
+
+=head2 getRevisionDirMap
+
+A "revision dir map" contains all the plain-file filenames associated
+with a particular revision (treeish), organized by directory:
+
+ $type = $out->{$dir}{$fullName}
+
+The type of each is "F" (for ordinary file) or "D" (for directory,
+for which the map $out->{$fullName} will also exist).
+
+=cut
+
+sub getRevisionDirMap
+{
+ my ($self,$ver)=@_;
+
+ if(!defined($self->{revisionDirMapCache}))
+ {
+ $self->{revisionDirMapCache}={};
+ }
+
+ # Get file list (previously cached results are dependent on HEAD,
+ # but are early in each case):
+ my $cacheKey;
+ my (@fileList);
+ if( !defined($ver) || $ver eq "" )
+ {
+ $cacheKey="";
+ if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+ {
+ return $self->{revisionDirMapCache}{$cacheKey};
+ }
+
+ my @head = @{$self->gethead()};
+ foreach my $file ( @head )
+ {
+ next if ( $file->{filehash} eq "deleted" );
+
+ push @fileList,$file->{name};
+ }
+ }
+ else
+ {
+ my ($hash)=$self->lookupCommitRef($ver);
+ if( !defined($hash) )
+ {
+ return undef;
+ }
+
+ $cacheKey=$hash;
+ if( defined($self->{revisionDirMapCache}{$cacheKey}) )
+ {
+ return $self->{revisionDirMapCache}{$cacheKey};
+ }
+
+ open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
+ or die("Cannot call git-ls-tree : $!");
+ local $/ = "\0";
+ while ( <$filePipe> )
+ {
+ chomp;
+ unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+ {
+ die("Couldn't process git-ls-tree line : $_");
+ }
+
+ my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
+
+ push @fileList, $git_filename;
+ }
+ close $filePipe;
+ }
+
+ # Convert to normalized form:
+ my %revMap;
+ my $file;
+ foreach $file (@fileList)
+ {
+ my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
+ $dir='' if(!defined($dir));
+
+ # parent directories:
+ # ... create empty dir maps for parent dirs:
+ my($td)=$dir;
+ while(!defined($revMap{$td}))
+ {
+ $revMap{$td}={};
+
+ my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+ $tp='' if(!defined($tp));
+ $td=$tp;
+ }
+ # ... add children to parent maps (now that they exist):
+ $td=$dir;
+ while($td ne "")
+ {
+ my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
+ $tp='' if(!defined($tp));
+
+ if(defined($revMap{$tp}{$td}))
+ {
+ if($revMap{$tp}{$td} ne 'D')
+ {
+ die "Weird file/directory inconsistency in $cacheKey";
+ }
+ last; # loop exit
+ }
+ $revMap{$tp}{$td}='D';
+
+ $td=$tp;
+ }
+
+ # file
+ $revMap{$dir}{$file}='F';
+ }
+
+ # Save in cache:
+ $self->{revisionDirMapCache}{$cacheKey}=\%revMap;
+ return $self->{revisionDirMapCache}{$cacheKey};
+}
+
=head2 getlog
+See also gethistorydense().
+
=cut
sub getlog
{
my $self = shift;
my $filename = shift;
+ my $revFilter = shift;
+
my $tablename = $self->tablename("revision");
+ # Filters:
+ # TODO: date, state, or by specific logins filters?
+ # TODO: Handle comma-separated list of revFilter items, each item
+ # can be a range [only case currently handled] or individual
+ # rev or branch or "branch.".
+ # TODO: Adjust $db_query WHERE clause based on revFilter, instead of
+ # manually filtering the results of the query?
+ my ( $minrev, $maxrev );
+ if( defined($revFilter) and
+ $state->{opt}{r} =~ /^(1.(\d+))?(::?)(1.(\d.+))?$/ )
+ {
+ my $control = $3;
+ $minrev = $2;
+ $maxrev = $5;
+ $minrev++ if ( defined($minrev) and $control eq "::" );
+ }
+
my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
$db_query->execute($filename);
+ my $totalRevs=0;
my $tree = [];
while ( my $file = $db_query->fetchrow_hashref )
{
+ $totalRevs++;
+ if( defined($minrev) and $file->{revision} < $minrev )
+ {
+ next;
+ }
+ if( defined($maxrev) and $file->{revision} > $maxrev )
+ {
+ next;
+ }
+
+ $file->{revision} = "1." . $file->{revision};
push @$tree, $file;
}
- return $tree;
+ return ($tree,$totalRevs);
}
=head2 getmeta
my $tablename_head = $self->tablename("head");
my $db_query;
- if ( defined($revision) and $revision =~ /^\d+$/ )
+ if ( defined($revision) and $revision =~ /^1\.(\d+)$/ )
{
+ my ($intRev) = $1;
$db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
- $db_query->execute($filename, $revision);
+ $db_query->execute($filename, $intRev);
}
elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
{
$db_query->execute($filename);
}
- return $db_query->fetchrow_hashref;
+ my $meta = $db_query->fetchrow_hashref;
+ if($meta)
+ {
+ $meta->{revision} = "1.$meta->{revision}";
+ }
+ return $meta;
+}
+
+sub getMetaFromCommithash
+{
+ my $self = shift;
+ my $filename = shift;
+ my $revCommit = shift;
+
+ # NOTE: This function doesn't scale well (lots of forks), especially
+ # if you have many files that have not been modified for many commits
+ # (each git-rev-parse redoes a lot of work for each file
+ # that theoretically could be done in parallel by smarter
+ # graph traversal).
+ #
+ # TODO: Possible optimization strategies:
+ # - Solve the issue of assigning and remembering "real" CVS
+ # revision numbers for branches, and ensure the
+ # data structure can do this efficiently. Perhaps something
+ # similar to "git notes", and carefully structured to take
+ # advantage same-sha1-is-same-contents, to roll the same
+ # unmodified subdirectory data onto multiple commits?
+ # - Write and use a C tool that is like git-blame, but
+ # operates on multiple files with file granularity, instead
+ # of one file with line granularity. Cache
+ # most-recently-modified in $self->{commitRefCache}{$revCommit}.
+ # Try to be intelligent about how many files we do with
+ # one fork (perhaps one directory at a time, without recursion,
+ # and/or include directory as one line item, recurse from here
+ # instead of in C tool?).
+ # - Perhaps we could ask the DB for (filename,fileHash),
+ # and just guess that it is correct (that the file hadn't
+ # changed between $revCommit and the found commit, then
+ # changed back, confusing anything trying to interpret
+ # history). Probably need to add another index to revisions
+ # DB table for this.
+ # - NOTE: Trying to store all (commit,file) keys in DB [to
+ # find "lastModfiedCommit] (instead of
+ # just files that changed in each commit as we do now) is
+ # probably not practical from a disk space perspective.
+
+ # Does the file exist in $revCommit?
+ # TODO: Include file hash in dirmap cache.
+ my($dirMap)=$self->getRevisionDirMap($revCommit);
+ my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
+ if(!defined($dir))
+ {
+ $dir="";
+ }
+ if( !defined($dirMap->{$dir}) ||
+ !defined($dirMap->{$dir}{$filename}) )
+ {
+ my($fileHash)="deleted";
+
+ my($retVal)={};
+ $retVal->{name}=$filename;
+ $retVal->{filehash}=$fileHash;
+
+ # not needed and difficult to compute:
+ $retVal->{revision}="0"; # $revision;
+ $retVal->{commithash}=$revCommit;
+ #$retVal->{author}=$commit->{author};
+ #$retVal->{modified}=convertToCvsDate($commit->{date});
+ #$retVal->{mode}=convertToDbMode($mode);
+
+ return $retVal;
+ }
+
+ my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
+ chomp $fileHash;
+ if(!($fileHash=~/^[0-9a-f]{40}$/))
+ {
+ die "Invalid fileHash '$fileHash' looking up"
+ ." '$revCommit:$filename'\n";
+ }
+
+ # information about most recent commit to modify $filename:
+ open(my $gitLogPipe, '-|', 'git', 'rev-list',
+ '--max-count=1', '--pretty', '--parents',
+ $revCommit, '--', $filename)
+ or die "Cannot call git-rev-list: $!";
+ my @commits=readCommits($gitLogPipe);
+ close $gitLogPipe;
+ if(scalar(@commits)!=1)
+ {
+ die "Can't find most recent commit changing $filename\n";
+ }
+ my($commit)=$commits[0];
+ if( !defined($commit) || !defined($commit->{hash}) )
+ {
+ return undef;
+ }
+
+ # does this (commit,file) have a real assigned CVS revision number?
+ my $tablename_rev = $self->tablename("revision");
+ my $db_query;
+ $db_query = $self->{dbh}->prepare_cached(
+ "SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
+ {},1);
+ $db_query->execute($filename, $commit->{hash});
+ my($meta)=$db_query->fetchrow_hashref;
+ if($meta)
+ {
+ $meta->{revision} = "1.$meta->{revision}";
+ return $meta;
+ }
+
+ # fall back on special revision number
+ my($revision)=$commit->{hash};
+ $revision=~s/(..)/'.' . (hex($1)+100)/eg;
+ $revision="2.1.1.2000$revision";
+
+ # meta data about $filename:
+ open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
+ $commit->{hash}, '--', $filename)
+ or die("Cannot call git-ls-tree : $!");
+ local $/ = "\0";
+ my $line;
+ $line=<$filePipe>;
+ if(defined(<$filePipe>))
+ {
+ die "Expected only a single file for git-ls-tree $filename\n";
+ }
+ close $filePipe;
+
+ chomp $line;
+ unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
+ {
+ die("Couldn't process git-ls-tree line : $line\n");
+ }
+ my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
+
+ # save result:
+ my($retVal)={};
+ $retVal->{name}=$filename;
+ $retVal->{revision}=$revision;
+ $retVal->{filehash}=$fileHash;
+ $retVal->{commithash}=$revCommit;
+ $retVal->{author}=$commit->{author};
+ $retVal->{modified}=convertToCvsDate($commit->{date});
+ $retVal->{mode}=convertToDbMode($mode);
+
+ return $retVal;
+}
+
+=head2 lookupCommitRef
+
+Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches
+the result so looking it up again is fast.
+
+=cut
+
+sub lookupCommitRef
+{
+ my $self = shift;
+ my $ref = shift;
+
+ my $commitHash = $self->{commitRefCache}{$ref};
+ if(defined($commitHash))
+ {
+ return $commitHash;
+ }
+
+ $commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
+ $self->unescapeRefName($ref));
+ $commitHash=~s/\s*$//;
+ if(!($commitHash=~/^[0-9a-f]{40}$/))
+ {
+ $commitHash=undef;
+ }
+
+ if( defined($commitHash) )
+ {
+ my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
+ if( ! ($type=~/^commit\s*$/ ) )
+ {
+ $commitHash=undef;
+ }
+ }
+ if(defined($commitHash))
+ {
+ $self->{commitRefCache}{$ref}=$commitHash;
+ }
+ return $commitHash;
+}
+
+=head2 clearCommitRefCaches
+
+Clears cached commit cache (sha1's for various tags/abbeviations/etc),
+and related caches.
+
+=cut
+
+sub clearCommitRefCaches
+{
+ my $self = shift;
+ $self->{commitRefCache} = {};
+ $self->{revisionDirMapCache} = undef;
+ $self->{gethead_cache} = undef;
}
=head2 commitmessage
return $message;
}
-=head2 gethistory
+=head2 gethistorydense
This function takes a filename (with path) argument and returns an arrayofarrays
-containing revision,filehash,commithash ordered by revision descending
+containing revision,filehash,commithash ordered by revision descending.
+
+This version of gethistory skips deleted entries -- so it is useful for annotate.
+The 'dense' part is a reference to a '--dense' option available for git-rev-list
+and other git tools that depend on it.
+
+See also getlog().
=cut
-sub gethistory
+sub gethistorydense
{
my $self = shift;
my $filename = shift;
my $tablename = $self->tablename("revision");
my $db_query;
- $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? ORDER BY revision DESC",{},1);
+ $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
$db_query->execute($filename);
- return $db_query->fetchall_arrayref;
+ my $result = $db_query->fetchall_arrayref;
+
+ my $i;
+ for($i=0 ; $i<scalar(@$result) ; $i++)
+ {
+ $result->[$i][0]="1." . $result->[$i][0];
+ }
+
+ return $result;
}
-=head2 gethistorydense
+=head2 escapeRefName
-This function takes a filename (with path) argument and returns an arrayofarrays
-containing revision,filehash,commithash ordered by revision descending.
+Apply an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
-This version of gethistory skips deleted entries -- so it is useful for annotate.
-The 'dense' part is a reference to a '--dense' option available for git-rev-list
-and other git tools that depend on it.
+=cut
+sub escapeRefName
+{
+ my($self,$refName)=@_;
+
+ # CVS officially only allows [-_A-Za-z0-9] in tag names (or in
+ # many contexts it can also be a CVS revision number).
+ #
+ # Git tags commonly use '/' and '.' as well, but also handle
+ # anything else just in case:
+ #
+ # = "_-s-" For '/'.
+ # = "_-p-" For '.'.
+ # = "_-u-" For underscore, in case someone wants a literal "_-" in
+ # a tag name.
+ # = "_-xx-" Where "xx" is the hexadecimal representation of the
+ # desired ASCII character byte. (for anything else)
+
+ if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
+ {
+ $refName=~s/_-/_-u--/g;
+ $refName=~s/\./_-p-/g;
+ $refName=~s%/%_-s-%g;
+ $refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
+ }
+}
+
+=head2 unescapeRefName
+
+Undo an escape mechanism to compensate for characters that
+git ref names can have that CVS tags can not.
=cut
-sub gethistorydense
+sub unescapeRefName
{
- my $self = shift;
- my $filename = shift;
- my $tablename = $self->tablename("revision");
+ my($self,$refName)=@_;
- my $db_query;
- $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM $tablename WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1);
- $db_query->execute($filename);
+ # see escapeRefName() for description of escape mechanism.
+
+ $refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
+
+ # allowed tag names
+ # TODO: Perhaps use git check-ref-format, with an in-process cache of
+ # validated names?
+ if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
+ ( $refName=~m%[/.]$% ) ||
+ ( $refName=~/\.lock$/ ) ||
+ ( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) ) # matching }
+ {
+ # Error:
+ $log->warn("illegal refName: $refName");
+ $refName=undef;
+ }
+ return $refName;
+}
+
+sub unescapeRefNameChar
+{
+ my($char)=@_;
+
+ if($char eq "s")
+ {
+ $char="/";
+ }
+ elsif($char eq "p")
+ {
+ $char=".";
+ }
+ elsif($char eq "u")
+ {
+ $char="_";
+ }
+ elsif($char=~/^[0-9a-f][0-9a-f]$/)
+ {
+ $char=chr(hex($char));
+ }
+ else
+ {
+ # Error case: Maybe it has come straight from user, and
+ # wasn't supposed to be escaped? Restore it the way we got it:
+ $char="_-$char-";
+ }
- return $db_query->fetchall_arrayref;
+ return $char;
}
=head2 in_array()