cvsserver: support -r and sticky tags for most operations

- Split off prepDirForOutput for "update" and "commit". Some low level protocol details were changed to more closely resemble CVS even in non-tagged cases. Hopefully it still works with finicky clients like Eclipse. - Substantial changes to "diff". The output is now closer to standard CVS (including exit status), and can be used as a patch, but there are still a number of differences compared to CVS. - Tweaks to "add", "remove", "status", and "commit". - FUTURE: CVS revision numbers for branches simply encode git commit IDs in a way that resembles CVS revision numbers, dropping all normal CVS structural relations between different revision numbers. - FUTURE: "log" doesn't try to work properly at all with branches and tags. - FUTURE: "annotate" probably doesn't work with branches or tags either (untested)? Signed-off-by: Matthew Ogilvie <mmogilvi_git@miniinfo.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthew Ogilvie committed Oct 13, 2012 at 23:42 UTC 61717661e6ffc8aa4d0928d04edafa0961e6aadb
1 file changed +510 -203
git-cvsserver.perl
+510 -203
@@ -611,7 +611,10 @@ sub req_add
611 {
612 $filename = filecleanup($filename);
613
614 - my $meta = $updater->getmeta($filename);
614 + # no -r, -A, or -D with add
615 + my $stickyInfo = resolveStickyInfo($filename);
616 +
617 + my $meta = $updater->getmeta($filename,$stickyInfo);
618 my $wrev = revparse($filename);
619
620 if ($wrev && $meta && ($wrev=~/^-/))
@@ -634,8 +637,10 @@ sub req_add
637
638 # this is an "entries" line
639 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
637 - $log->debug("/$filepart/$meta->{revision}//$kopts/");
638 - print "/$filepart/$meta->{revision}//$kopts/\n";
640 + my $entryLine = "/$filepart/$meta->{revision}//$kopts/";
641 + $entryLine .= getStickyTagOrDate($stickyInfo);
642 + $log->debug($entryLine);
643 + print "$entryLine\n";
644 # permissions
645 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
646 print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
@@ -666,7 +671,8 @@ sub req_add
671 print "$filename\n";
672 my $kopts = kopts_from_path($filename,"file",
673 $state->{entries}{$filename}{modified_filename});
669 - print "/$filepart/0//$kopts/\n";
674 + print "/$filepart/0//$kopts/" .
675 + getStickyTagOrDate($stickyInfo) . "\n";
676
677 my $requestedKopts = $state->{opt}{k};
678 if(defined($requestedKopts))
@@ -734,7 +740,10 @@ sub req_remove
740 next;
741 }
742
737 - my $meta = $updater->getmeta($filename);
743 + # only from entries
744 + my $stickyInfo = resolveStickyInfo($filename);
745 +
746 + my $meta = $updater->getmeta($filename,$stickyInfo);
747 my $wrev = revparse($filename);
748
749 unless ( defined ( $wrev ) )
@@ -764,7 +773,7 @@ sub req_remove
773 print "Checked-in $dirpart\n";
774 print "$filename\n";
775 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
767 - print "/$filepart/-$wrev//$kopts/\n";
776 + print "/$filepart/-$wrev//$kopts/" . getStickyTagOrDate($stickyInfo) . "\n";
777
778 $rmcount++;
779 }
@@ -944,6 +953,9 @@ sub req_co
953 return 1;
954 }
955
956 + my $stickyInfo = { 'tag' => $state->{opt}{r},
957 + 'date' => $state->{opt}{D} };
958 +
959 my $module = $state->{args}[0];
960 $state->{module} = $module;
961 my $checkout_path = $module;
@@ -961,64 +973,32 @@ sub req_co
973 my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
974 $updater->update();
975
964 - $checkout_path =~ s|/$||; # get rid of trailing slashes
976 + my $headHash;
977 + if( defined($stickyInfo) && defined($stickyInfo->{tag}) )
978 + {
979 + $headHash = $updater->lookupCommitRef($stickyInfo->{tag});
980 + if( !defined($headHash) )
981 + {
982 + print "error 1 no such tag `$stickyInfo->{tag}'\n";
983 + cleanupWorkTree();
984 + exit;
985 + }
986 + }
987
966 - # Eclipse seems to need the Clear-sticky command
967 - # to prepare the 'Entries' file for the new directory.
968 - print "Clear-sticky $checkout_path/\n";
969 - print $state->{CVSROOT} . "/$module/\n";
970 - print "Clear-static-directory $checkout_path/\n";
971 - print $state->{CVSROOT} . "/$module/\n";
972 - print "Clear-sticky $checkout_path/\n"; # yes, twice
973 - print $state->{CVSROOT} . "/$module/\n";
974 - print "Template $checkout_path/\n";
975 - print $state->{CVSROOT} . "/$module/\n";
976 - print "0\n";
977 -
978 - # instruct the client that we're checking out to $checkout_path
979 - print "E cvs checkout: Updating $checkout_path\n";
988 + $checkout_path =~ s|/$||; # get rid of trailing slashes
989
990 my %seendirs = ();
991 my $lastdir ='';
992
984 - # recursive
985 - sub prepdir {
986 - my ($dir, $repodir, $remotedir, $seendirs) = @_;
987 - my $parent = dirname($dir);
988 - $dir =~ s|/+$||;
989 - $repodir =~ s|/+$||;
990 - $remotedir =~ s|/+$||;
991 - $parent =~ s|/+$||;
992 - $log->debug("announcedir $dir, $repodir, $remotedir" );
993 -
994 - if ($parent eq '.' || $parent eq './') {
995 - $parent = '';
996 - }
997 - # recurse to announce unseen parents first
998 - if (length($parent) && !exists($seendirs->{$parent})) {
999 - prepdir($parent, $repodir, $remotedir, $seendirs);
1000 - }
1001 - # Announce that we are going to modify at the parent level
1002 - if ($parent) {
1003 - print "E cvs checkout: Updating $remotedir/$parent\n";
1004 - } else {
1005 - print "E cvs checkout: Updating $remotedir\n";
1006 - }
1007 - print "Clear-sticky $remotedir/$parent/\n";
1008 - print "$repodir/$parent/\n";
1009 -
1010 - print "Clear-static-directory $remotedir/$dir/\n";
1011 - print "$repodir/$dir/\n";
1012 - print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
1013 - print "$repodir/$parent/\n";
1014 - print "Template $remotedir/$dir/\n";
1015 - print "$repodir/$dir/\n";
1016 - print "0\n";
1017 -
1018 - $seendirs->{$dir} = 1;
1019 - }
1020 -
1021 - foreach my $git ( @{$updater->gethead} )
993 + prepDirForOutput(
994 + ".",
995 + $state->{CVSROOT} . "/$module",
996 + $checkout_path,
997 + \%seendirs,
998 + 'checkout',
999 + $state->{dirArgs} );
1000 +
1001 + foreach my $git ( @{$updater->getAnyHead($headHash)} )
1002 {
1003 # Don't want to check out deleted files
1004 next if ( $git->{filehash} eq "deleted" );
@@ -1026,16 +1006,13 @@ sub req_co
1006 my $fullName = $git->{name};
1007 ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
1008
1029 - if (length($git->{dir}) && $git->{dir} ne './'
1030 - && $git->{dir} ne $lastdir ) {
1031 - unless (exists($seendirs{$git->{dir}})) {
1032 - prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
1033 - $checkout_path, \%seendirs);
1034 - $lastdir = $git->{dir};
1035 - $seendirs{$git->{dir}} = 1;
1036 - }
1037 - print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
1038 - }
1009 + unless (exists($seendirs{$git->{dir}})) {
1010 + prepDirForOutput($git->{dir}, $state->{CVSROOT} . "/$module/",
1011 + $checkout_path, \%seendirs, 'checkout',
1012 + $state->{dirArgs} );
1013 + $lastdir = $git->{dir};
1014 + $seendirs{$git->{dir}} = 1;
1015 + }
1016
1017 # modification time of this file
1018 print "Mod-time $git->{modified}\n";
@@ -1055,7 +1032,8 @@ sub req_co
1032
1033 # this is an "entries" line
1034 my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
1058 - print "/$git->{name}/$git->{revision}//$kopts/\n";
1035 + print "/$git->{name}/$git->{revision}//$kopts/" .
1036 + getStickyTagOrDate($stickyInfo) . "\n";
1037 # permissions
1038 print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
1039
@@ -1068,6 +1046,119 @@ sub req_co
1046 statecleanup();
1047 }
1048
1049 +# used by req_co and req_update to set up directories for files
1050 +# recursively handles parents
1051 +sub prepDirForOutput
1052 +{
1053 + my ($dir, $repodir, $remotedir, $seendirs, $request, $dirArgs) = @_;
1054 +
1055 + my $parent = dirname($dir);
1056 + $dir =~ s|/+$||;
1057 + $repodir =~ s|/+$||;
1058 + $remotedir =~ s|/+$||;
1059 + $parent =~ s|/+$||;
1060 +
1061 + if ($parent eq '.' || $parent eq './')
1062 + {
1063 + $parent = '';
1064 + }
1065 + # recurse to announce unseen parents first
1066 + if( length($parent) &&
1067 + !exists($seendirs->{$parent}) &&
1068 + ( $request eq "checkout" ||
1069 + exists($dirArgs->{$parent}) ) )
1070 + {
1071 + prepDirForOutput($parent, $repodir, $remotedir,
1072 + $seendirs, $request, $dirArgs);
1073 + }
1074 + # Announce that we are going to modify at the parent level
1075 + if ($dir eq '.' || $dir eq './')
1076 + {
1077 + $dir = '';
1078 + }
1079 + if(exists($seendirs->{$dir}))
1080 + {
1081 + return;
1082 + }
1083 + $log->debug("announcedir $dir, $repodir, $remotedir" );
1084 + my($thisRemoteDir,$thisRepoDir);
1085 + if ($dir ne "")
1086 + {
1087 + $thisRepoDir="$repodir/$dir";
1088 + if($remotedir eq ".")
1089 + {
1090 + $thisRemoteDir=$dir;
1091 + }
1092 + else
1093 + {
1094 + $thisRemoteDir="$remotedir/$dir";
1095 + }
1096 + }
1097 + else
1098 + {
1099 + $thisRepoDir=$repodir;
1100 + $thisRemoteDir=$remotedir;
1101 + }
1102 + unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
1103 + {
1104 + print "E cvs $request: Updating $thisRemoteDir\n";
1105 + }
1106 +
1107 + my ($opt_r)=$state->{opt}{r};
1108 + my $stickyInfo;
1109 + if(exists($state->{opt}{A}))
1110 + {
1111 + # $stickyInfo=undef;
1112 + }
1113 + elsif( defined($opt_r) && $opt_r ne "" )
1114 + # || ( defined($state->{opt}{D}) && $state->{opt}{D} ne "" ) # TODO
1115 + {
1116 + $stickyInfo={ 'tag' => (defined($opt_r)?$opt_r:undef) };
1117 +
1118 + # TODO: Convert -D value into the form 2011.04.10.04.46.57,
1119 + # similar to an entry line's sticky date, without the D prefix.
1120 + # It sometimes (always?) arrives as something more like
1121 + # '10 Apr 2011 04:46:57 -0000'...
1122 + # $stickyInfo={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
1123 + }
1124 + else
1125 + {
1126 + $stickyInfo=getDirStickyInfo($state->{prependdir} . $dir);
1127 + }
1128 +
1129 + my $stickyResponse;
1130 + if(defined($stickyInfo))
1131 + {
1132 + $stickyResponse = "Set-sticky $thisRemoteDir/\n" .
1133 + "$thisRepoDir/\n" .
1134 + getStickyTagOrDate($stickyInfo) . "\n";
1135 + }
1136 + else
1137 + {
1138 + $stickyResponse = "Clear-sticky $thisRemoteDir/\n" .
1139 + "$thisRepoDir/\n";
1140 + }
1141 +
1142 + unless ( $state->{globaloptions}{-n} )
1143 + {
1144 + print $stickyResponse;
1145 +
1146 + print "Clear-static-directory $thisRemoteDir/\n";
1147 + print "$thisRepoDir/\n";
1148 + print $stickyResponse; # yes, twice
1149 + print "Template $thisRemoteDir/\n";
1150 + print "$thisRepoDir/\n";
1151 + print "0\n";
1152 + }
1153 +
1154 + $seendirs->{$dir} = 1;
1155 +
1156 + # FUTURE: This would more accurately emulate CVS by sending
1157 + # another copy of sticky after processing the files in that
1158 + # directory. Or intermediate: perhaps send all sticky's for
1159 + # $seendirs after after processing all files.
1160 +}
1161 +
1162 # update \n
1163 # Response expected: yes. Actually do a cvs update command. This uses any
1164 # previous Argument, Directory, Entry, or Modified requests, if they have
@@ -1111,29 +1202,19 @@ sub req_update
1202
1203 #$log->debug("update state : " . Dumper($state));
1204
1114 - my $last_dirname = "///";
1205 + my($repoDir);
1206 + $repoDir=$state->{CVSROOT} . "/$state->{module}/$state->{prependdir}";
1207 +
1208 + my %seendirs = ();
1209
1210 # foreach file specified on the command line ...
1117 - foreach my $filename ( @{$state->{args}} )
1211 + foreach my $argsFilename ( @{$state->{args}} )
1212 {
1119 - $filename = filecleanup($filename);
1213 + my $filename;
1214 + $filename = filecleanup($argsFilename);
1215
1216 $log->debug("Processing file $filename");
1217
1123 - unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
1124 - {
1125 - my $cur_dirname = dirname($filename);
1126 - if ( $cur_dirname ne $last_dirname )
1127 - {
1128 - $last_dirname = $cur_dirname;
1129 - if ( $cur_dirname eq "" )
1130 - {
1131 - $cur_dirname = ".";
1132 - }
1133 - print "E cvs update: Updating $cur_dirname\n";
1134 - }
1135 - }
1136 -
1218 # if we have a -C we should pretend we never saw modified stuff
1219 if ( exists ( $state->{opt}{C} ) )
1220 {
@@ -1142,13 +1223,11 @@ sub req_update
1223 $state->{entries}{$filename}{unchanged} = 1;
1224 }
1225
1145 - my $meta;
1146 - if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
1147 - {
1148 - $meta = $updater->getmeta($filename, $1);
1149 - } else {
1150 - $meta = $updater->getmeta($filename);
1151 - }
1226 + my $stickyInfo = resolveStickyInfo($filename,
1227 + $state->{opt}{r},
1228 + $state->{opt}{D},
1229 + exists($state->{opt}{A}));
1230 + my $meta = $updater->getmeta($filename, $stickyInfo);
1231
1232 # If -p was given, "print" the contents of the requested revision.
1233 if ( exists ( $state->{opt}{p} ) ) {
@@ -1161,6 +1240,17 @@ sub req_update
1240 next;
1241 }
1242
1243 + # Directories:
1244 + prepDirForOutput(
1245 + dirname($argsFilename),
1246 + $repoDir,
1247 + ".",
1248 + \%seendirs,
1249 + "update",
1250 + $state->{dirArgs} );
1251 +
1252 + my $wrev = revparse($filename);
1253 +
1254 if ( ! defined $meta )
1255 {
1256 $meta = {
@@ -1168,16 +1258,23 @@ sub req_update
1258 revision => '0',
1259 filehash => 'added'
1260 };
1261 + if($wrev ne "0")
1262 + {
1263 + $meta->{filehash}='deleted';
1264 + }
1265 }
1266
1267 my $oldmeta = $meta;
1268
1175 - my $wrev = revparse($filename);
1176 -
1269 # If the working copy is an old revision, lets get that version too for comparison.
1178 - if ( defined($wrev) and $wrev ne $meta->{revision} )
1270 + my $oldWrev=$wrev;
1271 + if(defined($oldWrev))
1272 {
1180 - $oldmeta = $updater->getmeta($filename, $wrev);
1273 + $oldWrev=~s/^-//;
1274 + if($oldWrev ne $meta->{revision})
1275 + {
1276 + $oldmeta = $updater->getmeta($filename, $oldWrev);
1277 + }
1278 }
1279
1280 #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
@@ -1195,6 +1292,7 @@ sub req_update
1292 if ( defined ( $wrev )
1293 and defined($meta->{revision})
1294 and $wrev eq $meta->{revision}
1295 + and $wrev ne "0"
1296 and defined($state->{entries}{$filename}{modified_hash})
1297 and not exists ( $state->{opt}{C} ) )
1298 {
@@ -1205,7 +1303,7 @@ sub req_update
1303 next;
1304 }
1305
1208 - if ( $meta->{filehash} eq "deleted" )
1306 + if ( $meta->{filehash} eq "deleted" && $wrev ne "0" )
1307 {
1308 # TODO: If it has been modified in the sandbox, error out
1309 # with the appropriate message, rather than deleting a modified
@@ -1267,10 +1365,6 @@ sub req_update
1365 $log->debug("Updating existing file 'Update-existing $dirpart'");
1366 } else {
1367 # instruct client we're sending a file to put in this path as a new file
1270 - print "Clear-static-directory $dirpart\n";
1271 - print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1272 - print "Clear-sticky $dirpart\n";
1273 - print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
1368
1369 $log->debug("Creating new file 'Created $dirpart'");
1370 print "Created $dirpart\n";
@@ -1279,8 +1373,10 @@ sub req_update
1373
1374 # this is an "entries" line
1375 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1282 - $log->debug("/$filepart/$meta->{revision}//$kopts/");
1283 - print "/$filepart/$meta->{revision}//$kopts/\n";
1376 + my $entriesLine = "/$filepart/$meta->{revision}//$kopts/";
1377 + $entriesLine .= getStickyTagOrDate($stickyInfo);
1378 + $log->debug($entriesLine);
1379 + print "$entriesLine\n";
1380
1381 # permissions
1382 $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
@@ -1328,7 +1424,9 @@ sub req_update
1424 my $kopts = kopts_from_path("$dirpart/$filepart",
1425 "file",$mergedFile);
1426 $log->debug("/$filepart/$meta->{revision}//$kopts/");
1331 - print "/$filepart/$meta->{revision}//$kopts/\n";
1427 + my $entriesLine="/$filepart/$meta->{revision}//$kopts/";
1428 + $entriesLine .= getStickyTagOrDate($stickyInfo);
1429 + print "$entriesLine\n";
1430 }
1431 }
1432 elsif ( $return == 1 )
@@ -1344,7 +1442,9 @@ sub req_update
1442 print $state->{CVSROOT} . "/$state->{module}/$filename\n";
1443 my $kopts = kopts_from_path("$dirpart/$filepart",
1444 "file",$mergedFile);
1347 - print "/$filepart/$meta->{revision}/+/$kopts/\n";
1445 + my $entriesLine = "/$filepart/$meta->{revision}/+/$kopts/";
1446 + $entriesLine .= getStickyTagOrDate($stickyInfo);
1447 + print "$entriesLine\n";
1448 }
1449 }
1450 else
@@ -1372,6 +1472,43 @@ sub req_update
1472
1473 }
1474
1475 + # prepDirForOutput() any other existing directories unless they already
1476 + # have the right sticky tag:
1477 + unless ( $state->{globaloptions}{n} )
1478 + {
1479 + my $dir;
1480 + foreach $dir (keys(%{$state->{dirMap}}))
1481 + {
1482 + if( ! $seendirs{$dir} &&
1483 + exists($state->{dirArgs}{$dir}) )
1484 + {
1485 + my($oldTag);
1486 + $oldTag=$state->{dirMap}{$dir}{tagspec};
1487 +
1488 + unless( ( exists($state->{opt}{A}) &&
1489 + defined($oldTag) ) ||
1490 + ( defined($state->{opt}{r}) &&
1491 + ( !defined($oldTag) ||
1492 + $state->{opt}{r} ne $oldTag ) ) )
1493 + # TODO?: OR sticky dir is different...
1494 + {
1495 + next;
1496 + }
1497 +
1498 + prepDirForOutput(
1499 + $dir,
1500 + $repoDir,
1501 + ".",
1502 + \%seendirs,
1503 + 'update',
1504 + $state->{dirArgs} );
1505 + }
1506 +
1507 + # TODO?: Consider sending a final duplicate Sticky response
1508 + # to more closely mimic real CVS.
1509 + }
1510 + }
1511 +
1512 print "ok\n";
1513 }
1514
@@ -1404,23 +1541,11 @@ sub req_ci
1541 my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
1542 $updater->update();
1543
1407 - # Remember where the head was at the beginning.
1408 - my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
1409 - chomp $parenthash;
1410 - if ($parenthash !~ /^[0-9a-f]{40}$/) {
1411 - print "error 1 pserver cannot find the current HEAD of module";
1412 - cleanupWorkTree();
1413 - exit;
1414 - }
1415 -
1416 - setupWorkTree($parenthash);
1417 -
1418 - $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1419 -
1420 - $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
1421 -
1544 my @committedfiles = ();
1545 my %oldmeta;
1546 + my $stickyInfo;
1547 + my $branchRef;
1548 + my $parenthash;
1549
1550 # foreach file specified on the command line ...
1551 foreach my $filename ( @{$state->{args}} )
@@ -1430,7 +1555,67 @@ sub req_ci
1555
1556 next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
1557
1433 - my $meta = $updater->getmeta($filename);
1558 + #####
1559 + # Figure out which branch and parenthash we are committing
1560 + # to, and setup worktree:
1561 +
1562 + # should always come from entries:
1563 + my $fileStickyInfo = resolveStickyInfo($filename);
1564 + if( !defined($branchRef) )
1565 + {
1566 + $stickyInfo = $fileStickyInfo;
1567 + if( defined($stickyInfo) &&
1568 + ( defined($stickyInfo->{date}) ||
1569 + !defined($stickyInfo->{tag}) ) )
1570 + {
1571 + print "error 1 cannot commit with sticky date for file `$filename'\n";
1572 + cleanupWorkTree();
1573 + exit;
1574 + }
1575 +
1576 + $branchRef = "refs/heads/$state->{module}";
1577 + if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
1578 + {
1579 + $branchRef = "refs/heads/$stickyInfo->{tag}";
1580 + }
1581 +
1582 + $parenthash = `git show-ref -s $branchRef`;
1583 + chomp $parenthash;
1584 + if ($parenthash !~ /^[0-9a-f]{40}$/)
1585 + {
1586 + if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
1587 + {
1588 + print "error 1 sticky tag `$stickyInfo->{tag}' for file `$filename' is not a branch\n";
1589 + }
1590 + else
1591 + {
1592 + print "error 1 pserver cannot find the current HEAD of module";
1593 + }
1594 + cleanupWorkTree();
1595 + exit;
1596 + }
1597 +
1598 + setupWorkTree($parenthash);
1599 +
1600 + $log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
1601 +
1602 + $log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
1603 + }
1604 + elsif( !refHashEqual($stickyInfo,$fileStickyInfo) )
1605 + {
1606 + #TODO: We could split the cvs commit into multiple
1607 + # git commits by distinct stickyTag values, but that
1608 + # is lowish priority.
1609 + print "error 1 Committing different files to different"
1610 + . " branches is not currently supported\n";
1611 + cleanupWorkTree();
1612 + exit;
1613 + }
1614 +
1615 + #####
1616 + # Process this file:
1617 +
1618 + my $meta = $updater->getmeta($filename,$stickyInfo);
1619 $oldmeta{$filename} = $meta;
1620
1621 my $wrev = revparse($filename);
@@ -1532,7 +1717,7 @@ sub req_ci
1717 }
1718
1719 ### Emulate git-receive-pack by running hooks/update
1535 - my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
1720 + my @hook = ( $ENV{GIT_DIR}.'hooks/update', $branchRef,
1721 $parenthash, $commithash );
1722 if( -x $hook[0] ) {
1723 unless( system( @hook ) == 0 )
@@ -1546,7 +1731,7 @@ sub req_ci
1731
1732 ### Update the ref
1733 if (system(qw(git update-ref -m), "cvsserver ci",
1549 - "refs/heads/$state->{module}", $commithash, $parenthash)) {
1734 + $branchRef, $commithash, $parenthash)) {
1735 $log->warn("update-ref for $state->{module} failed.");
1736 print "error 1 Cannot commit -- update first\n";
1737 cleanupWorkTree();
@@ -1560,7 +1745,7 @@ sub req_ci
1745
1746 local $SIG{PIPE} = sub { die 'pipe broke' };
1747
1563 - print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
1748 + print $pipe "$parenthash $commithash $branchRef\n";
1749
1750 close $pipe || die "bad pipe: $! $?";
1751 }
@@ -1570,7 +1755,7 @@ sub req_ci
1755 ### Then hooks/post-update
1756 $hook = $ENV{GIT_DIR}.'hooks/post-update';
1757 if (-x $hook) {
1573 - system($hook, "refs/heads/$state->{module}");
1758 + system($hook, $branchRef);
1759 }
1760
1761 # foreach file specified on the command line ...
@@ -1578,7 +1763,7 @@ sub req_ci
1763 {
1764 $filename = filecleanup($filename);
1765
1581 - my $meta = $updater->getmeta($filename);
1766 + my $meta = $updater->getmeta($filename,$stickyInfo);
1767 unless (defined $meta->{revision}) {
1768 $meta->{revision} = "1.1";
1769 }
@@ -1602,7 +1787,8 @@ sub req_ci
1787 print "Checked-in $dirpart\n";
1788 print "$filename\n";
1789 my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
1605 - print "/$filepart/$meta->{revision}//$kopts/\n";
1790 + print "/$filepart/$meta->{revision}//$kopts/" .
1791 + getStickyTagOrDate($stickyInfo) . "\n";
1792 }
1793 }
1794
@@ -1639,16 +1825,19 @@ sub req_status
1825 next;
1826 }
1827
1642 - my $meta = $updater->getmeta($filename);
1643 - my $oldmeta = $meta;
1644 -
1828 my $wrev = revparse($filename);
1829
1830 + my $stickyInfo = resolveStickyInfo($filename);
1831 + my $meta = $updater->getmeta($filename,$stickyInfo);
1832 + my $oldmeta = $meta;
1833 +
1834 # If the working copy is an old revision, lets get that
1835 # version too for comparison.
1836 if ( defined($wrev) and $wrev ne $meta->{revision} )
1837 {
1651 - $oldmeta = $updater->getmeta($filename, $wrev);
1838 + my($rmRev)=$wrev;
1839 + $rmRev=~s/^-//;
1840 + $oldmeta = $updater->getmeta($filename, $rmRev);
1841 }
1842
1843 # TODO : All possible statuses aren't yet implemented
@@ -1691,6 +1880,7 @@ sub req_status
1880 # same revision but there are local changes
1881 if ( defined ( $wrev ) and defined($meta->{revision}) and
1882 $wrev eq $meta->{revision} and
1883 + $wrev ne "0" and
1884 $state->{entries}{$filename}{modified_filename} )
1885 {
1886 $status ||= "Locally Modified";
@@ -1706,7 +1896,8 @@ sub req_status
1896 }
1897
1898 if ( defined ( $state->{entries}{$filename}{revision} ) and
1709 - not defined ( $meta->{revision} ) )
1899 + ( !defined($meta->{revision}) ||
1900 + $meta->{revision} eq "0" ) )
1901 {
1902 $status ||= "Locally Added";
1903 }
@@ -1802,98 +1993,133 @@ sub req_diff
1993 # be providing status on ...
1994 argsfromdir($updater);
1995
1996 + my($foundDiff);
1997 +
1998 # foreach file specified on the command line ...
1806 - foreach my $filename ( @{$state->{args}} )
1999 + foreach my $argFilename ( @{$state->{args}} )
2000 {
1808 - $filename = filecleanup($filename);
2001 + my($filename) = filecleanup($argFilename);
2002
2003 my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
2004
2005 my $wrev = revparse($filename);
2006
1814 - # We need _something_ to diff against
1815 - next unless ( defined ( $wrev ) );
2007 + # Priority for revision1:
2008 + # 1. First -r (missing file: check -N)
2009 + # 2. wrev from client's Entry line
2010 + # - missing line/file: check -N
2011 + # - "0": added file not committed (empty contents for rev1)
2012 + # - Prefixed with dash (to be removed): check -N
2013
1817 - # if we have a -r switch, use it
2014 if ( defined ( $revision1 ) )
2015 {
1820 - ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
2016 $meta1 = $updater->getmeta($filename, $revision1);
1822 - unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
2017 + }
2018 + elsif( defined($wrev) && $wrev ne "0" )
2019 + {
2020 + my($rmRev)=$wrev;
2021 + $rmRev=~s/^-//;
2022 + $meta1 = $updater->getmeta($filename, $rmRev);
2023 + }
2024 + if ( !defined($meta1) ||
2025 + $meta1->{filehash} eq "deleted" )
2026 + {
2027 + if( !exists($state->{opt}{N}) )
2028 {
1824 - print "E File $filename at revision $revision1 doesn't exist\n";
2029 + if(!defined($revision1))
2030 + {
2031 + print "E File $filename at revision $revision1 doesn't exist\n";
2032 + }
2033 next;
2034 }
1827 - transmitfile($meta1->{filehash}, { targetfile => $file1 });
1828 - }
1829 - # otherwise we just use the working copy revision
1830 - else
1831 - {
1832 - ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
1833 - $meta1 = $updater->getmeta($filename, $wrev);
1834 - transmitfile($meta1->{filehash}, { targetfile => $file1 });
2035 + elsif( !defined($meta1) )
2036 + {
2037 + $meta1 = {
2038 + name => $filename,
2039 + revision => '0',
2040 + filehash => 'deleted'
2041 + };
2042 + }
2043 }
2044
2045 + # Priority for revision2:
2046 + # 1. Second -r (missing file: check -N)
2047 + # 2. Modified file contents from client
2048 + # 3. wrev from client's Entry line
2049 + # - missing line/file: check -N
2050 + # - Prefixed with dash (to be removed): check -N
2051 +
2052 # if we have a second -r switch, use it too
2053 if ( defined ( $revision2 ) )
2054 {
1840 - ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
2055 $meta2 = $updater->getmeta($filename, $revision2);
1842 -
1843 - unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
1844 - {
1845 - print "E File $filename at revision $revision2 doesn't exist\n";
1846 - next;
1847 - }
1848 -
1849 - transmitfile($meta2->{filehash}, { targetfile => $file2 });
2056 }
1851 - # otherwise we just use the working copy
1852 - else
2057 + elsif(defined($state->{entries}{$filename}{modified_filename}))
2058 {
2059 $file2 = $state->{entries}{$filename}{modified_filename};
2060 + $meta2 = {
2061 + name => $filename,
2062 + revision => '0',
2063 + filehash => 'modified'
2064 + };
2065 }
1856 -
1857 - # if we have been given -r, and we don't have a $file2 yet, lets
1858 - # get one
1859 - if ( defined ( $revision1 ) and not defined ( $file2 ) )
2066 + elsif( defined($wrev) && ($wrev!~/^-/) )
2067 {
1861 - ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
2068 + if(!defined($revision1)) # no revision and no modifications:
2069 + {
2070 + next;
2071 + }
2072 $meta2 = $updater->getmeta($filename, $wrev);
1863 - transmitfile($meta2->{filehash}, { targetfile => $file2 });
2073 + }
2074 + if(!defined($file2))
2075 + {
2076 + if ( !defined($meta2) ||
2077 + $meta2->{filehash} eq "deleted" )
2078 + {
2079 + if( !exists($state->{opt}{N}) )
2080 + {
2081 + if(!defined($revision2))
2082 + {
2083 + print "E File $filename at revision $revision2 doesn't exist\n";
2084 + }
2085 + next;
2086 + }
2087 + elsif( !defined($meta2) )
2088 + {
2089 + $meta2 = {
2090 + name => $filename,
2091 + revision => '0',
2092 + filehash => 'deleted'
2093 + };
2094 + }
2095 + }
2096 }
2097
1866 - # We need to have retrieved something useful
1867 - next unless ( defined ( $meta1 ) );
1868 -
1869 - # Files to date if the working copy and repo copy have the same
1870 - # revision, and the working copy is unmodified
1871 - if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
1872 - ( ( $state->{entries}{$filename}{unchanged} and
1873 - ( not defined ( $state->{entries}{$filename}{conflict} ) or
1874 - $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
1875 - ( defined($state->{entries}{$filename}{modified_hash}) and
1876 - $state->{entries}{$filename}{modified_hash} eq
1877 - $meta1->{filehash} ) ) )
2098 + if( $meta1->{filehash} eq $meta2->{filehash} )
2099 {
2100 + $log->info("unchanged $filename");
2101 next;
2102 }
2103
1882 - # Apparently we only show diffs for locally modified files
1883 - unless ( defined($meta2) or
1884 - defined ( $state->{entries}{$filename}{modified_filename} ) )
2104 + # Retrieve revision contents:
2105 + ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
2106 + transmitfile($meta1->{filehash}, { targetfile => $file1 });
2107 +
2108 + if(!defined($file2))
2109 {
1886 - next;
2110 + ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
2111 + transmitfile($meta2->{filehash}, { targetfile => $file2 });
2112 }
2113
1889 - print "M Index: $filename\n";
2114 + # Generate the actual diff:
2115 + print "M Index: $argFilename\n";
2116 print "M =======" . ( "=" x 60 ) . "\n";
2117 print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
1892 - if ( defined ( $meta1 ) )
2118 + if ( defined ( $meta1 ) && $meta1->{revision} ne "0" )
2119 {
2120 print "M retrieving revision $meta1->{revision}\n"
2121 }
1896 - if ( defined ( $meta2 ) )
2122 + if ( defined ( $meta2 ) && $meta2->{revision} ne "0" )
2123 {
2124 print "M retrieving revision $meta2->{revision}\n"
2125 }
@@ -1914,33 +2140,73 @@ sub req_diff
2140 }
2141 }
2142 }
1917 - print "$filename\n";
2143 + print "$argFilename\n";
2144
2145 $log->info("Diffing $filename -r $meta1->{revision} -r " .
2146 ( $meta2->{revision} or "workingcopy" ));
2147
1922 - ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
1923 -
1924 - if ( exists $state->{opt}{u} )
2148 + # TODO: Use --label instead of -L because -L is no longer
2149 + # documented and may go away someday. Not sure if there there are
2150 + # versions that only support -L, which would make this change risky?
2151 + # http://osdir.com/ml/bug-gnu-utils-gnu/2010-12/msg00060.html
2152 + # ("man diff" should actually document the best migration strategy,
2153 + # [current behavior, future changes, old compatibility issues
2154 + # or lack thereof, etc], not just stop mentioning the option...)
2155 + # TODO: Real CVS seems to include a date in the label, before
2156 + # the revision part, without the keyword "revision". The following
2157 + # has minimal changes compared to original versions of
2158 + # git-cvsserver.perl. (Mostly tab vs space after filename.)
2159 +
2160 + my (@diffCmd) = ( 'diff' );
2161 + if ( exists($state->{opt}{N}) )
2162 {
1926 - system("diff -u -L '$filename revision $meta1->{revision}'" .
1927 - " -L '$filename " .
1928 - ( defined($meta2->{revision}) ?
1929 - "revision $meta2->{revision}" :
1930 - "working copy" ) .
1931 - "' $file1 $file2 > $filediff" );
1932 - } else {
1933 - system("diff $file1 $file2 > $filediff");
2163 + push @diffCmd,"-N";
2164 }
2165 + if ( exists $state->{opt}{u} )
2166 + {
2167 + push @diffCmd,("-u","-L");
2168 + if( $meta1->{filehash} eq "deleted" )
2169 + {
2170 + push @diffCmd,"/dev/null";
2171 + } else {
2172 + push @diffCmd,("$argFilename\trevision $meta1->{revision}");
2173 + }
2174
1936 - while ( <$fh> )
2175 + if( defined($meta2->{filehash}) )
2176 + {
2177 + if( $meta2->{filehash} eq "deleted" )
2178 + {
2179 + push @diffCmd,("-L","/dev/null");
2180 + } else {
2181 + push @diffCmd,("-L",
2182 + "$argFilename\trevision $meta2->{revision}");
2183 + }
2184 + } else {
2185 + push @diffCmd,("-L","$argFilename\tworking copy");
2186 + }
2187 + }
2188 + push @diffCmd,($file1,$file2);
2189 + if(!open(DIFF,"-|",@diffCmd))
2190 {
1938 - print "M $_";
2191 + $log->warn("Unable to run diff: $!");
2192 }
1940 - close $fh;
2193 + my($diffLine);
2194 + while(defined($diffLine=<DIFF>))
2195 + {
2196 + print "M $diffLine";
2197 + $foundDiff=1;
2198 + }
2199 + close(DIFF);
2200 }
2201
1943 - print "ok\n";
2202 + if($foundDiff)
2203 + {
2204 + print "error \n";
2205 + }
2206 + else
2207 + {
2208 + print "ok\n";
2209 + }
2210 }
2211
2212 sub req_log
@@ -2160,7 +2426,7 @@ sub argsplit
2426 $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
2427 $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
2428 $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
2163 - $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
2429 + $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2, N => 0 } if ( $type eq "diff" );
2430 $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
2431 $opt = { k => 1, m => 1 } if ( $type eq "add" );
2432 $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
@@ -3101,6 +3367,45 @@ sub descramble
3367 return $ret;
3368 }
3369
3370 +# Test if the (deep) values of two references to a hash are the same.
3371 +sub refHashEqual
3372 +{
3373 + my($v1,$v2) = @_;
3374 +
3375 + my $out;
3376 + if(!defined($v1))
3377 + {
3378 + if(!defined($v2))
3379 + {
3380 + $out=1;
3381 + }
3382 + }
3383 + elsif( !defined($v2) ||
3384 + scalar(keys(%{$v1})) != scalar(keys(%{$v2})) )
3385 + {
3386 + # $out=undef;
3387 + }
3388 + else
3389 + {
3390 + $out=1;
3391 +
3392 + my $key;
3393 + foreach $key (keys(%{$v1}))
3394 + {
3395 + if( !exists($v2->{$key}) ||
3396 + defined($v1->{$key}) ne defined($v2->{$key}) ||
3397 + ( defined($v1->{$key}) &&
3398 + $v1->{$key} ne $v2->{$key} ) )
3399 + {
3400 + $out=undef;
3401 + last;
3402 + }
3403 + }
3404 + }
3405 +
3406 + return $out;
3407 +}
3408 +
3409
3410 package GITCVS::log;
3411
@@ -3506,6 +3811,8 @@ sub update
3811 my $lastcommit = $self->_get_prop("last_commit");
3812
3813 if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
3814 + # invalidate the gethead cache
3815 + $self->clearCommitRefCaches();
3816 return 1;
3817 }
3818