diff options
Diffstat (limited to 'contrib')
117 files changed, 20197 insertions, 3132 deletions
diff --git a/contrib/buildsystems/Generators.pm b/contrib/buildsystems/Generators.pm new file mode 100644 index 0000000000..408ef714b8 --- /dev/null +++ b/contrib/buildsystems/Generators.pm @@ -0,0 +1,42 @@ +package Generators; +require Exporter; + +use strict; +use File::Basename; +no strict 'refs'; +use vars qw($VERSION @AVAILABLE); + +our $VERSION = '1.00'; +our(@ISA, @EXPORT, @EXPORT_OK, @AVAILABLE); +@ISA = qw(Exporter); + +BEGIN { + local(*D); + my $me = $INC{"Generators.pm"}; + die "Couldn't find myself in \@INC, which is required to load the generators!" if ("$me" eq ""); + $me = dirname($me); + if (opendir(D,"$me/Generators")) { + foreach my $gen (readdir(D)) { + next if ($gen =~ /^\.\.?$/); + require "${me}/Generators/$gen"; + $gen =~ s,\.pm,,; + push(@AVAILABLE, $gen); + } + closedir(D); + my $gens = join(', ', @AVAILABLE); + } + + push @EXPORT_OK, qw(available); +} + +sub available { + return @AVAILABLE; +} + +sub generate { + my ($gen, $git_dir, $out_dir, $rel_dir, %build_structure) = @_; + return eval("Generators::${gen}::generate(\$git_dir, \$out_dir, \$rel_dir, \%build_structure)") if grep(/^$gen$/, @AVAILABLE); + die "Generator \"${gen}\" is not available!\nAvailable generators are: @AVAILABLE\n"; +} + +1; diff --git a/contrib/buildsystems/Generators/QMake.pm b/contrib/buildsystems/Generators/QMake.pm new file mode 100644 index 0000000000..ff3b657e61 --- /dev/null +++ b/contrib/buildsystems/Generators/QMake.pm @@ -0,0 +1,189 @@ +package Generators::QMake; +require Exporter; + +use strict; +use vars qw($VERSION); + +our $VERSION = '1.00'; +our(@ISA, @EXPORT, @EXPORT_OK, @AVAILABLE); +@ISA = qw(Exporter); + +BEGIN { + push @EXPORT_OK, qw(generate); +} + +sub generate { + my ($git_dir, $out_dir, $rel_dir, %build_structure) = @_; + + my @libs = @{$build_structure{"LIBS"}}; + foreach (@libs) { + createLibProject($_, $git_dir, $out_dir, $rel_dir, %build_structure); + } + + my @apps = @{$build_structure{"APPS"}}; + foreach (@apps) { + createAppProject($_, $git_dir, $out_dir, $rel_dir, %build_structure); + } + + createGlueProject($git_dir, $out_dir, $rel_dir, %build_structure); + return 0; +} + +sub createLibProject { + my ($libname, $git_dir, $out_dir, $rel_dir, %build_structure) = @_; + print "Generate $libname lib project\n"; + $rel_dir = "../$rel_dir"; + + my $sources = join(" \\\n\t", sort(map("$rel_dir/$_", @{$build_structure{"LIBS_${libname}_SOURCES"}}))); + my $defines = join(" \\\n\t", sort(@{$build_structure{"LIBS_${libname}_DEFINES"}})); + my $includes= join(" \\\n\t", sort(map("$rel_dir/$_", @{$build_structure{"LIBS_${libname}_INCLUDES"}}))); + my $cflags = join(" ", sort(@{$build_structure{"LIBS_${libname}_CFLAGS"}})); + + my $cflags_debug = $cflags; + $cflags_debug =~ s/-MT/-MTd/; + $cflags_debug =~ s/-O.//; + + my $cflags_release = $cflags; + $cflags_release =~ s/-MTd/-MT/; + + my @tmp = @{$build_structure{"LIBS_${libname}_LFLAGS"}}; + my @tmp2 = (); + foreach (@tmp) { + if (/^-LTCG/) { + } elsif (/^-L/) { + $_ =~ s/^-L/-LIBPATH:$rel_dir\//; + } + push(@tmp2, $_); + } + my $lflags = join(" ", sort(@tmp)); + + my $target = $libname; + $target =~ s/\//_/g; + $defines =~ s/-D//g; + $defines =~ s/"/\\\\"/g; + $includes =~ s/-I//g; + mkdir "$target" || die "Could not create the directory $target for lib project!\n"; + open F, ">$target/$target.pro" || die "Could not open $target/$target.pro for writing!\n"; + print F << "EOM"; +TEMPLATE = lib +TARGET = $target +DESTDIR = $rel_dir + +CONFIG -= qt +CONFIG += static + +QMAKE_CFLAGS = +QMAKE_CFLAGS_RELEASE = $cflags_release +QMAKE_CFLAGS_DEBUG = $cflags_debug +QMAKE_LIBFLAGS = $lflags + +DEFINES += \\ + $defines + +INCLUDEPATH += \\ + $includes + +SOURCES += \\ + $sources +EOM + close F; +} + +sub createAppProject { + my ($appname, $git_dir, $out_dir, $rel_dir, %build_structure) = @_; + print "Generate $appname app project\n"; + $rel_dir = "../$rel_dir"; + + my $sources = join(" \\\n\t", sort(map("$rel_dir/$_", @{$build_structure{"APPS_${appname}_SOURCES"}}))); + my $defines = join(" \\\n\t", sort(@{$build_structure{"APPS_${appname}_DEFINES"}})); + my $includes= join(" \\\n\t", sort(map("$rel_dir/$_", @{$build_structure{"APPS_${appname}_INCLUDES"}}))); + my $cflags = join(" ", sort(@{$build_structure{"APPS_${appname}_CFLAGS"}})); + + my $cflags_debug = $cflags; + $cflags_debug =~ s/-MT/-MTd/; + $cflags_debug =~ s/-O.//; + + my $cflags_release = $cflags; + $cflags_release =~ s/-MTd/-MT/; + + my $libs; + foreach (sort(@{$build_structure{"APPS_${appname}_LIBS"}})) { + $_ =~ s/\//_/g; + $libs .= " $_"; + } + my @tmp = @{$build_structure{"APPS_${appname}_LFLAGS"}}; + my @tmp2 = (); + foreach (@tmp) { + # next if ($_ eq "-NODEFAULTLIB:MSVCRT.lib"); + if (/^-LTCG/) { + } elsif (/^-L/) { + $_ =~ s/^-L/-LIBPATH:$rel_dir\//; + } + push(@tmp2, $_); + } + my $lflags = join(" ", sort(@tmp)); + + my $target = $appname; + $target =~ s/\.exe//; + $target =~ s/\//_/g; + $defines =~ s/-D//g; + $defines =~ s/"/\\\\"/g; + $includes =~ s/-I//g; + mkdir "$target" || die "Could not create the directory $target for app project!\n"; + open F, ">$target/$target.pro" || die "Could not open $target/$target.pro for writing!\n"; + print F << "EOM"; +TEMPLATE = app +TARGET = $target +DESTDIR = $rel_dir + +CONFIG -= qt embed_manifest_exe +CONFIG += console + +QMAKE_CFLAGS = +QMAKE_CFLAGS_RELEASE = $cflags_release +QMAKE_CFLAGS_DEBUG = $cflags_debug +QMAKE_LFLAGS = $lflags +LIBS = $libs + +DEFINES += \\ + $defines + +INCLUDEPATH += \\ + $includes + +win32:QMAKE_LFLAGS += -LIBPATH:$rel_dir +else: QMAKE_LFLAGS += -L$rel_dir + +SOURCES += \\ + $sources +EOM + close F; +} + +sub createGlueProject { + my ($git_dir, $out_dir, $rel_dir, %build_structure) = @_; + my $libs = join(" \\ \n", map("\t$_|$_.pro", @{$build_structure{"LIBS"}})); + my $apps = join(" \\ \n", map("\t$_|$_.pro", @{$build_structure{"APPS"}})); + $libs =~ s/\.a//g; + $libs =~ s/\//_/g; + $libs =~ s/\|/\//g; + $apps =~ s/\.exe//g; + $apps =~ s/\//_/g; + $apps =~ s/\|/\//g; + + my $filename = $out_dir; + $filename =~ s/.*\/([^\/]+)$/$1/; + $filename =~ s/\/$//; + print "Generate glue project $filename.pro\n"; + open F, ">$filename.pro" || die "Could not open $filename.pro for writing!\n"; + print F << "EOM"; +TEMPLATE = subdirs +CONFIG += ordered +SUBDIRS += \\ +$libs \\ +$apps +EOM + close F; +} + +1; diff --git a/contrib/buildsystems/Generators/Vcproj.pm b/contrib/buildsystems/Generators/Vcproj.pm new file mode 100644 index 0000000000..cfa74adcc2 --- /dev/null +++ b/contrib/buildsystems/Generators/Vcproj.pm @@ -0,0 +1,626 @@ +package Generators::Vcproj; +require Exporter; + +use strict; +use vars qw($VERSION); + +our $VERSION = '1.00'; +our(@ISA, @EXPORT, @EXPORT_OK, @AVAILABLE); +@ISA = qw(Exporter); + +BEGIN { + push @EXPORT_OK, qw(generate); +} + +my $guid_index = 0; +my @GUIDS = ( + "{E07B9989-2BF7-4F21-8918-BE22BA467AC3}", + "{278FFB51-0296-4A44-A81A-22B87B7C3592}", + "{7346A2C4-F0FD-444F-9EBE-1AF23B2B5650}", + "{67F421AC-EB34-4D49-820B-3196807B423F}", + "{385DCFE1-CC8C-4211-A451-80FCFC31CA51}", + "{97CC46C5-D2CC-4D26-B634-E75792B79916}", + "{C7CE21FE-6EF8-4012-A5C7-A22BCEDFBA11}", + "{51575134-3FDF-42D1-BABD-3FB12669C6C9}", + "{0AE195E4-9823-4B87-8E6F-20C5614AF2FF}", + "{4B918255-67CA-43BB-A46C-26704B666E6B}", + "{18CCFEEF-C8EE-4CC1-A265-26F95C9F4649}", + "{5D5D90FA-01B7-4973-AFE5-CA88C53AC197}", + "{1F054320-036D-49E1-B384-FB5DF0BC8AC0}", + "{7CED65EE-F2D9-4171-825B-C7D561FE5786}", + "{8D341679-0F07-4664-9A56-3BA0DE88B9BC}", + "{C189FEDC-2957-4BD7-9FA4-7622241EA145}", + "{66844203-1B9F-4C53-9274-164FFF95B847}", + "{E4FEA145-DECC-440D-AEEA-598CF381FD43}", + "{73300A8E-C8AC-41B0-B555-4F596B681BA7}", + "{873FDEB1-D01D-40BF-A1BF-8BBC58EC0F51}", + "{7922C8BE-76C5-4AC6-8BF7-885C0F93B782}", + "{E245D370-308B-4A49-BFC1-1E527827975F}", + "{F6FA957B-66FC-4ED7-B260-E59BBE4FE813}", + "{E6055070-0198-431A-BC49-8DB6CEE770AE}", + "{54159234-C3EB-43DA-906B-CE5DA5C74654}", + "{594CFC35-0B60-46F6-B8EF-9983ACC1187D}", + "{D93FCAB7-1F01-48D2-B832-F761B83231A5}", + "{DBA5E6AC-E7BE-42D3-8703-4E787141526E}", + "{6171953F-DD26-44C7-A3BE-CC45F86FC11F}", + "{9E19DDBE-F5E4-4A26-A2FE-0616E04879B8}", + "{AE81A615-99E3-4885-9CE0-D9CAA193E867}", + "{FBF4067E-1855-4F6C-8BCD-4D62E801A04D}", + "{17007948-6593-4AEB-8106-F7884B4F2C19}", + "{199D4C8D-8639-4DA6-82EF-08668C35DEE0}", + "{E085E50E-C140-4CF3-BE4B-094B14F0DDD6}", + "{00785268-A9CC-4E40-AC29-BAC0019159CE}", + "{4C06F56A-DCDB-46A6-B67C-02339935CF12}", + "{3A62D3FD-519E-4EC9-8171-D2C1BFEA022F}", + "{3A62D3FD-519E-4EC9-8171-D2C1BFEA022F}", + "{9392EB58-D7BA-410B-B1F0-B2FAA6BC89A7}", + "{2ACAB2D5-E0CE-4027-BCA0-D78B2D7A6C66}", + "{86E216C3-43CE-481A-BCB2-BE5E62850635}", + "{FB631291-7923-4B91-9A57-7B18FDBB7A42}", + "{0A176EC9-E934-45B8-B87F-16C7F4C80039}", + "{DF55CA80-46E8-4C53-B65B-4990A23DD444}", + "{3A0F9895-55D2-4710-BE5E-AD7498B5BF44}", + "{294BDC5A-F448-48B6-8110-DD0A81820F8C}", + "{4B9F66E9-FAC9-47AB-B1EF-C16756FBFD06}", + "{72EA49C6-2806-48BD-B81B-D4905102E19C}", + "{5728EB7E-8929-486C-8CD5-3238D060E768}" +); + +sub generate { + my ($git_dir, $out_dir, $rel_dir, %build_structure) = @_; + my @libs = @{$build_structure{"LIBS"}}; + foreach (@libs) { + createLibProject($_, $git_dir, $out_dir, $rel_dir, \%build_structure); + } + + my @apps = @{$build_structure{"APPS"}}; + foreach (@apps) { + createAppProject($_, $git_dir, $out_dir, $rel_dir, \%build_structure); + } + + createGlueProject($git_dir, $out_dir, $rel_dir, %build_structure); + return 0; +} + +sub createLibProject { + my ($libname, $git_dir, $out_dir, $rel_dir, $build_structure) = @_; + print "Generate $libname vcproj lib project\n"; + $rel_dir = "..\\$rel_dir"; + $rel_dir =~ s/\//\\/g; + + my $target = $libname; + $target =~ s/\//_/g; + $target =~ s/\.a//; + + my $uuid = $GUIDS[$guid_index]; + $$build_structure{"LIBS_${target}_GUID"} = $uuid; + $guid_index += 1; + + my @srcs = sort(map("$rel_dir\\$_", @{$$build_structure{"LIBS_${libname}_SOURCES"}})); + my @sources; + foreach (@srcs) { + $_ =~ s/\//\\/g; + push(@sources, $_); + } + my $defines = join(",", sort(@{$$build_structure{"LIBS_${libname}_DEFINES"}})); + my $includes= join(";", sort(map(""$rel_dir\\$_"", @{$$build_structure{"LIBS_${libname}_INCLUDES"}}))); + my $cflags = join(" ", sort(@{$$build_structure{"LIBS_${libname}_CFLAGS"}})); + $cflags =~ s/\"/"/g; + + my $cflags_debug = $cflags; + $cflags_debug =~ s/-MT/-MTd/; + $cflags_debug =~ s/-O.//; + + my $cflags_release = $cflags; + $cflags_release =~ s/-MTd/-MT/; + + my @tmp = @{$$build_structure{"LIBS_${libname}_LFLAGS"}}; + my @tmp2 = (); + foreach (@tmp) { + if (/^-LTCG/) { + } elsif (/^-L/) { + $_ =~ s/^-L/-LIBPATH:$rel_dir\//; + } + push(@tmp2, $_); + } + my $lflags = join(" ", sort(@tmp)); + + $defines =~ s/-D//g; + $defines =~ s/\"/\\"/g; + $defines =~ s/\'//g; + $includes =~ s/-I//g; + mkdir "$target" || die "Could not create the directory $target for lib project!\n"; + open F, ">$target/$target.vcproj" || die "Could not open $target/$target.pro for writing!\n"; + binmode F, ":crlf"; + print F << "EOM"; +<?xml version="1.0" encoding = "Windows-1252"?> +<VisualStudioProject + ProjectType="Visual C++" + Version="9,00" + Name="$target" + ProjectGUID="$uuid"> + <Platforms> + <Platform + Name="Win32"/> + </Platforms> + <ToolFiles> + </ToolFiles> + <Configurations> + <Configuration + Name="Debug|Win32" + OutputDirectory="$rel_dir" + ConfigurationType="4" + CharacterSet="0" + IntermediateDirectory="\$(ProjectDir)\$(ConfigurationName)" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + AdditionalOptions="$cflags_debug" + Optimization="0" + InlineFunctionExpansion="1" + AdditionalIncludeDirectories="$includes" + PreprocessorDefinitions="WIN32,_DEBUG,$defines" + MinimalRebuild="true" + RuntimeLibrary="1" + UsePrecompiledHeader="0" + ProgramDataBaseFileName="\$(IntDir)\\\$(TargetName).pdb" + WarningLevel="3" + DebugInformationFormat="3" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLibrarianTool" + SuppressStartupBanner="true" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + <Configuration + Name="Release|Win32" + OutputDirectory="$rel_dir" + ConfigurationType="4" + CharacterSet="0" + WholeProgramOptimization="1" + IntermediateDirectory="\$(ProjectDir)\$(ConfigurationName)" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + AdditionalOptions="$cflags_release" + Optimization="2" + InlineFunctionExpansion="1" + EnableIntrinsicFunctions="true" + AdditionalIncludeDirectories="$includes" + PreprocessorDefinitions="WIN32,NDEBUG,$defines" + RuntimeLibrary="0" + EnableFunctionLevelLinking="true" + UsePrecompiledHeader="0" + ProgramDataBaseFileName="\$(IntDir)\\\$(TargetName).pdb" + WarningLevel="3" + DebugInformationFormat="3" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLibrarianTool" + SuppressStartupBanner="true" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + </Configurations> + <Files> + <Filter + Name="Source Files" + Filter="cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx" + UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"> +EOM + foreach(@sources) { + print F << "EOM"; + <File + RelativePath="$_"/> +EOM + } + print F << "EOM"; + </Filter> + </Files> + <Globals> + </Globals> +</VisualStudioProject> +EOM + close F; +} + +sub createAppProject { + my ($appname, $git_dir, $out_dir, $rel_dir, $build_structure) = @_; + print "Generate $appname vcproj app project\n"; + $rel_dir = "..\\$rel_dir"; + $rel_dir =~ s/\//\\/g; + + my $target = $appname; + $target =~ s/\//_/g; + $target =~ s/\.exe//; + + my $uuid = $GUIDS[$guid_index]; + $$build_structure{"APPS_${target}_GUID"} = $uuid; + $guid_index += 1; + + my @srcs = sort(map("$rel_dir\\$_", @{$$build_structure{"APPS_${appname}_SOURCES"}})); + my @sources; + foreach (@srcs) { + $_ =~ s/\//\\/g; + push(@sources, $_); + } + my $defines = join(",", sort(@{$$build_structure{"APPS_${appname}_DEFINES"}})); + my $includes= join(";", sort(map(""$rel_dir\\$_"", @{$$build_structure{"APPS_${appname}_INCLUDES"}}))); + my $cflags = join(" ", sort(@{$$build_structure{"APPS_${appname}_CFLAGS"}})); + $cflags =~ s/\"/"/g; + + my $cflags_debug = $cflags; + $cflags_debug =~ s/-MT/-MTd/; + $cflags_debug =~ s/-O.//; + + my $cflags_release = $cflags; + $cflags_release =~ s/-MTd/-MT/; + + my $libs; + foreach (sort(@{$$build_structure{"APPS_${appname}_LIBS"}})) { + $_ =~ s/\//_/g; + $libs .= " $_"; + } + my @tmp = @{$$build_structure{"APPS_${appname}_LFLAGS"}}; + my @tmp2 = (); + foreach (@tmp) { + if (/^-LTCG/) { + } elsif (/^-L/) { + $_ =~ s/^-L/-LIBPATH:$rel_dir\//; + } + push(@tmp2, $_); + } + my $lflags = join(" ", sort(@tmp)) . " -LIBPATH:$rel_dir"; + + $defines =~ s/-D//g; + $defines =~ s/\"/\\"/g; + $defines =~ s/\'//g; + $defines =~ s/\\\\/\\/g; + $includes =~ s/-I//g; + mkdir "$target" || die "Could not create the directory $target for lib project!\n"; + open F, ">$target/$target.vcproj" || die "Could not open $target/$target.pro for writing!\n"; + binmode F, ":crlf"; + print F << "EOM"; +<?xml version="1.0" encoding = "Windows-1252"?> +<VisualStudioProject + ProjectType="Visual C++" + Version="9,00" + Name="$target" + ProjectGUID="$uuid"> + <Platforms> + <Platform + Name="Win32"/> + </Platforms> + <ToolFiles> + </ToolFiles> + <Configurations> + <Configuration + Name="Debug|Win32" + OutputDirectory="$rel_dir" + ConfigurationType="1" + CharacterSet="0" + IntermediateDirectory="\$(ProjectDir)\$(ConfigurationName)" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + AdditionalOptions="$cflags_debug" + Optimization="0" + InlineFunctionExpansion="1" + AdditionalIncludeDirectories="$includes" + PreprocessorDefinitions="WIN32,_DEBUG,$defines" + MinimalRebuild="true" + RuntimeLibrary="1" + UsePrecompiledHeader="0" + ProgramDataBaseFileName="\$(IntDir)\\\$(TargetName).pdb" + WarningLevel="3" + DebugInformationFormat="3" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="$libs" + AdditionalOptions="$lflags" + LinkIncremental="2" + GenerateDebugInformation="true" + SubSystem="1" + TargetMachine="1" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + <Configuration + Name="Release|Win32" + OutputDirectory="$rel_dir" + ConfigurationType="1" + CharacterSet="0" + WholeProgramOptimization="1" + IntermediateDirectory="\$(ProjectDir)\$(ConfigurationName)" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + AdditionalOptions="$cflags_release" + Optimization="2" + InlineFunctionExpansion="1" + EnableIntrinsicFunctions="true" + AdditionalIncludeDirectories="$includes" + PreprocessorDefinitions="WIN32,NDEBUG,$defines" + RuntimeLibrary="0" + EnableFunctionLevelLinking="true" + UsePrecompiledHeader="0" + ProgramDataBaseFileName="\$(IntDir)\\\$(TargetName).pdb" + WarningLevel="3" + DebugInformationFormat="3" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="$libs" + AdditionalOptions="$lflags" + LinkIncremental="1" + GenerateDebugInformation="true" + SubSystem="1" + TargetMachine="1" + OptimizeReferences="2" + EnableCOMDATFolding="2" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + </Configurations> + <Files> + <Filter + Name="Source Files" + Filter="cpp;c;cxx;def;odl;idl;hpj;bat;asm;asmx" + UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"> +EOM + foreach(@sources) { + print F << "EOM"; + <File + RelativePath="$_"/> +EOM + } + print F << "EOM"; + </Filter> + </Files> + <Globals> + </Globals> +</VisualStudioProject> +EOM + close F; +} + +sub createGlueProject { + my ($git_dir, $out_dir, $rel_dir, %build_structure) = @_; + print "Generate solutions file\n"; + $rel_dir = "..\\$rel_dir"; + $rel_dir =~ s/\//\\/g; + my $SLN_HEAD = "Microsoft Visual Studio Solution File, Format Version 10.00\n# Visual Studio 2008\n"; + my $SLN_PRE = "Project(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = "; + my $SLN_POST = "\nEndProject\n"; + + my @libs = @{$build_structure{"LIBS"}}; + my @tmp; + foreach (@libs) { + $_ =~ s/\//_/g; + $_ =~ s/\.a//; + push(@tmp, $_); + } + @libs = @tmp; + + my @apps = @{$build_structure{"APPS"}}; + @tmp = (); + foreach (@apps) { + $_ =~ s/\//_/g; + $_ =~ s/\.exe//; + push(@tmp, $_); + } + @apps = @tmp; + + open F, ">git.sln" || die "Could not open git.sln for writing!\n"; + binmode F, ":crlf"; + print F "$SLN_HEAD"; + foreach (@libs) { + my $libname = $_; + my $uuid = $build_structure{"LIBS_${libname}_GUID"}; + print F "$SLN_PRE"; + print F "\"${libname}\", \"${libname}\\${libname}.vcproj\", \"${uuid}\""; + print F "$SLN_POST"; + } + my $uuid_libgit = $build_structure{"LIBS_libgit_GUID"}; + my $uuid_xdiff_lib = $build_structure{"LIBS_xdiff_lib_GUID"}; + foreach (@apps) { + my $appname = $_; + my $uuid = $build_structure{"APPS_${appname}_GUID"}; + print F "$SLN_PRE"; + print F "\"${appname}\", \"${appname}\\${appname}.vcproj\", \"${uuid}\"\n"; + print F " ProjectSection(ProjectDependencies) = postProject\n"; + print F " ${uuid_libgit} = ${uuid_libgit}\n"; + print F " ${uuid_xdiff_lib} = ${uuid_xdiff_lib}\n"; + print F " EndProjectSection"; + print F "$SLN_POST"; + } + + print F << "EOM"; +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection +EOM + print F << "EOM"; + GlobalSection(ProjectConfigurationPlatforms) = postSolution +EOM + foreach (@libs) { + my $libname = $_; + my $uuid = $build_structure{"LIBS_${libname}_GUID"}; + print F "\t\t${uuid}.Debug|Win32.ActiveCfg = Debug|Win32\n"; + print F "\t\t${uuid}.Debug|Win32.Build.0 = Debug|Win32\n"; + print F "\t\t${uuid}.Release|Win32.ActiveCfg = Release|Win32\n"; + print F "\t\t${uuid}.Release|Win32.Build.0 = Release|Win32\n"; + } + foreach (@apps) { + my $appname = $_; + my $uuid = $build_structure{"APPS_${appname}_GUID"}; + print F "\t\t${uuid}.Debug|Win32.ActiveCfg = Debug|Win32\n"; + print F "\t\t${uuid}.Debug|Win32.Build.0 = Debug|Win32\n"; + print F "\t\t${uuid}.Release|Win32.ActiveCfg = Release|Win32\n"; + print F "\t\t${uuid}.Release|Win32.Build.0 = Release|Win32\n"; + } + + print F << "EOM"; + EndGlobalSection +EndGlobal +EOM + close F; +} + +1; diff --git a/contrib/buildsystems/engine.pl b/contrib/buildsystems/engine.pl new file mode 100644 index 0000000000..23da787dc5 --- /dev/null +++ b/contrib/buildsystems/engine.pl @@ -0,0 +1,359 @@ +#!/usr/bin/perl -w +###################################################################### +# Do not call this script directly! +# +# The generate script ensures that @INC is correct before the engine +# is executed. +# +# Copyright (C) 2009 Marius Storm-Olsen <mstormo@gmail.com> +###################################################################### +use strict; +use File::Basename; +use File::Spec; +use Cwd; +use Generators; + +my (%build_structure, %compile_options, @makedry); +my $out_dir = getcwd(); +my $git_dir = $out_dir; +$git_dir =~ s=\\=/=g; +$git_dir = dirname($git_dir) while (!-e "$git_dir/git.c" && "$git_dir" ne ""); +die "Couldn't find Git repo" if ("$git_dir" eq ""); + +my @gens = Generators::available(); +my $gen = "Vcproj"; + +sub showUsage +{ + my $genlist = join(', ', @gens); + print << "EOM"; +generate usage: + -g <GENERATOR> --gen <GENERATOR> Specify the buildsystem generator (default: $gen) + Available: $genlist + -o <PATH> --out <PATH> Specify output directory generation (default: .) + -i <FILE> --in <FILE> Specify input file, instead of running GNU Make + -h,-? --help This help +EOM + exit 0; +} + +# Parse command-line options +while (@ARGV) { + my $arg = shift @ARGV; + if ("$arg" eq "-h" || "$arg" eq "--help" || "$arg" eq "-?") { + showUsage(); + exit(0); + } elsif("$arg" eq "--out" || "$arg" eq "-o") { + $out_dir = shift @ARGV; + } elsif("$arg" eq "--gen" || "$arg" eq "-g") { + $gen = shift @ARGV; + } elsif("$arg" eq "--in" || "$arg" eq "-i") { + my $infile = shift @ARGV; + open(F, "<$infile") || die "Couldn't open file $infile"; + @makedry = <F>; + close(F); + } +} + +# NOT using File::Spec->rel2abs($path, $base) here, as +# it fails badly for me in the msysgit environment +$git_dir = File::Spec->rel2abs($git_dir); +$out_dir = File::Spec->rel2abs($out_dir); +my $rel_dir = makeOutRel2Git($git_dir, $out_dir); + +# Print some information so the user feels informed +print << "EOM"; +----- +Generator: $gen +Git dir: $git_dir +Out dir: $out_dir +----- +Running GNU Make to figure out build structure... +EOM + +# Pipe a make --dry-run into a variable, if not already loaded from file +@makedry = `cd $git_dir && make -n MSVC=1 V=1 2>/dev/null` if !@makedry; + +# Parse the make output into usable info +parseMakeOutput(); + +# Finally, ask the generator to start generating.. +Generators::generate($gen, $git_dir, $out_dir, $rel_dir, %build_structure); + +# main flow ends here +# ------------------------------------------------------------------------------------------------- + + +# 1) path: /foo/bar/baz 2) path: /foo/bar/baz 3) path: /foo/bar/baz +# base: /foo/bar/baz/temp base: /foo/bar base: /tmp +# rel: .. rel: baz rel: ../foo/bar/baz +sub makeOutRel2Git +{ + my ($path, $base) = @_; + my $rel; + if ("$path" eq "$base") { + return "."; + } elsif ($base =~ /^$path/) { + # case 1 + my $tmp = $base; + $tmp =~ s/^$path//; + foreach (split('/', $tmp)) { + $rel .= "../" if ("$_" ne ""); + } + } elsif ($path =~ /^$base/) { + # case 2 + $rel = $path; + $rel =~ s/^$base//; + $rel = "./$rel"; + } else { + my $tmp = $base; + foreach (split('/', $tmp)) { + $rel .= "../" if ("$_" ne ""); + } + $rel .= $path; + } + $rel =~ s/\/\//\//g; # simplify + $rel =~ s/\/$//; # don't end with / + return $rel; +} + +sub parseMakeOutput +{ + print "Parsing GNU Make output to figure out build structure...\n"; + my $line = 0; + while (my $text = shift @makedry) { + my $ate_next; + do { + $ate_next = 0; + $line++; + chomp $text; + chop $text if ($text =~ /\r$/); + if ($text =~ /\\$/) { + $text =~ s/\\$//; + $text .= shift @makedry; + $ate_next = 1; + } + } while($ate_next); + + if ($text =~ /^test /) { + # options to test (eg -o) may be mistaken for linker options + next; + } + + if($text =~ / -c /) { + # compilation + handleCompileLine($text, $line); + + } elsif ($text =~ / -o /) { + # linking executable + handleLinkLine($text, $line); + + } elsif ($text =~ /\.o / && $text =~ /\.a /) { + # libifying + handleLibLine($text, $line); +# +# } elsif ($text =~ /^cp /) { +# # copy file around +# +# } elsif ($text =~ /^rm -f /) { +# # shell command +# +# } elsif ($text =~ /^make[ \[]/) { +# # make output +# +# } elsif ($text =~ /^echo /) { +# # echo to file +# +# } elsif ($text =~ /^if /) { +# # shell conditional +# +# } elsif ($text =~ /^tclsh /) { +# # translation stuff +# +# } elsif ($text =~ /^umask /) { +# # handling boilerplates +# +# } elsif ($text =~ /\$\(\:\)/) { +# # ignore +# +# } elsif ($text =~ /^FLAGS=/) { +# # flags check for dependencies +# +# } elsif ($text =~ /^'\/usr\/bin\/perl' -MError -e/) { +# # perl commands for copying files +# +# } elsif ($text =~ /generate-cmdlist\.sh/) { +# # command for generating list of commands +# +# } elsif ($text =~ /new locations or Tcl/) { +# # command for detecting Tcl/Tk changes +# +# } elsif ($text =~ /mkdir -p/) { +# # command creating path +# +# } elsif ($text =~ /: no custom templates yet/) { +# # whatever +# +# } else { +# print "Unhandled (line: $line): $text\n"; + } + } + +# use Data::Dumper; +# print "Parsed build structure:\n"; +# print Dumper(%build_structure); +} + +# variables for the compilation part of each step +my (@defines, @incpaths, @cflags, @sources); + +sub clearCompileStep +{ + @defines = (); + @incpaths = (); + @cflags = (); + @sources = (); +} + +sub removeDuplicates +{ + my (%dupHash, $entry); + %dupHash = map { $_, 1 } @defines; + @defines = keys %dupHash; + + %dupHash = map { $_, 1 } @incpaths; + @incpaths = keys %dupHash; + + %dupHash = map { $_, 1 } @cflags; + @cflags = keys %dupHash; +} + +sub handleCompileLine +{ + my ($line, $lineno) = @_; + my @parts = split(' ', $line); + my $sourcefile; + shift(@parts); # ignore cmd + while (my $part = shift @parts) { + if ("$part" eq "-o") { + # ignore object file + shift @parts; + } elsif ("$part" eq "-c") { + # ignore compile flag + } elsif ("$part" eq "-c") { + } elsif ($part =~ /^.?-I/) { + push(@incpaths, $part); + } elsif ($part =~ /^.?-D/) { + push(@defines, $part); + } elsif ($part =~ /^-/) { + push(@cflags, $part); + } elsif ($part =~ /\.(c|cc|cpp)$/) { + $sourcefile = $part; + } else { + die "Unhandled compiler option @ line $lineno: $part"; + } + } + @{$compile_options{"${sourcefile}_CFLAGS"}} = @cflags; + @{$compile_options{"${sourcefile}_DEFINES"}} = @defines; + @{$compile_options{"${sourcefile}_INCPATHS"}} = @incpaths; + clearCompileStep(); +} + +sub handleLibLine +{ + my ($line, $lineno) = @_; + my (@objfiles, @lflags, $libout, $part); + # kill cmd and rm 'prefix' + $line =~ s/^rm -f .* && .* rcs //; + my @parts = split(' ', $line); + while ($part = shift @parts) { + if ($part =~ /^-/) { + push(@lflags, $part); + } elsif ($part =~ /\.(o|obj)$/) { + push(@objfiles, $part); + } elsif ($part =~ /\.(a|lib)$/) { + $libout = $part; + $libout =~ s/\.a$//; + } else { + die "Unhandled lib option @ line $lineno: $part"; + } + } +# print "LibOut: '$libout'\nLFlags: @lflags\nOfiles: @objfiles\n"; +# exit(1); + foreach (@objfiles) { + my $sourcefile = $_; + $sourcefile =~ s/\.o/.c/; + push(@sources, $sourcefile); + push(@cflags, @{$compile_options{"${sourcefile}_CFLAGS"}}); + push(@defines, @{$compile_options{"${sourcefile}_DEFINES"}}); + push(@incpaths, @{$compile_options{"${sourcefile}_INCPATHS"}}); + } + removeDuplicates(); + + push(@{$build_structure{"LIBS"}}, $libout); + @{$build_structure{"LIBS_${libout}"}} = ("_DEFINES", "_INCLUDES", "_CFLAGS", "_SOURCES", + "_OBJECTS"); + @{$build_structure{"LIBS_${libout}_DEFINES"}} = @defines; + @{$build_structure{"LIBS_${libout}_INCLUDES"}} = @incpaths; + @{$build_structure{"LIBS_${libout}_CFLAGS"}} = @cflags; + @{$build_structure{"LIBS_${libout}_LFLAGS"}} = @lflags; + @{$build_structure{"LIBS_${libout}_SOURCES"}} = @sources; + @{$build_structure{"LIBS_${libout}_OBJECTS"}} = @objfiles; + clearCompileStep(); +} + +sub handleLinkLine +{ + my ($line, $lineno) = @_; + my (@objfiles, @lflags, @libs, $appout, $part); + my @parts = split(' ', $line); + shift(@parts); # ignore cmd + while ($part = shift @parts) { + if ($part =~ /^-IGNORE/) { + push(@lflags, $part); + } elsif ($part =~ /^-[GRIMDO]/) { + # eat compiler flags + } elsif ("$part" eq "-o") { + $appout = shift @parts; + } elsif ("$part" eq "-lz") { + push(@libs, "zlib.lib"); + } elsif ("$part" eq "-lcrypto") { + push(@libs, "libeay32.lib"); + } elsif ("$part" eq "-lssl") { + push(@libs, "ssleay32.lib"); + } elsif ($part =~ /^-/) { + push(@lflags, $part); + } elsif ($part =~ /\.(a|lib)$/) { + $part =~ s/\.a$/.lib/; + push(@libs, $part); + } elsif ($part =~ /\.(o|obj)$/) { + push(@objfiles, $part); + } else { + die "Unhandled lib option @ line $lineno: $part"; + } + } +# print "AppOut: '$appout'\nLFlags: @lflags\nLibs : @libs\nOfiles: @objfiles\n"; +# exit(1); + foreach (@objfiles) { + my $sourcefile = $_; + $sourcefile =~ s/\.o/.c/; + push(@sources, $sourcefile); + push(@cflags, @{$compile_options{"${sourcefile}_CFLAGS"}}); + push(@defines, @{$compile_options{"${sourcefile}_DEFINES"}}); + push(@incpaths, @{$compile_options{"${sourcefile}_INCPATHS"}}); + } + removeDuplicates(); + + removeDuplicates(); + push(@{$build_structure{"APPS"}}, $appout); + @{$build_structure{"APPS_${appout}"}} = ("_DEFINES", "_INCLUDES", "_CFLAGS", "_LFLAGS", + "_SOURCES", "_OBJECTS", "_LIBS"); + @{$build_structure{"APPS_${appout}_DEFINES"}} = @defines; + @{$build_structure{"APPS_${appout}_INCLUDES"}} = @incpaths; + @{$build_structure{"APPS_${appout}_CFLAGS"}} = @cflags; + @{$build_structure{"APPS_${appout}_LFLAGS"}} = @lflags; + @{$build_structure{"APPS_${appout}_SOURCES"}} = @sources; + @{$build_structure{"APPS_${appout}_OBJECTS"}} = @objfiles; + @{$build_structure{"APPS_${appout}_LIBS"}} = @libs; + clearCompileStep(); +} diff --git a/contrib/buildsystems/generate b/contrib/buildsystems/generate new file mode 100644 index 0000000000..bc10f25ff2 --- /dev/null +++ b/contrib/buildsystems/generate @@ -0,0 +1,29 @@ +#!/usr/bin/perl -w +###################################################################### +# Generate buildsystem files +# +# This script generate buildsystem files based on the output of a +# GNU Make --dry-run, enabling Windows users to develop Git with their +# trusted IDE with native projects. +# +# Note: +# It is not meant as *the* way of building Git with MSVC, but merely a +# convenience. The correct way of building Git with MSVC is to use the +# GNU Make tool to build with the maintained Makefile in the root of +# the project. If you have the msysgit environment installed and +# available in your current console, together with the Visual Studio +# environment you wish to build for, all you have to do is run the +# command: +# make MSVC=1 +# +# Copyright (C) 2009 Marius Storm-Olsen <mstormo@gmail.com> +###################################################################### +use strict; +use File::Basename; +use Cwd; + +my $git_dir = getcwd(); +$git_dir =~ s=\\=/=g; +$git_dir = dirname($git_dir) while (!-e "$git_dir/git.c" && "$git_dir" ne ""); +die "Couldn't find Git repo" if ("$git_dir" eq ""); +exec join(" ", ("PERL5LIB=${git_dir}/contrib/buildsystems ${git_dir}/contrib/buildsystems/engine.pl", @ARGV)); diff --git a/contrib/buildsystems/parse.pl b/contrib/buildsystems/parse.pl new file mode 100644 index 0000000000..c9656ece99 --- /dev/null +++ b/contrib/buildsystems/parse.pl @@ -0,0 +1,228 @@ +#!/usr/bin/perl -w +###################################################################### +# Do not call this script directly! +# +# The generate script ensures that @INC is correct before the engine +# is executed. +# +# Copyright (C) 2009 Marius Storm-Olsen <mstormo@gmail.com> +###################################################################### +use strict; +use File::Basename; +use Cwd; + +my $file = $ARGV[0]; +die "No file provided!" if !defined $file; + +my ($cflags, $target, $type, $line); + +open(F, "<$file") || die "Couldn't open file $file"; +my @data = <F>; +close(F); + +while (my $text = shift @data) { + my $ate_next; + do { + $ate_next = 0; + $line++; + chomp $text; + chop $text if ($text =~ /\r$/); + if ($text =~ /\\$/) { + $text =~ s/\\$//; + $text .= shift @data; + $ate_next = 1; + } + } while($ate_next); + + if($text =~ / -c /) { + # compilation + handleCompileLine($text, $line); + + } elsif ($text =~ / -o /) { + # linking executable + handleLinkLine($text, $line); + + } elsif ($text =~ /\.o / && $text =~ /\.a /) { + # libifying + handleLibLine($text, $line); + +# } elsif ($text =~ /^cp /) { +# # copy file around +# +# } elsif ($text =~ /^rm -f /) { +# # shell command +# +# } elsif ($text =~ /^make[ \[]/) { +# # make output +# +# } elsif ($text =~ /^echo /) { +# # echo to file +# +# } elsif ($text =~ /^if /) { +# # shell conditional +# +# } elsif ($text =~ /^tclsh /) { +# # translation stuff +# +# } elsif ($text =~ /^umask /) { +# # handling boilerplates +# +# } elsif ($text =~ /\$\(\:\)/) { +# # ignore +# +# } elsif ($text =~ /^FLAGS=/) { +# # flags check for dependencies +# +# } elsif ($text =~ /^'\/usr\/bin\/perl' -MError -e/) { +# # perl commands for copying files +# +# } elsif ($text =~ /generate-cmdlist\.sh/) { +# # command for generating list of commands +# +# } elsif ($text =~ /^test / && $text =~ /|| rm -f /) { +# # commands removing executables, if they exist +# +# } elsif ($text =~ /new locations or Tcl/) { +# # command for detecting Tcl/Tk changes +# +# } elsif ($text =~ /mkdir -p/) { +# # command creating path +# +# } elsif ($text =~ /: no custom templates yet/) { +# # whatever + + } else { +# print "Unhandled (line: $line): $text\n"; + } +} +close(F); + +# use Data::Dumper; +# print "Parsed build structure:\n"; +# print Dumper(%build_structure); + +# ------------------------------------------------------------------- +# Functions under here +# ------------------------------------------------------------------- +my (%build_structure, @defines, @incpaths, @cflags, @sources); + +sub clearCompileStep +{ + @defines = (); + @incpaths = (); + @cflags = (); + @sources = (); +} + +sub removeDuplicates +{ + my (%dupHash, $entry); + %dupHash = map { $_, 1 } @defines; + @defines = keys %dupHash; + + %dupHash = map { $_, 1 } @incpaths; + @incpaths = keys %dupHash; + + %dupHash = map { $_, 1 } @cflags; + @cflags = keys %dupHash; + + %dupHash = map { $_, 1 } @sources; + @sources = keys %dupHash; +} + +sub handleCompileLine +{ + my ($line, $lineno) = @_; + my @parts = split(' ', $line); + shift(@parts); # ignore cmd + while (my $part = shift @parts) { + if ("$part" eq "-o") { + # ignore object file + shift @parts; + } elsif ("$part" eq "-c") { + # ignore compile flag + } elsif ("$part" eq "-c") { + } elsif ($part =~ /^.?-I/) { + push(@incpaths, $part); + } elsif ($part =~ /^.?-D/) { + push(@defines, $part); + } elsif ($part =~ /^-/) { + push(@cflags, $part); + } elsif ($part =~ /\.(c|cc|cpp)$/) { + push(@sources, $part); + } else { + die "Unhandled compiler option @ line $lineno: $part"; + } + } + #print "Sources: @sources\nCFlags: @cflags\nDefine: @defines\nIncpat: @incpaths\n"; + #exit(1); +} + +sub handleLibLine +{ + my ($line, $lineno) = @_; + my (@objfiles, @lflags, $libout, $part); + # kill cmd and rm 'prefix' + $line =~ s/^rm -f .* && .* rcs //; + my @parts = split(' ', $line); + while ($part = shift @parts) { + if ($part =~ /^-/) { + push(@lflags, $part); + } elsif ($part =~ /\.(o|obj)$/) { + push(@objfiles, $part); + } elsif ($part =~ /\.(a|lib)$/) { + $libout = $part; + } else { + die "Unhandled lib option @ line $lineno: $part"; + } + } + #print "LibOut: '$libout'\nLFlags: @lflags\nOfiles: @objfiles\n"; + #exit(1); + removeDuplicates(); + push(@{$build_structure{"LIBS"}}, $libout); + @{$build_structure{"LIBS_${libout}"}} = ("_DEFINES", "_INCLUDES", "_CFLAGS", "_SOURCES", + "_OBJECTS"); + @{$build_structure{"LIBS_${libout}_DEFINES"}} = @defines; + @{$build_structure{"LIBS_${libout}_INCLUDES"}} = @incpaths; + @{$build_structure{"LIBS_${libout}_CFLAGS"}} = @cflags; + @{$build_structure{"LIBS_${libout}_SOURCES"}} = @sources; + @{$build_structure{"LIBS_${libout}_OBJECTS"}} = @objfiles; + clearCompileStep(); +} + +sub handleLinkLine +{ + my ($line, $lineno) = @_; + my (@objfiles, @lflags, @libs, $appout, $part); + my @parts = split(' ', $line); + shift(@parts); # ignore cmd + while ($part = shift @parts) { + if ($part =~ /^-[GRIDO]/) { + # eat compiler flags + } elsif ("$part" eq "-o") { + $appout = shift @parts; + } elsif ($part =~ /^-/) { + push(@lflags, $part); + } elsif ($part =~ /\.(a|lib)$/) { + push(@libs, $part); + } elsif ($part =~ /\.(o|obj)$/) { + push(@objfiles, $part); + } else { + die "Unhandled lib option @ line $lineno: $part"; + } + } + #print "AppOut: '$appout'\nLFlags: @lflags\nLibs : @libs\nOfiles: @objfiles\n"; + #exit(1); + removeDuplicates(); + push(@{$build_structure{"APPS"}}, $appout); + @{$build_structure{"APPS_${appout}"}} = ("_DEFINES", "_INCLUDES", "_CFLAGS", "_LFLAGS", + "_SOURCES", "_OBJECTS", "_LIBS"); + @{$build_structure{"APPS_${appout}_DEFINES"}} = @defines; + @{$build_structure{"APPS_${appout}_INCLUDES"}} = @incpaths; + @{$build_structure{"APPS_${appout}_CFLAGS"}} = @cflags; + @{$build_structure{"APPS_${appout}_LFLAGS"}} = @lflags; + @{$build_structure{"APPS_${appout}_SOURCES"}} = @sources; + @{$build_structure{"APPS_${appout}_OBJECTS"}} = @objfiles; + @{$build_structure{"APPS_${appout}_LIBS"}} = @libs; + clearCompileStep(); +} diff --git a/contrib/ciabot/README b/contrib/ciabot/README new file mode 100644 index 0000000000..3b916acece --- /dev/null +++ b/contrib/ciabot/README @@ -0,0 +1,12 @@ +These are hook scripts for the CIA notification service at <http://cia.vc/> + +They are maintained by Eric S. Raymond <esr@thyrsus.com>. There is an +upstream resource page for them at <http://www.catb.org/esr/ciabot/>, +but they are unlikely to change rapidly. + +You probably want the Python version; it's faster, more capable, and +better documented. The shell version is maintained only as a fallback +for use on hosting sites that don't permit Python hook scripts. + +You will find installation instructions for each script in its comment +header. diff --git a/contrib/ciabot/ciabot.py b/contrib/ciabot/ciabot.py new file mode 100755 index 0000000000..9775dffb5d --- /dev/null +++ b/contrib/ciabot/ciabot.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com> +# Distributed under BSD terms. +# +# This script contains porcelain and porcelain byproducts. +# It's Python because the Python standard libraries avoid portability/security +# issues raised by callouts in the ancestral Perl and sh scripts. It should +# be compatible back to Python 2.1.5 +# +# usage: ciabot.py [-V] [-n] [-p projectname] [refname [commits...]] +# +# This script is meant to be run either in a post-commit hook or in an +# update hook. If there's nothing unusual about your hosting setup, +# you can specify the project name with a -p option and avoid having +# to modify this script. Try it with -n to see the notification mail +# dumped to stdout and verify that it looks sane. With -V it dumps its +# version and exits. +# +# In post-commit, run it without arguments (other than possibly a -p +# option). It will query for current HEAD and the latest commit ID to +# get the information it needs. +# +# In update, call it with a refname followed by a list of commits: +# You want to reverse the order git rev-list emits becxause it lists +# from most recent to oldest. +# +# /path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac) +# +# Note: this script uses mail, not XML-RPC, in order to avoid stalling +# until timeout when the CIA XML-RPC server is down. +# + +# +# The project as known to CIA. You will either want to change this +# or invoke the script with a -p option to set it. +# +project=None + +# +# You may not need to change these: +# +import os, sys, commands, socket, urllib + +# Name of the repository. +# You can hardwire this to make the script faster. +repo = os.path.basename(os.getcwd()) + +# Fully-qualified domain name of this host. +# You can hardwire this to make the script faster. +host = socket.getfqdn() + +# Changeset URL prefix for your repo: when the commit ID is appended +# to this, it should point at a CGI that will display the commit +# through gitweb or something similar. The defaults will probably +# work if you have a typical gitweb/cgit setup. +# +#urlprefix="http://%(host)s/cgi-bin/gitweb.cgi?p=%(repo)s;a=commit;h=" +urlprefix="http://%(host)s/cgi-bin/cgit.cgi/%(repo)s/commit/?id=" + +# The service used to turn your gitwebbish URL into a tinyurl so it +# will take up less space on the IRC notification line. +tinyifier = "http://tinyurl.com/api-create.php?url=" + +# The template used to generate the XML messages to CIA. You can make +# visible changes to the IRC-bot notification lines by hacking this. +# The default will produce a notfication line that looks like this: +# +# ${project}: ${author} ${repo}:${branch} * ${rev} ${files}: ${logmsg} ${url} +# +# By omitting $files you can collapse the files part to a single slash. +xml = '''\ +<message> + <generator> + <name>CIA Python client for Git</name> + <version>%(gitver)s</version> + <url>%(generator)s</url> + </generator> + <source> + <project>%(project)s</project> + <branch>%(repo)s:%(branch)s</branch> + </source> + <timestamp>%(ts)s</timestamp> + <body> + <commit> + <author>%(author)s</author> + <revision>%(rev)s</revision> + <files> + %(files)s + </files> + <log>%(logmsg)s %(url)s</log> + <url>%(url)s</url> + </commit> + </body> +</message> +''' + +# +# No user-serviceable parts below this line: +# + +# Addresses for the e-mail. The from address is a dummy, since CIA +# will never reply to this mail. +fromaddr = "CIABOT-NOREPLY@" + host +toaddr = "cia@cia.navi.cx" + +# Identify the generator script. +# Should only change when the script itself gets a new home and maintainer. +generator="http://www.catb.org/~esr/ciabot.py" + +def do(command): + return commands.getstatusoutput(command)[1] + +def report(refname, merged): + "Generate a commit notification to be reported to CIA" + + # Try to tinyfy a reference to a web view for this commit. + try: + url = open(urllib.urlretrieve(tinyifier + urlprefix + merged)[0]).read() + except: + url = urlprefix + merged + + branch = os.path.basename(refname) + + # Compute a shortnane for the revision + rev = do("git describe '"+ merged +"' 2>/dev/null") or merged[:12] + + # Extract the neta-information for the commit + rawcommit = do("git cat-file commit " + merged) + files=do("git diff-tree -r --name-only '"+ merged +"' | sed -e '1d' -e 's-.*-<file>&</file>-'") + inheader = True + headers = {} + logmsg = "" + for line in rawcommit.split("\n"): + if inheader: + if line: + fields = line.split() + headers[fields[0]] = " ".join(fields[1:]) + else: + inheader = False + else: + logmsg = line + break + (author, ts) = headers["author"].split(">") + + # This discards the part of the authors addrsss after @. + # Might be bnicece to ship the full email address, if not + # for spammers' address harvesters - getting this wrong + # would make the freenode #commits channel into harvester heaven. + author = author.replace("<", "").split("@")[0].split()[-1] + + # This ignores the timezone. Not clear what to do with it... + ts = ts.strip().split()[0] + + context = locals() + context.update(globals()) + + out = xml % context + + message = '''\ +Message-ID: <%(merged)s.%(author)s@%(project)s> +From: %(fromaddr)s +To: %(toaddr)s +Content-type: text/xml +Subject: DeliverXML + +%(out)s''' % locals() + + return message + +if __name__ == "__main__": + import getopt + + try: + (options, arguments) = getopt.getopt(sys.argv[1:], "np:V") + except getopt.GetoptError, msg: + print "ciabot.py: " + str(msg) + raise SystemExit, 1 + + mailit = True + for (switch, val) in options: + if switch == '-p': + project = val + elif switch == '-n': + mailit = False + elif switch == '-V': + print "ciabot.py: version 3.2" + sys.exit(0) + + # Cough and die if user has not specified a project + if not project: + sys.stderr.write("ciabot.py: no project specified, bailing out.\n") + sys.exit(1) + + # We'll need the git version number. + gitver = do("git --version").split()[0] + + urlprefix = urlprefix % globals() + + # The script wants a reference to head followed by the list of + # commit ID to report about. + if len(arguments) == 0: + refname = do("git symbolic-ref HEAD 2>/dev/null") + merges = [do("git rev-parse HEAD")] + else: + refname = arguments[0] + merges = arguments[1:] + + if mailit: + import smtplib + server = smtplib.SMTP('localhost') + + for merged in merges: + message = report(refname, merged) + if mailit: + server.sendmail(fromaddr, [toaddr], message) + else: + print message + + if mailit: + server.quit() + +#End diff --git a/contrib/ciabot/ciabot.sh b/contrib/ciabot/ciabot.sh new file mode 100755 index 0000000000..eb87bba38e --- /dev/null +++ b/contrib/ciabot/ciabot.sh @@ -0,0 +1,192 @@ +#!/bin/sh +# Distributed under the terms of the GNU General Public License v2 +# Copyright (c) 2006 Fernando J. Pereda <ferdy@gentoo.org> +# Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com> +# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com> +# +# This is a version 3.x of ciabot.sh; use -V to find the exact +# version. Versions 1 and 2 were shipped in 2006 and 2008 and are not +# version-stamped. The version 2 maintainer has passed the baton. +# +# Note: This script should be considered obsolete. +# There is a faster, better-documented rewrite in Python: find it as ciabot.py +# Use this only if your hosting site forbids Python hooks. +# +# Originally based on Git ciabot.pl by Petr Baudis. +# This script contains porcelain and porcelain byproducts. +# +# usage: ciabot.sh [-V] [-n] [-p projectname] [refname commit] +# +# This script is meant to be run either in a post-commit hook or in an +# update hook. If there's nothing unusual about your hosting setup, +# you can specify the project name with a -p option and avoid having +# to modify this script. Try it with -n first to see the notification +# mail dumped to stdout and verify that it looks sane. Use -V to dump +# the version and exit. +# +# In post-commit, run it without arguments (other than possibly a -p +# option). It will query for current HEAD and the latest commit ID to +# get the information it needs. +# +# In update, you have to call it once per merged commit: +# +# refname=$1 +# oldhead=$2 +# newhead=$3 +# for merged in $(git rev-list ${oldhead}..${newhead} | tac) ; do +# /path/to/ciabot.bash ${refname} ${merged} +# done +# +# The reason for the tac call ids that git rev-list emits commits from +# most recent to least - better to ship notifactions from oldest to newest. +# +# Note: this script uses mail, not XML-RPC, in order to avoid stalling +# until timeout when the CIA XML-RPC server is down. +# + +# +# The project as known to CIA. You will either want to change this +# or set the project name with a -p option. +# +project= + +# +# You may not need to change these: +# + +# Name of the repository. +# You can hardwire this to make the script faster. +repo="`basename ${PWD}`" + +# Fully qualified domain name of the repo host. +# You can hardwire this to make the script faster. +host=`hostname --fqdn` + +# Changeset URL prefix for your repo: when the commit ID is appended +# to this, it should point at a CGI that will display the commit +# through gitweb or something similar. The defaults will probably +# work if you have a typical gitweb/cgit setup. +#urlprefix="http://${host}/cgi-bin/gitweb.cgi?p=${repo};a=commit;h=" +urlprefix="http://${host}/cgi-bin/cgit.cgi/${repo}/commit/?id=" + +# +# You probably will not need to change the following: +# + +# Identify the script. Should change only when the script itself +# gets a new home and maintainer. +generator="http://www.catb.org/~esr/ciabot/ciabot.sh" + +# Addresses for the e-mail +from="CIABOT-NOREPLY@${host}" +to="cia@cia.navi.cx" + +# SMTP client to use - may need to edit the absolute pathname for your system +sendmail="sendmail -t -f ${from}" + +# +# No user-serviceable parts below this line: +# + +# Should include all places sendmail is likely to lurk. +PATH="$PATH:/usr/sbin/" + +mode=mailit +while getopts pnV opt +do + case $opt in + p) project=$2; shift ; shift ;; + n) mode=dumpit; shift ;; + V) echo "ciabot.sh: version 3.2"; exit 0; shift ;; + esac +done + +# Cough and die if user has not specified a project +if [ -z "$project" ] +then + echo "ciabot.sh: no project specified, bailing out." >&2 + exit 1 +fi + +if [ $# -eq 0 ] ; then + refname=$(git symbolic-ref HEAD 2>/dev/null) + merged=$(git rev-parse HEAD) +else + refname=$1 + merged=$2 +fi + +# This tries to turn your gitwebbish URL into a tinyurl so it will take up +# less space on the IRC notification line. Some repo sites (I'm looking at +# you, berlios.de!) forbid wget calls for security reasons. On these, +# the code will fall back to the full un-tinyfied URL. +longurl=${urlprefix}${merged} +url=$(wget -O - -q http://tinyurl.com/api-create.php?url=${longurl} 2>/dev/null) +if [ -z "$url" ]; then + url="${longurl}" +fi + +refname=${refname##refs/heads/} + +gitver=$(git --version) +gitver=${gitver##* } + +rev=$(git describe ${merged} 2>/dev/null) +# ${merged:0:12} was the only bashism left in the 2008 version of this +# script, according to checkbashisms. Replace it with ${merged} here +# because it was just a fallback anyway, and it's worth accepting a +# longer fallback for faster execution and removing the bash +# dependency. +[ -z ${rev} ] && rev=${merged} + +# This discards the part of the author's address after @. +# Might be nice to ship the full email address, if not +# for spammers' address harvesters - getting this wrong +# would make the freenode #commits channel into harvester heaven. +rawcommit=$(git cat-file commit ${merged}) +author=$(echo "$rawcommit" | sed -n -e '/^author .*<\([^@]*\).*$/s--\1-p') +logmessage=$(echo "$rawcommit" | sed -e '1,/^$/d' | head -n 1) +logmessage=$(echo "$logmessage" | sed 's/\&/&\;/g; s/</<\;/g; s/>/>\;/g') +ts=$(echo "$rawcommit" | sed -n -e '/^author .*> \([0-9]\+\).*$/s--\1-p') +files=$(git diff-tree -r --name-only ${merged} | sed -e '1d' -e 's-.*-<file>&</file>-') + +out=" +<message> + <generator> + <name>CIA Shell client for Git</name> + <version>${gitver}</version> + <url>${generator}</url> + </generator> + <source> + <project>${project}</project> + <branch>$repo:${refname}</branch> + </source> + <timestamp>${ts}</timestamp> + <body> + <commit> + <author>${author}</author> + <revision>${rev}</revision> + <files> + ${files} + </files> + <log>${logmessage} ${url}</log> + <url>${url}</url> + </commit> + </body> +</message>" + +if [ "$mode" = "dumpit" ] +then + sendmail=cat +fi + +${sendmail} << EOM +Message-ID: <${merged}.${author}@${project}> +From: ${from} +To: ${to} +Content-type: text/xml +Subject: DeliverXML +${out} +EOM + +# vim: set tw=70 : diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index a652c88b27..ffedce751c 100755..100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1,5 +1,6 @@ +#!bash # -# bash completion support for core Git. +# bash/zsh completion support for core Git. # # Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org> # Conceptually based on gitcompletion (http://gitweb.hawaga.org.uk/). @@ -17,39 +18,32 @@ # To use these routines: # # 1) Copy this file to somewhere (e.g. ~/.git-completion.sh). -# 2) Added the following line to your .bashrc: +# 2) Add the following line to your .bashrc/.zshrc: # source ~/.git-completion.sh -# -# 3) You may want to make sure the git executable is available -# in your PATH before this script is sourced, as some caching -# is performed while the script loads. If git isn't found -# at source time then all lookups will be done on demand, -# which may be slightly slower. -# -# 4) Consider changing your PS1 to also show the current branch: -# PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' -# -# The argument to __git_ps1 will be displayed only if you -# are currently in a git repository. The %s token will be -# the name of the current branch. -# -# To submit patches: -# -# *) Read Documentation/SubmittingPatches -# *) Send all patches to the current maintainer: -# -# "Shawn O. Pearce" <spearce@spearce.org> -# -# *) Always CC the Git mailing list: -# -# git@vger.kernel.org -# +# 3) Consider changing your PS1 to also show the current branch, +# see git-prompt.sh for details. + +if [[ -n ${ZSH_VERSION-} ]]; then + autoload -U +X bashcompinit && bashcompinit +fi + +case "$COMP_WORDBREAKS" in +*:*) : great ;; +*) COMP_WORDBREAKS="$COMP_WORDBREAKS:" +esac +# __gitdir accepts 0 or 1 arguments (i.e., location) +# returns location of .git repo __gitdir () { - if [ -z "$1" ]; then - if [ -n "$__git_dir" ]; then + # Note: this function is duplicated in git-prompt.sh + # When updating it, make sure you update the other one to match. + if [ -z "${1-}" ]; then + if [ -n "${__git_dir-}" ]; then echo "$__git_dir" + elif [ -n "${GIT_DIR-}" ]; then + test -d "${GIT_DIR-}" || return 1 + echo "$GIT_DIR" elif [ -d .git ]; then echo .git else @@ -62,87 +56,299 @@ __gitdir () fi } -__git_ps1 () +__gitcomp_1 () { - local b="$(git symbolic-ref HEAD 2>/dev/null)" - if [ -n "$b" ]; then - if [ -n "$1" ]; then - printf "$1" "${b##refs/heads/}" - else - printf " (%s)" "${b##refs/heads/}" - fi + local c IFS=$' \t\n' + for c in $1; do + c="$c$2" + case $c in + --*=*|*.) ;; + *) c="$c " ;; + esac + printf '%s\n' "$c" + done +} + +# The following function is based on code from: +# +# bash_completion - programmable completion functions for bash 3.2+ +# +# Copyright © 2006-2008, Ian Macdonald <ian@caliban.org> +# © 2009-2010, Bash Completion Maintainers +# <bash-completion-devel@lists.alioth.debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# The latest version of this software can be obtained here: +# +# http://bash-completion.alioth.debian.org/ +# +# RELEASE: 2.x + +# This function can be used to access a tokenized list of words +# on the command line: +# +# __git_reassemble_comp_words_by_ref '=:' +# if test "${words_[cword_-1]}" = -w +# then +# ... +# fi +# +# The argument should be a collection of characters from the list of +# word completion separators (COMP_WORDBREAKS) to treat as ordinary +# characters. +# +# This is roughly equivalent to going back in time and setting +# COMP_WORDBREAKS to exclude those characters. The intent is to +# make option types like --date=<type> and <rev>:<path> easy to +# recognize by treating each shell word as a single token. +# +# It is best not to set COMP_WORDBREAKS directly because the value is +# shared with other completion scripts. By the time the completion +# function gets called, COMP_WORDS has already been populated so local +# changes to COMP_WORDBREAKS have no effect. +# +# Output: words_, cword_, cur_. + +__git_reassemble_comp_words_by_ref() +{ + local exclude i j first + # Which word separators to exclude? + exclude="${1//[^$COMP_WORDBREAKS]}" + cword_=$COMP_CWORD + if [ -z "$exclude" ]; then + words_=("${COMP_WORDS[@]}") + return fi + # List of word completion separators has shrunk; + # re-assemble words to complete. + for ((i=0, j=0; i < ${#COMP_WORDS[@]}; i++, j++)); do + # Append each nonempty word consisting of just + # word separator characters to the current word. + first=t + while + [ $i -gt 0 ] && + [ -n "${COMP_WORDS[$i]}" ] && + # word consists of excluded word separators + [ "${COMP_WORDS[$i]//[^$exclude]}" = "${COMP_WORDS[$i]}" ] + do + # Attach to the previous token, + # unless the previous token is the command name. + if [ $j -ge 2 ] && [ -n "$first" ]; then + ((j--)) + fi + first= + words_[$j]=${words_[j]}${COMP_WORDS[i]} + if [ $i = $COMP_CWORD ]; then + cword_=$j + fi + if (($i < ${#COMP_WORDS[@]} - 1)); then + ((i++)) + else + # Done. + return + fi + done + words_[$j]=${words_[j]}${COMP_WORDS[i]} + if [ $i = $COMP_CWORD ]; then + cword_=$j + fi + done } -__gitcomp () +if ! type _get_comp_words_by_ref >/dev/null 2>&1; then +if [[ -z ${ZSH_VERSION:+set} ]]; then +_get_comp_words_by_ref () { - local all c s=$'\n' IFS=' '$'\t'$'\n' - local cur="${COMP_WORDS[COMP_CWORD]}" - if [ $# -gt 2 ]; then - cur="$3" + local exclude cur_ words_ cword_ + if [ "$1" = "-n" ]; then + exclude=$2 + shift 2 fi - for c in $1; do - case "$c$4" in - --*=*) all="$all$c$4$s" ;; - *.) all="$all$c$4$s" ;; - *) all="$all$c$4 $s" ;; + __git_reassemble_comp_words_by_ref "$exclude" + cur_=${words_[cword_]} + while [ $# -gt 0 ]; do + case "$1" in + cur) + cur=$cur_ + ;; + prev) + prev=${words_[$cword_-1]} + ;; + words) + words=("${words_[@]}") + ;; + cword) + cword=$cword_ + ;; esac + shift done - IFS=$s - COMPREPLY=($(compgen -P "$2" -W "$all" -- "$cur")) - return +} +else +_get_comp_words_by_ref () +{ + while [ $# -gt 0 ]; do + case "$1" in + cur) + cur=${COMP_WORDS[COMP_CWORD]} + ;; + prev) + prev=${COMP_WORDS[COMP_CWORD-1]} + ;; + words) + words=("${COMP_WORDS[@]}") + ;; + cword) + cword=$COMP_CWORD + ;; + -n) + # assume COMP_WORDBREAKS is already set sanely + shift + ;; + esac + shift + done +} +fi +fi + +# Generates completion reply with compgen, appending a space to possible +# completion words, if necessary. +# It accepts 1 to 4 arguments: +# 1: List of possible completion words. +# 2: A prefix to be added to each possible completion word (optional). +# 3: Generate possible completion matches for this word (optional). +# 4: A suffix to be appended to each possible completion word (optional). +__gitcomp () +{ + local cur_="${3-$cur}" + + case "$cur_" in + --*=) + COMPREPLY=() + ;; + *) + local IFS=$'\n' + COMPREPLY=($(compgen -P "${2-}" \ + -W "$(__gitcomp_1 "${1-}" "${4-}")" \ + -- "$cur_")) + ;; + esac +} + +# Generates completion reply with compgen from newline-separated possible +# completion words by appending a space to all of them. +# It accepts 1 to 4 arguments: +# 1: List of possible completion words, separated by a single newline. +# 2: A prefix to be added to each possible completion word (optional). +# 3: Generate possible completion matches for this word (optional). +# 4: A suffix to be appended to each possible completion word instead of +# the default space (optional). If specified but empty, nothing is +# appended. +__gitcomp_nl () +{ + local IFS=$'\n' + COMPREPLY=($(compgen -P "${2-}" -S "${4- }" -W "$1" -- "${3-$cur}")) } __git_heads () { - local cmd i is_hash=y dir="$(__gitdir "$1")" + local dir="$(__gitdir)" if [ -d "$dir" ]; then - for i in $(git --git-dir="$dir" \ - for-each-ref --format='%(refname)' \ - refs/heads ); do - echo "${i#refs/heads/}" - done + git --git-dir="$dir" for-each-ref --format='%(refname:short)' \ + refs/heads return fi - for i in $(git-ls-remote "$1" 2>/dev/null); do - case "$is_hash,$i" in - y,*) is_hash=n ;; - n,*^{}) is_hash=y ;; - n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;; - n,*) is_hash=y; echo "$i" ;; - esac - done } +__git_tags () +{ + local dir="$(__gitdir)" + if [ -d "$dir" ]; then + git --git-dir="$dir" for-each-ref --format='%(refname:short)' \ + refs/tags + return + fi +} + +# __git_refs accepts 0, 1 (to pass to __gitdir), or 2 arguments +# presence of 2nd argument means use the guess heuristic employed +# by checkout for tracking branches __git_refs () { - local cmd i is_hash=y dir="$(__gitdir "$1")" + local i hash dir="$(__gitdir "${1-}")" track="${2-}" + local format refs if [ -d "$dir" ]; then - if [ -e "$dir/HEAD" ]; then echo HEAD; fi - for i in $(git --git-dir="$dir" \ - for-each-ref --format='%(refname)' \ - refs/tags refs/heads refs/remotes); do + case "$cur" in + refs|refs/*) + format="refname" + refs="${cur%/*}" + track="" + ;; + *) + for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do + if [ -e "$dir/$i" ]; then echo $i; fi + done + format="refname:short" + refs="refs/tags refs/heads refs/remotes" + ;; + esac + git --git-dir="$dir" for-each-ref --format="%($format)" \ + $refs + if [ -n "$track" ]; then + # employ the heuristic used by git checkout + # Try to find a remote branch that matches the completion word + # but only output if the branch name is unique + local ref entry + git --git-dir="$dir" for-each-ref --shell --format="ref=%(refname:short)" \ + "refs/remotes/" | \ + while read -r entry; do + eval "$entry" + ref="${ref#*/}" + if [[ "$ref" == "$cur"* ]]; then + echo "$ref" + fi + done | uniq -u + fi + return + fi + case "$cur" in + refs|refs/*) + git ls-remote "$dir" "$cur*" 2>/dev/null | \ + while read -r hash i; do case "$i" in - refs/tags/*) echo "${i#refs/tags/}" ;; - refs/heads/*) echo "${i#refs/heads/}" ;; - refs/remotes/*) echo "${i#refs/remotes/}" ;; - *) echo "$i" ;; + *^{}) ;; + *) echo "$i" ;; esac done - return - fi - for i in $(git-ls-remote "$dir" 2>/dev/null); do - case "$is_hash,$i" in - y,*) is_hash=n ;; - n,*^{}) is_hash=y ;; - n,refs/tags/*) is_hash=y; echo "${i#refs/tags/}" ;; - n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;; - n,refs/remotes/*) is_hash=y; echo "${i#refs/remotes/}" ;; - n,*) is_hash=y; echo "$i" ;; - esac - done + ;; + *) + git ls-remote "$dir" HEAD ORIG_HEAD 'refs/tags/*' 'refs/heads/*' 'refs/remotes/*' 2>/dev/null | \ + while read -r hash i; do + case "$i" in + *^{}) ;; + refs/*) echo "${i#refs/*/}" ;; + *) echo "$i" ;; + esac + done + ;; + esac } +# __git_refs2 requires 1 argument (to pass to __git_refs) __git_refs2 () { local i @@ -151,134 +357,249 @@ __git_refs2 () done } +# __git_refs_remotes requires 1 argument (to pass to ls-remote) __git_refs_remotes () { - local cmd i is_hash=y - for i in $(git-ls-remote "$1" 2>/dev/null); do - case "$is_hash,$i" in - n,refs/heads/*) - is_hash=y - echo "$i:refs/remotes/$1/${i#refs/heads/}" - ;; - y,*) is_hash=n ;; - n,*^{}) is_hash=y ;; - n,refs/tags/*) is_hash=y;; - n,*) is_hash=y; ;; - esac + local i hash + git ls-remote "$1" 'refs/heads/*' 2>/dev/null | \ + while read -r hash i; do + echo "$i:refs/remotes/$1/${i#refs/heads/}" done } __git_remotes () { - local i ngoff IFS=$'\n' d="$(__gitdir)" - shopt -q nullglob || ngoff=1 - shopt -s nullglob - for i in "$d/remotes"/*; do - echo ${i#$d/remotes/} - done - [ "$ngoff" ] && shopt -u nullglob - for i in $(git --git-dir="$d" config --list); do - case "$i" in - remote.*.url=*) - i="${i#remote.}" - echo "${i/.url=*/}" - ;; - esac + local i IFS=$'\n' d="$(__gitdir)" + test -d "$d/remotes" && ls -1 "$d/remotes" + for i in $(git --git-dir="$d" config --get-regexp 'remote\..*\.url' 2>/dev/null); do + i="${i#remote.}" + echo "${i/.url*/}" done } -__git_merge_strategies () +__git_list_merge_strategies () { - if [ -n "$__git_merge_strategylist" ]; then - echo "$__git_merge_strategylist" - return - fi - sed -n "/^all_strategies='/{ - s/^all_strategies='// - s/'// + git merge -s help 2>&1 | + sed -n -e '/[Aa]vailable strategies are: /,/^$/{ + s/\.$// + s/.*:// + s/^[ ]*// + s/[ ]*$// p - q - }" "$(git --exec-path)/git-merge" + }' } -__git_merge_strategylist= -__git_merge_strategylist="$(__git_merge_strategies 2>/dev/null)" -__git_complete_file () +__git_merge_strategies= +# 'git merge -s help' (and thus detection of the merge strategy +# list) fails, unfortunately, if run outside of any git working +# tree. __git_merge_strategies is set to the empty string in +# that case, and the detection will be repeated the next time it +# is needed. +__git_compute_merge_strategies () { - local pfx ls ref cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in + test -n "$__git_merge_strategies" || + __git_merge_strategies=$(__git_list_merge_strategies) +} + +__git_complete_revlist_file () +{ + local pfx ls ref cur_="$cur" + case "$cur_" in + *..?*:*) + return + ;; ?*:*) - ref="${cur%%:*}" - cur="${cur#*:}" - case "$cur" in + ref="${cur_%%:*}" + cur_="${cur_#*:}" + case "$cur_" in ?*/*) - pfx="${cur%/*}" - cur="${cur##*/}" + pfx="${cur_%/*}" + cur_="${cur_##*/}" ls="$ref:$pfx" pfx="$pfx/" ;; *) ls="$ref" ;; - esac - COMPREPLY=($(compgen -P "$pfx" \ - -W "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ - | sed '/^100... blob /s,^.* ,, + esac + + case "$COMP_WORDBREAKS" in + *:*) : great ;; + *) pfx="$ref:$pfx" ;; + esac + + __gitcomp_nl "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ + | sed '/^100... blob /{ + s,^.* ,, + s,$, , + } + /^120000 blob /{ + s,^.* ,, + s,$, , + } /^040000 tree /{ s,^.* ,, s,$,/, } s/^.* //')" \ - -- "$cur")) + "$pfx" "$cur_" "" + ;; + *...*) + pfx="${cur_%...*}..." + cur_="${cur_#*...}" + __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_" + ;; + *..*) + pfx="${cur_%..*}.." + cur_="${cur_#*..}" + __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_" ;; *) - __gitcomp "$(__git_refs)" + __gitcomp_nl "$(__git_refs)" ;; esac } + +__git_complete_file () +{ + __git_complete_revlist_file +} + __git_complete_revlist () { - local pfx cur="${COMP_WORDS[COMP_CWORD]}" - case "$cur" in - *...*) - pfx="${cur%...*}..." - cur="${cur#*...}" - __gitcomp "$(__git_refs)" "$pfx" "$cur" + __git_complete_revlist_file +} + +__git_complete_remote_or_refspec () +{ + local cur_="$cur" cmd="${words[1]}" + local i c=2 remote="" pfx="" lhs=1 no_complete_refspec=0 + if [ "$cmd" = "remote" ]; then + ((c++)) + fi + while [ $c -lt $cword ]; do + i="${words[c]}" + case "$i" in + --mirror) [ "$cmd" = "push" ] && no_complete_refspec=1 ;; + --all) + case "$cmd" in + push) no_complete_refspec=1 ;; + fetch) + COMPREPLY=() + return + ;; + *) ;; + esac + ;; + -*) ;; + *) remote="$i"; break ;; + esac + ((c++)) + done + if [ -z "$remote" ]; then + __gitcomp_nl "$(__git_remotes)" + return + fi + if [ $no_complete_refspec = 1 ]; then + COMPREPLY=() + return + fi + [ "$remote" = "." ] && remote= + case "$cur_" in + *:*) + case "$COMP_WORDBREAKS" in + *:*) : great ;; + *) pfx="${cur_%%:*}:" ;; + esac + cur_="${cur_#*:}" + lhs=0 ;; - *..*) - pfx="${cur%..*}.." - cur="${cur#*..}" - __gitcomp "$(__git_refs)" "$pfx" "$cur" + +*) + pfx="+" + cur_="${cur_#+}" ;; - *.) - __gitcomp "$cur." + esac + case "$cmd" in + fetch) + if [ $lhs = 1 ]; then + __gitcomp_nl "$(__git_refs2 "$remote")" "$pfx" "$cur_" + else + __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_" + fi ;; - *) - __gitcomp "$(__git_refs)" + pull|remote) + if [ $lhs = 1 ]; then + __gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_" + else + __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_" + fi + ;; + push) + if [ $lhs = 1 ]; then + __gitcomp_nl "$(__git_refs)" "$pfx" "$cur_" + else + __gitcomp_nl "$(__git_refs "$remote")" "$pfx" "$cur_" + fi ;; esac } -__git_commands () +__git_complete_strategy () +{ + __git_compute_merge_strategies + case "$prev" in + -s|--strategy) + __gitcomp "$__git_merge_strategies" + return 0 + esac + case "$cur" in + --strategy=*) + __gitcomp "$__git_merge_strategies" "" "${cur##--strategy=}" + return 0 + ;; + esac + return 1 +} + +__git_list_all_commands () { - if [ -n "$__git_commandlist" ]; then - echo "$__git_commandlist" - return - fi local i IFS=" "$'\n' - for i in $(git help -a|egrep '^ ') + for i in $(git help -a|egrep '^ [a-zA-Z0-9]') do case $i in - add--interactive) : plumbing;; + *--*) : helper pattern;; + *) echo $i;; + esac + done +} + +__git_all_commands= +__git_compute_all_commands () +{ + test -n "$__git_all_commands" || + __git_all_commands=$(__git_list_all_commands) +} + +__git_list_porcelain_commands () +{ + local i IFS=" "$'\n' + __git_compute_all_commands + for i in "help" $__git_all_commands + do + case $i in + *--*) : helper pattern;; applymbox) : ask gittus;; applypatch) : ask gittus;; archimport) : import;; cat-file) : plumbing;; check-attr) : plumbing;; check-ref-format) : plumbing;; + checkout-index) : plumbing;; commit-tree) : plumbing;; - convert-objects) : plumbing;; + count-objects) : infrequent;; + credential-cache) : credentials helper;; + credential-store) : credentials helper;; cvsexportcommit) : export;; cvsimport) : import;; cvsserver) : daemon;; @@ -287,8 +608,8 @@ __git_commands () diff-index) : plumbing;; diff-tree) : plumbing;; fast-import) : import;; + fast-export) : export;; fsck-objects) : plumbing;; - fetch--tool) : plumbing;; fetch-pack) : plumbing;; fmt-merge-msg) : plumbing;; for-each-ref) : plumbing;; @@ -297,6 +618,10 @@ __git_commands () index-pack) : plumbing;; init-db) : deprecated;; local-fetch) : plumbing;; + lost-found) : infrequent;; + ls-files) : plumbing;; + ls-remote) : plumbing;; + ls-tree) : plumbing;; mailinfo) : plumbing;; mailsplit) : plumbing;; merge-*) : plumbing;; @@ -313,20 +638,19 @@ __git_commands () quiltimport) : import;; read-tree) : plumbing;; receive-pack) : plumbing;; - reflog) : plumbing;; - repo-config) : plumbing;; + remote-*) : transport;; + repo-config) : deprecated;; rerere) : plumbing;; rev-list) : plumbing;; rev-parse) : plumbing;; runstatus) : plumbing;; sh-setup) : internal;; shell) : daemon;; + show-ref) : plumbing;; send-pack) : plumbing;; show-index) : plumbing;; ssh-*) : transport;; stripspace) : plumbing;; - svn) : import export;; - svnimport) : import;; symbolic-ref) : plumbing;; tar-tree) : deprecated;; unpack-file) : plumbing;; @@ -337,46 +661,105 @@ __git_commands () upload-archive) : plumbing;; upload-pack) : plumbing;; write-tree) : plumbing;; + var) : infrequent;; + verify-pack) : infrequent;; verify-tag) : plumbing;; *) echo $i;; esac done } -__git_commandlist= -__git_commandlist="$(__git_commands 2>/dev/null)" + +__git_porcelain_commands= +__git_compute_porcelain_commands () +{ + __git_compute_all_commands + test -n "$__git_porcelain_commands" || + __git_porcelain_commands=$(__git_list_porcelain_commands) +} + +__git_pretty_aliases () +{ + local i IFS=$'\n' + for i in $(git --git-dir="$(__gitdir)" config --get-regexp "pretty\..*" 2>/dev/null); do + case "$i" in + pretty.*) + i="${i#pretty.}" + echo "${i/ */}" + ;; + esac + done +} __git_aliases () { local i IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --list); do + for i in $(git --git-dir="$(__gitdir)" config --get-regexp "alias\..*" 2>/dev/null); do case "$i" in alias.*) i="${i#alias.}" - echo "${i/=*/}" + echo "${i/ */}" ;; esac done } +# __git_aliased_command requires 1 argument __git_aliased_command () { local word cmdline=$(git --git-dir="$(__gitdir)" \ config --get "alias.$1") for word in $cmdline; do - if [ "${word##-*}" ]; then - echo $word + case "$word" in + \!gitk|gitk) + echo "gitk" + return + ;; + \!*) : shell command alias ;; + -*) : option ;; + *=*) : setting env ;; + git) : git itself ;; + *) + echo "$word" return + esac + done +} + +# __git_find_on_cmdline requires 1 argument +__git_find_on_cmdline () +{ + local word subcommand c=1 + while [ $c -lt $cword ]; do + word="${words[c]}" + for subcommand in $1; do + if [ "$subcommand" = "$word" ]; then + echo "$subcommand" + return + fi + done + ((c++)) + done +} + +__git_has_doubledash () +{ + local c=1 + while [ $c -lt $cword ]; do + if [ "--" = "${words[c]}" ]; then + return 0 fi + ((c++)) done + return 1 } -__git_whitespacelist="nowarn warn error error-all strip" +__git_whitespacelist="nowarn warn error error-all fix" _git_am () { - local cur="${COMP_WORDS[COMP_CWORD]}" - if [ -d .dotest ]; then - __gitcomp "--skip --resolved" + local dir="$(__gitdir)" + if [ -d "$dir"/rebase-apply ]; then + __gitcomp "--skip --continue --resolved --abort" return fi case "$cur" in @@ -386,8 +769,10 @@ _git_am () ;; --*) __gitcomp " - --signoff --utf8 --binary --3way --interactive - --whitespace= + --3way --committer-date-is-author-date --ignore-date + --ignore-whitespace --ignore-space-change + --interactive --keep --no-utf8 --signoff --utf8 + --whitespace= --scissors " return esac @@ -396,7 +781,6 @@ _git_am () _git_apply () { - local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in --whitespace=*) __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}" @@ -407,6 +791,7 @@ _git_apply () --stat --numstat --summary --check --index --cached --index-info --reverse --reject --unidiff-zero --apply --no-add --exclude= + --ignore-whitespace --ignore-space-change --whitespace= --inaccurate-eof --verbose " return @@ -416,37 +801,59 @@ _git_apply () _git_add () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + case "$cur" in --*) - __gitcomp "--interactive --refresh" + __gitcomp " + --interactive --refresh --patch --update --dry-run + --ignore-errors --intent-to-add + " return esac COMPREPLY=() } +_git_archive () +{ + case "$cur" in + --format=*) + __gitcomp "$(git archive --list)" "" "${cur##--format=}" + return + ;; + --remote=*) + __gitcomp_nl "$(__git_remotes)" "" "${cur##--remote=}" + return + ;; + --*) + __gitcomp " + --format= --list --verbose + --prefix= --remote= --exec= + " + return + ;; + esac + __git_complete_file +} + _git_bisect () { - local i c=1 command - while [ $c -lt $COMP_CWORD ]; do - i="${COMP_WORDS[c]}" - case "$i" in - start|bad|good|reset|visualize|replay|log) - command="$i" - break - ;; - esac - c=$((++c)) - done + __git_has_doubledash && return - if [ $c -eq $COMP_CWORD -a -z "$command" ]; then - __gitcomp "start bad good reset visualize replay log" + local subcommands="start bad good skip reset visualize replay log run" + local subcommand="$(__git_find_on_cmdline "$subcommands")" + if [ -z "$subcommand" ]; then + if [ -f "$(__gitdir)"/BISECT_START ]; then + __gitcomp "$subcommands" + else + __gitcomp "replay start" + fi return fi - case "$command" in - bad|good|reset) - __gitcomp "$(__git_refs)" + case "$subcommand" in + bad|good|reset|skip|start) + __gitcomp_nl "$(__git_refs)" ;; *) COMPREPLY=() @@ -456,26 +863,43 @@ _git_bisect () _git_branch () { - __gitcomp "$(__git_refs)" -} + local i c=1 only_local_ref="n" has_r="n" -_git_bundle () -{ - local mycword="$COMP_CWORD" - case "${COMP_WORDS[0]}" in - git) - local cmd="${COMP_WORDS[2]}" - mycword="$((mycword-1))" + while [ $c -lt $cword ]; do + i="${words[c]}" + case "$i" in + -d|-m) only_local_ref="y" ;; + -r) has_r="y" ;; + esac + ((c++)) + done + + case "$cur" in + --*) + __gitcomp " + --color --no-color --verbose --abbrev= --no-abbrev + --track --no-track --contains --merged --no-merged + --set-upstream --edit-description --list + " ;; - git-bundle*) - local cmd="${COMP_WORDS[1]}" + *) + if [ $only_local_ref = "y" -a $has_r = "n" ]; then + __gitcomp_nl "$(__git_heads)" + else + __gitcomp_nl "$(__git_refs)" + fi ;; esac - case "$mycword" in - 1) +} + +_git_bundle () +{ + local cmd="${words[2]}" + case "$cword" in + 2) __gitcomp "create list-heads verify unbundle" ;; - 2) + 3) # looking for a file ;; *) @@ -490,7 +914,28 @@ _git_bundle () _git_checkout () { - __gitcomp "$(__git_refs)" + __git_has_doubledash && return + + case "$cur" in + --conflict=*) + __gitcomp "diff3 merge" "" "${cur##--conflict=}" + ;; + --*) + __gitcomp " + --quiet --ours --theirs --track --no-track --merge + --conflict= --orphan --patch + " + ;; + *) + # check if --track, --no-track, or --no-guess was specified + # if so, disable DWIM mode + local flags="--track --no-track --no-guess" track=1 + if [ -n "$(__git_find_on_cmdline "$flags")" ]; then + track='' + fi + __gitcomp_nl "$(__git_refs '' $track)" + ;; + esac } _git_cherry () @@ -500,25 +945,80 @@ _git_cherry () _git_cherry_pick () { - local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in --*) __gitcomp "--edit --no-commit" ;; *) - __gitcomp "$(__git_refs)" + __gitcomp_nl "$(__git_refs)" + ;; + esac +} + +_git_clean () +{ + __git_has_doubledash && return + + case "$cur" in + --*) + __gitcomp "--dry-run --quiet" + return ;; esac + COMPREPLY=() +} + +_git_clone () +{ + case "$cur" in + --*) + __gitcomp " + --local + --no-hardlinks + --shared + --reference + --quiet + --no-checkout + --bare + --mirror + --origin + --upload-pack + --template= + --depth + " + return + ;; + esac + COMPREPLY=() } _git_commit () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + case "$cur" in + --cleanup=*) + __gitcomp "default strip verbatim whitespace + " "" "${cur##--cleanup=}" + return + ;; + --reuse-message=*|--reedit-message=*|\ + --fixup=*|--squash=*) + __gitcomp_nl "$(__git_refs)" "" "${cur#*=}" + return + ;; + --untracked-files=*) + __gitcomp "all no normal" "" "${cur##--untracked-files=}" + return + ;; --*) __gitcomp " --all --author= --signoff --verify --no-verify - --edit --amend --include --only + --edit --amend --include --only --interactive + --dry-run --reuse-message= --reedit-message= + --reset-author --file= --message= --template= + --cleanup= --untracked-files --untracked-files= + --verbose --quiet --fixup= --squash= " return esac @@ -527,63 +1027,114 @@ _git_commit () _git_describe () { - __gitcomp "$(__git_refs)" + case "$cur" in + --*) + __gitcomp " + --all --tags --contains --abbrev= --candidates= + --exact-match --debug --long --match --always + " + return + esac + __gitcomp_nl "$(__git_refs)" } +__git_diff_common_options="--stat --numstat --shortstat --summary + --patch-with-stat --name-only --name-status --color + --no-color --color-words --no-renames --check + --full-index --binary --abbrev --diff-filter= + --find-copies-harder + --text --ignore-space-at-eol --ignore-space-change + --ignore-all-space --exit-code --quiet --ext-diff + --no-ext-diff + --no-prefix --src-prefix= --dst-prefix= + --inter-hunk-context= + --patience + --raw + --dirstat --dirstat= --dirstat-by-file + --dirstat-by-file= --cumulative +" + _git_diff () { - __git_complete_file -} + __git_has_doubledash && return -_git_diff_tree () -{ - __gitcomp "$(__git_refs)" + case "$cur" in + --*) + __gitcomp "--cached --staged --pickaxe-all --pickaxe-regex + --base --ours --theirs --no-index + $__git_diff_common_options + " + return + ;; + esac + __git_complete_revlist_file } -_git_fetch () +__git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff + tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 +" + +_git_difftool () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return - case "${COMP_WORDS[0]},$COMP_CWORD" in - git-fetch*,1) - __gitcomp "$(__git_remotes)" + case "$cur" in + --tool=*) + __gitcomp "$__git_mergetools_common kompare" "" "${cur##--tool=}" + return ;; - git,2) - __gitcomp "$(__git_remotes)" + --*) + __gitcomp "--cached --staged --pickaxe-all --pickaxe-regex + --base --ours --theirs + --no-renames --diff-filter= --find-copies-harder + --relative --ignore-submodules + --tool=" + return ;; - *) - case "$cur" in - *:*) - __gitcomp "$(__git_refs)" "" "${cur#*:}" - ;; - *) - local remote - case "${COMP_WORDS[0]}" in - git-fetch) remote="${COMP_WORDS[1]}" ;; - git) remote="${COMP_WORDS[2]}" ;; - esac - __gitcomp "$(__git_refs2 "$remote")" - ;; - esac + esac + __git_complete_file +} + +__git_fetch_options=" + --quiet --verbose --append --upload-pack --force --keep --depth= + --tags --no-tags --all --prune --dry-run +" + +_git_fetch () +{ + case "$cur" in + --*) + __gitcomp "$__git_fetch_options" + return ;; esac + __git_complete_remote_or_refspec } _git_format_patch () { - local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in + --thread=*) + __gitcomp " + deep shallow + " "" "${cur##--thread=}" + return + ;; --*) __gitcomp " - --stdout --attach --thread + --stdout --attach --no-attach --thread --thread= --output-directory --numbered --start-number --numbered-files --keep-subject - --signoff - --in-reply-to= + --signoff --signature --no-signature + --in-reply-to= --cc= --full-index --binary --not --all + --cover-letter + --no-prefix --src-prefix= --dst-prefix= + --inline --suffix= --ignore-if-in-upstream + --subject-prefix= " return ;; @@ -591,9 +1142,22 @@ _git_format_patch () __git_complete_revlist } +_git_fsck () +{ + case "$cur" in + --*) + __gitcomp " + --tags --root --unreachable --cache --no-reflogs --full + --strict --verbose --lost-found + " + return + ;; + esac + COMPREPLY=() +} + _git_gc () { - local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in --*) __gitcomp "--prune --aggressive" @@ -603,9 +1167,105 @@ _git_gc () COMPREPLY=() } +_git_gitk () +{ + _gitk +} + +__git_match_ctag() { + awk "/^${1////\\/}/ { print \$1 }" "$2" +} + +_git_grep () +{ + __git_has_doubledash && return + + case "$cur" in + --*) + __gitcomp " + --cached + --text --ignore-case --word-regexp --invert-match + --full-name --line-number + --extended-regexp --basic-regexp --fixed-strings + --perl-regexp + --files-with-matches --name-only + --files-without-match + --max-depth + --count + --and --or --not --all-match + " + return + ;; + esac + + case "$cword,$prev" in + 2,*|*,-*) + if test -r tags; then + __gitcomp_nl "$(__git_match_ctag "$cur" tags)" + return + fi + ;; + esac + + __gitcomp_nl "$(__git_refs)" +} + +_git_help () +{ + case "$cur" in + --*) + __gitcomp "--all --info --man --web" + return + ;; + esac + __git_compute_all_commands + __gitcomp "$__git_all_commands $(__git_aliases) + attributes cli core-tutorial cvs-migration + diffcore gitk glossary hooks ignore modules + namespaces repository-layout tutorial tutorial-2 + workflows + " +} + +_git_init () +{ + case "$cur" in + --shared=*) + __gitcomp " + false true umask group all world everybody + " "" "${cur##--shared=}" + return + ;; + --*) + __gitcomp "--quiet --bare --template= --shared --shared=" + return + ;; + esac + COMPREPLY=() +} + +_git_ls_files () +{ + __git_has_doubledash && return + + case "$cur" in + --*) + __gitcomp "--cached --deleted --modified --others --ignored + --stage --directory --no-empty-directory --unmerged + --killed --exclude= --exclude-from= + --exclude-per-directory= --exclude-standard + --error-unmatch --with-tree= --full-name + --abbrev --ignored --exclude-per-directory + " + return + ;; + esac + COMPREPLY=() +} + _git_ls_remote () { - __gitcomp "$(__git_remotes)" + __gitcomp_nl "$(__git_remotes)" } _git_ls_tree () @@ -613,34 +1273,73 @@ _git_ls_tree () __git_complete_file } +# Options that go well for log, shortlog and gitk +__git_log_common_options=" + --not --all + --branches --tags --remotes + --first-parent --merges --no-merges + --max-count= + --max-age= --since= --after= + --min-age= --until= --before= + --min-parents= --max-parents= + --no-min-parents --no-max-parents +" +# Options that go well for log and gitk (not shortlog) +__git_log_gitk_options=" + --dense --sparse --full-history + --simplify-merges --simplify-by-decoration + --left-right --notes --no-notes +" +# Options that go well for log and shortlog (not gitk) +__git_log_shortlog_options=" + --author= --committer= --grep= + --all-match +" + +__git_log_pretty_formats="oneline short medium full fuller email raw format:" +__git_log_date_formats="relative iso8601 rfc2822 short local default raw" + _git_log () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + + local g="$(git rev-parse --git-dir 2>/dev/null)" + local merge="" + if [ -f "$g/MERGE_HEAD" ]; then + merge="--merge" + fi case "$cur" in - --pretty=*) - __gitcomp " - oneline short medium full fuller email raw - " "" "${cur##--pretty=}" + --pretty=*|--format=*) + __gitcomp "$__git_log_pretty_formats $(__git_pretty_aliases) + " "" "${cur#*=}" return ;; --date=*) - __gitcomp " - relative iso8601 rfc2822 short local default - " "" "${cur##--date=}" + __gitcomp "$__git_log_date_formats" "" "${cur##--date=}" + return + ;; + --decorate=*) + __gitcomp "long short" "" "${cur##--decorate=}" return ;; --*) __gitcomp " - --max-count= --max-age= --since= --after= - --min-age= --before= --until= + $__git_log_common_options + $__git_log_shortlog_options + $__git_log_gitk_options --root --topo-order --date-order --reverse - --no-merges --follow + --follow --full-diff --abbrev-commit --abbrev= --relative-date --date= - --author= --committer= --grep= - --all-match - --pretty= --name-status --name-only --raw - --not --all + --pretty= --format= --oneline + --cherry-pick + --graph + --decorate --decorate= + --walk-reflogs + --parents --children + $merge + $__git_diff_common_options + --pickaxe-all --pickaxe-regex " return ;; @@ -648,31 +1347,52 @@ _git_log () __git_complete_revlist } +__git_merge_options=" + --no-commit --no-stat --log --no-log --squash --strategy + --commit --stat --no-squash --ff --no-ff --ff-only --edit --no-edit +" + _git_merge () { - local cur="${COMP_WORDS[COMP_CWORD]}" - case "${COMP_WORDS[COMP_CWORD-1]}" in - -s|--strategy) - __gitcomp "$(__git_merge_strategies)" + __git_complete_strategy && return + + case "$cur" in + --*) + __gitcomp "$__git_merge_options" return esac + __gitcomp_nl "$(__git_refs)" +} + +_git_mergetool () +{ case "$cur" in - --strategy=*) - __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}" + --tool=*) + __gitcomp "$__git_mergetools_common tortoisemerge" "" "${cur##--tool=}" return ;; --*) - __gitcomp " - --no-commit --no-summary --squash --strategy - " + __gitcomp "--tool=" return + ;; esac - __gitcomp "$(__git_refs)" + COMPREPLY=() } _git_merge_base () { - __gitcomp "$(__git_refs)" + __gitcomp_nl "$(__git_refs)" +} + +_git_mv () +{ + case "$cur" in + --*) + __gitcomp "--dry-run" + return + ;; + esac + COMPREPLY=() } _git_name_rev () @@ -680,126 +1400,280 @@ _git_name_rev () __gitcomp "--tags --all --stdin" } -_git_pull () +_git_notes () { - local cur="${COMP_WORDS[COMP_CWORD]}" + local subcommands='add append copy edit list prune remove show' + local subcommand="$(__git_find_on_cmdline "$subcommands")" - case "${COMP_WORDS[0]},$COMP_CWORD" in - git-pull*,1) - __gitcomp "$(__git_remotes)" + case "$subcommand,$cur" in + ,--*) + __gitcomp '--ref' + ;; + ,*) + case "$prev" in + --ref) + __gitcomp_nl "$(__git_refs)" + ;; + *) + __gitcomp "$subcommands --ref" + ;; + esac + ;; + add,--reuse-message=*|append,--reuse-message=*|\ + add,--reedit-message=*|append,--reedit-message=*) + __gitcomp_nl "$(__git_refs)" "" "${cur#*=}" + ;; + add,--*|append,--*) + __gitcomp '--file= --message= --reedit-message= + --reuse-message=' ;; - git,2) - __gitcomp "$(__git_remotes)" + copy,--*) + __gitcomp '--stdin' + ;; + prune,--*) + __gitcomp '--dry-run --verbose' + ;; + prune,*) ;; *) - local remote - case "${COMP_WORDS[0]}" in - git-pull) remote="${COMP_WORDS[1]}" ;; - git) remote="${COMP_WORDS[2]}" ;; + case "$prev" in + -m|-F) + ;; + *) + __gitcomp_nl "$(__git_refs)" + ;; esac - __gitcomp "$(__git_refs "$remote")" ;; esac } -_git_push () +_git_pull () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_complete_strategy && return - case "${COMP_WORDS[0]},$COMP_CWORD" in - git-push*,1) - __gitcomp "$(__git_remotes)" + case "$cur" in + --*) + __gitcomp " + --rebase --no-rebase + $__git_merge_options + $__git_fetch_options + " + return ;; - git,2) - __gitcomp "$(__git_remotes)" + esac + __git_complete_remote_or_refspec +} + +_git_push () +{ + case "$prev" in + --repo) + __gitcomp_nl "$(__git_remotes)" + return + esac + case "$cur" in + --repo=*) + __gitcomp_nl "$(__git_remotes)" "" "${cur##--repo=}" + return ;; - *) - case "$cur" in - *:*) - local remote - case "${COMP_WORDS[0]}" in - git-push) remote="${COMP_WORDS[1]}" ;; - git) remote="${COMP_WORDS[2]}" ;; - esac - __gitcomp "$(__git_refs "$remote")" "" "${cur#*:}" - ;; - +*) - __gitcomp "$(__git_refs)" + "${cur#+}" - ;; - *) - __gitcomp "$(__git_refs)" - ;; - esac + --*) + __gitcomp " + --all --mirror --tags --dry-run --force --verbose + --receive-pack= --repo= --set-upstream + " + return ;; esac + __git_complete_remote_or_refspec } _git_rebase () { - local cur="${COMP_WORDS[COMP_CWORD]}" - if [ -d .dotest ] || [ -d .git/.dotest-merge ]; then + local dir="$(__gitdir)" + if [ -d "$dir"/rebase-apply ] || [ -d "$dir"/rebase-merge ]; then __gitcomp "--continue --skip --abort" return fi - case "${COMP_WORDS[COMP_CWORD-1]}" in - -s|--strategy) - __gitcomp "$(__git_merge_strategies)" + __git_complete_strategy && return + case "$cur" in + --whitespace=*) + __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}" + return + ;; + --*) + __gitcomp " + --onto --merge --strategy --interactive + --preserve-merges --stat --no-stat + --committer-date-is-author-date --ignore-date + --ignore-whitespace --whitespace= + --autosquash + " + return esac + __gitcomp_nl "$(__git_refs)" +} + +_git_reflog () +{ + local subcommands="show delete expire" + local subcommand="$(__git_find_on_cmdline "$subcommands")" + + if [ -z "$subcommand" ]; then + __gitcomp "$subcommands" + else + __gitcomp_nl "$(__git_refs)" + fi +} + +__git_send_email_confirm_options="always never auto cc compose" +__git_send_email_suppresscc_options="author self cc bodycc sob cccmd body all" + +_git_send_email () +{ case "$cur" in - --strategy=*) - __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}" + --confirm=*) + __gitcomp " + $__git_send_email_confirm_options + " "" "${cur##--confirm=}" + return + ;; + --suppress-cc=*) + __gitcomp " + $__git_send_email_suppresscc_options + " "" "${cur##--suppress-cc=}" + + return + ;; + --smtp-encryption=*) + __gitcomp "ssl tls" "" "${cur##--smtp-encryption=}" return ;; --*) - __gitcomp "--onto --merge --strategy" + __gitcomp "--annotate --bcc --cc --cc-cmd --chain-reply-to + --compose --confirm= --dry-run --envelope-sender + --from --identity + --in-reply-to --no-chain-reply-to --no-signed-off-by-cc + --no-suppress-from --no-thread --quiet + --signed-off-by-cc --smtp-pass --smtp-server + --smtp-server-port --smtp-encryption= --smtp-user + --subject --suppress-cc= --suppress-from --thread --to + --validate --no-validate" return + ;; esac - __gitcomp "$(__git_refs)" + COMPREPLY=() +} + +_git_stage () +{ + _git_add +} + +__git_config_get_set_variables () +{ + local prevword word config_file= c=$cword + while [ $c -gt 1 ]; do + word="${words[c]}" + case "$word" in + --global|--system|--file=*) + config_file="$word" + break + ;; + -f|--file) + config_file="$word $prevword" + break + ;; + esac + prevword=$word + c=$((--c)) + done + + git --git-dir="$(__gitdir)" config $config_file --list 2>/dev/null | + while read -r line + do + case "$line" in + *.*=*) + echo "${line/=*/}" + ;; + esac + done } _git_config () { - local cur="${COMP_WORDS[COMP_CWORD]}" - local prv="${COMP_WORDS[COMP_CWORD-1]}" - case "$prv" in + case "$prev" in branch.*.remote) - __gitcomp "$(__git_remotes)" + __gitcomp_nl "$(__git_remotes)" return ;; branch.*.merge) - __gitcomp "$(__git_refs)" + __gitcomp_nl "$(__git_refs)" return ;; remote.*.fetch) - local remote="${prv#remote.}" + local remote="${prev#remote.}" remote="${remote%.fetch}" - __gitcomp "$(__git_refs_remotes "$remote")" + if [ -z "$cur" ]; then + COMPREPLY=("refs/heads/") + return + fi + __gitcomp_nl "$(__git_refs_remotes "$remote")" return ;; remote.*.push) - local remote="${prv#remote.}" + local remote="${prev#remote.}" remote="${remote%.push}" - __gitcomp "$(git --git-dir="$(__gitdir)" \ + __gitcomp_nl "$(git --git-dir="$(__gitdir)" \ for-each-ref --format='%(refname):%(refname)' \ refs/heads)" return ;; pull.twohead|pull.octopus) - __gitcomp "$(__git_merge_strategies)" + __git_compute_merge_strategies + __gitcomp "$__git_merge_strategies" return ;; - color.branch|color.diff|color.status) + color.branch|color.diff|color.interactive|\ + color.showbranch|color.status|color.ui) __gitcomp "always never auto" return ;; + color.pager) + __gitcomp "false true" + return + ;; color.*.*) __gitcomp " - black red green yellow blue magenta cyan white + normal black red green yellow blue magenta cyan white bold dim ul blink reverse " return ;; + help.format) + __gitcomp "man info web html" + return + ;; + log.date) + __gitcomp "$__git_log_date_formats" + return + ;; + sendemail.aliasesfiletype) + __gitcomp "mutt mailrc pine elm gnus" + return + ;; + sendemail.confirm) + __gitcomp "$__git_send_email_confirm_options" + return + ;; + sendemail.suppresscc) + __gitcomp "$__git_send_email_suppresscc_options" + return + ;; + --get|--get-all|--unset|--unset-all) + __gitcomp_nl "$(__git_config_get_set_variables)" + return + ;; *.*) COMPREPLY=() return @@ -817,149 +1691,366 @@ _git_config () return ;; branch.*.*) - local pfx="${cur%.*}." - cur="${cur##*.}" - __gitcomp "remote merge" "$pfx" "$cur" + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp "remote merge mergeoptions rebase" "$pfx" "$cur_" return ;; branch.*) - local pfx="${cur%.*}." - cur="${cur#*.}" - __gitcomp "$(__git_heads)" "$pfx" "$cur" "." + local pfx="${cur%.*}." cur_="${cur#*.}" + __gitcomp_nl "$(__git_heads)" "$pfx" "$cur_" "." + return + ;; + guitool.*.*) + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp " + argprompt cmd confirm needsfile noconsole norescan + prompt revprompt revunmerged title + " "$pfx" "$cur_" + return + ;; + difftool.*.*) + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp "cmd path" "$pfx" "$cur_" + return + ;; + man.*.*) + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp "cmd path" "$pfx" "$cur_" + return + ;; + mergetool.*.*) + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp "cmd path trustExitCode" "$pfx" "$cur_" + return + ;; + pager.*) + local pfx="${cur%.*}." cur_="${cur#*.}" + __git_compute_all_commands + __gitcomp_nl "$__git_all_commands" "$pfx" "$cur_" return ;; remote.*.*) - local pfx="${cur%.*}." - cur="${cur##*.}" + local pfx="${cur%.*}." cur_="${cur##*.}" __gitcomp " - url fetch push skipDefaultUpdate - receivepack uploadpack tagopt - " "$pfx" "$cur" + url proxy fetch push mirror skipDefaultUpdate + receivepack uploadpack tagopt pushurl + " "$pfx" "$cur_" return ;; remote.*) - local pfx="${cur%.*}." - cur="${cur#*.}" - __gitcomp "$(__git_remotes)" "$pfx" "$cur" "." + local pfx="${cur%.*}." cur_="${cur#*.}" + __gitcomp_nl "$(__git_remotes)" "$pfx" "$cur_" "." + return + ;; + url.*.*) + local pfx="${cur%.*}." cur_="${cur##*.}" + __gitcomp "insteadOf pushInsteadOf" "$pfx" "$cur_" return ;; esac __gitcomp " + add.ignoreErrors + advice.commitBeforeMerge + advice.detachedHead + advice.implicitIdentity + advice.pushNonFastForward + advice.resolveConflict + advice.statusHints + alias. + am.keepcr + apply.ignorewhitespace apply.whitespace - core.fileMode - core.gitProxy - core.ignoreStat - core.preferSymlinkRefs - core.logAllRefUpdates - core.loosecompression - core.repositoryFormatVersion - core.sharedRepository - core.warnAmbiguousRefs - core.compression - core.legacyHeaders - core.packedGitWindowSize - core.packedGitLimit + branch.autosetupmerge + branch.autosetuprebase + browser. clean.requireForce color.branch color.branch.current color.branch.local - color.branch.remote color.branch.plain + color.branch.remote + color.decorate.HEAD + color.decorate.branch + color.decorate.remoteBranch + color.decorate.stash + color.decorate.tag color.diff - color.diff.plain - color.diff.meta + color.diff.commit color.diff.frag - color.diff.old + color.diff.func + color.diff.meta color.diff.new - color.diff.commit + color.diff.old + color.diff.plain color.diff.whitespace + color.grep + color.grep.context + color.grep.filename + color.grep.function + color.grep.linenumber + color.grep.match + color.grep.selected + color.grep.separator + color.interactive + color.interactive.error + color.interactive.header + color.interactive.help + color.interactive.prompt color.pager + color.showbranch color.status - color.status.header color.status.added color.status.changed + color.status.header + color.status.nobranch color.status.untracked + color.status.updated + color.ui + commit.status + commit.template + core.abbrev + core.askpass + core.attributesfile + core.autocrlf + core.bare + core.bigFileThreshold + core.compression + core.createObject + core.deltaBaseCacheLimit + core.editor + core.eol + core.excludesfile + core.fileMode + core.fsyncobjectfiles + core.gitProxy + core.ignoreCygwinFSTricks + core.ignoreStat + core.ignorecase + core.logAllRefUpdates + core.loosecompression + core.notesRef + core.packedGitLimit + core.packedGitWindowSize + core.pager + core.preferSymlinkRefs + core.preloadindex + core.quotepath + core.repositoryFormatVersion + core.safecrlf + core.sharedRepository + core.sparseCheckout + core.symlinks + core.trustctime + core.warnAmbiguousRefs + core.whitespace + core.worktree + diff.autorefreshindex + diff.statGraphWidth + diff.external + diff.ignoreSubmodules + diff.mnemonicprefix + diff.noprefix diff.renameLimit diff.renames + diff.suppressBlankEmpty + diff.tool + diff.wordRegex + difftool. + difftool.prompt + fetch.recurseSubmodules fetch.unpackLimit + format.attach + format.cc format.headers + format.numbered + format.pretty + format.signature + format.signoff format.subjectprefix - gitcvs.enabled - gitcvs.logfile - gitcvs.allbinary - gitcvs.dbname gitcvs.dbdriver gitcvs.dbuser gitcvs.dvpass + format.suffix + format.thread + format.to + gc. + gc.aggressiveWindow + gc.auto + gc.autopacklimit gc.packrefs + gc.pruneexpire gc.reflogexpire gc.reflogexpireunreachable gc.rerereresolved gc.rerereunresolved - http.sslVerify - http.sslCert - http.sslKey - http.sslCAInfo - http.sslCAPath - http.maxRequests + gitcvs.allbinary + gitcvs.commitmsgannotation + gitcvs.dbTableNamePrefix + gitcvs.dbdriver + gitcvs.dbname + gitcvs.dbpass + gitcvs.dbuser + gitcvs.enabled + gitcvs.logfile + gitcvs.usecrlfattr + guitool. + gui.blamehistoryctx + gui.commitmsgwidth + gui.copyblamethreshold + gui.diffcontext + gui.encoding + gui.fastcopyblame + gui.matchtrackingbranch + gui.newbranchtemplate + gui.pruneduringfetch + gui.spellingdictionary + gui.trustmtime + help.autocorrect + help.browser + help.format http.lowSpeedLimit http.lowSpeedTime + http.maxRequests + http.minSessions http.noEPSV + http.postBuffer + http.proxy + http.sslCAInfo + http.sslCAPath + http.sslCert + http.sslCertPasswordProtected + http.sslKey + http.sslVerify + http.useragent i18n.commitEncoding i18n.logOutputEncoding + imap.authMethod + imap.folder + imap.host + imap.pass + imap.port + imap.preformattedHTML + imap.sslverify + imap.tunnel + imap.user + init.templatedir + instaweb.browser + instaweb.httpd + instaweb.local + instaweb.modulepath + instaweb.port + interactive.singlekey + log.date + log.decorate log.showroot + mailmap.file + man. + man.viewer + merge. + merge.conflictstyle + merge.log + merge.renameLimit + merge.renormalize + merge.stat merge.tool - merge.summary merge.verbosity - pack.window - pack.depth - pack.windowMemory + mergetool. + mergetool.keepBackup + mergetool.keepTemporaries + mergetool.prompt + notes.displayRef + notes.rewrite. + notes.rewrite.amend + notes.rewrite.rebase + notes.rewriteMode + notes.rewriteRef pack.compression - pack.deltaCacheSize pack.deltaCacheLimit + pack.deltaCacheSize + pack.depth + pack.indexVersion + pack.packSizeLimit + pack.threads + pack.window + pack.windowMemory + pager. + pretty. pull.octopus pull.twohead - repack.useDeltaBaseOffset - show.difftree + push.default + rebase.autosquash + rebase.stat + receive.autogc + receive.denyCurrentBranch + receive.denyDeleteCurrent + receive.denyDeletes + receive.denyNonFastForwards + receive.fsckObjects + receive.unpackLimit + receive.updateserverinfo + remotes. + repack.usedeltabaseoffset + rerere.autoupdate + rerere.enabled + sendemail. + sendemail.aliasesfile + sendemail.aliasfiletype + sendemail.bcc + sendemail.cc + sendemail.cccmd + sendemail.chainreplyto + sendemail.confirm + sendemail.envelopesender + sendemail.from + sendemail.identity + sendemail.multiedit + sendemail.signedoffbycc + sendemail.smtpdomain + sendemail.smtpencryption + sendemail.smtppass + sendemail.smtpserver + sendemail.smtpserveroption + sendemail.smtpserverport + sendemail.smtpuser + sendemail.suppresscc + sendemail.suppressfrom + sendemail.thread + sendemail.to + sendemail.validate showbranch.default + status.relativePaths + status.showUntrackedFiles + status.submodulesummary + submodule. tar.umask transfer.unpackLimit - receive.unpackLimit - receive.denyNonFastForwards - user.name + url. user.email + user.name user.signingkey - whatchanged.difftree + web.browser branch. remote. " } _git_remote () { - local i c=1 command - while [ $c -lt $COMP_CWORD ]; do - i="${COMP_WORDS[c]}" - case "$i" in - add|show|prune|update) command="$i"; break ;; - esac - c=$((++c)) - done - - if [ $c -eq $COMP_CWORD -a -z "$command" ]; then - __gitcomp "add show prune update" + local subcommands="add rename rm set-head set-branches set-url show prune update" + local subcommand="$(__git_find_on_cmdline "$subcommands")" + if [ -z "$subcommand" ]; then + __gitcomp "$subcommands" return fi - case "$command" in - show|prune) - __gitcomp "$(__git_remotes)" + case "$subcommand" in + rename|rm|set-url|show|prune) + __gitcomp_nl "$(__git_remotes)" + ;; + set-head|set-branches) + __git_complete_remote_or_refspec ;; update) local i c='' IFS=$'\n' - for i in $(git --git-dir="$(__gitdir)" config --list); do - case "$i" in - remotes.*) - i="${i#remotes.}" - c="$c ${i/=*/}" - ;; - esac + for i in $(git --git-dir="$(__gitdir)" config --get-regexp "remotes\..*" 2>/dev/null); do + i="${i#remotes.}" + c="$c ${i/ */}" done __gitcomp "$c" ;; @@ -969,30 +2060,57 @@ _git_remote () esac } +_git_replace () +{ + __gitcomp_nl "$(__git_refs)" +} + _git_reset () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + case "$cur" in --*) - __gitcomp "--mixed --hard --soft" + __gitcomp "--merge --mixed --hard --soft --patch" return ;; esac - __gitcomp "$(__git_refs)" + __gitcomp_nl "$(__git_refs)" +} + +_git_revert () +{ + case "$cur" in + --*) + __gitcomp "--edit --mainline --no-edit --no-commit --signoff" + return + ;; + esac + __gitcomp_nl "$(__git_refs)" +} + +_git_rm () +{ + __git_has_doubledash && return + + case "$cur" in + --*) + __gitcomp "--cached --dry-run --ignore-unmatch --quiet" + return + ;; + esac + COMPREPLY=() } _git_shortlog () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + case "$cur" in --*) __gitcomp " - --max-count= --max-age= --since= --after= - --min-age= --before= --until= - --no-merges - --author= --committer= --grep= - --all-match - --not --all + $__git_log_common_options + $__git_log_shortlog_options --numbered --summary " return @@ -1003,193 +2121,358 @@ _git_shortlog () _git_show () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + case "$cur" in - --pretty=*) - __gitcomp " - oneline short medium full fuller email raw - " "" "${cur##--pretty=}" + --pretty=*|--format=*) + __gitcomp "$__git_log_pretty_formats $(__git_pretty_aliases) + " "" "${cur#*=}" return ;; --*) - __gitcomp "--pretty=" + __gitcomp "--pretty= --format= --abbrev-commit --oneline + $__git_diff_common_options + " return ;; esac __git_complete_file } +_git_show_branch () +{ + case "$cur" in + --*) + __gitcomp " + --all --remotes --topo-order --current --more= + --list --independent --merge-base --no-name + --color --no-color + --sha1-name --sparse --topics --reflog + " + return + ;; + esac + __git_complete_revlist +} + _git_stash () { - __gitcomp 'list show apply clear' + local save_opts='--keep-index --no-keep-index --quiet --patch' + local subcommands='save list show apply clear drop pop create branch' + local subcommand="$(__git_find_on_cmdline "$subcommands")" + if [ -z "$subcommand" ]; then + case "$cur" in + --*) + __gitcomp "$save_opts" + ;; + *) + if [ -z "$(__git_find_on_cmdline "$save_opts")" ]; then + __gitcomp "$subcommands" + else + COMPREPLY=() + fi + ;; + esac + else + case "$subcommand,$cur" in + save,--*) + __gitcomp "$save_opts" + ;; + apply,--*|pop,--*) + __gitcomp "--index --quiet" + ;; + show,--*|drop,--*|branch,--*) + COMPREPLY=() + ;; + show,*|apply,*|drop,*|pop,*|branch,*) + __gitcomp_nl "$(git --git-dir="$(__gitdir)" stash list \ + | sed -n -e 's/:.*//p')" + ;; + *) + COMPREPLY=() + ;; + esac + fi } _git_submodule () { - local i c=1 command - while [ $c -lt $COMP_CWORD ]; do - i="${COMP_WORDS[c]}" - case "$i" in - add|status|init|update) command="$i"; break ;; - esac - c=$((++c)) - done + __git_has_doubledash && return - if [ $c -eq $COMP_CWORD -a -z "$command" ]; then - local cur="${COMP_WORDS[COMP_CWORD]}" + local subcommands="add status init update summary foreach sync" + if [ -z "$(__git_find_on_cmdline "$subcommands")" ]; then case "$cur" in --*) __gitcomp "--quiet --cached" ;; *) - __gitcomp "add status init update" + __gitcomp "$subcommands" ;; esac return fi } -_git () +_git_svn () +{ + local subcommands=" + init fetch clone rebase dcommit log find-rev + set-tree commit-diff info create-ignore propget + proplist show-ignore show-externals branch tag blame + migrate mkdirs reset gc + " + local subcommand="$(__git_find_on_cmdline "$subcommands")" + if [ -z "$subcommand" ]; then + __gitcomp "$subcommands" + else + local remote_opts="--username= --config-dir= --no-auth-cache" + local fc_opts=" + --follow-parent --authors-file= --repack= + --no-metadata --use-svm-props --use-svnsync-props + --log-window-size= --no-checkout --quiet + --repack-flags --use-log-author --localtime + --ignore-paths= $remote_opts + " + local init_opts=" + --template= --shared= --trunk= --tags= + --branches= --stdlayout --minimize-url + --no-metadata --use-svm-props --use-svnsync-props + --rewrite-root= --prefix= --use-log-author + --add-author-from $remote_opts + " + local cmt_opts=" + --edit --rmdir --find-copies-harder --copy-similarity= + " + + case "$subcommand,$cur" in + fetch,--*) + __gitcomp "--revision= --fetch-all $fc_opts" + ;; + clone,--*) + __gitcomp "--revision= $fc_opts $init_opts" + ;; + init,--*) + __gitcomp "$init_opts" + ;; + dcommit,--*) + __gitcomp " + --merge --strategy= --verbose --dry-run + --fetch-all --no-rebase --commit-url + --revision --interactive $cmt_opts $fc_opts + " + ;; + set-tree,--*) + __gitcomp "--stdin $cmt_opts $fc_opts" + ;; + create-ignore,--*|propget,--*|proplist,--*|show-ignore,--*|\ + show-externals,--*|mkdirs,--*) + __gitcomp "--revision=" + ;; + log,--*) + __gitcomp " + --limit= --revision= --verbose --incremental + --oneline --show-commit --non-recursive + --authors-file= --color + " + ;; + rebase,--*) + __gitcomp " + --merge --verbose --strategy= --local + --fetch-all --dry-run $fc_opts + " + ;; + commit-diff,--*) + __gitcomp "--message= --file= --revision= $cmt_opts" + ;; + info,--*) + __gitcomp "--url" + ;; + branch,--*) + __gitcomp "--dry-run --message --tag" + ;; + tag,--*) + __gitcomp "--dry-run --message" + ;; + blame,--*) + __gitcomp "--git-format" + ;; + migrate,--*) + __gitcomp " + --config-dir= --ignore-paths= --minimize + --no-auth-cache --username= + " + ;; + reset,--*) + __gitcomp "--revision= --parent" + ;; + *) + COMPREPLY=() + ;; + esac + fi +} + +_git_tag () +{ + local i c=1 f=0 + while [ $c -lt $cword ]; do + i="${words[c]}" + case "$i" in + -d|-v) + __gitcomp_nl "$(__git_tags)" + return + ;; + -f) + f=1 + ;; + esac + ((c++)) + done + + case "$prev" in + -m|-F) + COMPREPLY=() + ;; + -*|tag) + if [ $f = 1 ]; then + __gitcomp_nl "$(__git_tags)" + else + COMPREPLY=() + fi + ;; + *) + __gitcomp_nl "$(__git_refs)" + ;; + esac +} + +_git_whatchanged () +{ + _git_log +} + +__git_main () { local i c=1 command __git_dir - while [ $c -lt $COMP_CWORD ]; do - i="${COMP_WORDS[c]}" + while [ $c -lt $cword ]; do + i="${words[c]}" case "$i" in --git-dir=*) __git_dir="${i#--git-dir=}" ;; --bare) __git_dir="." ;; - --version|--help|-p|--paginate) ;; + --help) command="help"; break ;; + -c) c=$((++c)) ;; + -*) ;; *) command="$i"; break ;; esac - c=$((++c)) + ((c++)) done - if [ $c -eq $COMP_CWORD -a -z "$command" ]; then - case "${COMP_WORDS[COMP_CWORD]}" in - --*=*) COMPREPLY=() ;; + if [ -z "$command" ]; then + case "$cur" in --*) __gitcomp " + --paginate --no-pager --git-dir= --bare --version --exec-path + --exec-path= + --html-path + --info-path + --work-tree= + --namespace= + --no-replace-objects + --help " ;; - *) __gitcomp "$(__git_commands) $(__git_aliases)" ;; + *) __git_compute_porcelain_commands + __gitcomp "$__git_porcelain_commands $(__git_aliases)" ;; esac return fi + local completion_func="_git_${command//-/_}" + declare -f $completion_func >/dev/null && $completion_func && return + local expansion=$(__git_aliased_command "$command") - [ "$expansion" ] && command="$expansion" - - case "$command" in - am) _git_am ;; - add) _git_add ;; - apply) _git_apply ;; - bisect) _git_bisect ;; - bundle) _git_bundle ;; - branch) _git_branch ;; - checkout) _git_checkout ;; - cherry) _git_cherry ;; - cherry-pick) _git_cherry_pick ;; - commit) _git_commit ;; - config) _git_config ;; - describe) _git_describe ;; - diff) _git_diff ;; - fetch) _git_fetch ;; - format-patch) _git_format_patch ;; - gc) _git_gc ;; - log) _git_log ;; - ls-remote) _git_ls_remote ;; - ls-tree) _git_ls_tree ;; - merge) _git_merge;; - merge-base) _git_merge_base ;; - name-rev) _git_name_rev ;; - pull) _git_pull ;; - push) _git_push ;; - rebase) _git_rebase ;; - remote) _git_remote ;; - reset) _git_reset ;; - shortlog) _git_shortlog ;; - show) _git_show ;; - show-branch) _git_log ;; - stash) _git_stash ;; - submodule) _git_submodule ;; - whatchanged) _git_log ;; - *) COMPREPLY=() ;; - esac + if [ -n "$expansion" ]; then + completion_func="_git_${expansion//-/_}" + declare -f $completion_func >/dev/null && $completion_func + fi } -_gitk () +__gitk_main () { - local cur="${COMP_WORDS[COMP_CWORD]}" + __git_has_doubledash && return + + local g="$(__gitdir)" + local merge="" + if [ -f "$g/MERGE_HEAD" ]; then + merge="--merge" + fi case "$cur" in --*) - __gitcomp "--not --all" + __gitcomp " + $__git_log_common_options + $__git_log_gitk_options + $merge + " return ;; esac __git_complete_revlist } -complete -o default -o nospace -F _git git -complete -o default -o nospace -F _gitk gitk -complete -o default -o nospace -F _git_am git-am -complete -o default -o nospace -F _git_apply git-apply -complete -o default -o nospace -F _git_bisect git-bisect -complete -o default -o nospace -F _git_branch git-branch -complete -o default -o nospace -F _git_bundle git-bundle -complete -o default -o nospace -F _git_checkout git-checkout -complete -o default -o nospace -F _git_cherry git-cherry -complete -o default -o nospace -F _git_cherry_pick git-cherry-pick -complete -o default -o nospace -F _git_commit git-commit -complete -o default -o nospace -F _git_describe git-describe -complete -o default -o nospace -F _git_diff git-diff -complete -o default -o nospace -F _git_fetch git-fetch -complete -o default -o nospace -F _git_format_patch git-format-patch -complete -o default -o nospace -F _git_gc git-gc -complete -o default -o nospace -F _git_log git-log -complete -o default -o nospace -F _git_ls_remote git-ls-remote -complete -o default -o nospace -F _git_ls_tree git-ls-tree -complete -o default -o nospace -F _git_merge git-merge -complete -o default -o nospace -F _git_merge_base git-merge-base -complete -o default -o nospace -F _git_name_rev git-name-rev -complete -o default -o nospace -F _git_pull git-pull -complete -o default -o nospace -F _git_push git-push -complete -o default -o nospace -F _git_rebase git-rebase -complete -o default -o nospace -F _git_config git-config -complete -o default -o nospace -F _git_remote git-remote -complete -o default -o nospace -F _git_reset git-reset -complete -o default -o nospace -F _git_shortlog git-shortlog -complete -o default -o nospace -F _git_show git-show -complete -o default -o nospace -F _git_stash git-stash -complete -o default -o nospace -F _git_submodule git-submodule -complete -o default -o nospace -F _git_log git-show-branch -complete -o default -o nospace -F _git_log git-whatchanged +__git_func_wrap () +{ + if [[ -n ${ZSH_VERSION-} ]]; then + emulate -L bash + setopt KSH_TYPESET + + # workaround zsh's bug that leaves 'words' as a special + # variable in versions < 4.3.12 + typeset -h words + + # workaround zsh's bug that quotes spaces in the COMPREPLY + # array if IFS doesn't contain spaces. + typeset -h IFS + fi + local cur words cword prev + _get_comp_words_by_ref -n =: cur words cword prev + $1 +} + +# Setup completion for certain functions defined above by setting common +# variables and workarounds. +# This is NOT a public function; use at your own risk. +__git_complete () +{ + local wrapper="__git_wrap${2}" + eval "$wrapper () { __git_func_wrap $2 ; }" + complete -o bashdefault -o default -o nospace -F $wrapper $1 2>/dev/null \ + || complete -o default -o nospace -F $wrapper $1 +} + +# wrapper for backwards compatibility +_git () +{ + __git_wrap__git_main +} + +# wrapper for backwards compatibility +_gitk () +{ + __git_wrap__gitk_main +} + +__git_complete git __git_main +__git_complete gitk __gitk_main # The following are necessary only for Cygwin, and only are needed # when the user has tab-completed the executable name and consequently # included the '.exe' suffix. # if [ Cygwin = "$(uname -o 2>/dev/null)" ]; then -complete -o default -o nospace -F _git_add git-add.exe -complete -o default -o nospace -F _git_apply git-apply.exe -complete -o default -o nospace -F _git git.exe -complete -o default -o nospace -F _git_branch git-branch.exe -complete -o default -o nospace -F _git_bundle git-bundle.exe -complete -o default -o nospace -F _git_cherry git-cherry.exe -complete -o default -o nospace -F _git_describe git-describe.exe -complete -o default -o nospace -F _git_diff git-diff.exe -complete -o default -o nospace -F _git_format_patch git-format-patch.exe -complete -o default -o nospace -F _git_log git-log.exe -complete -o default -o nospace -F _git_ls_tree git-ls-tree.exe -complete -o default -o nospace -F _git_merge_base git-merge-base.exe -complete -o default -o nospace -F _git_name_rev git-name-rev.exe -complete -o default -o nospace -F _git_push git-push.exe -complete -o default -o nospace -F _git_config git-config -complete -o default -o nospace -F _git_shortlog git-shortlog.exe -complete -o default -o nospace -F _git_show git-show.exe -complete -o default -o nospace -F _git_log git-show-branch.exe -complete -o default -o nospace -F _git_log git-whatchanged.exe +__git_complete git.exe __git_main fi diff --git a/contrib/completion/git-prompt.sh b/contrib/completion/git-prompt.sh new file mode 100644 index 0000000000..29b1ec9eb1 --- /dev/null +++ b/contrib/completion/git-prompt.sh @@ -0,0 +1,289 @@ +# bash/zsh git prompt support +# +# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org> +# Distributed under the GNU General Public License, version 2.0. +# +# This script allows you to see the current branch in your prompt. +# +# To enable: +# +# 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh). +# 2) Add the following line to your .bashrc/.zshrc: +# source ~/.git-prompt.sh +# 3) Change your PS1 to also show the current branch: +# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' +# ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' +# +# The argument to __git_ps1 will be displayed only if you are currently +# in a git repository. The %s token will be the name of the current +# branch. +# +# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value, +# unstaged (*) and staged (+) changes will be shown next to the branch +# name. You can configure this per-repository with the +# bash.showDirtyState variable, which defaults to true once +# GIT_PS1_SHOWDIRTYSTATE is enabled. +# +# You can also see if currently something is stashed, by setting +# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed, +# then a '$' will be shown next to the branch name. +# +# If you would like to see if there're untracked files, then you can set +# GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're untracked +# files, then a '%' will be shown next to the branch name. +# +# If you would like to see the difference between HEAD and its upstream, +# set GIT_PS1_SHOWUPSTREAM="auto". A "<" indicates you are behind, ">" +# indicates you are ahead, and "<>" indicates you have diverged. You +# can further control behaviour by setting GIT_PS1_SHOWUPSTREAM to a +# space-separated list of values: +# +# verbose show number of commits ahead/behind (+/-) upstream +# legacy don't use the '--count' option available in recent +# versions of git-rev-list +# git always compare HEAD to @{upstream} +# svn always compare HEAD to your SVN upstream +# +# By default, __git_ps1 will compare HEAD to your SVN upstream if it can +# find one, or @{upstream} otherwise. Once you have set +# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by +# setting the bash.showUpstream config variable. + +# __gitdir accepts 0 or 1 arguments (i.e., location) +# returns location of .git repo +__gitdir () +{ + # Note: this function is duplicated in git-completion.bash + # When updating it, make sure you update the other one to match. + if [ -z "${1-}" ]; then + if [ -n "${__git_dir-}" ]; then + echo "$__git_dir" + elif [ -n "${GIT_DIR-}" ]; then + test -d "${GIT_DIR-}" || return 1 + echo "$GIT_DIR" + elif [ -d .git ]; then + echo .git + else + git rev-parse --git-dir 2>/dev/null + fi + elif [ -d "$1/.git" ]; then + echo "$1/.git" + else + echo "$1" + fi +} + +# stores the divergence from upstream in $p +# used by GIT_PS1_SHOWUPSTREAM +__git_ps1_show_upstream () +{ + local key value + local svn_remote svn_url_pattern count n + local upstream=git legacy="" verbose="" + + svn_remote=() + # get some config options from git-config + local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')" + while read -r key value; do + case "$key" in + bash.showupstream) + GIT_PS1_SHOWUPSTREAM="$value" + if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then + p="" + return + fi + ;; + svn-remote.*.url) + svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value" + svn_url_pattern+="\\|$value" + upstream=svn+git # default upstream is SVN if available, else git + ;; + esac + done <<< "$output" + + # parse configuration values + for option in ${GIT_PS1_SHOWUPSTREAM}; do + case "$option" in + git|svn) upstream="$option" ;; + verbose) verbose=1 ;; + legacy) legacy=1 ;; + esac + done + + # Find our upstream + case "$upstream" in + git) upstream="@{upstream}" ;; + svn*) + # get the upstream from the "git-svn-id: ..." in a commit message + # (git-svn uses essentially the same procedure internally) + local svn_upstream=($(git log --first-parent -1 \ + --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null)) + if [[ 0 -ne ${#svn_upstream[@]} ]]; then + svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]} + svn_upstream=${svn_upstream%@*} + local n_stop="${#svn_remote[@]}" + for ((n=1; n <= n_stop; n++)); do + svn_upstream=${svn_upstream#${svn_remote[$n]}} + done + + if [[ -z "$svn_upstream" ]]; then + # default branch name for checkouts with no layout: + upstream=${GIT_SVN_ID:-git-svn} + else + upstream=${svn_upstream#/} + fi + elif [[ "svn+git" = "$upstream" ]]; then + upstream="@{upstream}" + fi + ;; + esac + + # Find how many commits we are ahead/behind our upstream + if [[ -z "$legacy" ]]; then + count="$(git rev-list --count --left-right \ + "$upstream"...HEAD 2>/dev/null)" + else + # produce equivalent output to --count for older versions of git + local commits + if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)" + then + local commit behind=0 ahead=0 + for commit in $commits + do + case "$commit" in + "<"*) ((behind++)) ;; + *) ((ahead++)) ;; + esac + done + count="$behind $ahead" + else + count="" + fi + fi + + # calculate the result + if [[ -z "$verbose" ]]; then + case "$count" in + "") # no upstream + p="" ;; + "0 0") # equal to upstream + p="=" ;; + "0 "*) # ahead of upstream + p=">" ;; + *" 0") # behind upstream + p="<" ;; + *) # diverged from upstream + p="<>" ;; + esac + else + case "$count" in + "") # no upstream + p="" ;; + "0 0") # equal to upstream + p=" u=" ;; + "0 "*) # ahead of upstream + p=" u+${count#0 }" ;; + *" 0") # behind upstream + p=" u-${count% 0}" ;; + *) # diverged from upstream + p=" u+${count#* }-${count% *}" ;; + esac + fi + +} + + +# __git_ps1 accepts 0 or 1 arguments (i.e., format string) +# returns text to add to bash PS1 prompt (includes branch name) +__git_ps1 () +{ + local g="$(__gitdir)" + if [ -n "$g" ]; then + local r="" + local b="" + if [ -f "$g/rebase-merge/interactive" ]; then + r="|REBASE-i" + b="$(cat "$g/rebase-merge/head-name")" + elif [ -d "$g/rebase-merge" ]; then + r="|REBASE-m" + b="$(cat "$g/rebase-merge/head-name")" + else + if [ -d "$g/rebase-apply" ]; then + if [ -f "$g/rebase-apply/rebasing" ]; then + r="|REBASE" + elif [ -f "$g/rebase-apply/applying" ]; then + r="|AM" + else + r="|AM/REBASE" + fi + elif [ -f "$g/MERGE_HEAD" ]; then + r="|MERGING" + elif [ -f "$g/CHERRY_PICK_HEAD" ]; then + r="|CHERRY-PICKING" + elif [ -f "$g/BISECT_LOG" ]; then + r="|BISECTING" + fi + + b="$(git symbolic-ref HEAD 2>/dev/null)" || { + + b="$( + case "${GIT_PS1_DESCRIBE_STYLE-}" in + (contains) + git describe --contains HEAD ;; + (branch) + git describe --contains --all HEAD ;; + (describe) + git describe HEAD ;; + (* | default) + git describe --tags --exact-match HEAD ;; + esac 2>/dev/null)" || + + b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." || + b="unknown" + b="($b)" + } + fi + + local w="" + local i="" + local s="" + local u="" + local c="" + local p="" + + if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then + if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then + c="BARE:" + else + b="GIT_DIR!" + fi + elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then + if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ]; then + if [ "$(git config --bool bash.showDirtyState)" != "false" ]; then + git diff --no-ext-diff --quiet --exit-code || w="*" + if git rev-parse --quiet --verify HEAD >/dev/null; then + git diff-index --cached --quiet HEAD -- || i="+" + else + i="#" + fi + fi + fi + if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then + git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$" + fi + + if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then + if [ -n "$(git ls-files --others --exclude-standard)" ]; then + u="%" + fi + fi + + if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then + __git_ps1_show_upstream + fi + fi + + local f="$w$i$s$u" + printf -- "${1:- (%s)}" "$c${b##refs/heads/}${f:+ $f}$r$p" + fi +} diff --git a/contrib/convert-objects/convert-objects.c b/contrib/convert-objects/convert-objects.c new file mode 100644 index 0000000000..f3b57bf1d2 --- /dev/null +++ b/contrib/convert-objects/convert-objects.c @@ -0,0 +1,329 @@ +#include "cache.h" +#include "blob.h" +#include "commit.h" +#include "tree.h" + +struct entry { + unsigned char old_sha1[20]; + unsigned char new_sha1[20]; + int converted; +}; + +#define MAXOBJECTS (1000000) + +static struct entry *convert[MAXOBJECTS]; +static int nr_convert; + +static struct entry * convert_entry(unsigned char *sha1); + +static struct entry *insert_new(unsigned char *sha1, int pos) +{ + struct entry *new = xcalloc(1, sizeof(struct entry)); + hashcpy(new->old_sha1, sha1); + memmove(convert + pos + 1, convert + pos, (nr_convert - pos) * sizeof(struct entry *)); + convert[pos] = new; + nr_convert++; + if (nr_convert == MAXOBJECTS) + die("you're kidding me - hit maximum object limit"); + return new; +} + +static struct entry *lookup_entry(unsigned char *sha1) +{ + int low = 0, high = nr_convert; + + while (low < high) { + int next = (low + high) / 2; + struct entry *n = convert[next]; + int cmp = hashcmp(sha1, n->old_sha1); + if (!cmp) + return n; + if (cmp < 0) { + high = next; + continue; + } + low = next+1; + } + return insert_new(sha1, low); +} + +static void convert_binary_sha1(void *buffer) +{ + struct entry *entry = convert_entry(buffer); + hashcpy(buffer, entry->new_sha1); +} + +static void convert_ascii_sha1(void *buffer) +{ + unsigned char sha1[20]; + struct entry *entry; + + if (get_sha1_hex(buffer, sha1)) + die("expected sha1, got '%s'", (char *) buffer); + entry = convert_entry(sha1); + memcpy(buffer, sha1_to_hex(entry->new_sha1), 40); +} + +static unsigned int convert_mode(unsigned int mode) +{ + unsigned int newmode; + + newmode = mode & S_IFMT; + if (S_ISREG(mode)) + newmode |= (mode & 0100) ? 0755 : 0644; + return newmode; +} + +static int write_subdirectory(void *buffer, unsigned long size, const char *base, int baselen, unsigned char *result_sha1) +{ + char *new = xmalloc(size); + unsigned long newlen = 0; + unsigned long used; + + used = 0; + while (size) { + int len = 21 + strlen(buffer); + char *path = strchr(buffer, ' '); + unsigned char *sha1; + unsigned int mode; + char *slash, *origpath; + + if (!path || strtoul_ui(buffer, 8, &mode)) + die("bad tree conversion"); + mode = convert_mode(mode); + path++; + if (memcmp(path, base, baselen)) + break; + origpath = path; + path += baselen; + slash = strchr(path, '/'); + if (!slash) { + newlen += sprintf(new + newlen, "%o %s", mode, path); + new[newlen++] = '\0'; + hashcpy((unsigned char *)new + newlen, (unsigned char *) buffer + len - 20); + newlen += 20; + + used += len; + size -= len; + buffer = (char *) buffer + len; + continue; + } + + newlen += sprintf(new + newlen, "%o %.*s", S_IFDIR, (int)(slash - path), path); + new[newlen++] = 0; + sha1 = (unsigned char *)(new + newlen); + newlen += 20; + + len = write_subdirectory(buffer, size, origpath, slash-origpath+1, sha1); + + used += len; + size -= len; + buffer = (char *) buffer + len; + } + + write_sha1_file(new, newlen, tree_type, result_sha1); + free(new); + return used; +} + +static void convert_tree(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + void *orig_buffer = buffer; + unsigned long orig_size = size; + + while (size) { + size_t len = 1+strlen(buffer); + + convert_binary_sha1((char *) buffer + len); + + len += 20; + if (len > size) + die("corrupt tree object"); + size -= len; + buffer = (char *) buffer + len; + } + + write_subdirectory(orig_buffer, orig_size, "", 0, result_sha1); +} + +static unsigned long parse_oldstyle_date(const char *buf) +{ + char c, *p; + char buffer[100]; + struct tm tm; + const char *formats[] = { + "%c", + "%a %b %d %T", + "%Z", + "%Y", + " %Y", + NULL + }; + /* We only ever did two timezones in the bad old format .. */ + const char *timezones[] = { + "PDT", "PST", "CEST", NULL + }; + const char **fmt = formats; + + p = buffer; + while (isspace(c = *buf)) + buf++; + while ((c = *buf++) != '\n') + *p++ = c; + *p++ = 0; + buf = buffer; + memset(&tm, 0, sizeof(tm)); + do { + const char *next = strptime(buf, *fmt, &tm); + if (next) { + if (!*next) + return mktime(&tm); + buf = next; + } else { + const char **p = timezones; + while (isspace(*buf)) + buf++; + while (*p) { + if (!memcmp(buf, *p, strlen(*p))) { + buf += strlen(*p); + break; + } + p++; + } + } + fmt++; + } while (*buf && *fmt); + printf("left: %s\n", buf); + return mktime(&tm); +} + +static int convert_date_line(char *dst, void **buf, unsigned long *sp) +{ + unsigned long size = *sp; + char *line = *buf; + char *next = strchr(line, '\n'); + char *date = strchr(line, '>'); + int len; + + if (!next || !date) + die("missing or bad author/committer line %s", line); + next++; date += 2; + + *buf = next; + *sp = size - (next - line); + + len = date - line; + memcpy(dst, line, len); + dst += len; + + /* Is it already in new format? */ + if (isdigit(*date)) { + int datelen = next - date; + memcpy(dst, date, datelen); + return len + datelen; + } + + /* + * Hacky hacky: one of the sparse old-style commits does not have + * any date at all, but we can fake it by using the committer date. + */ + if (*date == '\n' && strchr(next, '>')) + date = strchr(next, '>')+2; + + return len + sprintf(dst, "%lu -0700\n", parse_oldstyle_date(date)); +} + +static void convert_date(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + char *new = xmalloc(size + 100); + unsigned long newlen = 0; + + /* "tree <sha1>\n" */ + memcpy(new + newlen, buffer, 46); + newlen += 46; + buffer = (char *) buffer + 46; + size -= 46; + + /* "parent <sha1>\n" */ + while (!memcmp(buffer, "parent ", 7)) { + memcpy(new + newlen, buffer, 48); + newlen += 48; + buffer = (char *) buffer + 48; + size -= 48; + } + + /* "author xyz <xyz> date" */ + newlen += convert_date_line(new + newlen, &buffer, &size); + /* "committer xyz <xyz> date" */ + newlen += convert_date_line(new + newlen, &buffer, &size); + + /* Rest */ + memcpy(new + newlen, buffer, size); + newlen += size; + + write_sha1_file(new, newlen, commit_type, result_sha1); + free(new); +} + +static void convert_commit(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + void *orig_buffer = buffer; + unsigned long orig_size = size; + + if (memcmp(buffer, "tree ", 5)) + die("Bad commit '%s'", (char *) buffer); + convert_ascii_sha1((char *) buffer + 5); + buffer = (char *) buffer + 46; /* "tree " + "hex sha1" + "\n" */ + while (!memcmp(buffer, "parent ", 7)) { + convert_ascii_sha1((char *) buffer + 7); + buffer = (char *) buffer + 48; + } + convert_date(orig_buffer, orig_size, result_sha1); +} + +static struct entry * convert_entry(unsigned char *sha1) +{ + struct entry *entry = lookup_entry(sha1); + enum object_type type; + void *buffer, *data; + unsigned long size; + + if (entry->converted) + return entry; + data = read_sha1_file(sha1, &type, &size); + if (!data) + die("unable to read object %s", sha1_to_hex(sha1)); + + buffer = xmalloc(size); + memcpy(buffer, data, size); + + if (type == OBJ_BLOB) { + write_sha1_file(buffer, size, blob_type, entry->new_sha1); + } else if (type == OBJ_TREE) + convert_tree(buffer, size, entry->new_sha1); + else if (type == OBJ_COMMIT) + convert_commit(buffer, size, entry->new_sha1); + else + die("unknown object type %d in %s", type, sha1_to_hex(sha1)); + entry->converted = 1; + free(buffer); + free(data); + return entry; +} + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + struct entry *entry; + + setup_git_directory(); + + if (argc != 2) + usage("git-convert-objects <sha1>"); + if (get_sha1(argv[1], sha1)) + die("Not a valid object name %s", argv[1]); + + entry = convert_entry(sha1); + printf("new sha1: %s\n", sha1_to_hex(entry->new_sha1)); + return 0; +} diff --git a/contrib/convert-objects/git-convert-objects.txt b/contrib/convert-objects/git-convert-objects.txt new file mode 100644 index 0000000000..0565d83fc4 --- /dev/null +++ b/contrib/convert-objects/git-convert-objects.txt @@ -0,0 +1,29 @@ +git-convert-objects(1) +====================== + +NAME +---- +git-convert-objects - Converts old-style git repository + + +SYNOPSIS +-------- +[verse] +'git-convert-objects' + +DESCRIPTION +----------- +Converts old-style git repository to the latest format + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/contrib/credential/osxkeychain/.gitignore b/contrib/credential/osxkeychain/.gitignore new file mode 100644 index 0000000000..6c5b7026c5 --- /dev/null +++ b/contrib/credential/osxkeychain/.gitignore @@ -0,0 +1 @@ +git-credential-osxkeychain diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile new file mode 100644 index 0000000000..4b3a08a2ba --- /dev/null +++ b/contrib/credential/osxkeychain/Makefile @@ -0,0 +1,17 @@ +all:: git-credential-osxkeychain + +CC = gcc +RM = rm -f +CFLAGS = -g -O2 -Wall + +-include ../../../config.mak.autogen +-include ../../../config.mak + +git-credential-osxkeychain: git-credential-osxkeychain.o + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) -Wl,-framework -Wl,Security + +git-credential-osxkeychain.o: git-credential-osxkeychain.c + $(CC) -c $(CFLAGS) $< + +clean: + $(RM) git-credential-osxkeychain git-credential-osxkeychain.o diff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c new file mode 100644 index 0000000000..6beed123ab --- /dev/null +++ b/contrib/credential/osxkeychain/git-credential-osxkeychain.c @@ -0,0 +1,173 @@ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <Security/Security.h> + +static SecProtocolType protocol; +static char *host; +static char *path; +static char *username; +static char *password; +static UInt16 port; + +static void die(const char *err, ...) +{ + char msg[4096]; + va_list params; + va_start(params, err); + vsnprintf(msg, sizeof(msg), err, params); + fprintf(stderr, "%s\n", msg); + va_end(params); + exit(1); +} + +static void *xstrdup(const char *s1) +{ + void *ret = strdup(s1); + if (!ret) + die("Out of memory"); + return ret; +} + +#define KEYCHAIN_ITEM(x) (x ? strlen(x) : 0), x +#define KEYCHAIN_ARGS \ + NULL, /* default keychain */ \ + KEYCHAIN_ITEM(host), \ + 0, NULL, /* account domain */ \ + KEYCHAIN_ITEM(username), \ + KEYCHAIN_ITEM(path), \ + port, \ + protocol, \ + kSecAuthenticationTypeDefault + +static void write_item(const char *what, const char *buf, int len) +{ + printf("%s=", what); + fwrite(buf, 1, len, stdout); + putchar('\n'); +} + +static void find_username_in_item(SecKeychainItemRef item) +{ + SecKeychainAttributeList list; + SecKeychainAttribute attr; + + list.count = 1; + list.attr = &attr; + attr.tag = kSecAccountItemAttr; + + if (SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL)) + return; + + write_item("username", attr.data, attr.length); + SecKeychainItemFreeContent(&list, NULL); +} + +static void find_internet_password(void) +{ + void *buf; + UInt32 len; + SecKeychainItemRef item; + + if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, &len, &buf, &item)) + return; + + write_item("password", buf, len); + if (!username) + find_username_in_item(item); + + SecKeychainItemFreeContent(NULL, buf); +} + +static void delete_internet_password(void) +{ + SecKeychainItemRef item; + + /* + * Require at least a protocol and host for removal, which is what git + * will give us; if you want to do something more fancy, use the + * Keychain manager. + */ + if (!protocol || !host) + return; + + if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, 0, NULL, &item)) + return; + + SecKeychainItemDelete(item); +} + +static void add_internet_password(void) +{ + /* Only store complete credentials */ + if (!protocol || !host || !username || !password) + return; + + if (SecKeychainAddInternetPassword( + KEYCHAIN_ARGS, + KEYCHAIN_ITEM(password), + NULL)) + return; +} + +static void read_credential(void) +{ + char buf[1024]; + + while (fgets(buf, sizeof(buf), stdin)) { + char *v; + + if (!strcmp(buf, "\n")) + break; + buf[strlen(buf)-1] = '\0'; + + v = strchr(buf, '='); + if (!v) + die("bad input: %s", buf); + *v++ = '\0'; + + if (!strcmp(buf, "protocol")) { + if (!strcmp(v, "https")) + protocol = kSecProtocolTypeHTTPS; + else if (!strcmp(v, "http")) + protocol = kSecProtocolTypeHTTP; + else /* we don't yet handle other protocols */ + exit(0); + } + else if (!strcmp(buf, "host")) { + char *colon = strchr(v, ':'); + if (colon) { + *colon++ = '\0'; + port = atoi(colon); + } + host = xstrdup(v); + } + else if (!strcmp(buf, "path")) + path = xstrdup(v); + else if (!strcmp(buf, "username")) + username = xstrdup(v); + else if (!strcmp(buf, "password")) + password = xstrdup(v); + } +} + +int main(int argc, const char **argv) +{ + const char *usage = + "Usage: git credential-osxkeychain <get|store|erase>"; + + if (!argv[1]) + die(usage); + + read_credential(); + + if (!strcmp(argv[1], "get")) + find_internet_password(); + else if (!strcmp(argv[1], "store")) + add_internet_password(); + else if (!strcmp(argv[1], "erase")) + delete_internet_password(); + /* otherwise, ignore unknown action */ + + return 0; +} diff --git a/contrib/diff-highlight/README b/contrib/diff-highlight/README new file mode 100644 index 0000000000..502e03b305 --- /dev/null +++ b/contrib/diff-highlight/README @@ -0,0 +1,152 @@ +diff-highlight +============== + +Line oriented diffs are great for reviewing code, because for most +hunks, you want to see the old and the new segments of code next to each +other. Sometimes, though, when an old line and a new line are very +similar, it's hard to immediately see the difference. + +You can use "--color-words" to highlight only the changed portions of +lines. However, this can often be hard to read for code, as it loses +the line structure, and you end up with oddly formatted bits. + +Instead, this script post-processes the line-oriented diff, finds pairs +of lines, and highlights the differing segments. It's currently very +simple and stupid about doing these tasks. In particular: + + 1. It will only highlight hunks in which the number of removed and + added lines is the same, and it will pair lines within the hunk by + position (so the first removed line is compared to the first added + line, and so forth). This is simple and tends to work well in + practice. More complex changes don't highlight well, so we tend to + exclude them due to the "same number of removed and added lines" + restriction. Or even if we do try to highlight them, they end up + not highlighting because of our "don't highlight if the whole line + would be highlighted" rule. + + 2. It will find the common prefix and suffix of two lines, and + consider everything in the middle to be "different". It could + instead do a real diff of the characters between the two lines and + find common subsequences. However, the point of the highlight is to + call attention to a certain area. Even if some small subset of the + highlighted area actually didn't change, that's OK. In practice it + ends up being more readable to just have a single blob on the line + showing the interesting bit. + +The goal of the script is therefore not to be exact about highlighting +changes, but to call attention to areas of interest without being +visually distracting. Non-diff lines and existing diff coloration is +preserved; the intent is that the output should look exactly the same as +the input, except for the occasional highlight. + +Use +--- + +You can try out the diff-highlight program with: + +--------------------------------------------- +git log -p --color | /path/to/diff-highlight +--------------------------------------------- + +If you want to use it all the time, drop it in your $PATH and put the +following in your git configuration: + +--------------------------------------------- +[pager] + log = diff-highlight | less + show = diff-highlight | less + diff = diff-highlight | less +--------------------------------------------- + +Bugs +---- + +Because diff-highlight relies on heuristics to guess which parts of +changes are important, there are some cases where the highlighting is +more distracting than useful. Fortunately, these cases are rare in +practice, and when they do occur, the worst case is simply a little +extra highlighting. This section documents some cases known to be +sub-optimal, in case somebody feels like working on improving the +heuristics. + +1. Two changes on the same line get highlighted in a blob. For example, + highlighting: + +---------------------------------------------- +-foo(buf, size); ++foo(obj->buf, obj->size); +---------------------------------------------- + + yields (where the inside of "+{}" would be highlighted): + +---------------------------------------------- +-foo(buf, size); ++foo(+{obj->buf, obj->}size); +---------------------------------------------- + + whereas a more semantically meaningful output would be: + +---------------------------------------------- +-foo(buf, size); ++foo(+{obj->}buf, +{obj->}size); +---------------------------------------------- + + Note that doing this right would probably involve a set of + content-specific boundary patterns, similar to word-diff. Otherwise + you get junk like: + +----------------------------------------------------- +-this line has some -{i}nt-{ere}sti-{ng} text on it ++this line has some +{fa}nt+{a}sti+{c} text on it +----------------------------------------------------- + + which is less readable than the current output. + +2. The multi-line matching assumes that lines in the pre- and post-image + match by position. This is often the case, but can be fooled when a + line is removed from the top and a new one added at the bottom (or + vice versa). Unless the lines in the middle are also changed, diffs + will show this as two hunks, and it will not get highlighted at all + (which is good). But if the lines in the middle are changed, the + highlighting can be misleading. Here's a pathological case: + +----------------------------------------------------- +-one +-two +-three +-four ++two 2 ++three 3 ++four 4 ++five 5 +----------------------------------------------------- + + which gets highlighted as: + +----------------------------------------------------- +-one +-t-{wo} +-three +-f-{our} ++two 2 ++t+{hree 3} ++four 4 ++f+{ive 5} +----------------------------------------------------- + + because it matches "two" to "three 3", and so forth. It would be + nicer as: + +----------------------------------------------------- +-one +-two +-three +-four ++two +{2} ++three +{3} ++four +{4} ++five 5 +----------------------------------------------------- + + which would probably involve pre-matching the lines into pairs + according to some heuristic. diff --git a/contrib/diff-highlight/diff-highlight b/contrib/diff-highlight/diff-highlight new file mode 100755 index 0000000000..c4404d49c9 --- /dev/null +++ b/contrib/diff-highlight/diff-highlight @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +use warnings FATAL => 'all'; +use strict; + +# Highlight by reversing foreground and background. You could do +# other things like bold or underline if you prefer. +my $HIGHLIGHT = "\x1b[7m"; +my $UNHIGHLIGHT = "\x1b[27m"; +my $COLOR = qr/\x1b\[[0-9;]*m/; +my $BORING = qr/$COLOR|\s/; + +my @removed; +my @added; +my $in_hunk; + +while (<>) { + if (!$in_hunk) { + print; + $in_hunk = /^$COLOR*\@/; + } + elsif (/^$COLOR*-/) { + push @removed, $_; + } + elsif (/^$COLOR*\+/) { + push @added, $_; + } + else { + show_hunk(\@removed, \@added); + @removed = (); + @added = (); + + print; + $in_hunk = /^$COLOR*[\@ ]/; + } + + # Most of the time there is enough output to keep things streaming, + # but for something like "git log -Sfoo", you can get one early + # commit and then many seconds of nothing. We want to show + # that one commit as soon as possible. + # + # Since we can receive arbitrary input, there's no optimal + # place to flush. Flushing on a blank line is a heuristic that + # happens to match git-log output. + if (!length) { + local $| = 1; + } +} + +# Flush any queued hunk (this can happen when there is no trailing context in +# the final diff of the input). +show_hunk(\@removed, \@added); + +exit 0; + +sub show_hunk { + my ($a, $b) = @_; + + # If one side is empty, then there is nothing to compare or highlight. + if (!@$a || !@$b) { + print @$a, @$b; + return; + } + + # If we have mismatched numbers of lines on each side, we could try to + # be clever and match up similar lines. But for now we are simple and + # stupid, and only handle multi-line hunks that remove and add the same + # number of lines. + if (@$a != @$b) { + print @$a, @$b; + return; + } + + my @queue; + for (my $i = 0; $i < @$a; $i++) { + my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]); + print $rm; + push @queue, $add; + } + print @queue; +} + +sub highlight_pair { + my @a = split_line(shift); + my @b = split_line(shift); + + # Find common prefix, taking care to skip any ansi + # color codes. + my $seen_plusminus; + my ($pa, $pb) = (0, 0); + while ($pa < @a && $pb < @b) { + if ($a[$pa] =~ /$COLOR/) { + $pa++; + } + elsif ($b[$pb] =~ /$COLOR/) { + $pb++; + } + elsif ($a[$pa] eq $b[$pb]) { + $pa++; + $pb++; + } + elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') { + $seen_plusminus = 1; + $pa++; + $pb++; + } + else { + last; + } + } + + # Find common suffix, ignoring colors. + my ($sa, $sb) = ($#a, $#b); + while ($sa >= $pa && $sb >= $pb) { + if ($a[$sa] =~ /$COLOR/) { + $sa--; + } + elsif ($b[$sb] =~ /$COLOR/) { + $sb--; + } + elsif ($a[$sa] eq $b[$sb]) { + $sa--; + $sb--; + } + else { + last; + } + } + + if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { + return highlight_line(\@a, $pa, $sa), + highlight_line(\@b, $pb, $sb); + } + else { + return join('', @a), + join('', @b); + } +} + +sub split_line { + local $_ = shift; + return map { /$COLOR/ ? $_ : (split //) } + split /($COLOR*)/; +} + +sub highlight_line { + my ($line, $prefix, $suffix) = @_; + + return join('', + @{$line}[0..($prefix-1)], + $HIGHLIGHT, + @{$line}[$prefix..$suffix], + $UNHIGHLIGHT, + @{$line}[($suffix+1)..$#$line] + ); +} + +# Pairs are interesting to highlight only if we are going to end up +# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting +# is just useless noise. We can detect this by finding either a matching prefix +# or suffix (disregarding boring bits like whitespace and colorization). +sub is_pair_interesting { + my ($a, $pa, $sa, $b, $pb, $sb) = @_; + my $prefix_a = join('', @$a[0..($pa-1)]); + my $prefix_b = join('', @$b[0..($pb-1)]); + my $suffix_a = join('', @$a[($sa+1)..$#$a]); + my $suffix_b = join('', @$b[($sb+1)..$#$b]); + + return $prefix_a !~ /^$COLOR*-$BORING*$/ || + $prefix_b !~ /^$COLOR*\+$BORING*$/ || + $suffix_a !~ /^$BORING*$/ || + $suffix_b !~ /^$BORING*$/; +} diff --git a/contrib/diffall/README b/contrib/diffall/README new file mode 100644 index 0000000000..507f17dcd6 --- /dev/null +++ b/contrib/diffall/README @@ -0,0 +1,31 @@ +The git-diffall script provides a directory based diff mechanism +for git. + +To determine what diff viewer is used, the script requires either +the 'diff.tool' or 'merge.tool' configuration option to be set. + +This script is compatible with most common forms used to specify a +range of revisions to diff: + + 1. git diffall: shows diff between working tree and staged changes + 2. git diffall --cached [<commit>]: shows diff between staged + changes and HEAD (or other named commit) + 3. git diffall <commit>: shows diff between working tree and named + commit + 4. git diffall <commit> <commit>: show diff between two named commits + 5. git diffall <commit>..<commit>: same as above + 6. git diffall <commit>...<commit>: show the changes on the branch + containing and up to the second, starting at a common ancestor + of both <commit> + +Note: all forms take an optional path limiter [-- <path>*] + +The '--extcmd=<command>' option allows the user to specify a custom +command for viewing diffs. When given, configured defaults are +ignored and the script runs $command $LOCAL $REMOTE. Additionally, +$BASE is set in the environment. + +This script is based on an example provided by Thomas Rast on the +Git list [1]: + +[1] http://thread.gmane.org/gmane.comp.version-control.git/124807 diff --git a/contrib/diffall/git-diffall b/contrib/diffall/git-diffall new file mode 100755 index 0000000000..84f2b654d7 --- /dev/null +++ b/contrib/diffall/git-diffall @@ -0,0 +1,257 @@ +#!/bin/sh +# Copyright 2010 - 2012, Tim Henigan <tim.henigan@gmail.com> +# +# Perform a directory diff between commits in the repository using +# the external diff or merge tool specified in the user's config. + +USAGE='[--cached] [--copy-back] [-x|--extcmd=<command>] <commit>{0,2} [-- <path>*] + + --cached Compare to the index rather than the working tree. + + --copy-back Copy files back to the working tree when the diff + tool exits (in case they were modified by the + user). This option is only valid if the diff + compared with the working tree. + + -x=<command> + --extcmd=<command> Specify a custom command for viewing diffs. + git-diffall ignores the configured defaults and + runs $command $LOCAL $REMOTE when this option is + specified. Additionally, $BASE is set in the + environment. +' + +SUBDIRECTORY_OK=1 +. "$(git --exec-path)/git-sh-setup" + +TOOL_MODE=diff +. "$(git --exec-path)/git-mergetool--lib" + +merge_tool="$(get_merge_tool)" +if test -z "$merge_tool" +then + echo "Error: Either the 'diff.tool' or 'merge.tool' option must be set." + usage +fi + +start_dir=$(pwd) + +# All the file paths returned by the diff command are relative to the root +# of the working copy. So if the script is called from a subdirectory, it +# must switch to the root of working copy before trying to use those paths. +cdup=$(git rev-parse --show-cdup) && +cd "$cdup" || { + echo >&2 "Cannot chdir to $cdup, the toplevel of the working tree" + exit 1 +} + +# set up temp dir +tmp=$(perl -e 'use File::Temp qw(tempdir); + $t=tempdir("/tmp/git-diffall.XXXXX") or exit(1); + print $t') || exit 1 +trap 'rm -rf "$tmp"' EXIT + +left= +right= +paths= +dashdash_seen= +compare_staged= +merge_base= +left_dir= +right_dir= +diff_tool= +copy_back= + +while test $# != 0 +do + case "$1" in + -h|--h|--he|--hel|--help) + usage + ;; + --cached) + compare_staged=1 + ;; + --copy-back) + copy_back=1 + ;; + -x|--e|--ex|--ext|--extc|--extcm|--extcmd) + if test $# = 1 + then + echo You must specify the tool for use with --extcmd + usage + else + diff_tool=$2 + shift + fi + ;; + --) + dashdash_seen=1 + ;; + -*) + echo Invalid option: "$1" + usage + ;; + *) + # could be commit, commit range or path limiter + case "$1" in + *...*) + left=${1%...*} + right=${1#*...} + merge_base=1 + ;; + *..*) + left=${1%..*} + right=${1#*..} + ;; + *) + if test -n "$dashdash_seen" + then + paths="$paths$1 " + elif test -z "$left" + then + left=$1 + elif test -z "$right" + then + right=$1 + else + paths="$paths$1 " + fi + ;; + esac + ;; + esac + shift +done + +# Determine the set of files which changed +if test -n "$left" && test -n "$right" +then + left_dir="cmt-$(git rev-parse --short $left)" + right_dir="cmt-$(git rev-parse --short $right)" + + if test -n "$compare_staged" + then + usage + elif test -n "$merge_base" + then + git diff --name-only "$left"..."$right" -- $paths >"$tmp/filelist" + else + git diff --name-only "$left" "$right" -- $paths >"$tmp/filelist" + fi +elif test -n "$left" +then + left_dir="cmt-$(git rev-parse --short $left)" + + if test -n "$compare_staged" + then + right_dir="staged" + git diff --name-only --cached "$left" -- $paths >"$tmp/filelist" + else + right_dir="working_tree" + git diff --name-only "$left" -- $paths >"$tmp/filelist" + fi +else + left_dir="HEAD" + + if test -n "$compare_staged" + then + right_dir="staged" + git diff --name-only --cached -- $paths >"$tmp/filelist" + else + right_dir="working_tree" + git diff --name-only -- $paths >"$tmp/filelist" + fi +fi + +# Exit immediately if there are no diffs +if test ! -s "$tmp/filelist" +then + exit 0 +fi + +if test -n "$copy_back" && test "$right_dir" != "working_tree" +then + echo "--copy-back is only valid when diff includes the working tree." + exit 1 +fi + +# Create the named tmp directories that will hold the files to be compared +mkdir -p "$tmp/$left_dir" "$tmp/$right_dir" + +# Populate the tmp/right_dir directory with the files to be compared +while read name +do + if test -n "$right" + then + ls_list=$(git ls-tree $right "$name") + if test -n "$ls_list" + then + mkdir -p "$tmp/$right_dir/$(dirname "$name")" + git show "$right":"$name" >"$tmp/$right_dir/$name" || true + fi + elif test -n "$compare_staged" + then + ls_list=$(git ls-files -- "$name") + if test -n "$ls_list" + then + mkdir -p "$tmp/$right_dir/$(dirname "$name")" + git show :"$name" >"$tmp/$right_dir/$name" + fi + else + if test -e "$name" + then + mkdir -p "$tmp/$right_dir/$(dirname "$name")" + cp "$name" "$tmp/$right_dir/$name" + fi + fi +done < "$tmp/filelist" + +# Populate the tmp/left_dir directory with the files to be compared +while read name +do + if test -n "$left" + then + ls_list=$(git ls-tree $left "$name") + if test -n "$ls_list" + then + mkdir -p "$tmp/$left_dir/$(dirname "$name")" + git show "$left":"$name" >"$tmp/$left_dir/$name" || true + fi + else + if test -n "$compare_staged" + then + ls_list=$(git ls-tree HEAD "$name") + if test -n "$ls_list" + then + mkdir -p "$tmp/$left_dir/$(dirname "$name")" + git show HEAD:"$name" >"$tmp/$left_dir/$name" + fi + else + mkdir -p "$tmp/$left_dir/$(dirname "$name")" + git show :"$name" >"$tmp/$left_dir/$name" + fi + fi +done < "$tmp/filelist" + +LOCAL="$tmp/$left_dir" +REMOTE="$tmp/$right_dir" + +if test -n "$diff_tool" +then + export BASE + eval $diff_tool '"$LOCAL"' '"$REMOTE"' +else + run_merge_tool "$merge_tool" false +fi + +# Copy files back to the working dir, if requested +if test -n "$copy_back" && test "$right_dir" = "working_tree" +then + cd "$start_dir" + git_top_dir=$(git rev-parse --show-toplevel) + find "$tmp/$right_dir" -type f | + while read file + do + cp "$file" "$git_top_dir/${file#$tmp/$right_dir/}" + done +fi diff --git a/contrib/emacs/Makefile b/contrib/emacs/Makefile index a48540a92b..24d9312941 100644 --- a/contrib/emacs/Makefile +++ b/contrib/emacs/Makefile @@ -2,7 +2,7 @@ EMACS = emacs -ELC = git.elc vc-git.elc git-blame.elc +ELC = git.elc git-blame.elc INSTALL ?= install INSTALL_ELC = $(INSTALL) -m 644 prefix ?= $(HOME) diff --git a/contrib/emacs/README b/contrib/emacs/README new file mode 100644 index 0000000000..82368bdbff --- /dev/null +++ b/contrib/emacs/README @@ -0,0 +1,39 @@ +This directory contains various modules for Emacs support. + +To make the modules available to Emacs, you should add this directory +to your load-path, and then require the modules you want. This can be +done by adding to your .emacs something like this: + + (add-to-list 'load-path ".../git/contrib/emacs") + (require 'git) + (require 'git-blame) + + +The following modules are available: + +* git.el: + + Status manager that displays the state of all the files of the + project, and provides easy access to the most frequently used git + commands. The user interface is as far as possible compatible with + the pcl-cvs mode. It can be started with `M-x git-status'. + +* git-blame.el: + + Emacs implementation of incremental git-blame. When you turn it on + while viewing a file, the editor buffer will be updated by setting + the background of individual lines to a color that reflects which + commit it comes from. And when you move around the buffer, a + one-line summary will be shown in the echo area. + +* vc-git.el: + + This file used to contain the VC-mode backend for git, but it is no + longer distributed with git. It is now maintained as part of Emacs + and included in standard Emacs distributions starting from version + 22.2. + + If you have an earlier Emacs version, upgrading to Emacs 22 is + recommended, since the VC mode in older Emacs is not generic enough + to be able to support git in a reasonable manner, and no attempt has + been made to backport vc-git.el. diff --git a/contrib/emacs/git-blame.el b/contrib/emacs/git-blame.el index bb671d561e..e671f6c1c6 100644 --- a/contrib/emacs/git-blame.el +++ b/contrib/emacs/git-blame.el @@ -79,6 +79,58 @@ ;;; Code: (eval-when-compile (require 'cl)) ; to use `push', `pop' +(require 'format-spec) + +(defface git-blame-prefix-face + '((((background dark)) (:foreground "gray" + :background "black")) + (((background light)) (:foreground "gray" + :background "white")) + (t (:weight bold))) + "The face used for the hash prefix." + :group 'git-blame) + +(defgroup git-blame nil + "A minor mode showing Git blame information." + :group 'git + :link '(function-link git-blame-mode)) + + +(defcustom git-blame-use-colors t + "Use colors to indicate commits in `git-blame-mode'." + :type 'boolean + :group 'git-blame) + +(defcustom git-blame-prefix-format + "%h %20A:" + "The format of the prefix added to each line in `git-blame' +mode. The format is passed to `format-spec' with the following format keys: + + %h - the abbreviated hash + %H - the full hash + %a - the author name + %A - the author email + %c - the committer name + %C - the committer email + %s - the commit summary +" + :group 'git-blame) + +(defcustom git-blame-mouseover-format + "%h %a %A: %s" + "The format of the description shown when pointing at a line in +`git-blame' mode. The format string is passed to `format-spec' +with the following format keys: + + %h - the abbreviated hash + %H - the full hash + %a - the author name + %A - the author email + %c - the committer name + %C - the committer email + %s - the commit summary +" + :group 'git-blame) (defun git-blame-color-scale (&rest elements) @@ -105,6 +157,13 @@ selected element from l." (setq ,l (remove e ,l)) e)) +(defvar git-blame-log-oneline-format + "format:[%cr] %cn: %s" + "*Formatting option used for describing current line in the minibuffer. + +This option is used to pass to git log --pretty= command-line option, +and describe which commit the current line was made.") + (defvar git-blame-dark-colors (git-blame-color-scale "0c" "04" "24" "1c" "2c" "34" "14" "3c") "*List of colors (format #RGB) to use in a dark environment. @@ -245,7 +304,7 @@ See also function `git-blame-mode'." (defun git-blame-cleanup () "Remove all blame properties" - (mapcar 'delete-overlay git-blame-overlays) + (mapc 'delete-overlay git-blame-overlays) (setq git-blame-overlays nil) (remove-git-blame-text-properties (point-min) (point-max))) @@ -278,16 +337,16 @@ See also function `git-blame-mode'." (defvar in-blame-filter nil) (defun git-blame-filter (proc str) - (save-excursion - (set-buffer (process-buffer proc)) - (goto-char (process-mark proc)) - (insert-before-markers str) - (goto-char 0) - (unless in-blame-filter - (let ((more t) - (in-blame-filter t)) - (while more - (setq more (git-blame-parse))))))) + (with-current-buffer (process-buffer proc) + (save-excursion + (goto-char (process-mark proc)) + (insert-before-markers str) + (goto-char (point-min)) + (unless in-blame-filter + (let ((more t) + (in-blame-filter t)) + (while more + (setq more (git-blame-parse)))))))) (defun git-blame-parse () (cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n") @@ -295,72 +354,70 @@ See also function `git-blame-mode'." (src-line (string-to-number (match-string 2))) (res-line (string-to-number (match-string 3))) (num-lines (string-to-number (match-string 4)))) - (setq git-blame-current - (if (string= hash "0000000000000000000000000000000000000000") - nil - (git-blame-new-commit - hash src-line res-line num-lines)))) - (delete-region (point) (match-end 0)) - t) - ((looking-at "filename \\(.+\\)\n") - (let ((filename (match-string 1))) - (git-blame-add-info "filename" filename)) - (delete-region (point) (match-end 0)) + (delete-region (point) (match-end 0)) + (setq git-blame-current (list (git-blame-new-commit hash) + src-line res-line num-lines))) t) ((looking-at "\\([a-z-]+\\) \\(.+\\)\n") (let ((key (match-string 1)) (value (match-string 2))) - (git-blame-add-info key value)) - (delete-region (point) (match-end 0)) - t) - ((looking-at "boundary\n") - (setq git-blame-current nil) - (delete-region (point) (match-end 0)) + (delete-region (point) (match-end 0)) + (git-blame-add-info (car git-blame-current) key value) + (when (string= key "filename") + (git-blame-create-overlay (car git-blame-current) + (caddr git-blame-current) + (cadddr git-blame-current)) + (setq git-blame-current nil))) t) (t nil))) -(defun git-blame-new-commit (hash src-line res-line num-lines) - (save-excursion - (set-buffer git-blame-file) - (let ((info (gethash hash git-blame-cache)) - (inhibit-point-motion-hooks t) - (inhibit-modification-hooks t)) - (when (not info) - ;; Assign a random color to each new commit info - ;; Take care not to select the same color multiple times - (let ((color (if git-blame-colors - (git-blame-random-pop git-blame-colors) - git-blame-ancient-color))) - (setq info (list hash src-line res-line num-lines - (git-describe-commit hash) - (cons 'color color)))) - (puthash hash info git-blame-cache)) - (goto-line res-line) - (while (> num-lines 0) - (if (get-text-property (point) 'git-blame) - (forward-line) - (let* ((start (point)) - (end (progn (forward-line 1) (point))) - (ovl (make-overlay start end))) - (push ovl git-blame-overlays) - (overlay-put ovl 'git-blame info) - (overlay-put ovl 'help-echo hash) - (overlay-put ovl 'face (list :background - (cdr (assq 'color (nthcdr 5 info))))) - ;; the point-entered property doesn't seem to work in overlays - ;;(overlay-put ovl 'point-entered - ;; `(lambda (x y) (git-blame-identify ,hash))) - (let ((modified (buffer-modified-p))) - (put-text-property (if (= start 1) start (1- start)) (1- end) - 'point-entered - `(lambda (x y) (git-blame-identify ,hash))) - (set-buffer-modified-p modified)))) - (setq num-lines (1- num-lines)))))) - -(defun git-blame-add-info (key value) - (if git-blame-current - (nconc git-blame-current (list (cons (intern key) value))))) +(defun git-blame-new-commit (hash) + (with-current-buffer git-blame-file + (or (gethash hash git-blame-cache) + ;; Assign a random color to each new commit info + ;; Take care not to select the same color multiple times + (let* ((color (if git-blame-colors + (git-blame-random-pop git-blame-colors) + git-blame-ancient-color)) + (info `(,hash (color . ,color)))) + (puthash hash info git-blame-cache) + info)))) + +(defun git-blame-create-overlay (info start-line num-lines) + (with-current-buffer git-blame-file + (save-excursion + (let ((inhibit-point-motion-hooks t) + (inhibit-modification-hooks t)) + (goto-char (point-min)) + (forward-line (1- start-line)) + (let* ((start (point)) + (end (progn (forward-line num-lines) (point))) + (ovl (make-overlay start end)) + (hash (car info)) + (spec `((?h . ,(substring hash 0 6)) + (?H . ,hash) + (?a . ,(git-blame-get-info info 'author)) + (?A . ,(git-blame-get-info info 'author-mail)) + (?c . ,(git-blame-get-info info 'committer)) + (?C . ,(git-blame-get-info info 'committer-mail)) + (?s . ,(git-blame-get-info info 'summary))))) + (push ovl git-blame-overlays) + (overlay-put ovl 'git-blame info) + (overlay-put ovl 'help-echo + (format-spec git-blame-mouseover-format spec)) + (if git-blame-use-colors + (overlay-put ovl 'face (list :background + (cdr (assq 'color (cdr info)))))) + (overlay-put ovl 'line-prefix + (propertize (format-spec git-blame-prefix-format spec) + 'face 'git-blame-prefix-face))))))) + +(defun git-blame-add-info (info key value) + (nconc info (list (cons (intern key) value)))) + +(defun git-blame-get-info (info key) + (cdr (assq key (cdr info)))) (defun git-blame-current-commit () (let ((info (get-char-property (point) 'git-blame))) @@ -371,9 +428,10 @@ See also function `git-blame-mode'." (defun git-describe-commit (hash) (with-temp-buffer (call-process "git" nil t nil - "log" "-1" "--pretty=oneline" + "log" "-1" + (concat "--pretty=" git-blame-log-oneline-format) hash) - (buffer-substring (point-min) (1- (point-max))))) + (buffer-substring (point-min) (point-max)))) (defvar git-blame-last-identification nil) (make-variable-buffer-local 'git-blame-last-identification) diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el index 280557ecd4..65c95d9d5a 100644 --- a/contrib/emacs/git.el +++ b/contrib/emacs/git.el @@ -1,6 +1,6 @@ ;;; git.el --- A user interface for git -;; Copyright (C) 2005, 2006, 2007 Alexandre Julliard <julliard@winehq.org> +;; Copyright (C) 2005, 2006, 2007, 2008, 2009 Alexandre Julliard <julliard@winehq.org> ;; Version: 1.0 @@ -34,22 +34,27 @@ ;; To start: `M-x git-status' ;; ;; TODO -;; - portability to XEmacs -;; - better handling of subprocess errors -;; - hook into file save (after-save-hook) ;; - diff against other branch ;; - renaming files from the status buffer ;; - creating tags ;; - fetch/pull -;; - switching branches ;; - revlist browser ;; - git-show-branch browser -;; - menus +;; + +;;; Compatibility: +;; +;; This file works on GNU Emacs 21 or later. It may work on older +;; versions but this is not guaranteed. +;; +;; It may work on XEmacs 21, provided that you first install the ewoc +;; and log-edit packages. ;; (eval-when-compile (require 'cl)) (require 'ewoc) (require 'log-edit) +(require 'easymenu) ;;;; Customizations @@ -97,6 +102,21 @@ if there is already one that displays the same directory." :group 'git :type 'string) +(defcustom git-show-uptodate nil + "Whether to display up-to-date files." + :group 'git + :type 'boolean) + +(defcustom git-show-ignored nil + "Whether to display ignored files." + :group 'git + :type 'boolean) + +(defcustom git-show-unknown t + "Whether to display unknown files." + :group 'git + :type 'boolean) + (defface git-status-face '((((class color) (background light)) (:foreground "purple")) @@ -159,7 +179,7 @@ if there is already one that displays the same directory." (defconst git-log-msg-separator "--- log message follows this line ---") (defvar git-log-edit-font-lock-keywords - `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)$" + `(("^\\(Author:\\|Date:\\|Merge:\\|Signed-off-by:\\)\\(.*\\)$" (1 font-lock-keyword-face) (2 font-lock-function-name-face)) (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$") @@ -169,20 +189,38 @@ if there is already one that displays the same directory." "Build a list of NAME=VALUE strings from a list of environment strings." (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env)) -(defun git-call-process-env (buffer env &rest args) +(defun git-call-process (buffer &rest args) "Wrapper for call-process that sets environment strings." - (if env - (apply #'call-process "env" nil buffer nil - (append (git-get-env-strings env) (list "git") args)) - (apply #'call-process "git" nil buffer nil args))) - -(defun git-call-process-env-string (env &rest args) - "Wrapper for call-process that sets environment strings, -and returns the process output as a string." + (apply #'call-process "git" nil buffer nil args)) + +(defun git-call-process-display-error (&rest args) + "Wrapper for call-process that displays error messages." + (let* ((dir default-directory) + (buffer (get-buffer-create "*Git Command Output*")) + (ok (with-current-buffer buffer + (let ((default-directory dir) + (buffer-read-only nil)) + (erase-buffer) + (eq 0 (apply #'git-call-process (list buffer t) args)))))) + (unless ok (display-message-or-buffer buffer)) + ok)) + +(defun git-call-process-string (&rest args) + "Wrapper for call-process that returns the process output as a string, +or nil if the git command failed." (with-temp-buffer - (and (eq 0 (apply #' git-call-process-env t env args)) + (and (eq 0 (apply #'git-call-process t args)) (buffer-string)))) +(defun git-call-process-string-display-error (&rest args) + "Wrapper for call-process that displays error message and returns +the process output as a string, or nil if the git command failed." + (with-temp-buffer + (if (eq 0 (apply #'git-call-process (list t t) args)) + (buffer-string) + (display-message-or-buffer (current-buffer)) + nil))) + (defun git-run-process-region (buffer start end program args) "Run a git process with a buffer region as input." (let ((output-buffer (current-buffer)) @@ -190,7 +228,7 @@ and returns the process output as a string." (with-current-buffer buffer (cd dir) (apply #'call-process-region start end program - nil (list output-buffer nil) nil args)))) + nil (list output-buffer t) nil args)))) (defun git-run-command-buffer (buffer-name &rest args) "Run a git command, sending the output to a buffer named BUFFER-NAME." @@ -201,26 +239,21 @@ and returns the process output as a string." (let ((default-directory dir) (buffer-read-only nil)) (erase-buffer) - (apply #'git-call-process-env buffer nil args))) + (apply #'git-call-process buffer args))) (message "Running git %s...done" (car args)) buffer)) -(defun git-run-command (buffer env &rest args) - (message "Running git %s..." (car args)) - (apply #'git-call-process-env buffer env args) - (message "Running git %s...done" (car args))) - (defun git-run-command-region (buffer start end env &rest args) "Run a git command with specified buffer region as input." - (message "Running git %s..." (car args)) - (unless (eq 0 (if env - (git-run-process-region - buffer start end "env" - (append (git-get-env-strings env) (list "git") args)) + (with-temp-buffer + (if (eq 0 (if env (git-run-process-region - buffer start end "git" args))) - (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string))) - (message "Running git %s...done" (car args))) + buffer start end "env" + (append (git-get-env-strings env) (list "git") args)) + (git-run-process-region buffer start end "git" args))) + (buffer-string) + (display-message-or-buffer (current-buffer)) + nil))) (defun git-run-hook (hook env &rest args) "Run a git hook and display its output if any." @@ -297,12 +330,19 @@ and returns the process output as a string." "\"") name)) +(defun git-success-message (text files) + "Print a success message after having handled FILES." + (let ((n (length files))) + (if (equal n 1) + (message "%s %s" text (car files)) + (message "%s %d files" text n)))) + (defun git-get-top-dir (dir) "Retrieve the top-level directory of a git tree." (let ((cdup (with-output-to-string (with-current-buffer standard-output (cd dir) - (unless (eq 0 (call-process "git" nil t nil "rev-parse" "--show-cdup")) + (unless (eq 0 (git-call-process t "rev-parse" "--show-cdup")) (error "cannot find top-level git tree for %s." dir)))))) (expand-file-name (concat (file-name-as-directory dir) (car (split-string cdup "\n")))))) @@ -323,8 +363,8 @@ and returns the process output as a string." (sort-lines nil (point-min) (point-max)) (save-buffer)) (when created - (git-run-command nil nil "update-index" "--add" "--" (file-relative-name ignore-name))) - (git-update-status-files (list (file-relative-name ignore-name)) 'unknown))) + (git-call-process nil "update-index" "--add" "--" (file-relative-name ignore-name))) + (git-update-status-files (list (file-relative-name ignore-name))))) ; propertize definition for XEmacs, stolen from erc-compat (eval-when-compile @@ -342,49 +382,66 @@ and returns the process output as a string." (defun git-rev-parse (rev) "Parse a revision name and return its SHA1." (git-get-string-sha1 - (git-call-process-env-string nil "rev-parse" rev))) + (git-call-process-string "rev-parse" rev))) (defun git-config (key) "Retrieve the value associated to KEY in the git repository config file." - (let ((str (git-call-process-env-string nil "config" key))) + (let ((str (git-call-process-string "config" key))) (and str (car (split-string str "\n"))))) (defun git-symbolic-ref (ref) "Wrapper for the git-symbolic-ref command." - (let ((str (git-call-process-env-string nil "symbolic-ref" ref))) + (let ((str (git-call-process-string "symbolic-ref" ref))) (and str (car (split-string str "\n"))))) (defun git-update-ref (ref newval &optional oldval reason) "Update a reference by calling git-update-ref." (let ((args (and oldval (list oldval)))) - (push newval args) + (when newval (push newval args)) (push ref args) (when reason (push reason args) (push "-m" args)) - (eq 0 (apply #'git-call-process-env nil nil "update-ref" args)))) + (unless newval (push "-d" args)) + (apply 'git-call-process-display-error "update-ref" args))) + +(defun git-for-each-ref (&rest specs) + "Return a list of refs using git-for-each-ref. +Each entry is a cons of (SHORT-NAME . FULL-NAME)." + (let (refs) + (with-temp-buffer + (apply #'git-call-process t "for-each-ref" "--format=%(refname)" specs) + (goto-char (point-min)) + (while (re-search-forward "^[^/\n]+/[^/\n]+/\\(.+\\)$" nil t) + (push (cons (match-string 1) (match-string 0)) refs))) + (nreverse refs))) (defun git-read-tree (tree &optional index-file) "Read a tree into the index file." - (apply #'git-call-process-env nil - (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil) - "read-tree" (if tree (list tree)))) + (let ((process-environment + (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file))) process-environment))) + (apply 'git-call-process-display-error "read-tree" (if tree (list tree))))) (defun git-write-tree (&optional index-file) "Call git-write-tree and return the resulting tree SHA1 as a string." - (git-get-string-sha1 - (git-call-process-env-string (and index-file `(("GIT_INDEX_FILE" . ,index-file))) "write-tree"))) - -(defun git-commit-tree (buffer tree head) - "Call git-commit-tree with buffer as input and return the resulting commit SHA1." + (let ((process-environment + (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file))) process-environment))) + (git-get-string-sha1 + (git-call-process-string-display-error "write-tree")))) + +(defun git-commit-tree (buffer tree parent) + "Create a commit and possibly update HEAD. +Create a commit with the message in BUFFER using the tree with hash TREE. +Use PARENT as the parent of the new commit. If PARENT is the current \"HEAD\", +update the \"HEAD\" reference to the new commit." (let ((author-name (git-get-committer-name)) (author-email (git-get-committer-email)) (subject "commit (initial): ") author-date log-start log-end args coding-system-for-write) - (when head + (when parent (setq subject "commit: ") (push "-p" args) - (push head args)) + (push parent args)) (with-current-buffer buffer (goto-char (point-min)) (if @@ -399,11 +456,11 @@ and returns the process output as a string." (when (re-search-forward "^Date: +\\(.*\\)$" nil t) (setq author-date (match-string 1))) (goto-char (point-min)) - (while (re-search-forward "^Parent: +\\([0-9a-f]+\\)" nil t) - (unless (string-equal head (match-string 1)) - (setq subject "commit (merge): ") + (when (re-search-forward "^Merge: +\\(.*\\)" nil t) + (setq subject "commit (merge): ") + (dolist (parent (split-string (match-string 1) " +" t)) (push "-p" args) - (push (match-string 1) args)))) + (push parent args)))) (setq log-start (point-min))) (setq log-end (point-max)) (goto-char log-start) @@ -412,22 +469,20 @@ and returns the process output as a string." (setq coding-system-for-write buffer-file-coding-system)) (let ((commit (git-get-string-sha1 - (with-output-to-string - (with-current-buffer standard-output - (let ((env `(("GIT_AUTHOR_NAME" . ,author-name) - ("GIT_AUTHOR_EMAIL" . ,author-email) - ("GIT_COMMITTER_NAME" . ,(git-get-committer-name)) - ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email))))) - (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env)) - (apply #'git-run-command-region - buffer log-start log-end env - "commit-tree" tree (nreverse args)))))))) - (and (git-update-ref "HEAD" commit head subject) - commit)))) + (let ((env `(("GIT_AUTHOR_NAME" . ,author-name) + ("GIT_AUTHOR_EMAIL" . ,author-email) + ("GIT_COMMITTER_NAME" . ,(git-get-committer-name)) + ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email))))) + (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env)) + (apply #'git-run-command-region + buffer log-start log-end env + "commit-tree" tree (nreverse args)))))) + (when commit (git-update-ref "HEAD" commit parent subject)) + commit))) (defun git-empty-db-p () "Check if the git db is empty (no commit done yet)." - (not (eq 0 (call-process "git" nil nil nil "rev-parse" "--verify" "HEAD")))) + (not (eq 0 (git-call-process nil "rev-parse" "--verify" "HEAD")))) (defun git-get-merge-heads () "Retrieve the merge heads from the MERGE_HEAD file if present." @@ -443,7 +498,7 @@ and returns the process output as a string." (defun git-get-commit-description (commit) "Get a one-line description of COMMIT." (let ((coding-system-for-read (git-get-logoutput-coding-system))) - (let ((descr (git-call-process-env-string nil "log" "--max-count=1" "--pretty=oneline" commit))) + (let ((descr (git-call-process-string "log" "--max-count=1" "--pretty=oneline" commit))) (if (and descr (string-match "\\`\\([0-9a-f]\\{40\\}\\) *\\(.*\\)$" descr)) (concat (substring (match-string 1 descr) 0 10) " - " (match-string 2 descr)) descr)))) @@ -462,22 +517,42 @@ and returns the process output as a string." old-perm new-perm ;; permission flags rename-state ;; rename or copy state orig-name ;; original name for renames or copies + needs-update ;; whether file needs to be updated needs-refresh) ;; whether file needs to be refreshed (defvar git-status nil) -(defun git-clear-status (status) - "Remove everything from the status list." - (ewoc-filter status (lambda (info) nil))) - -(defun git-set-files-state (files state) - "Set the state of a list of files." - (dolist (info files) - (unless (eq (git-fileinfo->state info) state) - (setf (git-fileinfo->state info) state) - (setf (git-fileinfo->rename-state info) nil) - (setf (git-fileinfo->orig-name info) nil) - (setf (git-fileinfo->needs-refresh info) t)))) +(defun git-set-fileinfo-state (info state) + "Set the state of a file info." + (unless (eq (git-fileinfo->state info) state) + (setf (git-fileinfo->state info) state + (git-fileinfo->new-perm info) (git-fileinfo->old-perm info) + (git-fileinfo->rename-state info) nil + (git-fileinfo->orig-name info) nil + (git-fileinfo->needs-update info) nil + (git-fileinfo->needs-refresh info) t))) + +(defun git-status-filenames-map (status func files &rest args) + "Apply FUNC to the status files names in the FILES list. +The list must be sorted." + (when files + (let ((file (pop files)) + (node (ewoc-nth status 0))) + (while (and file node) + (let* ((info (ewoc-data node)) + (name (git-fileinfo->name info))) + (if (string-lessp name file) + (setq node (ewoc-next status node)) + (if (string-equal name file) + (apply func info args)) + (setq file (pop files)))))))) + +(defun git-set-filenames-state (status files state) + "Set the state of a list of named files. The list must be sorted" + (when files + (git-status-filenames-map status #'git-set-fileinfo-state files state) + (unless state ;; delete files whose state has been set to nil + (ewoc-filter status (lambda (info) (git-fileinfo->state info)))))) (defun git-state-code (code) "Convert from a string to a added/deleted/modified state." @@ -487,6 +562,7 @@ and returns the process output as a string." (?A 'added) (?D 'deleted) (?U 'unmerged) + (?T 'modified) (t nil))) (defun git-status-code-as-string (code) @@ -501,6 +577,36 @@ and returns the process output as a string." ('ignored (propertize "Ignored " 'face 'git-ignored-face)) (t "? "))) +(defun git-file-type-as-string (old-perm new-perm) + "Return a string describing the file type based on its permissions." + (let* ((old-type (lsh (or old-perm 0) -9)) + (new-type (lsh (or new-perm 0) -9)) + (str (case new-type + (64 ;; file + (case old-type + (64 nil) + (80 " (type change symlink -> file)") + (112 " (type change subproject -> file)"))) + (80 ;; symlink + (case old-type + (64 " (type change file -> symlink)") + (112 " (type change subproject -> symlink)") + (t " (symlink)"))) + (112 ;; subproject + (case old-type + (64 " (type change file -> subproject)") + (80 " (type change symlink -> subproject)") + (t " (subproject)"))) + (72 nil) ;; directory (internal, not a real git state) + (0 ;; deleted or unknown + (case old-type + (80 " (symlink)") + (112 " (subproject)"))) + (t (format " (unknown type %o)" new-type))))) + (cond (str (propertize str 'face 'git-status-face)) + ((eq new-type 72) "/") + (t "")))) + (defun git-rename-as-string (info) "Return a string describing the copy or rename associated with INFO, or an empty string if none." (let ((state (git-fileinfo->rename-state info))) @@ -526,30 +632,64 @@ and returns the process output as a string." (defun git-fileinfo-prettyprint (info) "Pretty-printer for the git-fileinfo structure." - (insert (concat " " (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ") - " " (git-status-code-as-string (git-fileinfo->state info)) - " " (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info)) - " " (git-escape-file-name (git-fileinfo->name info)) - (git-rename-as-string info)))) - -(defun git-insert-fileinfo (status info &optional refresh) - "Insert INFO in the status buffer, optionally refreshing an existing one." - (let ((node (and refresh - (git-find-status-file status (git-fileinfo->name info))))) - (setf (git-fileinfo->needs-refresh info) t) - (when node ;preserve the marked flag - (setf (git-fileinfo->marked info) (git-fileinfo->marked (ewoc-data node)))) - (if node (setf (ewoc-data node) info) (ewoc-enter-last status info)))) + (let ((old-perm (git-fileinfo->old-perm info)) + (new-perm (git-fileinfo->new-perm info))) + (insert (concat " " (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ") + " " (git-status-code-as-string (git-fileinfo->state info)) + " " (git-permissions-as-string old-perm new-perm) + " " (git-escape-file-name (git-fileinfo->name info)) + (git-file-type-as-string old-perm new-perm) + (git-rename-as-string info))))) + +(defun git-update-node-fileinfo (node info) + "Update the fileinfo of the specified node. The names are assumed to match already." + (let ((data (ewoc-data node))) + (setf + ;; preserve the marked flag + (git-fileinfo->marked info) (git-fileinfo->marked data) + (git-fileinfo->needs-update data) nil) + (when (not (equal info data)) + (setf (git-fileinfo->needs-refresh info) t + (ewoc-data node) info)))) + +(defun git-insert-info-list (status infolist files) + "Insert a sorted list of file infos in the status buffer, replacing existing ones if any." + (let* ((info (pop infolist)) + (node (ewoc-nth status 0)) + (name (and info (git-fileinfo->name info))) + remaining) + (while info + (let ((nodename (and node (git-fileinfo->name (ewoc-data node))))) + (while (and files (string-lessp (car files) name)) + (push (pop files) remaining)) + (when (and files (string-equal (car files) name)) + (setq files (cdr files))) + (cond ((not nodename) + (setq node (ewoc-enter-last status info)) + (setq info (pop infolist)) + (setq name (and info (git-fileinfo->name info)))) + ((string-lessp nodename name) + (setq node (ewoc-next status node))) + ((string-equal nodename name) + ;; preserve the marked flag + (git-update-node-fileinfo node info) + (setq info (pop infolist)) + (setq name (and info (git-fileinfo->name info)))) + (t + (setq node (ewoc-enter-before status node info)) + (setq info (pop infolist)) + (setq name (and info (git-fileinfo->name info))))))) + (nconc (nreverse remaining) files))) (defun git-run-diff-index (status files) "Run git-diff-index on FILES and parse the results into STATUS. Return the list of files that haven't been handled." - (let ((refresh files)) + (let (infolist) (with-temp-buffer - (apply #'git-run-command t nil "diff-index" "-z" "-M" "HEAD" "--" files) + (apply #'git-call-process t "diff-index" "-z" "-M" "HEAD" "--" files) (goto-char (point-min)) (while (re-search-forward - ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMU]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0" + ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMUT]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0" nil t 1) (let ((old-perm (string-to-number (match-string 1) 8)) (new-perm (string-to-number (match-string 2) 8)) @@ -558,13 +698,15 @@ Return the list of files that haven't been handled." (new-name (match-string 8))) (if new-name ; copy or rename (if (eq ?C (string-to-char state)) - (git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) refresh) - (git-insert-fileinfo status (git-create-fileinfo 'deleted name 0 0 'rename new-name) refresh) - (git-insert-fileinfo status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name)) refresh) - (git-insert-fileinfo status (git-create-fileinfo (git-state-code state) name old-perm new-perm) refresh)) - (setq files (delete name files)) - (when new-name (setq files (delete new-name files))))))) - files) + (push (git-create-fileinfo 'added new-name old-perm new-perm 'copy name) infolist) + (push (git-create-fileinfo 'deleted name 0 0 'rename new-name) infolist) + (push (git-create-fileinfo 'added new-name old-perm new-perm 'rename name) infolist)) + (push (git-create-fileinfo (git-state-code state) name old-perm new-perm) infolist))))) + (setq infolist (sort (nreverse infolist) + (lambda (info1 info2) + (string-lessp (git-fileinfo->name info1) + (git-fileinfo->name info2))))) + (git-insert-info-list status infolist files))) (defun git-find-status-file (status file) "Find a given file in the status ewoc and return its node." @@ -576,27 +718,43 @@ Return the list of files that haven't been handled." (defun git-run-ls-files (status files default-state &rest options) "Run git-ls-files on FILES and parse the results into STATUS. Return the list of files that haven't been handled." - (let ((refresh files)) + (let (infolist) (with-temp-buffer - (apply #'git-run-command t nil "ls-files" "-z" "-t" (append options (list "--") files)) + (apply #'git-call-process t "ls-files" "-z" (append options (list "--") files)) (goto-char (point-min)) - (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1) - (let ((state (match-string 1)) - (name (match-string 2))) - (git-insert-fileinfo status (git-create-fileinfo (or (git-state-code state) default-state) name) refresh) - (setq files (delete name files)))))) - files) + (while (re-search-forward "\\([^\0]*?\\)\\(/?\\)\0" nil t 1) + (let ((name (match-string 1))) + (push (git-create-fileinfo default-state name 0 + (if (string-equal "/" (match-string 2)) (lsh ?\110 9) 0)) + infolist)))) + (setq infolist (nreverse infolist)) ;; assume it is sorted already + (git-insert-info-list status infolist files))) + +(defun git-run-ls-files-cached (status files default-state) + "Run git-ls-files -c on FILES and parse the results into STATUS. +Return the list of files that haven't been handled." + (let (infolist) + (with-temp-buffer + (apply #'git-call-process t "ls-files" "-z" "-s" "-c" "--" files) + (goto-char (point-min)) + (while (re-search-forward "\\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} 0\t\\([^\0]+\\)\0" nil t) + (let* ((new-perm (string-to-number (match-string 1) 8)) + (old-perm (if (eq default-state 'added) 0 new-perm)) + (name (match-string 2))) + (push (git-create-fileinfo default-state name old-perm new-perm) infolist)))) + (setq infolist (nreverse infolist)) ;; assume it is sorted already + (git-insert-info-list status infolist files))) (defun git-run-ls-unmerged (status files) "Run git-ls-files -u on FILES and parse the results into STATUS." (with-temp-buffer - (apply #'git-run-command t nil "ls-files" "-z" "-u" "--" files) + (apply #'git-call-process t "ls-files" "-z" "-u" "--" files) (goto-char (point-min)) (let (unmerged-files) (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t) - (let ((node (git-find-status-file status (match-string 1)))) - (when node (push (ewoc-data node) unmerged-files)))) - (git-set-files-state unmerged-files 'unmerged)))) + (push (match-string 1) unmerged-files)) + (setq unmerged-files (nreverse unmerged-files)) ;; assume it is sorted already + (git-set-filenames-state status unmerged-files 'unmerged)))) (defun git-get-exclude-files () "Get the list of exclude files to pass to git-ls-files." @@ -608,34 +766,62 @@ Return the list of files that haven't been handled." (push config files)) files)) -(defun git-update-status-files (files &optional default-state) - "Update the status of FILES from the index." +(defun git-run-ls-files-with-excludes (status files default-state &rest options) + "Run git-ls-files on FILES with appropriate --exclude-from options." + (let ((exclude-files (git-get-exclude-files))) + (apply #'git-run-ls-files status files default-state "--directory" "--no-empty-directory" + (concat "--exclude-per-directory=" git-per-dir-ignore-file) + (append options (mapcar (lambda (f) (concat "--exclude-from=" f)) exclude-files))))) + +(defun git-update-status-files (&optional files mark-files) + "Update the status of FILES from the index. +The FILES list must be sorted." (unless git-status (error "Not in git-status buffer.")) - (let* ((status git-status) - (remaining-files + ;; set the needs-update flag on existing files + (if files + (git-status-filenames-map + git-status (lambda (info) (setf (git-fileinfo->needs-update info) t)) files) + (ewoc-map (lambda (info) (setf (git-fileinfo->needs-update info) t) nil) git-status) + (git-call-process nil "update-index" "--refresh") + (when git-show-uptodate + (git-run-ls-files-cached git-status nil 'uptodate))) + (let ((remaining-files (if (git-empty-db-p) ; we need some special handling for an empty db - (git-run-ls-files status files 'added "-c") - (git-run-diff-index status files)))) - (git-run-ls-unmerged status files) - (when (or (not files) remaining-files) - (let ((exclude-files (git-get-exclude-files))) - (setq remaining-files (apply #'git-run-ls-files status remaining-files 'unknown "-o" - (concat "--exclude-per-directory=" git-per-dir-ignore-file) - (mapcar (lambda (f) (concat "--exclude-from=" f)) exclude-files))))) - ; mark remaining files with the default state (or remove them if nil) + (git-run-ls-files-cached git-status files 'added) + (git-run-diff-index git-status files)))) + (git-run-ls-unmerged git-status files) + (when (or remaining-files (and git-show-unknown (not files))) + (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'unknown "-o"))) + (when (or remaining-files (and git-show-ignored (not files))) + (setq remaining-files (git-run-ls-files-with-excludes git-status remaining-files 'ignored "-o" "-i"))) + (unless files + (setq remaining-files (git-get-filenames (ewoc-collect git-status #'git-fileinfo->needs-update)))) (when remaining-files - (if default-state - (ewoc-map (lambda (info) - (when (member (git-fileinfo->name info) remaining-files) - (git-set-files-state (list info) default-state)) - nil) - status) - (ewoc-filter status - (lambda (info files) - (not (member (git-fileinfo->name info) files))) - remaining-files))) + (setq remaining-files (git-run-ls-files-cached git-status remaining-files 'uptodate))) + (git-set-filenames-state git-status remaining-files nil) + (when mark-files (git-mark-files git-status files)) (git-refresh-files) - (git-refresh-ewoc-hf status))) + (git-refresh-ewoc-hf git-status))) + +(defun git-mark-files (status files) + "Mark all the specified FILES, and unmark the others." + (let ((file (and files (pop files))) + (node (ewoc-nth status 0))) + (while node + (let ((info (ewoc-data node))) + (if (and file (string-equal (git-fileinfo->name info) file)) + (progn + (unless (git-fileinfo->marked info) + (setf (git-fileinfo->marked info) t) + (setf (git-fileinfo->needs-refresh info) t)) + (setq file (pop files)) + (setq node (ewoc-next status node))) + (when (git-fileinfo->marked info) + (setf (git-fileinfo->marked info) nil) + (setf (git-fileinfo->needs-refresh info) t)) + (if (and file (string-lessp file (git-fileinfo->name info))) + (setq file (pop files)) + (setq node (ewoc-next status node)))))))) (defun git-marked-files () "Return a list of all marked files, or if none a list containing just the file at cursor position." @@ -644,13 +830,13 @@ Return the list of files that haven't been handled." (list (ewoc-data (ewoc-locate git-status))))) (defun git-marked-files-state (&rest states) - "Return marked files that are in the specified states." + "Return a sorted list of marked files that are in the specified states." (let ((files (git-marked-files)) result) (dolist (info files) (when (memq (git-fileinfo->state info) states) (push info result))) - result)) + (nreverse result))) (defun git-refresh-files () "Refresh all files that need it and clear the needs-refresh flag." @@ -690,19 +876,18 @@ Return the list of files that haven't been handled." (defun git-update-index (index-file files) "Run git-update-index on a list of files." - (let ((env (and index-file `(("GIT_INDEX_FILE" . ,index-file)))) + (let ((process-environment (append (and index-file (list (concat "GIT_INDEX_FILE=" index-file))) + process-environment)) added deleted modified) (dolist (info files) (case (git-fileinfo->state info) ('added (push info added)) ('deleted (push info deleted)) ('modified (push info modified)))) - (when added - (apply #'git-run-command nil env "update-index" "--add" "--" (git-get-filenames added))) - (when deleted - (apply #'git-run-command nil env "update-index" "--remove" "--" (git-get-filenames deleted))) - (when modified - (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified))))) + (and + (or (not added) (apply #'git-call-process-display-error "update-index" "--add" "--" (git-get-filenames added))) + (or (not deleted) (apply #'git-call-process-display-error "update-index" "--remove" "--" (git-get-filenames deleted))) + (or (not modified) (apply #'git-call-process-display-error "update-index" "--" (git-get-filenames modified)))))) (defun git-run-pre-commit-hook () "Run the pre-commit hook if any." @@ -728,30 +913,30 @@ Return the list of files that haven't been handled." (message "You cannot commit unmerged files, resolve them first.") (unwind-protect (let ((files (git-marked-files-state 'added 'deleted 'modified)) - head head-tree) + head tree head-tree) (unless (git-empty-db-p) (setq head (git-rev-parse "HEAD") head-tree (git-rev-parse "HEAD^{tree}"))) - (if files - (progn - (git-read-tree head-tree index-file) - (git-update-index nil files) ;update both the default index - (git-update-index index-file files) ;and the temporary one - (let ((tree (git-write-tree index-file))) - (if (or (not (string-equal tree head-tree)) - (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? ")) - (let ((commit (git-commit-tree buffer tree head))) - (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil)) - (condition-case nil (delete-file ".git/MERGE_MSG") (error nil)) - (with-current-buffer buffer (erase-buffer)) - (git-set-files-state files 'uptodate) - (git-run-command nil nil "rerere") - (git-refresh-files) - (git-refresh-ewoc-hf git-status) - (message "Committed %s." commit) - (git-run-hook "post-commit" nil)) - (message "Commit aborted.")))) - (message "No files to commit."))) + (message "Running git commit...") + (when + (and + (git-read-tree head-tree index-file) + (git-update-index nil files) ;update both the default index + (git-update-index index-file files) ;and the temporary one + (setq tree (git-write-tree index-file))) + (if (or (not (string-equal tree head-tree)) + (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? ")) + (let ((commit (git-commit-tree buffer tree head))) + (when commit + (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil)) + (condition-case nil (delete-file ".git/MERGE_MSG") (error nil)) + (with-current-buffer buffer (erase-buffer)) + (git-update-status-files (git-get-filenames files)) + (git-call-process nil "rerere") + (git-call-process nil "gc" "--auto") + (message "Committed %s." commit) + (git-run-hook "post-commit" nil))) + (message "Commit aborted.")))) (delete-file index-file)))))) @@ -792,7 +977,8 @@ Return the list of files that haven't been handled." "Mark all files." (interactive) (unless git-status (error "Not in git-status buffer.")) - (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status) + (ewoc-map (lambda (info) (unless (git-fileinfo->marked info) + (setf (git-fileinfo->marked info) t))) git-status) ; move back to goal column after invalidate (when goal-column (move-to-column goal-column))) @@ -800,7 +986,9 @@ Return the list of files that haven't been handled." "Unmark all files." (interactive) (unless git-status (error "Not in git-status buffer.")) - (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status) + (ewoc-map (lambda (info) (when (git-fileinfo->marked info) + (setf (git-fileinfo->marked info) nil) + t)) git-status) ; move back to goal column after invalidate (when goal-column (move-to-column goal-column))) @@ -850,14 +1038,21 @@ Return the list of files that haven't been handled." (setq node (ewoc-prev git-status node))) (ewoc-goto-node git-status last))) +(defun git-insert-file (file) + "Insert file(s) into the git-status buffer." + (interactive "fInsert file: ") + (git-update-status-files (list (file-relative-name file)))) + (defun git-add-file () "Add marked file(s) to the index cache." (interactive) - (let ((files (git-get-filenames (git-marked-files-state 'unknown)))) + (let ((files (git-get-filenames (git-marked-files-state 'unknown 'ignored 'unmerged)))) + ;; FIXME: add support for directories (unless files (push (file-relative-name (read-file-name "File to add: " nil nil t)) files)) - (apply #'git-run-command nil nil "update-index" "--add" "--" files) - (git-update-status-files files 'uptodate))) + (when (apply 'git-call-process-display-error "update-index" "--add" "--" files) + (git-update-status-files files) + (git-success-message "Added" files)))) (defun git-ignore-file () "Add marked file(s) to the ignore list." @@ -866,61 +1061,118 @@ Return the list of files that haven't been handled." (unless files (push (file-relative-name (read-file-name "File to ignore: " nil nil t)) files)) (dolist (f files) (git-append-to-ignore f)) - (git-update-status-files files 'ignored))) + (git-update-status-files files) + (git-success-message "Ignored" files))) (defun git-remove-file () "Remove the marked file(s)." (interactive) - (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate)))) + (let ((files (git-get-filenames (git-marked-files-state 'added 'modified 'unknown 'uptodate 'ignored)))) (unless files (push (file-relative-name (read-file-name "File to remove: " nil nil t)) files)) (if (yes-or-no-p - (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" ""))) + (if (cdr files) + (format "Remove %d files? " (length files)) + (format "Remove %s? " (car files)))) (progn (dolist (name files) - (when (file-exists-p name) (delete-file name))) - (apply #'git-run-command nil nil "update-index" "--remove" "--" files) - (git-update-status-files files nil)) + (ignore-errors + (if (file-directory-p name) + (delete-directory name) + (delete-file name)))) + (when (apply 'git-call-process-display-error "update-index" "--remove" "--" files) + (git-update-status-files files) + (git-success-message "Removed" files))) (message "Aborting")))) (defun git-revert-file () "Revert changes to the marked file(s)." (interactive) - (let ((files (git-marked-files)) + (let ((files (git-marked-files-state 'added 'deleted 'modified 'unmerged)) added modified) (when (and files (yes-or-no-p - (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" "")))) + (if (cdr files) + (format "Revert %d files? " (length files)) + (format "Revert %s? " (git-fileinfo->name (car files)))))) (dolist (info files) (case (git-fileinfo->state info) ('added (push (git-fileinfo->name info) added)) ('deleted (push (git-fileinfo->name info) modified)) ('unmerged (push (git-fileinfo->name info) modified)) ('modified (push (git-fileinfo->name info) modified)))) - (when added - (apply #'git-run-command nil nil "update-index" "--force-remove" "--" added)) - (when modified - (apply #'git-run-command nil nil "checkout" "HEAD" modified)) - (git-update-status-files (append added modified) 'uptodate)))) - -(defun git-resolve-file () - "Resolve conflicts in marked file(s)." - (interactive) - (let ((files (git-get-filenames (git-marked-files-state 'unmerged)))) - (when files - (apply #'git-run-command nil nil "update-index" "--" files) - (git-update-status-files files 'uptodate)))) + ;; check if a buffer contains one of the files and isn't saved + (dolist (file modified) + (let ((buffer (get-file-buffer file))) + (when (and buffer (buffer-modified-p buffer)) + (error "Buffer %s is modified. Please kill or save modified buffers before reverting." (buffer-name buffer))))) + (let ((ok (and + (or (not added) + (apply 'git-call-process-display-error "update-index" "--force-remove" "--" added)) + (or (not modified) + (apply 'git-call-process-display-error "checkout" "HEAD" modified)))) + (names (git-get-filenames files))) + (git-update-status-files names) + (when ok + (dolist (file modified) + (let ((buffer (get-file-buffer file))) + (when buffer (with-current-buffer buffer (revert-buffer t t t))))) + (git-success-message "Reverted" names)))))) (defun git-remove-handled () "Remove handled files from the status list." (interactive) (ewoc-filter git-status (lambda (info) - (not (or (eq (git-fileinfo->state info) 'ignored) - (eq (git-fileinfo->state info) 'uptodate))))) + (case (git-fileinfo->state info) + ('ignored git-show-ignored) + ('uptodate git-show-uptodate) + ('unknown git-show-unknown) + (t t)))) (unless (ewoc-nth git-status 0) ; refresh header if list is empty (git-refresh-ewoc-hf git-status))) +(defun git-toggle-show-uptodate () + "Toogle the option for showing up-to-date files." + (interactive) + (if (setq git-show-uptodate (not git-show-uptodate)) + (git-refresh-status) + (git-remove-handled))) + +(defun git-toggle-show-ignored () + "Toogle the option for showing ignored files." + (interactive) + (if (setq git-show-ignored (not git-show-ignored)) + (progn + (message "Inserting ignored files...") + (git-run-ls-files-with-excludes git-status nil 'ignored "-o" "-i") + (git-refresh-files) + (git-refresh-ewoc-hf git-status) + (message "Inserting ignored files...done")) + (git-remove-handled))) + +(defun git-toggle-show-unknown () + "Toogle the option for showing unknown files." + (interactive) + (if (setq git-show-unknown (not git-show-unknown)) + (progn + (message "Inserting unknown files...") + (git-run-ls-files-with-excludes git-status nil 'unknown "-o") + (git-refresh-files) + (git-refresh-ewoc-hf git-status) + (message "Inserting unknown files...done")) + (git-remove-handled))) + +(defun git-expand-directory (info) + "Expand the directory represented by INFO to list its files." + (when (eq (lsh (git-fileinfo->new-perm info) -9) ?\110) + (let ((dir (git-fileinfo->name info))) + (git-set-filenames-state git-status (list dir) nil) + (git-run-ls-files-with-excludes git-status (list (concat dir "/")) 'unknown "-o") + (git-refresh-files) + (git-refresh-ewoc-hf git-status) + t))) + (defun git-setup-diff-buffer (buffer) "Setup a buffer for displaying a diff." (let ((dir default-directory)) @@ -930,7 +1182,9 @@ Return the list of files that haven't been handled." (setq default-directory dir) (setq buffer-read-only t))) (display-buffer buffer) - (shrink-window-if-larger-than-buffer)) + ; shrink window only if it displays the status buffer + (when (eq (window-buffer) (current-buffer)) + (shrink-window-if-larger-than-buffer))) (defun git-diff-file () "Diff the marked file(s) against HEAD." @@ -1003,6 +1257,11 @@ Return the list of files that haven't been handled." (with-current-buffer log-edit-parent-buffer (git-get-filenames (git-marked-files-state 'added 'deleted 'modified)))) +(defun git-log-edit-diff () + "Run a diff of the current files being committed from a log-edit buffer." + (with-current-buffer log-edit-parent-buffer + (git-diff-file))) + (defun git-append-sign-off (name email) "Append a Signed-off-by entry to the current buffer, avoiding duplicates." (let ((sign-off (format "Signed-off-by: %s <%s>" name email)) @@ -1015,11 +1274,10 @@ Return the list of files that haven't been handled." (goto-char (point-max)) (insert sign-off "\n")))) -(defun git-setup-log-buffer (buffer &optional author-name author-email subject date msg) +(defun git-setup-log-buffer (buffer &optional merge-heads author-name author-email subject date msg) "Setup the log buffer for a commit." (unless git-status (error "Not in git-status buffer.")) - (let ((merge-heads (git-get-merge-heads)) - (dir default-directory) + (let ((dir default-directory) (committer-name (git-get-committer-name)) (committer-email (git-get-committer-email)) (sign-off git-append-signed-off-by)) @@ -1033,24 +1291,31 @@ Return the list of files that haven't been handled." (or author-email committer-email) (if date (format "Date: %s\n" date) "") (if merge-heads - (format "Parent: %s\n%s\n" - (git-rev-parse "HEAD") - (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n")) + (format "Merge: %s\n" + (mapconcat 'identity merge-heads " ")) "")) 'face 'git-header-face) (propertize git-log-msg-separator 'face 'git-separator-face) "\n") (when subject (insert subject "\n\n")) (cond (msg (insert msg "\n")) - ((file-readable-p ".dotest/msg") - (insert-file-contents ".dotest/msg")) + ((file-readable-p ".git/rebase-apply/msg") + (insert-file-contents ".git/rebase-apply/msg")) ((file-readable-p ".git/MERGE_MSG") (insert-file-contents ".git/MERGE_MSG"))) ; delete empty lines at end (goto-char (point-min)) (when (re-search-forward "\n+\\'" nil t) (replace-match "\n" t t)) - (when sign-off (git-append-sign-off committer-name committer-email))))) + (when sign-off (git-append-sign-off committer-name committer-email))) + buffer)) + +(define-derived-mode git-log-edit-mode log-edit-mode "Git-Log-Edit" + "Major mode for editing git log messages. + +Set up git-specific `font-lock-keywords' for `log-edit-mode'." + (set (make-local-variable 'font-lock-defaults) + '(git-log-edit-font-lock-keywords t t))) (defun git-commit-file () "Commit the marked file(s), asking for a commit message." @@ -1061,9 +1326,9 @@ Return the list of files that haven't been handled." (coding-system (git-get-commits-coding-system)) author-name author-email subject date) (when (eq 0 (buffer-size buffer)) - (when (file-readable-p ".dotest/info") + (when (file-readable-p ".git/rebase-apply/info") (with-temp-buffer - (insert-file-contents ".dotest/info") + (insert-file-contents ".git/rebase-apply/info") (goto-char (point-min)) (when (re-search-forward "^Author: \\(.*\\)\nEmail: \\(.*\\)$" nil t) (setq author-name (match-string 1)) @@ -1074,20 +1339,142 @@ Return the list of files that haven't been handled." (goto-char (point-min)) (when (re-search-forward "^Date: \\(.*\\)$" nil t) (setq date (match-string 1))))) - (git-setup-log-buffer buffer author-name author-email subject date)) - (log-edit #'git-do-commit nil #'git-log-edit-files buffer) - (setq font-lock-keywords (font-lock-compile-keywords git-log-edit-font-lock-keywords)) + (git-setup-log-buffer buffer (git-get-merge-heads) author-name author-email subject date)) + (if (boundp 'log-edit-diff-function) + (log-edit 'git-do-commit nil '((log-edit-listfun . git-log-edit-files) + (log-edit-diff-function . git-log-edit-diff)) buffer 'git-log-edit-mode) + (log-edit 'git-do-commit nil 'git-log-edit-files buffer + 'git-log-edit-mode)) + (setq paragraph-separate (concat (regexp-quote git-log-msg-separator) "$\\|Author: \\|Date: \\|Merge: \\|Signed-off-by: \\|\f\\|[ ]*$")) (setq buffer-file-coding-system coding-system) (re-search-forward (regexp-quote (concat git-log-msg-separator "\n")) nil t)))) +(defun git-setup-commit-buffer (commit) + "Setup the commit buffer with the contents of COMMIT." + (let (parents author-name author-email subject date msg) + (with-temp-buffer + (let ((coding-system (git-get-logoutput-coding-system))) + (git-call-process t "log" "-1" "--pretty=medium" "--abbrev=40" commit) + (goto-char (point-min)) + (when (re-search-forward "^Merge: *\\(.*\\)$" nil t) + (setq parents (cdr (split-string (match-string 1) " +")))) + (when (re-search-forward "^Author: *\\(.*\\) <\\(.*\\)>$" nil t) + (setq author-name (match-string 1)) + (setq author-email (match-string 2))) + (when (re-search-forward "^Date: *\\(.*\\)$" nil t) + (setq date (match-string 1))) + (while (re-search-forward "^ \\(.*\\)$" nil t) + (push (match-string 1) msg)) + (setq msg (nreverse msg)) + (setq subject (pop msg)) + (while (and msg (zerop (length (car msg))) (pop msg))))) + (git-setup-log-buffer (get-buffer-create "*git-commit*") + parents author-name author-email subject date + (mapconcat #'identity msg "\n")))) + +(defun git-get-commit-files (commit) + "Retrieve a sorted list of files modified by COMMIT." + (let (files) + (with-temp-buffer + (git-call-process t "diff-tree" "-m" "-r" "-z" "--name-only" "--no-commit-id" "--root" commit) + (goto-char (point-min)) + (while (re-search-forward "\\([^\0]*\\)\0" nil t 1) + (push (match-string 1) files))) + (sort files #'string-lessp))) + +(defun git-read-commit-name (prompt &optional default) + "Ask for a commit name, with completion for local branch, remote branch and tag." + (completing-read prompt + (list* "HEAD" "ORIG_HEAD" "FETCH_HEAD" (mapcar #'car (git-for-each-ref))) + nil nil nil nil default)) + +(defun git-checkout (branch &optional merge) + "Checkout a branch, tag, or any commit. +Use a prefix arg if git should merge while checking out." + (interactive + (list (git-read-commit-name "Checkout: ") + current-prefix-arg)) + (unless git-status (error "Not in git-status buffer.")) + (let ((args (list branch "--"))) + (when merge (push "-m" args)) + (when (apply #'git-call-process-display-error "checkout" args) + (git-update-status-files)))) + +(defun git-branch (branch) + "Create a branch from the current HEAD and switch to it." + (interactive (list (git-read-commit-name "Branch: "))) + (unless git-status (error "Not in git-status buffer.")) + (if (git-rev-parse (concat "refs/heads/" branch)) + (if (yes-or-no-p (format "Branch %s already exists, replace it? " branch)) + (and (git-call-process-display-error "branch" "-f" branch) + (git-call-process-display-error "checkout" branch)) + (message "Canceled.")) + (git-call-process-display-error "checkout" "-b" branch)) + (git-refresh-ewoc-hf git-status)) + +(defun git-amend-commit () + "Undo the last commit on HEAD, and set things up to commit an +amended version of it." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (when (git-empty-db-p) (error "No commit to amend.")) + (let* ((commit (git-rev-parse "HEAD")) + (files (git-get-commit-files commit))) + (when (if (git-rev-parse "HEAD^") + (git-call-process-display-error "reset" "--soft" "HEAD^") + (and (git-update-ref "ORIG_HEAD" commit) + (git-update-ref "HEAD" nil commit))) + (git-update-status-files files t) + (git-setup-commit-buffer commit) + (git-commit-file)))) + +(defun git-cherry-pick-commit (arg) + "Cherry-pick a commit." + (interactive (list (git-read-commit-name "Cherry-pick commit: "))) + (unless git-status (error "Not in git-status buffer.")) + (let ((commit (git-rev-parse (concat arg "^0")))) + (unless commit (error "Not a valid commit '%s'." arg)) + (when (git-rev-parse (concat commit "^2")) + (error "Cannot cherry-pick a merge commit.")) + (let ((files (git-get-commit-files commit)) + (ok (git-call-process-display-error "cherry-pick" "-n" commit))) + (git-update-status-files files ok) + (with-current-buffer (git-setup-commit-buffer commit) + (goto-char (point-min)) + (if (re-search-forward "^\n*Signed-off-by:" nil t 1) + (goto-char (match-beginning 0)) + (goto-char (point-max))) + (insert "(cherry picked from commit " commit ")\n")) + (when ok (git-commit-file))))) + +(defun git-revert-commit (arg) + "Revert a commit." + (interactive (list (git-read-commit-name "Revert commit: "))) + (unless git-status (error "Not in git-status buffer.")) + (let ((commit (git-rev-parse (concat arg "^0")))) + (unless commit (error "Not a valid commit '%s'." arg)) + (when (git-rev-parse (concat commit "^2")) + (error "Cannot revert a merge commit.")) + (let ((files (git-get-commit-files commit)) + (subject (git-get-commit-description commit)) + (ok (git-call-process-display-error "revert" "-n" commit))) + (git-update-status-files files ok) + (when (string-match "^[0-9a-f]+ - \\(.*\\)$" subject) + (setq subject (match-string 1 subject))) + (git-setup-log-buffer (get-buffer-create "*git-commit*") + (git-get-merge-heads) nil nil (format "Revert \"%s\"" subject) nil + (format "This reverts commit %s.\n" commit)) + (when ok (git-commit-file))))) + (defun git-find-file () "Visit the current file in its own buffer." (interactive) (unless git-status (error "Not in git-status buffer.")) (let ((info (ewoc-data (ewoc-locate git-status)))) - (find-file (git-fileinfo->name info)) - (when (eq 'unmerged (git-fileinfo->state info)) - (smerge-mode 1)))) + (unless (git-expand-directory info) + (find-file (git-fileinfo->name info)) + (when (eq 'unmerged (git-fileinfo->state info)) + (smerge-mode 1))))) (defun git-find-file-other-window () "Visit the current file in its own buffer in another window." @@ -1116,16 +1503,10 @@ Return the list of files that haven't been handled." (defun git-refresh-status () "Refresh the git status buffer." (interactive) - (let* ((status git-status) - (pos (ewoc-locate status)) - (cur-name (and pos (git-fileinfo->name (ewoc-data pos))))) - (unless status (error "Not in git-status buffer.")) - (git-run-command nil nil "update-index" "--refresh") - (git-clear-status status) - (git-update-status-files nil) - ; move point to the current file name if any - (let ((node (and cur-name (git-find-status-file status cur-name)))) - (when node (ewoc-goto-node status node))))) + (unless git-status (error "Not in git-status buffer.")) + (message "Refreshing git status...") + (git-update-status-files) + (message "Refreshing git status...done")) (defun git-status-quit () "Quit git-status mode." @@ -1146,19 +1527,23 @@ Return the list of files that haven't been handled." (unless git-status-mode-map (let ((map (make-keymap)) - (diff-map (make-sparse-keymap))) + (commit-map (make-sparse-keymap)) + (diff-map (make-sparse-keymap)) + (toggle-map (make-sparse-keymap))) (suppress-keymap map) (define-key map "?" 'git-help) (define-key map "h" 'git-help) (define-key map " " 'git-next-file) (define-key map "a" 'git-add-file) (define-key map "c" 'git-commit-file) + (define-key map "\C-c" commit-map) (define-key map "d" diff-map) (define-key map "=" 'git-diff-file) (define-key map "f" 'git-find-file) (define-key map "\r" 'git-find-file) (define-key map "g" 'git-refresh-status) (define-key map "i" 'git-ignore-file) + (define-key map "I" 'git-insert-file) (define-key map "l" 'git-log-file) (define-key map "m" 'git-mark-file) (define-key map "M" 'git-mark-all) @@ -1169,7 +1554,7 @@ Return the list of files that haven't been handled." (define-key map "P" 'git-prev-unmerged-file) (define-key map "q" 'git-status-quit) (define-key map "r" 'git-remove-file) - (define-key map "R" 'git-resolve-file) + (define-key map "t" toggle-map) (define-key map "T" 'git-toggle-all-marks) (define-key map "u" 'git-unmark-file) (define-key map "U" 'git-revert-file) @@ -1177,6 +1562,12 @@ Return the list of files that haven't been handled." (define-key map "x" 'git-remove-handled) (define-key map "\C-?" 'git-unmark-file-up) (define-key map "\M-\C-?" 'git-unmark-all) + ; the commit submap + (define-key commit-map "\C-a" 'git-amend-commit) + (define-key commit-map "\C-b" 'git-branch) + (define-key commit-map "\C-o" 'git-checkout) + (define-key commit-map "\C-p" 'git-cherry-pick-commit) + (define-key commit-map "\C-v" 'git-revert-commit) ; the diff submap (define-key diff-map "b" 'git-diff-file-base) (define-key diff-map "c" 'git-diff-file-combined) @@ -1186,7 +1577,56 @@ Return the list of files that haven't been handled." (define-key diff-map "h" 'git-diff-file-merge-head) (define-key diff-map "m" 'git-diff-file-mine) (define-key diff-map "o" 'git-diff-file-other) - (setq git-status-mode-map map))) + ; the toggle submap + (define-key toggle-map "u" 'git-toggle-show-uptodate) + (define-key toggle-map "i" 'git-toggle-show-ignored) + (define-key toggle-map "k" 'git-toggle-show-unknown) + (define-key toggle-map "m" 'git-toggle-all-marks) + (setq git-status-mode-map map)) + (easy-menu-define git-menu git-status-mode-map + "Git Menu" + `("Git" + ["Refresh" git-refresh-status t] + ["Commit" git-commit-file t] + ["Checkout..." git-checkout t] + ["New Branch..." git-branch t] + ["Cherry-pick Commit..." git-cherry-pick-commit t] + ["Revert Commit..." git-revert-commit t] + ("Merge" + ["Next Unmerged File" git-next-unmerged-file t] + ["Prev Unmerged File" git-prev-unmerged-file t] + ["Interactive Merge File" git-find-file-imerge t] + ["Diff Against Common Base File" git-diff-file-base t] + ["Diff Combined" git-diff-file-combined t] + ["Diff Against Merge Head" git-diff-file-merge-head t] + ["Diff Against Mine" git-diff-file-mine t] + ["Diff Against Other" git-diff-file-other t]) + "--------" + ["Add File" git-add-file t] + ["Revert File" git-revert-file t] + ["Ignore File" git-ignore-file t] + ["Remove File" git-remove-file t] + ["Insert File" git-insert-file t] + "--------" + ["Find File" git-find-file t] + ["View File" git-view-file t] + ["Diff File" git-diff-file t] + ["Interactive Diff File" git-diff-file-idiff t] + ["Log" git-log-file t] + "--------" + ["Mark" git-mark-file t] + ["Mark All" git-mark-all t] + ["Unmark" git-unmark-file t] + ["Unmark All" git-unmark-all t] + ["Toggle All Marks" git-toggle-all-marks t] + ["Hide Handled Files" git-remove-handled t] + "--------" + ["Show Uptodate Files" git-toggle-show-uptodate :style toggle :selected git-show-uptodate] + ["Show Ignored Files" git-toggle-show-ignored :style toggle :selected git-show-ignored] + ["Show Unknown Files" git-toggle-show-unknown :style toggle :selected git-show-unknown] + "--------" + ["Quit" git-status-quit t]))) + ;; git mode should only run in the *git status* buffer (put 'git-status-mode 'mode-class 'special) @@ -1207,6 +1647,9 @@ Commands: (let ((status (ewoc-create 'git-fileinfo-prettyprint "" ""))) (set (make-local-variable 'git-status) status)) (set (make-local-variable 'list-buffers-directory) default-directory) + (make-local-variable 'git-show-uptodate) + (make-local-variable 'git-show-ignored) + (make-local-variable 'git-show-unknown) (run-hooks 'git-status-mode-hook))) (defun git-find-status-buffer (dir) @@ -1219,7 +1662,7 @@ Commands: (with-current-buffer buffer (when (and list-buffers-directory (string-equal fulldir (expand-file-name list-buffers-directory)) - (string-match "\\*git-status\\*$" (buffer-name buffer))) + (eq major-mode 'git-status-mode)) (setq found buffer)))) (setq list (cdr list))) found)) @@ -1235,9 +1678,24 @@ Commands: (cd dir) (git-status-mode) (git-refresh-status) - (goto-char (point-min))) + (goto-char (point-min)) + (add-hook 'after-save-hook 'git-update-saved-file)) (message "%s is not a git working tree." dir))) +(defun git-update-saved-file () + "Update the corresponding git-status buffer when a file is saved. +Meant to be used in `after-save-hook'." + (let* ((file (expand-file-name buffer-file-name)) + (dir (condition-case nil (git-get-top-dir (file-name-directory file)) (error nil))) + (buffer (and dir (git-find-status-buffer dir)))) + (when buffer + (with-current-buffer buffer + (let ((filename (file-relative-name file dir))) + ; skip files located inside the .git directory + (unless (string-match "^\\.git/" filename) + (git-call-process nil "add" "--refresh" "--" filename) + (git-update-status-files (list filename)))))))) + (defun git-help () "Display help for Git mode." (interactive) diff --git a/contrib/emacs/vc-git.el b/contrib/emacs/vc-git.el deleted file mode 100644 index b8f6be5c0a..0000000000 --- a/contrib/emacs/vc-git.el +++ /dev/null @@ -1,216 +0,0 @@ -;;; vc-git.el --- VC backend for the git version control system - -;; Copyright (C) 2006 Alexandre Julliard - -;; This program is free software; you can redistribute it and/or -;; modify it under the terms of the GNU General Public License as -;; published by the Free Software Foundation; either version 2 of -;; the License, or (at your option) any later version. -;; -;; This program is distributed in the hope that it will be -;; useful, but WITHOUT ANY WARRANTY; without even the implied -;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -;; PURPOSE. See the GNU General Public License for more details. -;; -;; You should have received a copy of the GNU General Public -;; License along with this program; if not, write to the Free -;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, -;; MA 02111-1307 USA - -;;; Commentary: - -;; This file contains a VC backend for the git version control -;; system. -;; -;; To install: put this file on the load-path and add GIT to the list -;; of supported backends in `vc-handled-backends'; the following line, -;; placed in your ~/.emacs, will accomplish this: -;; -;; (add-to-list 'vc-handled-backends 'GIT) -;; -;; TODO -;; - changelog generation -;; - working with revisions other than HEAD -;; - -(eval-when-compile (require 'cl)) - -(defvar git-commits-coding-system 'utf-8 - "Default coding system for git commits.") - -(defun vc-git--run-command-string (file &rest args) - "Run a git command on FILE and return its output as string." - (let* ((ok t) - (str (with-output-to-string - (with-current-buffer standard-output - (unless (eq 0 (apply #'call-process "git" nil '(t nil) nil - (append args (list (file-relative-name file))))) - (setq ok nil)))))) - (and ok str))) - -(defun vc-git--run-command (file &rest args) - "Run a git command on FILE, discarding any output." - (let ((name (file-relative-name file))) - (eq 0 (apply #'call-process "git" nil (get-buffer "*Messages") nil (append args (list name)))))) - -(defun vc-git-registered (file) - "Check whether FILE is registered with git." - (with-temp-buffer - (let* ((dir (file-name-directory file)) - (name (file-relative-name file dir))) - (and (ignore-errors - (when dir (cd dir)) - (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name))) - (let ((str (buffer-string))) - (and (> (length str) (length name)) - (string= (substring str 0 (1+ (length name))) (concat name "\0")))))))) - -(defun vc-git-state (file) - "git-specific version of `vc-state'." - (let ((diff (vc-git--run-command-string file "diff-index" "-z" "HEAD" "--"))) - (if (and diff (string-match ":[0-7]\\{6\\} [0-7]\\{6\\} [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} [ADMU]\0[^\0]+\0" diff)) - 'edited - 'up-to-date))) - -(defun vc-git-workfile-version (file) - "git-specific version of `vc-workfile-version'." - (let ((str (with-output-to-string - (with-current-buffer standard-output - (call-process "git" nil '(t nil) nil "symbolic-ref" "HEAD"))))) - (if (string-match "^\\(refs/heads/\\)?\\(.+\\)$" str) - (match-string 2 str) - str))) - -(defun vc-git-symbolic-commit (commit) - "Translate COMMIT string into symbolic form. -Returns nil if not possible." - (and commit - (with-temp-buffer - (and - (zerop - (call-process "git" nil '(t nil) nil "name-rev" - "--name-only" "--tags" - commit)) - (goto-char (point-min)) - (= (forward-line 2) 1) - (bolp) - (buffer-substring-no-properties (point-min) (1- (point-max))))))) - -(defun vc-git-previous-version (file rev) - "git-specific version of `vc-previous-version'." - (let ((default-directory (file-name-directory (expand-file-name file))) - (file (file-name-nondirectory file))) - (vc-git-symbolic-commit - (with-temp-buffer - (and - (zerop - (call-process "git" nil '(t nil) nil "rev-list" - "-2" rev "--" file)) - (goto-char (point-max)) - (bolp) - (zerop (forward-line -1)) - (not (bobp)) - (buffer-substring-no-properties - (point) - (1- (point-max)))))))) - -(defun vc-git-next-version (file rev) - "git-specific version of `vc-next-version'." - (let* ((default-directory (file-name-directory - (expand-file-name file))) - (file (file-name-nondirectory file)) - (current-rev - (with-temp-buffer - (and - (zerop - (call-process "git" nil '(t nil) nil "rev-list" - "-1" rev "--" file)) - (goto-char (point-max)) - (bolp) - (zerop (forward-line -1)) - (bobp) - (buffer-substring-no-properties - (point) - (1- (point-max))))))) - (and current-rev - (vc-git-symbolic-commit - (with-temp-buffer - (and - (zerop - (call-process "git" nil '(t nil) nil "rev-list" - "HEAD" "--" file)) - (goto-char (point-min)) - (search-forward current-rev nil t) - (zerop (forward-line -1)) - (buffer-substring-no-properties - (point) - (progn (forward-line 1) (1- (point)))))))))) - -(defun vc-git-revert (file &optional contents-done) - "Revert FILE to the version stored in the git repository." - (if contents-done - (vc-git--run-command file "update-index" "--") - (vc-git--run-command file "checkout" "HEAD"))) - -(defun vc-git-checkout-model (file) - 'implicit) - -(defun vc-git-workfile-unchanged-p (file) - (let ((sha1 (vc-git--run-command-string file "hash-object" "--")) - (head (vc-git--run-command-string file "ls-tree" "-z" "HEAD" "--"))) - (and head - (string-match "[0-7]\\{6\\} blob \\([0-9a-f]\\{40\\}\\)\t[^\0]+\0" head) - (string= (car (split-string sha1 "\n")) (match-string 1 head))))) - -(defun vc-git-register (file &optional rev comment) - "Register FILE into the git version-control system." - (vc-git--run-command file "update-index" "--add" "--")) - -(defun vc-git-print-log (file &optional buffer) - (let ((name (file-relative-name file)) - (coding-system-for-read git-commits-coding-system)) - (vc-do-command buffer 'async "git" name "rev-list" "--pretty" "HEAD" "--"))) - -(defun vc-git-diff (file &optional rev1 rev2 buffer) - (let ((name (file-relative-name file)) - (buf (or buffer "*vc-diff*"))) - (if (and rev1 rev2) - (vc-do-command buf 0 "git" name "diff-tree" "-p" rev1 rev2 "--") - (vc-do-command buf 0 "git" name "diff-index" "-p" (or rev1 "HEAD") "--")) - ; git-diff-index doesn't set exit status like diff does - (if (vc-git-workfile-unchanged-p file) 0 1))) - -(defun vc-git-checkin (file rev comment) - (let ((coding-system-for-write git-commits-coding-system)) - (vc-git--run-command file "commit" "-m" comment "--only" "--"))) - -(defun vc-git-checkout (file &optional editable rev destfile) - (if destfile - (let ((fullname (substring - (vc-git--run-command-string file "ls-files" "-z" "--full-name" "--") - 0 -1)) - (coding-system-for-read 'no-conversion) - (coding-system-for-write 'no-conversion)) - (with-temp-file destfile - (eq 0 (call-process "git" nil t nil "cat-file" "blob" - (concat (or rev "HEAD") ":" fullname))))) - (vc-git--run-command file "checkout" (or rev "HEAD")))) - -(defun vc-git-annotate-command (file buf &optional rev) - ; FIXME: rev is ignored - (let ((name (file-relative-name file))) - (call-process "git" nil buf nil "blame" name))) - -(defun vc-git-annotate-time () - (and (re-search-forward "[0-9a-f]+ (.* \\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\) \\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\) \\([-+0-9]+\\) +[0-9]+)" nil t) - (vc-annotate-convert-time - (apply #'encode-time (mapcar (lambda (match) (string-to-number (match-string match))) '(6 5 4 3 2 1 7)))))) - -;; Not really useful since we can't do anything with the revision yet -;;(defun vc-annotate-extract-revision-at-line () -;; (save-excursion -;; (move-beginning-of-line 1) -;; (and (looking-at "[0-9a-f]+") -;; (buffer-substring (match-beginning 0) (match-end 0))))) - -(provide 'vc-git) diff --git a/contrib/examples/README b/contrib/examples/README new file mode 100644 index 0000000000..6946f3dd2a --- /dev/null +++ b/contrib/examples/README @@ -0,0 +1,3 @@ +These are original scripted implementations, kept primarily for their +reference value to any aspiring plumbing users who want to learn how +pieces can be fit together. diff --git a/contrib/examples/builtin-fetch--tool.c b/contrib/examples/builtin-fetch--tool.c new file mode 100644 index 0000000000..0d54aa7061 --- /dev/null +++ b/contrib/examples/builtin-fetch--tool.c @@ -0,0 +1,574 @@ +#include "builtin.h" +#include "cache.h" +#include "refs.h" +#include "commit.h" +#include "sigchain.h" + +static char *get_stdin(void) +{ + struct strbuf buf = STRBUF_INIT; + if (strbuf_read(&buf, 0, 1024) < 0) { + die_errno("error reading standard input"); + } + return strbuf_detach(&buf, NULL); +} + +static void show_new(enum object_type type, unsigned char *sha1_new) +{ + fprintf(stderr, " %s: %s\n", typename(type), + find_unique_abbrev(sha1_new, DEFAULT_ABBREV)); +} + +static int update_ref_env(const char *action, + const char *refname, + unsigned char *sha1, + unsigned char *oldval) +{ + char msg[1024]; + const char *rla = getenv("GIT_REFLOG_ACTION"); + + if (!rla) + rla = "(reflog update)"; + if (snprintf(msg, sizeof(msg), "%s: %s", rla, action) >= sizeof(msg)) + warning("reflog message too long: %.*s...", 50, msg); + return update_ref(msg, refname, sha1, oldval, 0, QUIET_ON_ERR); +} + +static int update_local_ref(const char *name, + const char *new_head, + const char *note, + int verbose, int force) +{ + unsigned char sha1_old[20], sha1_new[20]; + char oldh[41], newh[41]; + struct commit *current, *updated; + enum object_type type; + + if (get_sha1_hex(new_head, sha1_new)) + die("malformed object name %s", new_head); + + type = sha1_object_info(sha1_new, NULL); + if (type < 0) + die("object %s not found", new_head); + + if (!*name) { + /* Not storing */ + if (verbose) { + fprintf(stderr, "* fetched %s\n", note); + show_new(type, sha1_new); + } + return 0; + } + + if (get_sha1(name, sha1_old)) { + const char *msg; + just_store: + /* new ref */ + if (!strncmp(name, "refs/tags/", 10)) + msg = "storing tag"; + else + msg = "storing head"; + fprintf(stderr, "* %s: storing %s\n", + name, note); + show_new(type, sha1_new); + return update_ref_env(msg, name, sha1_new, NULL); + } + + if (!hashcmp(sha1_old, sha1_new)) { + if (verbose) { + fprintf(stderr, "* %s: same as %s\n", name, note); + show_new(type, sha1_new); + } + return 0; + } + + if (!strncmp(name, "refs/tags/", 10)) { + fprintf(stderr, "* %s: updating with %s\n", name, note); + show_new(type, sha1_new); + return update_ref_env("updating tag", name, sha1_new, NULL); + } + + current = lookup_commit_reference(sha1_old); + updated = lookup_commit_reference(sha1_new); + if (!current || !updated) + goto just_store; + + strcpy(oldh, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV)); + strcpy(newh, find_unique_abbrev(sha1_new, DEFAULT_ABBREV)); + + if (in_merge_bases(current, &updated, 1)) { + fprintf(stderr, "* %s: fast-forward to %s\n", + name, note); + fprintf(stderr, " old..new: %s..%s\n", oldh, newh); + return update_ref_env("fast-forward", name, sha1_new, sha1_old); + } + if (!force) { + fprintf(stderr, + "* %s: not updating to non-fast-forward %s\n", + name, note); + fprintf(stderr, + " old...new: %s...%s\n", oldh, newh); + return 1; + } + fprintf(stderr, + "* %s: forcing update to non-fast-forward %s\n", + name, note); + fprintf(stderr, " old...new: %s...%s\n", oldh, newh); + return update_ref_env("forced-update", name, sha1_new, sha1_old); +} + +static int append_fetch_head(FILE *fp, + const char *head, const char *remote, + const char *remote_name, const char *remote_nick, + const char *local_name, int not_for_merge, + int verbose, int force) +{ + struct commit *commit; + int remote_len, i, note_len; + unsigned char sha1[20]; + char note[1024]; + const char *what, *kind; + + if (get_sha1(head, sha1)) + return error("Not a valid object name: %s", head); + commit = lookup_commit_reference_gently(sha1, 1); + if (!commit) + not_for_merge = 1; + + if (!strcmp(remote_name, "HEAD")) { + kind = ""; + what = ""; + } + else if (!strncmp(remote_name, "refs/heads/", 11)) { + kind = "branch"; + what = remote_name + 11; + } + else if (!strncmp(remote_name, "refs/tags/", 10)) { + kind = "tag"; + what = remote_name + 10; + } + else if (!strncmp(remote_name, "refs/remotes/", 13)) { + kind = "remote-tracking branch"; + what = remote_name + 13; + } + else { + kind = ""; + what = remote_name; + } + + remote_len = strlen(remote); + for (i = remote_len - 1; remote[i] == '/' && 0 <= i; i--) + ; + remote_len = i + 1; + if (4 < i && !strncmp(".git", remote + i - 3, 4)) + remote_len = i - 3; + + note_len = 0; + if (*what) { + if (*kind) + note_len += sprintf(note + note_len, "%s ", kind); + note_len += sprintf(note + note_len, "'%s' of ", what); + } + note_len += sprintf(note + note_len, "%.*s", remote_len, remote); + fprintf(fp, "%s\t%s\t%s\n", + sha1_to_hex(commit ? commit->object.sha1 : sha1), + not_for_merge ? "not-for-merge" : "", + note); + return update_local_ref(local_name, head, note, verbose, force); +} + +static char *keep; +static void remove_keep(void) +{ + if (keep && *keep) + unlink(keep); +} + +static void remove_keep_on_signal(int signo) +{ + remove_keep(); + sigchain_pop(signo); + raise(signo); +} + +static char *find_local_name(const char *remote_name, const char *refs, + int *force_p, int *not_for_merge_p) +{ + const char *ref = refs; + int len = strlen(remote_name); + + while (ref) { + const char *next; + int single_force, not_for_merge; + + while (*ref == '\n') + ref++; + if (!*ref) + break; + next = strchr(ref, '\n'); + + single_force = not_for_merge = 0; + if (*ref == '+') { + single_force = 1; + ref++; + } + if (*ref == '.') { + not_for_merge = 1; + ref++; + if (*ref == '+') { + single_force = 1; + ref++; + } + } + if (!strncmp(remote_name, ref, len) && ref[len] == ':') { + const char *local_part = ref + len + 1; + int retlen; + + if (!next) + retlen = strlen(local_part); + else + retlen = next - local_part; + *force_p = single_force; + *not_for_merge_p = not_for_merge; + return xmemdupz(local_part, retlen); + } + ref = next; + } + return NULL; +} + +static int fetch_native_store(FILE *fp, + const char *remote, + const char *remote_nick, + const char *refs, + int verbose, int force) +{ + char buffer[1024]; + int err = 0; + + sigchain_push_common(remove_keep_on_signal); + atexit(remove_keep); + + while (fgets(buffer, sizeof(buffer), stdin)) { + int len; + char *cp; + char *local_name; + int single_force, not_for_merge; + + for (cp = buffer; *cp && !isspace(*cp); cp++) + ; + if (*cp) + *cp++ = 0; + len = strlen(cp); + if (len && cp[len-1] == '\n') + cp[--len] = 0; + if (!strcmp(buffer, "failed")) + die("Fetch failure: %s", remote); + if (!strcmp(buffer, "pack")) + continue; + if (!strcmp(buffer, "keep")) { + char *od = get_object_directory(); + int len = strlen(od) + strlen(cp) + 50; + keep = xmalloc(len); + sprintf(keep, "%s/pack/pack-%s.keep", od, cp); + continue; + } + + local_name = find_local_name(cp, refs, + &single_force, ¬_for_merge); + if (!local_name) + continue; + err |= append_fetch_head(fp, + buffer, remote, cp, remote_nick, + local_name, not_for_merge, + verbose, force || single_force); + } + return err; +} + +static int parse_reflist(const char *reflist) +{ + const char *ref; + + printf("refs='"); + for (ref = reflist; ref; ) { + const char *next; + while (*ref && isspace(*ref)) + ref++; + if (!*ref) + break; + for (next = ref; *next && !isspace(*next); next++) + ; + printf("\n%.*s", (int)(next - ref), ref); + ref = next; + } + printf("'\n"); + + printf("rref='"); + for (ref = reflist; ref; ) { + const char *next, *colon; + while (*ref && isspace(*ref)) + ref++; + if (!*ref) + break; + for (next = ref; *next && !isspace(*next); next++) + ; + if (*ref == '.') + ref++; + if (*ref == '+') + ref++; + colon = strchr(ref, ':'); + putchar('\n'); + printf("%.*s", (int)((colon ? colon : next) - ref), ref); + ref = next; + } + printf("'\n"); + return 0; +} + +static int expand_refs_wildcard(const char *ls_remote_result, int numrefs, + const char **refs) +{ + int i, matchlen, replacelen; + int found_one = 0; + const char *remote = *refs++; + numrefs--; + + if (numrefs == 0) { + fprintf(stderr, "Nothing specified for fetching with remote.%s.fetch\n", + remote); + printf("empty\n"); + } + + for (i = 0; i < numrefs; i++) { + const char *ref = refs[i]; + const char *lref = ref; + const char *colon; + const char *tail; + const char *ls; + const char *next; + + if (*lref == '+') + lref++; + colon = strchr(lref, ':'); + tail = lref + strlen(lref); + if (!(colon && + 2 < colon - lref && + colon[-1] == '*' && + colon[-2] == '/' && + 2 < tail - (colon + 1) && + tail[-1] == '*' && + tail[-2] == '/')) { + /* not a glob */ + if (!found_one++) + printf("explicit\n"); + printf("%s\n", ref); + continue; + } + + /* glob */ + if (!found_one++) + printf("glob\n"); + + /* lref to colon-2 is remote hierarchy name; + * colon+1 to tail-2 is local. + */ + matchlen = (colon-1) - lref; + replacelen = (tail-1) - (colon+1); + for (ls = ls_remote_result; ls; ls = next) { + const char *eol; + unsigned char sha1[20]; + int namelen; + + while (*ls && isspace(*ls)) + ls++; + next = strchr(ls, '\n'); + eol = !next ? (ls + strlen(ls)) : next; + if (!memcmp("^{}", eol-3, 3)) + continue; + if (eol - ls < 40) + continue; + if (get_sha1_hex(ls, sha1)) + continue; + ls += 40; + while (ls < eol && isspace(*ls)) + ls++; + /* ls to next (or eol) is the name. + * is it identical to lref to colon-2? + */ + if ((eol - ls) <= matchlen || + strncmp(ls, lref, matchlen)) + continue; + + /* Yes, it is a match */ + namelen = eol - ls; + if (lref != ref) + putchar('+'); + printf("%.*s:%.*s%.*s\n", + namelen, ls, + replacelen, colon + 1, + namelen - matchlen, ls + matchlen); + } + } + return 0; +} + +static int pick_rref(int sha1_only, const char *rref, const char *ls_remote_result) +{ + int err = 0; + int lrr_count = lrr_count, i, pass; + const char *cp; + struct lrr { + const char *line; + const char *name; + int namelen; + int shown; + } *lrr_list = lrr_list; + + for (pass = 0; pass < 2; pass++) { + /* pass 0 counts and allocates, pass 1 fills... */ + cp = ls_remote_result; + i = 0; + while (1) { + const char *np; + while (*cp && isspace(*cp)) + cp++; + if (!*cp) + break; + np = strchrnul(cp, '\n'); + if (pass) { + lrr_list[i].line = cp; + lrr_list[i].name = cp + 41; + lrr_list[i].namelen = np - (cp + 41); + } + i++; + cp = np; + } + if (!pass) { + lrr_count = i; + lrr_list = xcalloc(lrr_count, sizeof(*lrr_list)); + } + } + + while (1) { + const char *next; + int rreflen; + int i; + + while (*rref && isspace(*rref)) + rref++; + if (!*rref) + break; + next = strchrnul(rref, '\n'); + rreflen = next - rref; + + for (i = 0; i < lrr_count; i++) { + struct lrr *lrr = &(lrr_list[i]); + + if (rreflen == lrr->namelen && + !memcmp(lrr->name, rref, rreflen)) { + if (!lrr->shown) + printf("%.*s\n", + sha1_only ? 40 : lrr->namelen + 41, + lrr->line); + lrr->shown = 1; + break; + } + } + if (lrr_count <= i) { + error("pick-rref: %.*s not found", rreflen, rref); + err = 1; + } + rref = next; + } + free(lrr_list); + return err; +} + +int cmd_fetch__tool(int argc, const char **argv, const char *prefix) +{ + int verbose = 0; + int force = 0; + int sopt = 0; + + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp("-v", arg)) + verbose = 1; + else if (!strcmp("-f", arg)) + force = 1; + else if (!strcmp("-s", arg)) + sopt = 1; + else + break; + argc--; + argv++; + } + + if (argc <= 1) + return error("Missing subcommand"); + + if (!strcmp("append-fetch-head", argv[1])) { + int result; + FILE *fp; + char *filename; + + if (argc != 8) + return error("append-fetch-head takes 6 args"); + filename = git_path("FETCH_HEAD"); + fp = fopen(filename, "a"); + if (!fp) + return error("cannot open %s: %s", filename, strerror(errno)); + result = append_fetch_head(fp, argv[2], argv[3], + argv[4], argv[5], + argv[6], !!argv[7][0], + verbose, force); + fclose(fp); + return result; + } + if (!strcmp("native-store", argv[1])) { + int result; + FILE *fp; + char *filename; + + if (argc != 5) + return error("fetch-native-store takes 3 args"); + filename = git_path("FETCH_HEAD"); + fp = fopen(filename, "a"); + if (!fp) + return error("cannot open %s: %s", filename, strerror(errno)); + result = fetch_native_store(fp, argv[2], argv[3], argv[4], + verbose, force); + fclose(fp); + return result; + } + if (!strcmp("parse-reflist", argv[1])) { + const char *reflist; + if (argc != 3) + return error("parse-reflist takes 1 arg"); + reflist = argv[2]; + if (!strcmp(reflist, "-")) + reflist = get_stdin(); + return parse_reflist(reflist); + } + if (!strcmp("pick-rref", argv[1])) { + const char *ls_remote_result; + if (argc != 4) + return error("pick-rref takes 2 args"); + ls_remote_result = argv[3]; + if (!strcmp(ls_remote_result, "-")) + ls_remote_result = get_stdin(); + return pick_rref(sopt, argv[2], ls_remote_result); + } + if (!strcmp("expand-refs-wildcard", argv[1])) { + const char *reflist; + if (argc < 4) + return error("expand-refs-wildcard takes at least 2 args"); + reflist = argv[2]; + if (!strcmp(reflist, "-")) + reflist = get_stdin(); + return expand_refs_wildcard(reflist, argc - 3, argv + 3); + } + + return error("Unknown subcommand: %s", argv[1]); +} diff --git a/contrib/examples/git-checkout.sh b/contrib/examples/git-checkout.sh new file mode 100755 index 0000000000..1a7689a48f --- /dev/null +++ b/contrib/examples/git-checkout.sh @@ -0,0 +1,302 @@ +#!/bin/sh + +OPTIONS_KEEPDASHDASH=t +OPTIONS_SPEC="\ +git-checkout [options] [<branch>] [<paths>...] +-- +b= create a new branch started at <branch> +l create the new branch's reflog +track arrange that the new branch tracks the remote branch +f proceed even if the index or working tree is not HEAD +m merge local modifications into the new branch +q,quiet be quiet +" +SUBDIRECTORY_OK=Sometimes +. git-sh-setup +require_work_tree + +old_name=HEAD +old=$(git rev-parse --verify $old_name 2>/dev/null) +oldbranch=$(git symbolic-ref $old_name 2>/dev/null) +new= +new_name= +force= +branch= +track= +newbranch= +newbranch_log= +merge= +quiet= +v=-v +LF=' +' + +while test $# != 0; do + case "$1" in + -b) + shift + newbranch="$1" + [ -z "$newbranch" ] && + die "git checkout: -b needs a branch name" + git show-ref --verify --quiet -- "refs/heads/$newbranch" && + die "git checkout: branch $newbranch already exists" + git check-ref-format "heads/$newbranch" || + die "git checkout: we do not like '$newbranch' as a branch name." + ;; + -l) + newbranch_log=-l + ;; + --track|--no-track) + track="$1" + ;; + -f) + force=1 + ;; + -m) + merge=1 + ;; + -q|--quiet) + quiet=1 + v= + ;; + --) + shift + break + ;; + *) + usage + ;; + esac + shift +done + +arg="$1" +rev=$(git rev-parse --verify "$arg" 2>/dev/null) +if rev=$(git rev-parse --verify "$rev^0" 2>/dev/null) +then + [ -z "$rev" ] && die "unknown flag $arg" + new_name="$arg" + if git show-ref --verify --quiet -- "refs/heads/$arg" + then + rev=$(git rev-parse --verify "refs/heads/$arg^0") + branch="$arg" + fi + new="$rev" + shift +elif rev=$(git rev-parse --verify "$rev^{tree}" 2>/dev/null) +then + # checking out selected paths from a tree-ish. + new="$rev" + new_name="$rev^{tree}" + shift +fi +[ "$1" = "--" ] && shift + +case "$newbranch,$track" in +,--*) + die "git checkout: --track and --no-track require -b" +esac + +case "$force$merge" in +11) + die "git checkout: -f and -m are incompatible" +esac + +# The behaviour of the command with and without explicit path +# parameters is quite different. +# +# Without paths, we are checking out everything in the work tree, +# possibly switching branches. This is the traditional behaviour. +# +# With paths, we are _never_ switching branch, but checking out +# the named paths from either index (when no rev is given), +# or the named tree-ish (when rev is given). + +if test "$#" -ge 1 +then + hint= + if test "$#" -eq 1 + then + hint=" +Did you intend to checkout '$@' which can not be resolved as commit?" + fi + if test '' != "$newbranch$force$merge" + then + die "git checkout: updating paths is incompatible with switching branches/forcing$hint" + fi + if test '' != "$new" + then + # from a specific tree-ish; note that this is for + # rescuing paths and is never meant to remove what + # is not in the named tree-ish. + git ls-tree --full-name -r "$new" "$@" | + git update-index --index-info || exit $? + fi + + # Make sure the request is about existing paths. + git ls-files --full-name --error-unmatch -- "$@" >/dev/null || exit + git ls-files --full-name -- "$@" | + (cd_to_toplevel && git checkout-index -f -u --stdin) + + # Run a post-checkout hook -- the HEAD does not change so the + # current HEAD is passed in for both args + if test -x "$GIT_DIR"/hooks/post-checkout; then + "$GIT_DIR"/hooks/post-checkout $old $old 0 + fi + + exit $? +else + # Make sure we did not fall back on $arg^{tree} codepath + # since we are not checking out from an arbitrary tree-ish, + # but switching branches. + if test '' != "$new" + then + git rev-parse --verify "$new^{commit}" >/dev/null 2>&1 || + die "Cannot switch branch to a non-commit." + fi +fi + +# We are switching branches and checking out trees, so +# we *NEED* to be at the toplevel. +cd_to_toplevel + +[ -z "$new" ] && new=$old && new_name="$old_name" + +# If we don't have an existing branch that we're switching to, +# and we don't have a new branch name for the target we +# are switching to, then we are detaching our HEAD from any +# branch. However, if "git checkout HEAD" detaches the HEAD +# from the current branch, even though that may be logically +# correct, it feels somewhat funny. More importantly, we do not +# want "git checkout" nor "git checkout -f" to detach HEAD. + +detached= +detach_warn= + +describe_detached_head () { + test -n "$quiet" || { + printf >&2 "$1 " + GIT_PAGER= git log >&2 -1 --pretty=oneline --abbrev-commit "$2" -- + } +} + +if test -z "$branch$newbranch" && test "$new_name" != "$old_name" +then + detached="$new" + if test -n "$oldbranch" && test -z "$quiet" + then + detach_warn="Note: moving to \"$new_name\" which isn't a local branch +If you want to create a new branch from this checkout, you may do so +(now or later) by using -b with the checkout command again. Example: + git checkout -b <new_branch_name>" + fi +elif test -z "$oldbranch" && test "$new" != "$old" +then + describe_detached_head 'Previous HEAD position was' "$old" +fi + +if [ "X$old" = X ] +then + if test -z "$quiet" + then + echo >&2 "warning: You appear to be on a branch yet to be born." + echo >&2 "warning: Forcing checkout of $new_name." + fi + force=1 +fi + +if [ "$force" ] +then + git read-tree $v --reset -u $new +else + git update-index --refresh >/dev/null + git read-tree $v -m -u --exclude-per-directory=.gitignore $old $new || ( + case "$merge,$v" in + ,*) + exit 1 ;; + 1,) + ;; # quiet + *) + echo >&2 "Falling back to 3-way merge..." ;; + esac + + # Match the index to the working tree, and do a three-way. + git diff-files --name-only | git update-index --remove --stdin && + work=`git write-tree` && + git read-tree $v --reset -u $new || exit + + eval GITHEAD_$new='${new_name:-${branch:-$new}}' && + eval GITHEAD_$work=local && + export GITHEAD_$new GITHEAD_$work && + git merge-recursive $old -- $new $work + + # Do not register the cleanly merged paths in the index yet. + # this is not a real merge before committing, but just carrying + # the working tree changes along. + unmerged=`git ls-files -u` + git read-tree $v --reset $new + case "$unmerged" in + '') ;; + *) + ( + z40=0000000000000000000000000000000000000000 + echo "$unmerged" | + sed -e 's/^[0-7]* [0-9a-f]* /'"0 $z40 /" + echo "$unmerged" + ) | git update-index --index-info + ;; + esac + exit 0 + ) + saved_err=$? + if test "$saved_err" = 0 && test -z "$quiet" + then + git diff-index --name-status "$new" + fi + (exit $saved_err) +fi + +# +# Switch the HEAD pointer to the new branch if we +# checked out a branch head, and remove any potential +# old MERGE_HEAD's (subsequent commits will clearly not +# be based on them, since we re-set the index) +# +if [ "$?" -eq 0 ]; then + if [ "$newbranch" ]; then + git branch $track $newbranch_log "$newbranch" "$new_name" || exit + branch="$newbranch" + fi + if test -n "$branch" + then + old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'` + GIT_DIR="$GIT_DIR" git symbolic-ref -m "checkout: moving from ${old_branch_name:-$old} to $branch" HEAD "refs/heads/$branch" + if test -n "$quiet" + then + true # nothing + elif test "refs/heads/$branch" = "$oldbranch" + then + echo >&2 "Already on branch \"$branch\"" + else + echo >&2 "Switched to${newbranch:+ a new} branch \"$branch\"" + fi + elif test -n "$detached" + then + old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'` + git update-ref --no-deref -m "checkout: moving from ${old_branch_name:-$old} to $arg" HEAD "$detached" || + die "Cannot detach HEAD" + if test -n "$detach_warn" + then + echo >&2 "$detach_warn" + fi + describe_detached_head 'HEAD is now at' HEAD + fi + rm -f "$GIT_DIR/MERGE_HEAD" +else + exit 1 +fi + +# Run a post-checkout hook +if test -x "$GIT_DIR"/hooks/post-checkout; then + "$GIT_DIR"/hooks/post-checkout $old $new 1 +fi diff --git a/contrib/examples/git-clean.sh b/contrib/examples/git-clean.sh new file mode 100755 index 0000000000..01c95e9fe8 --- /dev/null +++ b/contrib/examples/git-clean.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# +# Copyright (c) 2005-2006 Pavel Roskin +# + +OPTIONS_KEEPDASHDASH= +OPTIONS_SPEC="\ +git-clean [options] <paths>... + +Clean untracked files from the working directory + +When optional <paths>... arguments are given, the paths +affected are further limited to those that match them. +-- +d remove directories as well +f override clean.requireForce and clean anyway +n don't remove anything, just show what would be done +q be quiet, only report errors +x remove ignored files as well +X remove only ignored files" + +SUBDIRECTORY_OK=Yes +. git-sh-setup +require_work_tree + +ignored= +ignoredonly= +cleandir= +rmf="rm -f --" +rmrf="rm -rf --" +rm_refuse="echo Not removing" +echo1="echo" + +disabled=$(git config --bool clean.requireForce) + +while test $# != 0 +do + case "$1" in + -d) + cleandir=1 + ;; + -f) + disabled=false + ;; + -n) + disabled=false + rmf="echo Would remove" + rmrf="echo Would remove" + rm_refuse="echo Would not remove" + echo1=":" + ;; + -q) + echo1=":" + ;; + -x) + ignored=1 + ;; + -X) + ignoredonly=1 + ;; + --) + shift + break + ;; + *) + usage # should not happen + ;; + esac + shift +done + +# requireForce used to default to false but now it defaults to true. +# IOW, lack of explicit "clean.requireForce = false" is taken as +# "clean.requireForce = true". +case "$disabled" in +"") + die "clean.requireForce not set and -n or -f not given; refusing to clean" + ;; +"true") + die "clean.requireForce set and -n or -f not given; refusing to clean" + ;; +esac + +if [ "$ignored,$ignoredonly" = "1,1" ]; then + die "-x and -X cannot be set together" +fi + +if [ -z "$ignored" ]; then + excl="--exclude-per-directory=.gitignore" + excl_info= excludes_file= + if [ -f "$GIT_DIR/info/exclude" ]; then + excl_info="--exclude-from=$GIT_DIR/info/exclude" + fi + if cfg_excl=$(git config core.excludesfile) && test -f "$cfg_excl" + then + excludes_file="--exclude-from=$cfg_excl" + fi + if [ "$ignoredonly" ]; then + excl="$excl --ignored" + fi +fi + +git ls-files --others --directory \ + $excl ${excl_info:+"$excl_info"} ${excludes_file:+"$excludes_file"} \ + -- "$@" | +while read -r file; do + if [ -d "$file" -a ! -L "$file" ]; then + if [ -z "$cleandir" ]; then + $rm_refuse "$file" + continue + fi + $echo1 "Removing $file" + $rmrf "$file" + else + $echo1 "Removing $file" + $rmf "$file" + fi +done diff --git a/contrib/examples/git-clone.sh b/contrib/examples/git-clone.sh new file mode 100755 index 0000000000..547228e13c --- /dev/null +++ b/contrib/examples/git-clone.sh @@ -0,0 +1,525 @@ +#!/bin/sh +# +# Copyright (c) 2005, Linus Torvalds +# Copyright (c) 2005, Junio C Hamano +# +# Clone a repository into a different directory that does not yet exist. + +# See git-sh-setup why. +unset CDPATH + +OPTIONS_SPEC="\ +git-clone [options] [--] <repo> [<dir>] +-- +n,no-checkout don't create a checkout +bare create a bare repository +naked create a bare repository +l,local to clone from a local repository +no-hardlinks don't use local hardlinks, always copy +s,shared setup as a shared repository +template= path to the template directory +q,quiet be quiet +reference= reference repository +o,origin= use <name> instead of 'origin' to track upstream +u,upload-pack= path to git-upload-pack on the remote +depth= create a shallow clone of that depth + +use-separate-remote compatibility, do not use +no-separate-remote compatibility, do not use" + +die() { + echo >&2 "$@" + exit 1 +} + +usage() { + exec "$0" -h +} + +eval "$(echo "$OPTIONS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" + +get_repo_base() { + ( + cd "`/bin/pwd`" && + cd "$1" || cd "$1.git" && + { + cd .git + pwd + } + ) 2>/dev/null +} + +if [ -n "$GIT_SSL_NO_VERIFY" -o \ + "`git config --bool http.sslVerify`" = false ]; then + curl_extra_args="-k" +fi + +http_fetch () { + # $1 = Remote, $2 = Local + curl -nsfL $curl_extra_args "$1" >"$2" + curl_exit_status=$? + case $curl_exit_status in + 126|127) exit ;; + *) return $curl_exit_status ;; + esac +} + +clone_dumb_http () { + # $1 - remote, $2 - local + cd "$2" && + clone_tmp="$GIT_DIR/clone-tmp" && + mkdir -p "$clone_tmp" || exit 1 + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git config --bool http.noEPSV`" = true ]; then + curl_extra_args="${curl_extra_args} --disable-epsv" + fi + http_fetch "$1/info/refs" "$clone_tmp/refs" || + die "Cannot get remote repository information. +Perhaps git-update-server-info needs to be run there?" + test "z$quiet" = z && v=-v || v= + while read sha1 refname + do + name=`expr "z$refname" : 'zrefs/\(.*\)'` && + case "$name" in + *^*) continue;; + esac + case "$bare,$name" in + yes,* | ,heads/* | ,tags/*) ;; + *) continue ;; + esac + if test -n "$use_separate_remote" && + branch_name=`expr "z$name" : 'zheads/\(.*\)'` + then + tname="remotes/$origin/$branch_name" + else + tname=$name + fi + git-http-fetch $v -a -w "$tname" "$sha1" "$1" || exit 1 + done <"$clone_tmp/refs" + rm -fr "$clone_tmp" + http_fetch "$1/HEAD" "$GIT_DIR/REMOTE_HEAD" || + rm -f "$GIT_DIR/REMOTE_HEAD" + if test -f "$GIT_DIR/REMOTE_HEAD"; then + head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"` + case "$head_sha1" in + 'ref: refs/'*) + ;; + *) + git-http-fetch $v -a "$head_sha1" "$1" || + rm -f "$GIT_DIR/REMOTE_HEAD" + ;; + esac + fi +} + +quiet= +local=no +use_local_hardlink=yes +local_shared=no +unset template +no_checkout= +upload_pack= +bare= +reference= +origin= +origin_override= +use_separate_remote=t +depth= +no_progress= +local_explicitly_asked_for= +test -t 1 || no_progress=--no-progress + +while test $# != 0 +do + case "$1" in + -n|--no-checkout) + no_checkout=yes ;; + --naked|--bare) + bare=yes ;; + -l|--local) + local_explicitly_asked_for=yes + use_local_hardlink=yes + ;; + --no-hardlinks) + use_local_hardlink=no ;; + -s|--shared) + local_shared=yes ;; + --template) + shift; template="--template=$1" ;; + -q|--quiet) + quiet=-q ;; + --use-separate-remote|--no-separate-remote) + die "clones are always made with separate-remote layout" ;; + --reference) + shift; reference="$1" ;; + -o|--origin) + shift; + case "$1" in + '') + usage ;; + */*) + die "'$1' is not suitable for an origin name" + esac + git check-ref-format "heads/$1" || + die "'$1' is not suitable for a branch name" + test -z "$origin_override" || + die "Do not give more than one --origin options." + origin_override=yes + origin="$1" + ;; + -u|--upload-pack) + shift + upload_pack="--upload-pack=$1" ;; + --depth) + shift + depth="--depth=$1" ;; + --) + shift + break ;; + *) + usage ;; + esac + shift +done + +repo="$1" +test -n "$repo" || + die 'you must specify a repository to clone.' + +# --bare implies --no-checkout and --no-separate-remote +if test yes = "$bare" +then + if test yes = "$origin_override" + then + die '--bare and --origin $origin options are incompatible.' + fi + no_checkout=yes + use_separate_remote= +fi + +if test -z "$origin" +then + origin=origin +fi + +# Turn the source into an absolute path if +# it is local +if base=$(get_repo_base "$repo"); then + repo="$base" + if test -z "$depth" + then + local=yes + fi +elif test -f "$repo" +then + case "$repo" in /*) ;; *) repo="$PWD/$repo" ;; esac +fi + +# Decide the directory name of the new repository +if test -n "$2" +then + dir="$2" + test $# = 2 || die "excess parameter to git-clone" +else + # Derive one from the repository name + # Try using "humanish" part of source repo if user didn't specify one + if test -f "$repo" + then + # Cloning from a bundle + dir=$(echo "$repo" | sed -e 's|/*\.bundle$||' -e 's|.*/||g') + else + dir=$(echo "$repo" | + sed -e 's|/$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g') + fi +fi + +[ -e "$dir" ] && die "destination directory '$dir' already exists." +[ yes = "$bare" ] && unset GIT_WORK_TREE +[ -n "$GIT_WORK_TREE" ] && [ -e "$GIT_WORK_TREE" ] && +die "working tree '$GIT_WORK_TREE' already exists." +D= +W= +cleanup() { + test -z "$D" && rm -rf "$dir" + test -z "$W" && test -n "$GIT_WORK_TREE" && rm -rf "$GIT_WORK_TREE" + cd .. + test -n "$D" && rm -rf "$D" + test -n "$W" && rm -rf "$W" + exit $err +} +trap 'err=$?; cleanup' 0 +mkdir -p "$dir" && D=$(cd "$dir" && pwd) || usage +test -n "$GIT_WORK_TREE" && mkdir -p "$GIT_WORK_TREE" && +W=$(cd "$GIT_WORK_TREE" && pwd) && GIT_WORK_TREE="$W" && export GIT_WORK_TREE +if test yes = "$bare" || test -n "$GIT_WORK_TREE"; then + GIT_DIR="$D" +else + GIT_DIR="$D/.git" +fi && +export GIT_DIR && +GIT_CONFIG="$GIT_DIR/config" git-init $quiet ${template+"$template"} || usage + +if test -n "$bare" +then + GIT_CONFIG="$GIT_DIR/config" git config core.bare true +fi + +if test -n "$reference" +then + ref_git= + if test -d "$reference" + then + if test -d "$reference/.git/objects" + then + ref_git="$reference/.git" + elif test -d "$reference/objects" + then + ref_git="$reference" + fi + fi + if test -n "$ref_git" + then + ref_git=$(cd "$ref_git" && pwd) + echo "$ref_git/objects" >"$GIT_DIR/objects/info/alternates" + ( + GIT_DIR="$ref_git" git for-each-ref \ + --format='%(objectname) %(*objectname)' + ) | + while read a b + do + test -z "$a" || + git update-ref "refs/reference-tmp/$a" "$a" + test -z "$b" || + git update-ref "refs/reference-tmp/$b" "$b" + done + else + die "reference repository '$reference' is not a local directory." + fi +fi + +rm -f "$GIT_DIR/CLONE_HEAD" + +# We do local magic only when the user tells us to. +case "$local" in +yes) + ( cd "$repo/objects" ) || + die "cannot chdir to local '$repo/objects'." + + if test "$local_shared" = yes + then + mkdir -p "$GIT_DIR/objects/info" + echo "$repo/objects" >>"$GIT_DIR/objects/info/alternates" + else + cpio_quiet_flag="" + cpio --help 2>&1 | grep -- --quiet >/dev/null && \ + cpio_quiet_flag=--quiet + l= && + if test "$use_local_hardlink" = yes + then + # See if we can hardlink and drop "l" if not. + sample_file=$(cd "$repo" && \ + find objects -type f -print | sed -e 1q) + # objects directory should not be empty because + # we are cloning! + test -f "$repo/$sample_file" || + die "fatal: cannot clone empty repository" + if ln "$repo/$sample_file" "$GIT_DIR/objects/sample" 2>/dev/null + then + rm -f "$GIT_DIR/objects/sample" + l=l + elif test -n "$local_explicitly_asked_for" + then + echo >&2 "Warning: -l asked but cannot hardlink to $repo" + fi + fi && + cd "$repo" && + # Create dirs using umask and permissions and destination + find objects -type d -print | (cd "$GIT_DIR" && xargs mkdir -p) && + # Copy existing 0444 permissions on content + find objects ! -type d -print | cpio $cpio_quiet_flag -pumd$l "$GIT_DIR/" || \ + exit 1 + fi + git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1 + ;; +*) + case "$repo" in + rsync://*) + case "$depth" in + "") ;; + *) die "shallow over rsync not supported" ;; + esac + rsync $quiet -av --ignore-existing \ + --exclude info "$repo/objects/" "$GIT_DIR/objects/" || + exit + # Look at objects/info/alternates for rsync -- http will + # support it natively and git native ones will do it on the + # remote end. Not having that file is not a crime. + rsync -q "$repo/objects/info/alternates" \ + "$GIT_DIR/TMP_ALT" 2>/dev/null || + rm -f "$GIT_DIR/TMP_ALT" + if test -f "$GIT_DIR/TMP_ALT" + then + ( cd "$D" && + . git-parse-remote && + resolve_alternates "$repo" <"$GIT_DIR/TMP_ALT" ) | + while read alt + do + case "$alt" in 'bad alternate: '*) die "$alt";; esac + case "$quiet" in + '') echo >&2 "Getting alternate: $alt" ;; + esac + rsync $quiet -av --ignore-existing \ + --exclude info "$alt" "$GIT_DIR/objects" || exit + done + rm -f "$GIT_DIR/TMP_ALT" + fi + git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1 + ;; + https://*|http://*|ftp://*) + case "$depth" in + "") ;; + *) die "shallow over http or ftp not supported" ;; + esac + if test -z "@@NO_CURL@@" + then + clone_dumb_http "$repo" "$D" + else + die "http transport not supported, rebuild Git with curl support" + fi + ;; + *) + if [ -f "$repo" ] ; then + git bundle unbundle "$repo" > "$GIT_DIR/CLONE_HEAD" || + die "unbundle from '$repo' failed." + else + case "$upload_pack" in + '') git-fetch-pack --all -k $quiet $depth $no_progress "$repo";; + *) git-fetch-pack --all -k \ + $quiet "$upload_pack" $depth $no_progress "$repo" ;; + esac >"$GIT_DIR/CLONE_HEAD" || + die "fetch-pack from '$repo' failed." + fi + ;; + esac + ;; +esac +test -d "$GIT_DIR/refs/reference-tmp" && rm -fr "$GIT_DIR/refs/reference-tmp" + +if test -f "$GIT_DIR/CLONE_HEAD" +then + # Read git-fetch-pack -k output and store the remote branches. + if [ -n "$use_separate_remote" ] + then + branch_top="remotes/$origin" + else + branch_top="heads" + fi + tag_top="tags" + while read sha1 name + do + case "$name" in + *'^{}') + continue ;; + HEAD) + destname="REMOTE_HEAD" ;; + refs/heads/*) + destname="refs/$branch_top/${name#refs/heads/}" ;; + refs/tags/*) + destname="refs/$tag_top/${name#refs/tags/}" ;; + *) + continue ;; + esac + git update-ref -m "clone: from $repo" "$destname" "$sha1" "" + done < "$GIT_DIR/CLONE_HEAD" +fi + +if test -n "$W"; then + cd "$W" || exit +else + cd "$D" || exit +fi + +if test -z "$bare" +then + # a non-bare repository is always in separate-remote layout + remote_top="refs/remotes/$origin" + head_sha1= + test ! -r "$GIT_DIR/REMOTE_HEAD" || head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"` + case "$head_sha1" in + 'ref: refs/'*) + # Uh-oh, the remote told us (http transport done against + # new style repository with a symref HEAD). + # Ideally we should skip the guesswork but for now + # opt for minimum change. + head_sha1=`expr "z$head_sha1" : 'zref: refs/heads/\(.*\)'` + head_sha1=`cat "$GIT_DIR/$remote_top/$head_sha1"` + ;; + esac + + # The name under $remote_top the remote HEAD seems to point at. + head_points_at=$( + ( + test -f "$GIT_DIR/$remote_top/master" && echo "master" + cd "$GIT_DIR/$remote_top" && + find . -type f -print | sed -e 's/^\.\///' + ) | ( + done=f + while read name + do + test t = $done && continue + branch_tip=`cat "$GIT_DIR/$remote_top/$name"` + if test "$head_sha1" = "$branch_tip" + then + echo "$name" + done=t + fi + done + ) + ) + + # Upstream URL + git config remote."$origin".url "$repo" && + + # Set up the mappings to track the remote branches. + git config remote."$origin".fetch \ + "+refs/heads/*:$remote_top/*" '^$' && + + # Write out remote.$origin config, and update our "$head_points_at". + case "$head_points_at" in + ?*) + # Local default branch + git symbolic-ref HEAD "refs/heads/$head_points_at" && + + # Tracking branch for the primary branch at the remote. + git update-ref HEAD "$head_sha1" && + + rm -f "refs/remotes/$origin/HEAD" + git symbolic-ref "refs/remotes/$origin/HEAD" \ + "refs/remotes/$origin/$head_points_at" && + + git config branch."$head_points_at".remote "$origin" && + git config branch."$head_points_at".merge "refs/heads/$head_points_at" + ;; + '') + if test -z "$head_sha1" + then + # Source had nonexistent ref in HEAD + echo >&2 "Warning: Remote HEAD refers to nonexistent ref, unable to checkout." + no_checkout=t + else + # Source had detached HEAD pointing nowhere + git update-ref --no-deref HEAD "$head_sha1" && + rm -f "refs/remotes/$origin/HEAD" + fi + ;; + esac + + case "$no_checkout" in + '') + test "z$quiet" = z -a "z$no_progress" = z && v=-v || v= + git read-tree -m -u $v HEAD HEAD + esac +fi +rm -f "$GIT_DIR/CLONE_HEAD" "$GIT_DIR/REMOTE_HEAD" + +trap - 0 diff --git a/contrib/examples/git-commit.sh b/contrib/examples/git-commit.sh new file mode 100755 index 0000000000..23ffb028d1 --- /dev/null +++ b/contrib/examples/git-commit.sh @@ -0,0 +1,639 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# Copyright (c) 2006 Junio C Hamano + +USAGE='[-a | --interactive] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit> | --amend] [-u] [-e] [--author <author>] [--template <file>] [[-i | -o] <path>...]' +SUBDIRECTORY_OK=Yes +OPTIONS_SPEC= +. git-sh-setup +require_work_tree + +git rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=t + +case "$0" in +*status) + status_only=t + ;; +*commit) + status_only= + ;; +esac + +refuse_partial () { + echo >&2 "$1" + echo >&2 "You might have meant to say 'git commit -i paths...', perhaps?" + exit 1 +} + +TMP_INDEX= +THIS_INDEX="${GIT_INDEX_FILE:-$GIT_DIR/index}" +NEXT_INDEX="$GIT_DIR/next-index$$" +rm -f "$NEXT_INDEX" +save_index () { + cp -p "$THIS_INDEX" "$NEXT_INDEX" +} + +run_status () { + # If TMP_INDEX is defined, that means we are doing + # "--only" partial commit, and that index file is used + # to build the tree for the commit. Otherwise, if + # NEXT_INDEX exists, that is the index file used to + # make the commit. Otherwise we are using as-is commit + # so the regular index file is what we use to compare. + if test '' != "$TMP_INDEX" + then + GIT_INDEX_FILE="$TMP_INDEX" + export GIT_INDEX_FILE + elif test -f "$NEXT_INDEX" + then + GIT_INDEX_FILE="$NEXT_INDEX" + export GIT_INDEX_FILE + fi + + if test "$status_only" = "t" -o "$use_status_color" = "t"; then + color= + else + color=--nocolor + fi + git runstatus ${color} \ + ${verbose:+--verbose} \ + ${amend:+--amend} \ + ${untracked_files:+--untracked} +} + +trap ' + test -z "$TMP_INDEX" || { + test -f "$TMP_INDEX" && rm -f "$TMP_INDEX" + } + rm -f "$NEXT_INDEX" +' 0 + +################################################################ +# Command line argument parsing and sanity checking + +all= +also= +allow_empty=f +interactive= +only= +logfile= +use_commit= +amend= +edit_flag= +no_edit= +log_given= +log_message= +verify=t +quiet= +verbose= +signoff= +force_author= +only_include_assumed= +untracked_files= +templatefile="`git config commit.template`" +while test $# != 0 +do + case "$1" in + -F|--F|-f|--f|--fi|--fil|--file) + case "$#" in 1) usage ;; esac + shift + no_edit=t + log_given=t$log_given + logfile="$1" + ;; + -F*|-f*) + no_edit=t + log_given=t$log_given + logfile="${1#-[Ff]}" + ;; + --F=*|--f=*|--fi=*|--fil=*|--file=*) + no_edit=t + log_given=t$log_given + logfile="${1#*=}" + ;; + -a|--a|--al|--all) + all=t + ;; + --allo|--allow|--allow-|--allow-e|--allow-em|--allow-emp|\ + --allow-empt|--allow-empty) + allow_empty=t + ;; + --au=*|--aut=*|--auth=*|--autho=*|--author=*) + force_author="${1#*=}" + ;; + --au|--aut|--auth|--autho|--author) + case "$#" in 1) usage ;; esac + shift + force_author="$1" + ;; + -e|--e|--ed|--edi|--edit) + edit_flag=t + ;; + -i|--i|--in|--inc|--incl|--inclu|--includ|--include) + also=t + ;; + --int|--inte|--inter|--intera|--interac|--interact|--interacti|\ + --interactiv|--interactive) + interactive=t + ;; + -o|--o|--on|--onl|--only) + only=t + ;; + -m|--m|--me|--mes|--mess|--messa|--messag|--message) + case "$#" in 1) usage ;; esac + shift + log_given=m$log_given + log_message="${log_message:+${log_message} + +}$1" + no_edit=t + ;; + -m*) + log_given=m$log_given + log_message="${log_message:+${log_message} + +}${1#-m}" + no_edit=t + ;; + --m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*) + log_given=m$log_given + log_message="${log_message:+${log_message} + +}${1#*=}" + no_edit=t + ;; + -n|--n|--no|--no-|--no-v|--no-ve|--no-ver|--no-veri|--no-verif|\ + --no-verify) + verify= + ;; + --a|--am|--ame|--amen|--amend) + amend=t + use_commit=HEAD + ;; + -c) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit= + ;; + --ree=*|--reed=*|--reedi=*|--reedit=*|--reedit-=*|--reedit-m=*|\ + --reedit-me=*|--reedit-mes=*|--reedit-mess=*|--reedit-messa=*|\ + --reedit-messag=*|--reedit-message=*) + log_given=t$log_given + use_commit="${1#*=}" + no_edit= + ;; + --ree|--reed|--reedi|--reedit|--reedit-|--reedit-m|--reedit-me|\ + --reedit-mes|--reedit-mess|--reedit-messa|--reedit-messag|\ + --reedit-message) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit= + ;; + -C) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit=t + ;; + --reu=*|--reus=*|--reuse=*|--reuse-=*|--reuse-m=*|--reuse-me=*|\ + --reuse-mes=*|--reuse-mess=*|--reuse-messa=*|--reuse-messag=*|\ + --reuse-message=*) + log_given=t$log_given + use_commit="${1#*=}" + no_edit=t + ;; + --reu|--reus|--reuse|--reuse-|--reuse-m|--reuse-me|--reuse-mes|\ + --reuse-mess|--reuse-messa|--reuse-messag|--reuse-message) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit=t + ;; + -s|--s|--si|--sig|--sign|--signo|--signof|--signoff) + signoff=t + ;; + -t|--t|--te|--tem|--temp|--templ|--templa|--templat|--template) + case "$#" in 1) usage ;; esac + shift + templatefile="$1" + no_edit= + ;; + -q|--q|--qu|--qui|--quie|--quiet) + quiet=t + ;; + -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) + verbose=t + ;; + -u|--u|--un|--unt|--untr|--untra|--untrac|--untrack|--untracke|\ + --untracked|--untracked-|--untracked-f|--untracked-fi|--untracked-fil|\ + --untracked-file|--untracked-files) + untracked_files=t + ;; + --) + shift + break + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done +case "$edit_flag" in t) no_edit= ;; esac + +################################################################ +# Sanity check options + +case "$amend,$initial_commit" in +t,t) + die "You do not have anything to amend." ;; +t,) + if [ -f "$GIT_DIR/MERGE_HEAD" ]; then + die "You are in the middle of a merge -- cannot amend." + fi ;; +esac + +case "$log_given" in +tt*) + die "Only one of -c/-C/-F can be used." ;; +*tm*|*mt*) + die "Option -m cannot be combined with -c/-C/-F." ;; +esac + +case "$#,$also,$only,$amend" in +*,t,t,*) + die "Only one of --include/--only can be used." ;; +0,t,,* | 0,,t,) + die "No paths with --include/--only does not make sense." ;; +0,,t,t) + only_include_assumed="# Clever... amending the last one with dirty index." ;; +0,,,*) + ;; +*,,,*) + only_include_assumed="# Explicit paths specified without -i nor -o; assuming --only paths..." + also= + ;; +esac +unset only +case "$all,$interactive,$also,$#" in +*t,*t,*) + die "Cannot use -a, --interactive or -i at the same time." ;; +t,,,[1-9]*) + die "Paths with -a does not make sense." ;; +,t,,[1-9]*) + die "Paths with --interactive does not make sense." ;; +,,t,0) + die "No paths with -i does not make sense." ;; +esac + +if test ! -z "$templatefile" -a -z "$log_given" +then + if test ! -f "$templatefile" + then + die "Commit template file does not exist." + fi +fi + +################################################################ +# Prepare index to have a tree to be committed + +case "$all,$also" in +t,) + if test ! -f "$THIS_INDEX" + then + die 'nothing to commit (use "git add file1 file2" to include for commit)' + fi + save_index && + ( + cd_to_toplevel && + GIT_INDEX_FILE="$NEXT_INDEX" && + export GIT_INDEX_FILE && + git diff-files --name-only -z | + git update-index --remove -z --stdin + ) || exit + ;; +,t) + save_index && + git ls-files --error-unmatch -- "$@" >/dev/null || exit + + git diff-files --name-only -z -- "$@" | + ( + cd_to_toplevel && + GIT_INDEX_FILE="$NEXT_INDEX" && + export GIT_INDEX_FILE && + git update-index --remove -z --stdin + ) || exit + ;; +,) + if test "$interactive" = t; then + git add --interactive || exit + fi + case "$#" in + 0) + ;; # commit as-is + *) + if test -f "$GIT_DIR/MERGE_HEAD" + then + refuse_partial "Cannot do a partial commit during a merge." + fi + + TMP_INDEX="$GIT_DIR/tmp-index$$" + W= + test -z "$initial_commit" && W=--with-tree=HEAD + commit_only=`git ls-files --error-unmatch $W -- "$@"` || exit + + # Build a temporary index and update the real index + # the same way. + if test -z "$initial_commit" + then + GIT_INDEX_FILE="$THIS_INDEX" \ + git read-tree --index-output="$TMP_INDEX" -i -m HEAD + else + rm -f "$TMP_INDEX" + fi || exit + + printf '%s\n' "$commit_only" | + GIT_INDEX_FILE="$TMP_INDEX" \ + git update-index --add --remove --stdin && + + save_index && + printf '%s\n' "$commit_only" | + ( + GIT_INDEX_FILE="$NEXT_INDEX" + export GIT_INDEX_FILE + git update-index --add --remove --stdin + ) || exit + ;; + esac + ;; +esac + +################################################################ +# If we do as-is commit, the index file will be THIS_INDEX, +# otherwise NEXT_INDEX after we make this commit. We leave +# the index as is if we abort. + +if test -f "$NEXT_INDEX" +then + USE_INDEX="$NEXT_INDEX" +else + USE_INDEX="$THIS_INDEX" +fi + +case "$status_only" in +t) + # This will silently fail in a read-only repository, which is + # what we want. + GIT_INDEX_FILE="$USE_INDEX" git update-index -q --unmerged --refresh + run_status + exit $? + ;; +'') + GIT_INDEX_FILE="$USE_INDEX" git update-index -q --refresh || exit + ;; +esac + +################################################################ +# Grab commit message, write out tree and make commit. + +if test t = "$verify" && test -x "$GIT_DIR"/hooks/pre-commit +then + GIT_INDEX_FILE="${TMP_INDEX:-${USE_INDEX}}" "$GIT_DIR"/hooks/pre-commit \ + || exit +fi + +if test "$log_message" != '' +then + printf '%s\n' "$log_message" +elif test "$logfile" != "" +then + if test "$logfile" = - + then + test -t 0 && + echo >&2 "(reading log message from standard input)" + cat + else + cat <"$logfile" + fi +elif test "$use_commit" != "" +then + encoding=$(git config i18n.commitencoding || echo UTF-8) + git show -s --pretty=raw --encoding="$encoding" "$use_commit" | + sed -e '1,/^$/d' -e 's/^ //' +elif test -f "$GIT_DIR/MERGE_MSG" +then + cat "$GIT_DIR/MERGE_MSG" +elif test -f "$GIT_DIR/SQUASH_MSG" +then + cat "$GIT_DIR/SQUASH_MSG" +elif test "$templatefile" != "" +then + cat "$templatefile" +fi | git stripspace >"$GIT_DIR"/COMMIT_EDITMSG + +case "$signoff" in +t) + sign=$(git var GIT_COMMITTER_IDENT | sed -e ' + s/>.*/>/ + s/^/Signed-off-by: / + ') + blank_before_signoff= + tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG | + grep 'Signed-off-by:' >/dev/null || blank_before_signoff=' +' + tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG | + grep "$sign"$ >/dev/null || + printf '%s%s\n' "$blank_before_signoff" "$sign" \ + >>"$GIT_DIR"/COMMIT_EDITMSG + ;; +esac + +if test -f "$GIT_DIR/MERGE_HEAD" && test -z "$no_edit"; then + echo "#" + echo "# It looks like you may be committing a MERGE." + echo "# If this is not correct, please remove the file" + printf '%s\n' "# $GIT_DIR/MERGE_HEAD" + echo "# and try again" + echo "#" +fi >>"$GIT_DIR"/COMMIT_EDITMSG + +# Author +if test '' != "$use_commit" +then + eval "$(get_author_ident_from_commit "$use_commit")" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE +fi +if test '' != "$force_author" +then + GIT_AUTHOR_NAME=`expr "z$force_author" : 'z\(.*[^ ]\) *<.*'` && + GIT_AUTHOR_EMAIL=`expr "z$force_author" : '.*\(<.*\)'` && + test '' != "$GIT_AUTHOR_NAME" && + test '' != "$GIT_AUTHOR_EMAIL" || + die "malformed --author parameter" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL +fi + +PARENTS="-p HEAD" +if test -z "$initial_commit" +then + rloga='commit' + if [ -f "$GIT_DIR/MERGE_HEAD" ]; then + rloga='commit (merge)' + PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"` + elif test -n "$amend"; then + rloga='commit (amend)' + PARENTS=$(git cat-file commit HEAD | + sed -n -e '/^$/q' -e 's/^parent /-p /p') + fi + current="$(git rev-parse --verify HEAD)" +else + if [ -z "$(git ls-files)" ]; then + echo >&2 'nothing to commit (use "git add file1 file2" to include for commit)' + exit 1 + fi + PARENTS="" + rloga='commit (initial)' + current='' +fi +set_reflog_action "$rloga" + +if test -z "$no_edit" +then + { + echo "" + echo "# Please enter the commit message for your changes." + echo "# (Comment lines starting with '#' will not be included)" + test -z "$only_include_assumed" || echo "$only_include_assumed" + run_status + } >>"$GIT_DIR"/COMMIT_EDITMSG +else + # we need to check if there is anything to commit + run_status >/dev/null +fi +case "$allow_empty,$?,$PARENTS" in +t,* | ?,0,* | ?,*,-p' '?*-p' '?*) + # an explicit --allow-empty, or a merge commit can record the + # same tree as its parent. Otherwise having commitable paths + # is required. + ;; +*) + rm -f "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" + use_status_color=t + run_status + exit 1 +esac + +case "$no_edit" in +'') + git var GIT_AUTHOR_IDENT > /dev/null || die + git var GIT_COMMITTER_IDENT > /dev/null || die + git_editor "$GIT_DIR/COMMIT_EDITMSG" + ;; +esac + +case "$verify" in +t) + if test -x "$GIT_DIR"/hooks/commit-msg + then + "$GIT_DIR"/hooks/commit-msg "$GIT_DIR"/COMMIT_EDITMSG || exit + fi +esac + +if test -z "$no_edit" +then + sed -e ' + /^diff --git a\/.*/{ + s/// + q + } + /^#/d + ' "$GIT_DIR"/COMMIT_EDITMSG +else + cat "$GIT_DIR"/COMMIT_EDITMSG +fi | +git stripspace >"$GIT_DIR"/COMMIT_MSG + +# Test whether the commit message has any content we didn't supply. +have_commitmsg= +grep -v -i '^Signed-off-by' "$GIT_DIR"/COMMIT_MSG | + git stripspace > "$GIT_DIR"/COMMIT_BAREMSG + +# Is the commit message totally empty? +if test -s "$GIT_DIR"/COMMIT_BAREMSG +then + if test "$templatefile" != "" + then + # Test whether this is just the unaltered template. + if cnt=`sed -e '/^#/d' < "$templatefile" | + git stripspace | + diff "$GIT_DIR"/COMMIT_BAREMSG - | + wc -l` && + test 0 -lt $cnt + then + have_commitmsg=t + fi + else + # No template, so the content in the commit message must + # have come from the user. + have_commitmsg=t + fi +fi + +rm -f "$GIT_DIR"/COMMIT_BAREMSG + +if test "$have_commitmsg" = "t" +then + if test -z "$TMP_INDEX" + then + tree=$(GIT_INDEX_FILE="$USE_INDEX" git write-tree) + else + tree=$(GIT_INDEX_FILE="$TMP_INDEX" git write-tree) && + rm -f "$TMP_INDEX" + fi && + commit=$(git commit-tree $tree $PARENTS <"$GIT_DIR/COMMIT_MSG") && + rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) && + git update-ref -m "$GIT_REFLOG_ACTION: $rlogm" HEAD $commit "$current" && + rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" && + if test -f "$NEXT_INDEX" + then + mv "$NEXT_INDEX" "$THIS_INDEX" + else + : ;# happy + fi +else + echo >&2 "* no commit message? aborting commit." + false +fi +ret="$?" +rm -f "$GIT_DIR/COMMIT_MSG" "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" + +cd_to_toplevel + +git rerere + +if test "$ret" = 0 +then + git gc --auto + if test -x "$GIT_DIR"/hooks/post-commit + then + "$GIT_DIR"/hooks/post-commit + fi + if test -z "$quiet" + then + commit=`git diff-tree --always --shortstat --pretty="format:%h: %s"\ + --abbrev --summary --root HEAD --` + echo "Created${initial_commit:+ initial} commit $commit" + fi +fi + +exit "$ret" diff --git a/contrib/examples/git-fetch.sh b/contrib/examples/git-fetch.sh new file mode 100755 index 0000000000..a314273bd5 --- /dev/null +++ b/contrib/examples/git-fetch.sh @@ -0,0 +1,379 @@ +#!/bin/sh +# + +USAGE='<fetch-options> <repository> <refspec>...' +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action "fetch $*" +cd_to_toplevel ;# probably unnecessary... + +. git-parse-remote +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" + +LF=' +' +IFS="$LF" + +no_tags= +tags= +append= +force= +verbose= +update_head_ok= +exec= +keep= +shallow_depth= +no_progress= +test -t 1 || no_progress=--no-progress +quiet= +while test $# != 0 +do + case "$1" in + -a|--a|--ap|--app|--appe|--appen|--append) + append=t + ;; + --upl|--uplo|--uploa|--upload|--upload-|--upload-p|\ + --upload-pa|--upload-pac|--upload-pack) + shift + exec="--upload-pack=$1" + ;; + --upl=*|--uplo=*|--uploa=*|--upload=*|\ + --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*) + exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)') + shift + ;; + -f|--f|--fo|--for|--forc|--force) + force=t + ;; + -t|--t|--ta|--tag|--tags) + tags=t + ;; + -n|--n|--no|--no-|--no-t|--no-ta|--no-tag|--no-tags) + no_tags=t + ;; + -u|--u|--up|--upd|--upda|--updat|--update|--update-|--update-h|\ + --update-he|--update-hea|--update-head|--update-head-|\ + --update-head-o|--update-head-ok) + update_head_ok=t + ;; + -q|--q|--qu|--qui|--quie|--quiet) + quiet=--quiet + ;; + -v|--verbose) + verbose="$verbose"Yes + ;; + -k|--k|--ke|--kee|--keep) + keep='-k -k' + ;; + --depth=*) + shallow_depth="--depth=`expr "z$1" : 'z-[^=]*=\(.*\)'`" + ;; + --depth) + shift + shallow_depth="--depth=$1" + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +case "$#" in +0) + origin=$(get_default_remote) + test -n "$(get_remote_url ${origin})" || + die "Where do you want to fetch from today?" + set x $origin ; shift ;; +esac + +if test -z "$exec" +then + # No command line override and we have configuration for the remote. + exec="--upload-pack=$(get_uploadpack $1)" +fi + +remote_nick="$1" +remote=$(get_remote_url "$@") +refs= +rref= +rsync_slurped_objects= + +if test "" = "$append" +then + : >"$GIT_DIR/FETCH_HEAD" +fi + +# Global that is reused later +ls_remote_result=$(git ls-remote $exec "$remote") || + die "Cannot get the repository state from $remote" + +append_fetch_head () { + flags= + test -n "$verbose" && flags="$flags$LF-v" + test -n "$force$single_force" && flags="$flags$LF-f" + GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \ + git fetch--tool $flags append-fetch-head "$@" +} + +# updating the current HEAD with git-fetch in a bare +# repository is always fine. +if test -z "$update_head_ok" && test $(is_bare_repository) = false +then + orig_head=$(git rev-parse --verify HEAD 2>/dev/null) +fi + +# Allow --tags/--notags from remote.$1.tagopt +case "$tags$no_tags" in +'') + case "$(git config --get "remote.$1.tagopt")" in + --tags) + tags=t ;; + --no-tags) + no_tags=t ;; + esac +esac + +# If --tags (and later --heads or --all) is specified, then we are +# not talking about defaults stored in Pull: line of remotes or +# branches file, and just fetch those and refspecs explicitly given. +# Otherwise we do what we always did. + +reflist=$(get_remote_refs_for_fetch "$@") +if test "$tags" +then + taglist=`IFS=' ' && + echo "$ls_remote_result" | + git show-ref --exclude-existing=refs/tags/ | + while read sha1 name + do + echo ".${name}:${name}" + done` || exit + if test "$#" -gt 1 + then + # remote URL plus explicit refspecs; we need to merge them. + reflist="$reflist$LF$taglist" + else + # No explicit refspecs; fetch tags only. + reflist=$taglist + fi +fi + +fetch_all_at_once () { + + eval=$(echo "$1" | git fetch--tool parse-reflist "-") + eval "$eval" + + ( : subshell because we muck with IFS + IFS=" $LF" + ( + if test "$remote" = . ; then + git show-ref $rref || echo failed "$remote" + elif test -f "$remote" ; then + test -n "$shallow_depth" && + die "shallow clone with bundle is not supported" + git bundle unbundle "$remote" $rref || + echo failed "$remote" + else + if test -d "$remote" && + + # The remote might be our alternate. With + # this optimization we will bypass fetch-pack + # altogether, which means we cannot be doing + # the shallow stuff at all. + test ! -f "$GIT_DIR/shallow" && + test -z "$shallow_depth" && + + # See if all of what we are going to fetch are + # connected to our repository's tips, in which + # case we do not have to do any fetch. + theirs=$(echo "$ls_remote_result" | \ + git fetch--tool -s pick-rref "$rref" "-") && + + # This will barf when $theirs reach an object that + # we do not have in our repository. Otherwise, + # we already have everything the fetch would bring in. + git rev-list --objects $theirs --not --all \ + >/dev/null 2>/dev/null + then + echo "$ls_remote_result" | \ + git fetch--tool pick-rref "$rref" "-" + else + flags= + case $verbose in + YesYes*) + flags="-v" + ;; + esac + git-fetch-pack --thin $exec $keep $shallow_depth \ + $quiet $no_progress $flags "$remote" $rref || + echo failed "$remote" + fi + fi + ) | + ( + flags= + test -n "$verbose" && flags="$flags -v" + test -n "$force" && flags="$flags -f" + GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \ + git fetch--tool $flags native-store \ + "$remote" "$remote_nick" "$refs" + ) + ) || exit + +} + +fetch_per_ref () { + reflist="$1" + refs= + rref= + + for ref in $reflist + do + refs="$refs$LF$ref" + + # These are relative path from $GIT_DIR, typically starting at refs/ + # but may be HEAD + if expr "z$ref" : 'z\.' >/dev/null + then + not_for_merge=t + ref=$(expr "z$ref" : 'z\.\(.*\)') + else + not_for_merge= + fi + if expr "z$ref" : 'z+' >/dev/null + then + single_force=t + ref=$(expr "z$ref" : 'z+\(.*\)') + else + single_force= + fi + remote_name=$(expr "z$ref" : 'z\([^:]*\):') + local_name=$(expr "z$ref" : 'z[^:]*:\(.*\)') + + rref="$rref$LF$remote_name" + + # There are transports that can fetch only one head at a time... + case "$remote" in + http://* | https://* | ftp://*) + test -n "$shallow_depth" && + die "shallow clone with http not supported" + proto=`expr "$remote" : '\([^:]*\):'` + if [ -n "$GIT_SSL_NO_VERIFY" ]; then + curl_extra_args="-k" + fi + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git config --bool http.noEPSV`" = true ]; then + noepsv_opt="--disable-epsv" + fi + + # Find $remote_name from ls-remote output. + head=$(echo "$ls_remote_result" | \ + git fetch--tool -s pick-rref "$remote_name" "-") + expr "z$head" : "z$_x40\$" >/dev/null || + die "No such ref $remote_name at $remote" + echo >&2 "Fetching $remote_name from $remote using $proto" + case "$quiet" in '') v=-v ;; *) v= ;; esac + git-http-fetch $v -a "$head" "$remote" || exit + ;; + rsync://*) + test -n "$shallow_depth" && + die "shallow clone with rsync not supported" + TMP_HEAD="$GIT_DIR/TMP_HEAD" + rsync -L -q "$remote/$remote_name" "$TMP_HEAD" || exit 1 + head=$(git rev-parse --verify TMP_HEAD) + rm -f "$TMP_HEAD" + case "$quiet" in '') v=-v ;; *) v= ;; esac + test "$rsync_slurped_objects" || { + rsync -a $v --ignore-existing --exclude info \ + "$remote/objects/" "$GIT_OBJECT_DIRECTORY/" || exit + + # Look at objects/info/alternates for rsync -- http will + # support it natively and git native ones will do it on + # the remote end. Not having that file is not a crime. + rsync -q "$remote/objects/info/alternates" \ + "$GIT_DIR/TMP_ALT" 2>/dev/null || + rm -f "$GIT_DIR/TMP_ALT" + if test -f "$GIT_DIR/TMP_ALT" + then + resolve_alternates "$remote" <"$GIT_DIR/TMP_ALT" | + while read alt + do + case "$alt" in 'bad alternate: '*) die "$alt";; esac + echo >&2 "Getting alternate: $alt" + rsync -av --ignore-existing --exclude info \ + "$alt" "$GIT_OBJECT_DIRECTORY/" || exit + done + rm -f "$GIT_DIR/TMP_ALT" + fi + rsync_slurped_objects=t + } + ;; + esac + + append_fetch_head "$head" "$remote" \ + "$remote_name" "$remote_nick" "$local_name" "$not_for_merge" || exit + + done + +} + +fetch_main () { + case "$remote" in + http://* | https://* | ftp://* | rsync://* ) + fetch_per_ref "$@" + ;; + *) + fetch_all_at_once "$@" + ;; + esac +} + +fetch_main "$reflist" || exit + +# automated tag following +case "$no_tags$tags" in +'') + case "$reflist" in + *:refs/*) + # effective only when we are following remote branch + # using local tracking branch. + taglist=$(IFS=' ' && + echo "$ls_remote_result" | + git show-ref --exclude-existing=refs/tags/ | + while read sha1 name + do + git cat-file -t "$sha1" >/dev/null 2>&1 || continue + echo >&2 "Auto-following $name" + echo ".${name}:${name}" + done) + esac + case "$taglist" in + '') ;; + ?*) + # do not deepen a shallow tree when following tags + shallow_depth= + fetch_main "$taglist" || exit ;; + esac +esac + +# If the original head was empty (i.e. no "master" yet), or +# if we were told not to worry, we do not have to check. +case "$orig_head" in +'') + ;; +?*) + curr_head=$(git rev-parse --verify HEAD 2>/dev/null) + if test "$curr_head" != "$orig_head" + then + git update-ref \ + -m "$GIT_REFLOG_ACTION: Undoing incorrectly fetched HEAD." \ + HEAD "$orig_head" + die "Cannot fetch into the current branch." + fi + ;; +esac diff --git a/contrib/examples/git-gc.sh b/contrib/examples/git-gc.sh index 2ae235b081..1597e9f33f 100755 --- a/contrib/examples/git-gc.sh +++ b/contrib/examples/git-gc.sh @@ -9,7 +9,7 @@ SUBDIRECTORY_OK=Yes . git-sh-setup no_prune=: -while case $# in 0) break ;; esac +while test $# != 0 do case "$1" in --prune) diff --git a/contrib/examples/git-ls-remote.sh b/contrib/examples/git-ls-remote.sh new file mode 100755 index 0000000000..fec70bbf88 --- /dev/null +++ b/contrib/examples/git-ls-remote.sh @@ -0,0 +1,142 @@ +#!/bin/sh +# + +usage () { + echo >&2 "usage: $0 [--heads] [--tags] [-u|--upload-pack <upload-pack>]" + echo >&2 " <repository> <refs>..." + exit 1; +} + +die () { + echo >&2 "$*" + exit 1 +} + +exec= +while test $# != 0 +do + case "$1" in + -h|--h|--he|--hea|--head|--heads) + heads=heads; shift ;; + -t|--t|--ta|--tag|--tags) + tags=tags; shift ;; + -u|--u|--up|--upl|--uploa|--upload|--upload-|--upload-p|--upload-pa|\ + --upload-pac|--upload-pack) + shift + exec="--upload-pack=$1" + shift;; + -u=*|--u=*|--up=*|--upl=*|--uplo=*|--uploa=*|--upload=*|\ + --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*) + exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)') + shift;; + --) + shift; break ;; + -*) + usage ;; + *) + break ;; + esac +done + +case "$#" in 0) usage ;; esac + +case ",$heads,$tags," in +,,,) heads=heads tags=tags other=other ;; +esac + +. git-parse-remote +peek_repo="$(get_remote_url "$@")" +shift + +tmp=.ls-remote-$$ +trap "rm -fr $tmp-*" 0 1 2 3 15 +tmpdir=$tmp-d + +case "$peek_repo" in +http://* | https://* | ftp://* ) + if [ -n "$GIT_SSL_NO_VERIFY" -o \ + "`git config --bool http.sslVerify`" = false ]; then + curl_extra_args="-k" + fi + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git config --bool http.noEPSV`" = true ]; then + curl_extra_args="${curl_extra_args} --disable-epsv" + fi + curl -nsf $curl_extra_args --header "Pragma: no-cache" "$peek_repo/info/refs" || + echo "failed slurping" + ;; + +rsync://* ) + mkdir $tmpdir && + rsync -rlq "$peek_repo/HEAD" $tmpdir && + rsync -rq "$peek_repo/refs" $tmpdir || { + echo "failed slurping" + exit + } + head=$(cat "$tmpdir/HEAD") && + case "$head" in + ref:' '*) + head=$(expr "z$head" : 'zref: \(.*\)') && + head=$(cat "$tmpdir/$head") || exit + esac && + echo "$head HEAD" + (cd $tmpdir && find refs -type f) | + while read path + do + tr -d '\012' <"$tmpdir/$path" + echo " $path" + done && + rm -fr $tmpdir + ;; + +* ) + if test -f "$peek_repo" ; then + git bundle list-heads "$peek_repo" || + echo "failed slurping" + else + git-peek-remote $exec "$peek_repo" || + echo "failed slurping" + fi + ;; +esac | +sort -t ' ' -k 2 | +while read sha1 path +do + case "$sha1" in + failed) + exit 1 ;; + esac + case "$path" in + refs/heads/*) + group=heads ;; + refs/tags/*) + group=tags ;; + *) + group=other ;; + esac + case ",$heads,$tags,$other," in + *,$group,*) + ;; + *) + continue;; + esac + case "$#" in + 0) + match=yes ;; + *) + match=no + for pat + do + case "/$path" in + */$pat ) + match=yes + break ;; + esac + done + esac + case "$match" in + no) + continue ;; + esac + echo "$sha1 $path" +done diff --git a/contrib/examples/git-merge-ours.sh b/contrib/examples/git-merge-ours.sh new file mode 100755 index 0000000000..29dba4ba3a --- /dev/null +++ b/contrib/examples/git-merge-ours.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# +# Pretend we resolved the heads, but declare our tree trumps everybody else. +# + +# We need to exit with 2 if the index does not match our HEAD tree, +# because the current index is what we will be committing as the +# merge result. + +git diff-index --quiet --cached HEAD -- || exit 2 + +exit 0 diff --git a/contrib/examples/git-merge.sh b/contrib/examples/git-merge.sh new file mode 100755 index 0000000000..7b922c3948 --- /dev/null +++ b/contrib/examples/git-merge.sh @@ -0,0 +1,620 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +OPTIONS_KEEPDASHDASH= +OPTIONS_SPEC="\ +git merge [options] <remote>... +git merge [options] <msg> HEAD <remote> +-- +stat show a diffstat at the end of the merge +n don't show a diffstat at the end of the merge +summary (synonym to --stat) +log add list of one-line log to merge commit message +squash create a single commit instead of doing a merge +commit perform a commit if the merge succeeds (default) +ff allow fast-forward (default) +ff-only abort if fast-forward is not possible +rerere-autoupdate update index with any reused conflict resolution +s,strategy= merge strategy to use +X= option for selected merge strategy +m,message= message to be used for the merge commit (if any) +" + +SUBDIRECTORY_OK=Yes +. git-sh-setup +require_work_tree +cd_to_toplevel + +test -z "$(git ls-files -u)" || + die "Merge is not possible because you have unmerged files." + +! test -e "$GIT_DIR/MERGE_HEAD" || + die 'You have not concluded your merge (MERGE_HEAD exists).' + +LF=' +' + +all_strategies='recur recursive octopus resolve stupid ours subtree' +all_strategies="$all_strategies recursive-ours recursive-theirs" +not_strategies='base file index tree' +default_twohead_strategies='recursive' +default_octopus_strategies='octopus' +no_fast_forward_strategies='subtree ours' +no_trivial_strategies='recursive recur subtree ours recursive-ours recursive-theirs' +use_strategies= +xopt= + +allow_fast_forward=t +fast_forward_only= +allow_trivial_merge=t +squash= no_commit= log_arg= rr_arg= + +dropsave() { + rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" \ + "$GIT_DIR/MERGE_STASH" "$GIT_DIR/MERGE_MODE" || exit 1 +} + +savestate() { + # Stash away any local modifications. + git stash create >"$GIT_DIR/MERGE_STASH" +} + +restorestate() { + if test -f "$GIT_DIR/MERGE_STASH" + then + git reset --hard $head >/dev/null + git stash apply $(cat "$GIT_DIR/MERGE_STASH") + git update-index --refresh >/dev/null + fi +} + +finish_up_to_date () { + case "$squash" in + t) + echo "$1 (nothing to squash)" ;; + '') + echo "$1" ;; + esac + dropsave +} + +squash_message () { + echo Squashed commit of the following: + echo + git log --no-merges --pretty=medium ^"$head" $remoteheads +} + +finish () { + if test '' = "$2" + then + rlogm="$GIT_REFLOG_ACTION" + else + echo "$2" + rlogm="$GIT_REFLOG_ACTION: $2" + fi + case "$squash" in + t) + echo "Squash commit -- not updating HEAD" + squash_message >"$GIT_DIR/SQUASH_MSG" + ;; + '') + case "$merge_msg" in + '') + echo "No merge message -- not updating HEAD" + ;; + *) + git update-ref -m "$rlogm" HEAD "$1" "$head" || exit 1 + git gc --auto + ;; + esac + ;; + esac + case "$1" in + '') + ;; + ?*) + if test "$show_diffstat" = t + then + # We want color (if set), but no pager + GIT_PAGER='' git diff --stat --summary -M "$head" "$1" + fi + ;; + esac + + # Run a post-merge hook + if test -x "$GIT_DIR"/hooks/post-merge + then + case "$squash" in + t) + "$GIT_DIR"/hooks/post-merge 1 + ;; + '') + "$GIT_DIR"/hooks/post-merge 0 + ;; + esac + fi +} + +merge_name () { + remote="$1" + rh=$(git rev-parse --verify "$remote^0" 2>/dev/null) || return + if truname=$(expr "$remote" : '\(.*\)~[0-9]*$') && + git show-ref -q --verify "refs/heads/$truname" 2>/dev/null + then + echo "$rh branch '$truname' (early part) of ." + return + fi + if found_ref=$(git rev-parse --symbolic-full-name --verify \ + "$remote" 2>/dev/null) + then + expanded=$(git check-ref-format --branch "$remote") || + exit + if test "${found_ref#refs/heads/}" != "$found_ref" + then + echo "$rh branch '$expanded' of ." + return + elif test "${found_ref#refs/remotes/}" != "$found_ref" + then + echo "$rh remote branch '$expanded' of ." + return + fi + fi + if test "$remote" = "FETCH_HEAD" -a -r "$GIT_DIR/FETCH_HEAD" + then + sed -e 's/ not-for-merge / /' -e 1q \ + "$GIT_DIR/FETCH_HEAD" + return + fi + echo "$rh commit '$remote'" +} + +parse_config () { + while test $# != 0; do + case "$1" in + -n|--no-stat|--no-summary) + show_diffstat=false ;; + --stat|--summary) + show_diffstat=t ;; + --log|--no-log) + log_arg=$1 ;; + --squash) + test "$allow_fast_forward" = t || + die "You cannot combine --squash with --no-ff." + squash=t no_commit=t ;; + --no-squash) + squash= no_commit= ;; + --commit) + no_commit= ;; + --no-commit) + no_commit=t ;; + --ff) + allow_fast_forward=t ;; + --no-ff) + test "$squash" != t || + die "You cannot combine --squash with --no-ff." + test "$fast_forward_only" != t || + die "You cannot combine --ff-only with --no-ff." + allow_fast_forward=f ;; + --ff-only) + test "$allow_fast_forward" != f || + die "You cannot combine --ff-only with --no-ff." + fast_forward_only=t ;; + --rerere-autoupdate|--no-rerere-autoupdate) + rr_arg=$1 ;; + -s|--strategy) + shift + case " $all_strategies " in + *" $1 "*) + use_strategies="$use_strategies$1 " + ;; + *) + case " $not_strategies " in + *" $1 "*) + false + esac && + type "git-merge-$1" >/dev/null 2>&1 || + die "available strategies are: $all_strategies" + use_strategies="$use_strategies$1 " + ;; + esac + ;; + -X) + shift + xopt="${xopt:+$xopt }$(git rev-parse --sq-quote "--$1")" + ;; + -m|--message) + shift + merge_msg="$1" + have_message=t + ;; + --) + shift + break ;; + *) usage ;; + esac + shift + done + args_left=$# +} + +test $# != 0 || usage + +have_message= + +if branch=$(git-symbolic-ref -q HEAD) +then + mergeopts=$(git config "branch.${branch#refs/heads/}.mergeoptions") + if test -n "$mergeopts" + then + parse_config $mergeopts -- + fi +fi + +parse_config "$@" +while test $args_left -lt $#; do shift; done + +if test -z "$show_diffstat"; then + test "$(git config --bool merge.diffstat)" = false && show_diffstat=false + test "$(git config --bool merge.stat)" = false && show_diffstat=false + test -z "$show_diffstat" && show_diffstat=t +fi + +# This could be traditional "merge <msg> HEAD <commit>..." and the +# way we can tell it is to see if the second token is HEAD, but some +# people might have misused the interface and used a committish that +# is the same as HEAD there instead. Traditional format never would +# have "-m" so it is an additional safety measure to check for it. + +if test -z "$have_message" && + second_token=$(git rev-parse --verify "$2^0" 2>/dev/null) && + head_commit=$(git rev-parse --verify "HEAD" 2>/dev/null) && + test "$second_token" = "$head_commit" +then + merge_msg="$1" + shift + head_arg="$1" + shift +elif ! git rev-parse --verify HEAD >/dev/null 2>&1 +then + # If the merged head is a valid one there is no reason to + # forbid "git merge" into a branch yet to be born. We do + # the same for "git pull". + if test 1 -ne $# + then + echo >&2 "Can merge only exactly one commit into empty head" + exit 1 + fi + + test "$squash" != t || + die "Squash commit into empty head not supported yet" + test "$allow_fast_forward" = t || + die "Non-fast-forward into an empty head does not make sense" + rh=$(git rev-parse --verify "$1^0") || + die "$1 - not something we can merge" + + git update-ref -m "initial pull" HEAD "$rh" "" && + git read-tree --reset -u HEAD + exit + +else + # We are invoked directly as the first-class UI. + head_arg=HEAD + + # All the rest are the commits being merged; prepare + # the standard merge summary message to be appended to + # the given message. If remote is invalid we will die + # later in the common codepath so we discard the error + # in this loop. + merge_msg="$( + for remote + do + merge_name "$remote" + done | + if test "$have_message" = t + then + git fmt-merge-msg -m "$merge_msg" $log_arg + else + git fmt-merge-msg $log_arg + fi + )" +fi +head=$(git rev-parse --verify "$head_arg"^0) || usage + +# All the rest are remote heads +test "$#" = 0 && usage ;# we need at least one remote head. +set_reflog_action "merge $*" + +remoteheads= +for remote +do + remotehead=$(git rev-parse --verify "$remote"^0 2>/dev/null) || + die "$remote - not something we can merge" + remoteheads="${remoteheads}$remotehead " + eval GITHEAD_$remotehead='"$remote"' + export GITHEAD_$remotehead +done +set x $remoteheads ; shift + +case "$use_strategies" in +'') + case "$#" in + 1) + var="`git config --get pull.twohead`" + if test -n "$var" + then + use_strategies="$var" + else + use_strategies="$default_twohead_strategies" + fi ;; + *) + var="`git config --get pull.octopus`" + if test -n "$var" + then + use_strategies="$var" + else + use_strategies="$default_octopus_strategies" + fi ;; + esac + ;; +esac + +for s in $use_strategies +do + for ss in $no_fast_forward_strategies + do + case " $s " in + *" $ss "*) + allow_fast_forward=f + break + ;; + esac + done + for ss in $no_trivial_strategies + do + case " $s " in + *" $ss "*) + allow_trivial_merge=f + break + ;; + esac + done +done + +case "$#" in +1) + common=$(git merge-base --all $head "$@") + ;; +*) + common=$(git merge-base --all --octopus $head "$@") + ;; +esac +echo "$head" >"$GIT_DIR/ORIG_HEAD" + +case "$allow_fast_forward,$#,$common,$no_commit" in +?,*,'',*) + # No common ancestors found. We need a real merge. + ;; +?,1,"$1",*) + # If head can reach all the merge then we are up to date. + # but first the most common case of merging one remote. + finish_up_to_date "Already up-to-date." + exit 0 + ;; +t,1,"$head",*) + # Again the most common case of merging one remote. + echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)" + git update-index --refresh 2>/dev/null + msg="Fast-forward" + if test -n "$have_message" + then + msg="$msg (no commit created; -m option ignored)" + fi + new_head=$(git rev-parse --verify "$1^0") && + git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" && + finish "$new_head" "$msg" || exit + dropsave + exit 0 + ;; +?,1,?*"$LF"?*,*) + # We are not doing octopus and not fast-forward. Need a + # real merge. + ;; +?,1,*,) + # We are not doing octopus, not fast-forward, and have only + # one common. + git update-index --refresh 2>/dev/null + case "$allow_trivial_merge,$fast_forward_only" in + t,) + # See if it is really trivial. + git var GIT_COMMITTER_IDENT >/dev/null || exit + echo "Trying really trivial in-index merge..." + if git read-tree --trivial -m -u -v $common $head "$1" && + result_tree=$(git write-tree) + then + echo "Wonderful." + result_commit=$( + printf '%s\n' "$merge_msg" | + git commit-tree $result_tree -p HEAD -p "$1" + ) || exit + finish "$result_commit" "In-index merge" + dropsave + exit 0 + fi + echo "Nope." + esac + ;; +*) + # An octopus. If we can reach all the remote we are up to date. + up_to_date=t + for remote + do + common_one=$(git merge-base --all $head $remote) + if test "$common_one" != "$remote" + then + up_to_date=f + break + fi + done + if test "$up_to_date" = t + then + finish_up_to_date "Already up-to-date. Yeeah!" + exit 0 + fi + ;; +esac + +if test "$fast_forward_only" = t +then + die "Not possible to fast-forward, aborting." +fi + +# We are going to make a new commit. +git var GIT_COMMITTER_IDENT >/dev/null || exit + +# At this point, we need a real merge. No matter what strategy +# we use, it would operate on the index, possibly affecting the +# working tree, and when resolved cleanly, have the desired tree +# in the index -- this means that the index must be in sync with +# the $head commit. The strategies are responsible to ensure this. + +case "$use_strategies" in +?*' '?*) + # Stash away the local changes so that we can try more than one. + savestate + single_strategy=no + ;; +*) + rm -f "$GIT_DIR/MERGE_STASH" + single_strategy=yes + ;; +esac + +result_tree= best_cnt=-1 best_strategy= wt_strategy= +merge_was_ok= +for strategy in $use_strategies +do + test "$wt_strategy" = '' || { + echo "Rewinding the tree to pristine..." + restorestate + } + case "$single_strategy" in + no) + echo "Trying merge strategy $strategy..." + ;; + esac + + # Remember which strategy left the state in the working tree + wt_strategy=$strategy + + eval 'git-merge-$strategy '"$xopt"' $common -- "$head_arg" "$@"' + exit=$? + if test "$no_commit" = t && test "$exit" = 0 + then + merge_was_ok=t + exit=1 ;# pretend it left conflicts. + fi + + test "$exit" = 0 || { + + # The backend exits with 1 when conflicts are left to be resolved, + # with 2 when it does not handle the given merge at all. + + if test "$exit" -eq 1 + then + cnt=`{ + git diff-files --name-only + git ls-files --unmerged + } | wc -l` + if test $best_cnt -le 0 -o $cnt -le $best_cnt + then + best_strategy=$strategy + best_cnt=$cnt + fi + fi + continue + } + + # Automerge succeeded. + result_tree=$(git write-tree) && break +done + +# If we have a resulting tree, that means the strategy module +# auto resolved the merge cleanly. +if test '' != "$result_tree" +then + if test "$allow_fast_forward" = "t" + then + parents=$(git merge-base --independent "$head" "$@") + else + parents=$(git rev-parse "$head" "$@") + fi + parents=$(echo "$parents" | sed -e 's/^/-p /') + result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit + finish "$result_commit" "Merge made by $wt_strategy." + dropsave + exit 0 +fi + +# Pick the result from the best strategy and have the user fix it up. +case "$best_strategy" in +'') + restorestate + case "$use_strategies" in + ?*' '?*) + echo >&2 "No merge strategy handled the merge." + ;; + *) + echo >&2 "Merge with strategy $use_strategies failed." + ;; + esac + exit 2 + ;; +"$wt_strategy") + # We already have its result in the working tree. + ;; +*) + echo "Rewinding the tree to pristine..." + restorestate + echo "Using the $best_strategy to prepare resolving by hand." + git-merge-$best_strategy $common -- "$head_arg" "$@" + ;; +esac + +if test "$squash" = t +then + finish +else + for remote + do + echo $remote + done >"$GIT_DIR/MERGE_HEAD" + printf '%s\n' "$merge_msg" >"$GIT_DIR/MERGE_MSG" || + die "Could not write to $GIT_DIR/MERGE_MSG" + if test "$allow_fast_forward" != t + then + printf "%s" no-ff + else + : + fi >"$GIT_DIR/MERGE_MODE" || + die "Could not write to $GIT_DIR/MERGE_MODE" +fi + +if test "$merge_was_ok" = t +then + echo >&2 \ + "Automatic merge went well; stopped before committing as requested" + exit 0 +else + { + echo ' +Conflicts: +' + git ls-files --unmerged | + sed -e 's/^[^ ]* / /' | + uniq + } >>"$GIT_DIR/MERGE_MSG" + git rerere $rr_arg + die "Automatic merge failed; fix conflicts and then commit the result." +fi diff --git a/contrib/examples/git-notes.sh b/contrib/examples/git-notes.sh new file mode 100755 index 0000000000..e642e47d9f --- /dev/null +++ b/contrib/examples/git-notes.sh @@ -0,0 +1,121 @@ +#!/bin/sh + +USAGE="(edit [-F <file> | -m <msg>] | show) [commit]" +. git-sh-setup + +test -z "$1" && usage +ACTION="$1"; shift + +test -z "$GIT_NOTES_REF" && GIT_NOTES_REF="$(git config core.notesref)" +test -z "$GIT_NOTES_REF" && GIT_NOTES_REF="refs/notes/commits" + +MESSAGE= +while test $# != 0 +do + case "$1" in + -m) + test "$ACTION" = "edit" || usage + shift + if test "$#" = "0"; then + die "error: option -m needs an argument" + else + if [ -z "$MESSAGE" ]; then + MESSAGE="$1" + else + MESSAGE="$MESSAGE + +$1" + fi + shift + fi + ;; + -F) + test "$ACTION" = "edit" || usage + shift + if test "$#" = "0"; then + die "error: option -F needs an argument" + else + if [ -z "$MESSAGE" ]; then + MESSAGE="$(cat "$1")" + else + MESSAGE="$MESSAGE + +$(cat "$1")" + fi + shift + fi + ;; + -*) + usage + ;; + *) + break + ;; + esac +done + +COMMIT=$(git rev-parse --verify --default HEAD "$@") || +die "Invalid commit: $@" + +case "$ACTION" in +edit) + if [ "${GIT_NOTES_REF#refs/notes/}" = "$GIT_NOTES_REF" ]; then + die "Refusing to edit notes in $GIT_NOTES_REF (outside of refs/notes/)" + fi + + MSG_FILE="$GIT_DIR/new-notes-$COMMIT" + GIT_INDEX_FILE="$MSG_FILE.idx" + export GIT_INDEX_FILE + + trap ' + test -f "$MSG_FILE" && rm "$MSG_FILE" + test -f "$GIT_INDEX_FILE" && rm "$GIT_INDEX_FILE" + ' 0 + + CURRENT_HEAD=$(git show-ref "$GIT_NOTES_REF" | cut -f 1 -d ' ') + if [ -z "$CURRENT_HEAD" ]; then + PARENT= + else + PARENT="-p $CURRENT_HEAD" + git read-tree "$GIT_NOTES_REF" || die "Could not read index" + fi + + if [ -z "$MESSAGE" ]; then + GIT_NOTES_REF= git log -1 $COMMIT | sed "s/^/#/" > "$MSG_FILE" + if [ ! -z "$CURRENT_HEAD" ]; then + git cat-file blob :$COMMIT >> "$MSG_FILE" 2> /dev/null + fi + core_editor="$(git config core.editor)" + ${GIT_EDITOR:-${core_editor:-${VISUAL:-${EDITOR:-vi}}}} "$MSG_FILE" + else + echo "$MESSAGE" > "$MSG_FILE" + fi + + grep -v ^# < "$MSG_FILE" | git stripspace > "$MSG_FILE".processed + mv "$MSG_FILE".processed "$MSG_FILE" + if [ -s "$MSG_FILE" ]; then + BLOB=$(git hash-object -w "$MSG_FILE") || + die "Could not write into object database" + git update-index --add --cacheinfo 0644 $BLOB $COMMIT || + die "Could not write index" + else + test -z "$CURRENT_HEAD" && + die "Will not initialise with empty tree" + git update-index --force-remove $COMMIT || + die "Could not update index" + fi + + TREE=$(git write-tree) || die "Could not write tree" + NEW_HEAD=$(echo Annotate $COMMIT | git commit-tree $TREE $PARENT) || + die "Could not annotate" + git update-ref -m "Annotate $COMMIT" \ + "$GIT_NOTES_REF" $NEW_HEAD $CURRENT_HEAD +;; +show) + git rev-parse -q --verify "$GIT_NOTES_REF":$COMMIT > /dev/null || + die "No note for commit $COMMIT." + git show "$GIT_NOTES_REF":$COMMIT +;; +*) + usage +esac diff --git a/contrib/examples/git-remote.perl b/contrib/examples/git-remote.perl new file mode 100755 index 0000000000..b17952a785 --- /dev/null +++ b/contrib/examples/git-remote.perl @@ -0,0 +1,474 @@ +#!/usr/bin/perl -w + +use strict; +use Git; +my $git = Git->repository(); + +sub add_remote_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'url') { + # Having more than one is Ok -- it is used for push. + if (! exists $hash->{'URL'}) { + $hash->{$name}{'URL'} = $value; + } + } + elsif ($what eq 'fetch') { + $hash->{$name}{'FETCH'} ||= []; + push @{$hash->{$name}{'FETCH'}}, $value; + } + elsif ($what eq 'push') { + $hash->{$name}{'PUSH'} ||= []; + push @{$hash->{$name}{'PUSH'}}, $value; + } + if (!exists $hash->{$name}{'SOURCE'}) { + $hash->{$name}{'SOURCE'} = 'config'; + } +} + +sub add_remote_remotes { + my ($hash, $file, $name) = @_; + + if (exists $hash->{$name}) { + $hash->{$name}{'WARNING'} = 'ignored due to config'; + return; + } + + my $fh; + if (!open($fh, '<', $file)) { + print STDERR "Warning: cannot open $file\n"; + return; + } + my $it = { 'SOURCE' => 'remotes' }; + $hash->{$name} = $it; + while (<$fh>) { + chomp; + if (/^URL:\s*(.*)$/) { + # Having more than one is Ok -- it is used for push. + if (! exists $it->{'URL'}) { + $it->{'URL'} = $1; + } + } + elsif (/^Push:\s*(.*)$/) { + $it->{'PUSH'} ||= []; + push @{$it->{'PUSH'}}, $1; + } + elsif (/^Pull:\s*(.*)$/) { + $it->{'FETCH'} ||= []; + push @{$it->{'FETCH'}}, $1; + } + elsif (/^\#/) { + ; # ignore + } + else { + print STDERR "Warning: funny line in $file: $_\n"; + } + } + close($fh); +} + +sub list_remote { + my ($git) = @_; + my %seen = (); + my @remotes = eval { + $git->command(qw(config --get-regexp), '^remote\.'); + }; + for (@remotes) { + if (/^remote\.(\S+?)\.([^.\s]+)\s+(.*)$/) { + add_remote_config(\%seen, $1, $2, $3); + } + } + + my $dir = $git->repo_path() . "/remotes"; + if (opendir(my $dh, $dir)) { + local $_; + while ($_ = readdir($dh)) { + chomp; + next if (! -f "$dir/$_" || ! -r _); + add_remote_remotes(\%seen, "$dir/$_", $_); + } + } + + return \%seen; +} + +sub add_branch_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'remote') { + if (exists $hash->{$name}{'REMOTE'}) { + print STDERR "Warning: more than one branch.$name.remote\n"; + } + $hash->{$name}{'REMOTE'} = $value; + } + elsif ($what eq 'merge') { + $hash->{$name}{'MERGE'} ||= []; + push @{$hash->{$name}{'MERGE'}}, $value; + } +} + +sub list_branch { + my ($git) = @_; + my %seen = (); + my @branches = eval { + $git->command(qw(config --get-regexp), '^branch\.'); + }; + for (@branches) { + if (/^branch\.([^.]*)\.(\S*)\s+(.*)$/) { + add_branch_config(\%seen, $1, $2, $3); + } + } + + return \%seen; +} + +my $remote = list_remote($git); +my $branch = list_branch($git); + +sub update_ls_remote { + my ($harder, $info) = @_; + + return if (($harder == 0) || + (($harder == 1) && exists $info->{'LS_REMOTE'})); + + my @ref = map { s|refs/heads/||; $_; } keys %{$git->remote_refs($info->{'URL'}, [ 'heads' ])}; + $info->{'LS_REMOTE'} = \@ref; +} + +sub list_wildcard_mapping { + my ($forced, $ours, $ls) = @_; + my %refs; + for (@$ls) { + $refs{$_} = 01; # bit #0 to say "they have" + } + for ($git->command('for-each-ref', "refs/remotes/$ours")) { + chomp; + next unless (s|^[0-9a-f]{40}\s[a-z]+\srefs/remotes/$ours/||); + next if ($_ eq 'HEAD'); + $refs{$_} ||= 0; + $refs{$_} |= 02; # bit #1 to say "we have" + } + my (@new, @stale, @tracked); + for (sort keys %refs) { + my $have = $refs{$_}; + if ($have == 1) { + push @new, $_; + } + elsif ($have == 2) { + push @stale, $_; + } + elsif ($have == 3) { + push @tracked, $_; + } + } + return \@new, \@stale, \@tracked; +} + +sub list_mapping { + my ($name, $info) = @_; + my $fetch = $info->{'FETCH'}; + my $ls = $info->{'LS_REMOTE'}; + my (@new, @stale, @tracked); + + for (@$fetch) { + next unless (/(\+)?([^:]+):(.*)/); + my ($forced, $theirs, $ours) = ($1, $2, $3); + if ($theirs eq 'refs/heads/*' && + $ours =~ /^refs\/remotes\/(.*)\/\*$/) { + # wildcard mapping + my ($w_new, $w_stale, $w_tracked) + = list_wildcard_mapping($forced, $1, $ls); + push @new, @$w_new; + push @stale, @$w_stale; + push @tracked, @$w_tracked; + } + elsif ($theirs =~ /\*/ || $ours =~ /\*/) { + print STDERR "Warning: unrecognized mapping in remotes.$name.fetch: $_\n"; + } + elsif ($theirs =~ s|^refs/heads/||) { + if (!grep { $_ eq $theirs } @$ls) { + push @stale, $theirs; + } + elsif ($ours ne '') { + push @tracked, $theirs; + } + } + } + return \@new, \@stale, \@tracked; +} + +sub show_mapping { + my ($name, $info) = @_; + my ($new, $stale, $tracked) = list_mapping($name, $info); + if (@$new) { + print " New remote branches (next fetch will store in remotes/$name)\n"; + print " @$new\n"; + } + if (@$stale) { + print " Stale tracking branches in remotes/$name (use 'git remote prune')\n"; + print " @$stale\n"; + } + if (@$tracked) { + print " Tracked remote branches\n"; + print " @$tracked\n"; + } +} + +sub prune_remote { + my ($name, $ls_remote) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return 1; + } + my $info = $remote->{$name}; + update_ls_remote($ls_remote, $info); + + my ($new, $stale, $tracked) = list_mapping($name, $info); + my $prefix = "refs/remotes/$name"; + foreach my $to_prune (@$stale) { + my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune"); + $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]); + } + return 0; +} + +sub show_remote { + my ($name, $ls_remote) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return 1; + } + my $info = $remote->{$name}; + update_ls_remote($ls_remote, $info); + + print "* remote $name\n"; + print " URL: $info->{'URL'}\n"; + for my $branchname (sort keys %$branch) { + next unless (defined $branch->{$branchname}{'REMOTE'} && + $branch->{$branchname}{'REMOTE'} eq $name); + my @merged = map { + s|^refs/heads/||; + $_; + } split(' ',"@{$branch->{$branchname}{'MERGE'}}"); + next unless (@merged); + print " Remote branch(es) merged with 'git pull' while on branch $branchname\n"; + print " @merged\n"; + } + if ($info->{'LS_REMOTE'}) { + show_mapping($name, $info); + } + if ($info->{'PUSH'}) { + my @pushed = map { + s|^refs/heads/||; + s|^\+refs/heads/|+|; + s|:refs/heads/|:|; + $_; + } @{$info->{'PUSH'}}; + print " Local branch(es) pushed with 'git push'\n"; + print " @pushed\n"; + } + return 0; +} + +sub add_remote { + my ($name, $url, $opts) = @_; + if (exists $remote->{$name}) { + print STDERR "remote $name already exists.\n"; + exit(1); + } + $git->command('config', "remote.$name.url", $url); + my $track = $opts->{'track'} || ["*"]; + + for (@$track) { + $git->command('config', '--add', "remote.$name.fetch", + $opts->{'mirror'} ? + "+refs/$_:refs/$_" : + "+refs/heads/$_:refs/remotes/$name/$_"); + } + if ($opts->{'fetch'}) { + $git->command('fetch', $name); + } + if (exists $opts->{'master'}) { + $git->command('symbolic-ref', "refs/remotes/$name/HEAD", + "refs/remotes/$name/$opts->{'master'}"); + } +} + +sub update_remote { + my ($name) = @_; + my @remotes; + + my $conf = $git->config("remotes." . $name); + if (defined($conf)) { + @remotes = split(' ', $conf); + } elsif ($name eq 'default') { + @remotes = (); + for (sort keys %$remote) { + my $do_fetch = $git->config_bool("remote." . $_ . + ".skipDefaultUpdate"); + unless ($do_fetch) { + push @remotes, $_; + } + } + } else { + print STDERR "Remote group $name does not exist.\n"; + exit(1); + } + for (@remotes) { + print "Updating $_\n"; + $git->command('fetch', "$_"); + } +} + +sub rm_remote { + my ($name) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return 1; + } + + $git->command('config', '--remove-section', "remote.$name"); + + eval { + my @trackers = $git->command('config', '--get-regexp', + 'branch.*.remote', $name); + for (@trackers) { + /^branch\.(.*)?\.remote/; + $git->config('--unset', "branch.$1.remote"); + $git->config('--unset', "branch.$1.merge"); + } + }; + + my @refs = $git->command('for-each-ref', + '--format=%(refname) %(objectname)', "refs/remotes/$name"); + for (@refs) { + my ($ref, $object) = split; + $git->command(qw(update-ref -d), $ref, $object); + } + return 0; +} + +sub add_usage { + print STDERR "Usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n"; + exit(1); +} + +my $VERBOSE = 0; +@ARGV = grep { + if ($_ eq '-v' or $_ eq '--verbose') { + $VERBOSE=1; + 0 + } else { + 1 + } +} @ARGV; + +if (!@ARGV) { + for (sort keys %$remote) { + print "$_"; + print "\t$remote->{$_}->{URL}" if $VERBOSE; + print "\n"; + } +} +elsif ($ARGV[0] eq 'show') { + my $ls_remote = 1; + my $i; + for ($i = 1; $i < @ARGV; $i++) { + if ($ARGV[$i] eq '-n') { + $ls_remote = 0; + } + else { + last; + } + } + if ($i >= @ARGV) { + print STDERR "Usage: git remote show <remote>\n"; + exit(1); + } + my $status = 0; + for (; $i < @ARGV; $i++) { + $status |= show_remote($ARGV[$i], $ls_remote); + } + exit($status); +} +elsif ($ARGV[0] eq 'update') { + if (@ARGV <= 1) { + update_remote("default"); + exit(1); + } + for (my $i = 1; $i < @ARGV; $i++) { + update_remote($ARGV[$i]); + } +} +elsif ($ARGV[0] eq 'prune') { + my $ls_remote = 1; + my $i; + for ($i = 1; $i < @ARGV; $i++) { + if ($ARGV[$i] eq '-n') { + $ls_remote = 0; + } + else { + last; + } + } + if ($i >= @ARGV) { + print STDERR "Usage: git remote prune <remote>\n"; + exit(1); + } + my $status = 0; + for (; $i < @ARGV; $i++) { + $status |= prune_remote($ARGV[$i], $ls_remote); + } + exit($status); +} +elsif ($ARGV[0] eq 'add') { + my %opts = (); + while (1 < @ARGV && $ARGV[1] =~ /^-/) { + my $opt = $ARGV[1]; + shift @ARGV; + if ($opt eq '-f' || $opt eq '--fetch') { + $opts{'fetch'} = 1; + next; + } + if ($opt eq '-t' || $opt eq '--track') { + if (@ARGV < 1) { + add_usage(); + } + $opts{'track'} ||= []; + push @{$opts{'track'}}, $ARGV[1]; + shift @ARGV; + next; + } + if ($opt eq '-m' || $opt eq '--master') { + if ((@ARGV < 1) || exists $opts{'master'}) { + add_usage(); + } + $opts{'master'} = $ARGV[1]; + shift @ARGV; + next; + } + if ($opt eq '--mirror') { + $opts{'mirror'} = 1; + next; + } + add_usage(); + } + if (@ARGV != 3) { + add_usage(); + } + add_remote($ARGV[1], $ARGV[2], \%opts); +} +elsif ($ARGV[0] eq 'rm') { + if (@ARGV <= 1) { + print STDERR "Usage: git remote rm <remote>\n"; + exit(1); + } + exit(rm_remote($ARGV[1])); +} +else { + print STDERR "Usage: git remote\n"; + print STDERR " git remote add <name> <url>\n"; + print STDERR " git remote rm <name>\n"; + print STDERR " git remote show <name>\n"; + print STDERR " git remote prune <name>\n"; + print STDERR " git remote update [group]\n"; + exit(1); +} diff --git a/contrib/examples/git-rerere.perl b/contrib/examples/git-rerere.perl new file mode 100755 index 0000000000..4f692091e7 --- /dev/null +++ b/contrib/examples/git-rerere.perl @@ -0,0 +1,284 @@ +#!/usr/bin/perl +# +# REuse REcorded REsolve. This tool records a conflicted automerge +# result and its hand resolution, and helps to resolve future +# automerge that results in the same conflict. +# +# To enable this feature, create a directory 'rr-cache' under your +# .git/ directory. + +use Digest; +use File::Path; +use File::Copy; + +my $git_dir = $::ENV{GIT_DIR} || ".git"; +my $rr_dir = "$git_dir/rr-cache"; +my $merge_rr = "$git_dir/rr-cache/MERGE_RR"; + +my %merge_rr = (); + +sub read_rr { + if (!-f $merge_rr) { + %merge_rr = (); + return; + } + my $in; + local $/ = "\0"; + open $in, "<$merge_rr" or die "$!: $merge_rr"; + while (<$in>) { + chomp; + my ($name, $path) = /^([0-9a-f]{40})\t(.*)$/s; + $merge_rr{$path} = $name; + } + close $in; +} + +sub write_rr { + my $out; + open $out, ">$merge_rr" or die "$!: $merge_rr"; + for my $path (sort keys %merge_rr) { + my $name = $merge_rr{$path}; + print $out "$name\t$path\0"; + } + close $out; +} + +sub compute_conflict_name { + my ($path) = @_; + my @side = (); + my $in; + open $in, "<$path" or die "$!: $path"; + + my $sha1 = Digest->new("SHA-1"); + my $hunk = 0; + while (<$in>) { + if (/^<<<<<<< .*/) { + $hunk++; + @side = ([], undef); + } + elsif (/^=======$/) { + $side[1] = []; + } + elsif (/^>>>>>>> .*/) { + my ($one, $two); + $one = join('', @{$side[0]}); + $two = join('', @{$side[1]}); + if ($two le $one) { + ($one, $two) = ($two, $one); + } + $sha1->add($one); + $sha1->add("\0"); + $sha1->add($two); + $sha1->add("\0"); + @side = (); + } + elsif (@side == 0) { + next; + } + elsif (defined $side[1]) { + push @{$side[1]}, $_; + } + else { + push @{$side[0]}, $_; + } + } + close $in; + return ($sha1->hexdigest, $hunk); +} + +sub record_preimage { + my ($path, $name) = @_; + my @side = (); + my ($in, $out); + open $in, "<$path" or die "$!: $path"; + open $out, ">$name" or die "$!: $name"; + + while (<$in>) { + if (/^<<<<<<< .*/) { + @side = ([], undef); + } + elsif (/^=======$/) { + $side[1] = []; + } + elsif (/^>>>>>>> .*/) { + my ($one, $two); + $one = join('', @{$side[0]}); + $two = join('', @{$side[1]}); + if ($two le $one) { + ($one, $two) = ($two, $one); + } + print $out "<<<<<<<\n"; + print $out $one; + print $out "=======\n"; + print $out $two; + print $out ">>>>>>>\n"; + @side = (); + } + elsif (@side == 0) { + print $out $_; + } + elsif (defined $side[1]) { + push @{$side[1]}, $_; + } + else { + push @{$side[0]}, $_; + } + } + close $out; + close $in; +} + +sub find_conflict { + my $in; + local $/ = "\0"; + my $pid = open($in, '-|'); + die "$!" unless defined $pid; + if (!$pid) { + exec(qw(git ls-files -z -u)) or die "$!: ls-files"; + } + my %path = (); + my @path = (); + while (<$in>) { + chomp; + my ($mode, $sha1, $stage, $path) = + /^([0-7]+) ([0-9a-f]{40}) ([123])\t(.*)$/s; + $path{$path} |= (1 << $stage); + } + close $in; + while (my ($path, $status) = each %path) { + if ($status == 14) { push @path, $path; } + } + return @path; +} + +sub merge { + my ($name, $path) = @_; + record_preimage($path, "$rr_dir/$name/thisimage"); + unless (system('git', 'merge-file', map { "$rr_dir/$name/${_}image" } + qw(this pre post))) { + my $in; + open $in, "<$rr_dir/$name/thisimage" or + die "$!: $name/thisimage"; + my $out; + open $out, ">$path" or die "$!: $path"; + while (<$in>) { print $out $_; } + close $in; + close $out; + return 1; + } + return 0; +} + +sub garbage_collect_rerere { + # We should allow specifying these from the command line and + # that is why the caller gives @ARGV to us, but I am lazy. + + my $cutoff_noresolve = 15; # two weeks + my $cutoff_resolve = 60; # two months + my @to_remove; + while (<$rr_dir/*/preimage>) { + my ($dir) = /^(.*)\/preimage$/; + my $cutoff = ((-f "$dir/postimage") + ? $cutoff_resolve + : $cutoff_noresolve); + my $age = -M "$_"; + if ($cutoff <= $age) { + push @to_remove, $dir; + } + } + if (@to_remove) { + rmtree(\@to_remove); + } +} + +-d "$rr_dir" || exit(0); + +read_rr(); + +if (@ARGV) { + my $arg = shift @ARGV; + if ($arg eq 'clear') { + for my $path (keys %merge_rr) { + my $name = $merge_rr{$path}; + if (-d "$rr_dir/$name" && + ! -f "$rr_dir/$name/postimage") { + rmtree(["$rr_dir/$name"]); + } + } + unlink $merge_rr; + } + elsif ($arg eq 'status') { + for my $path (keys %merge_rr) { + print $path, "\n"; + } + } + elsif ($arg eq 'diff') { + for my $path (keys %merge_rr) { + my $name = $merge_rr{$path}; + system('diff', ((@ARGV == 0) ? ('-u') : @ARGV), + '-L', "a/$path", '-L', "b/$path", + "$rr_dir/$name/preimage", $path); + } + } + elsif ($arg eq 'gc') { + garbage_collect_rerere(@ARGV); + } + else { + die "$0 unknown command: $arg\n"; + } + exit 0; +} + +my %conflict = map { $_ => 1 } find_conflict(); + +# MERGE_RR records paths with conflicts immediately after merge +# failed. Some of the conflicted paths might have been hand resolved +# in the working tree since then, but the initial run would catch all +# and register their preimages. + +for my $path (keys %conflict) { + # This path has conflict. If it is not recorded yet, + # record the pre-image. + if (!exists $merge_rr{$path}) { + my ($name, $hunk) = compute_conflict_name($path); + next unless ($hunk); + $merge_rr{$path} = $name; + if (! -d "$rr_dir/$name") { + mkpath("$rr_dir/$name", 0, 0777); + print STDERR "Recorded preimage for '$path'\n"; + record_preimage($path, "$rr_dir/$name/preimage"); + } + } +} + +# Now some of the paths that had conflicts earlier might have been +# hand resolved. Others may be similar to a conflict already that +# was resolved before. + +for my $path (keys %merge_rr) { + my $name = $merge_rr{$path}; + + # We could resolve this automatically if we have images. + if (-f "$rr_dir/$name/preimage" && + -f "$rr_dir/$name/postimage") { + if (merge($name, $path)) { + print STDERR "Resolved '$path' using previous resolution.\n"; + # Then we do not have to worry about this path + # anymore. + delete $merge_rr{$path}; + next; + } + } + + # Let's see if we have resolved it. + (undef, my $hunk) = compute_conflict_name($path); + next if ($hunk); + + print STDERR "Recorded resolution for '$path'.\n"; + copy($path, "$rr_dir/$name/postimage"); + # And we do not have to worry about this path anymore. + delete $merge_rr{$path}; +} + +# Write out the rest. +write_rr(); diff --git a/contrib/examples/git-reset.sh b/contrib/examples/git-reset.sh new file mode 100755 index 0000000000..bafeb52cd1 --- /dev/null +++ b/contrib/examples/git-reset.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# +# Copyright (c) 2005, 2006 Linus Torvalds and Junio C Hamano +# +USAGE='[--mixed | --soft | --hard] [<commit-ish>] [ [--] <paths>...]' +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action "reset $*" +require_work_tree + +update= reset_type=--mixed +unset rev + +while test $# != 0 +do + case "$1" in + --mixed | --soft | --hard) + reset_type="$1" + ;; + --) + break + ;; + -*) + usage + ;; + *) + rev=$(git rev-parse --verify "$1") || exit + shift + break + ;; + esac + shift +done + +: ${rev=HEAD} +rev=$(git rev-parse --verify $rev^0) || exit + +# Skip -- in "git reset HEAD -- foo" and "git reset -- foo". +case "$1" in --) shift ;; esac + +# git reset --mixed tree [--] paths... can be used to +# load chosen paths from the tree into the index without +# affecting the working tree nor HEAD. +if test $# != 0 +then + test "$reset_type" = "--mixed" || + die "Cannot do partial $reset_type reset." + + git diff-index --cached $rev -- "$@" | + sed -e 's/^:\([0-7][0-7]*\) [0-7][0-7]* \([0-9a-f][0-9a-f]*\) [0-9a-f][0-9a-f]* [A-Z] \(.*\)$/\1 \2 \3/' | + git update-index --add --remove --index-info || exit + git update-index --refresh + exit +fi + +cd_to_toplevel + +if test "$reset_type" = "--hard" +then + update=-u +fi + +# Soft reset does not touch the index file nor the working tree +# at all, but requires them in a good order. Other resets reset +# the index file to the tree object we are switching to. +if test "$reset_type" = "--soft" +then + if test -f "$GIT_DIR/MERGE_HEAD" || + test "" != "$(git ls-files --unmerged)" + then + die "Cannot do a soft reset in the middle of a merge." + fi +else + git read-tree -v --reset $update "$rev" || exit +fi + +# Any resets update HEAD to the head being switched to. +if orig=$(git rev-parse --verify HEAD 2>/dev/null) +then + echo "$orig" >"$GIT_DIR/ORIG_HEAD" +else + rm -f "$GIT_DIR/ORIG_HEAD" +fi +git update-ref -m "$GIT_REFLOG_ACTION" HEAD "$rev" +update_ref_status=$? + +case "$reset_type" in +--hard ) + test $update_ref_status = 0 && { + printf "HEAD is now at " + GIT_PAGER= git log --max-count=1 --pretty=oneline \ + --abbrev-commit HEAD + } + ;; +--soft ) + ;; # Nothing else to do +--mixed ) + # Report what has not been updated. + git update-index --refresh + ;; +esac + +rm -f "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/rr-cache/MERGE_RR" \ + "$GIT_DIR/SQUASH_MSG" "$GIT_DIR/MERGE_MSG" + +exit $update_ref_status diff --git a/contrib/examples/git-resolve.sh b/contrib/examples/git-resolve.sh index 0ee1bd898e..8f98142f77 100755 --- a/contrib/examples/git-resolve.sh +++ b/contrib/examples/git-resolve.sh @@ -48,7 +48,7 @@ case "$common" in "$head") echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $merge)" git read-tree -u -m $head $merge || exit 1 - git update-ref -m "resolve $merge_name: Fast forward" \ + git update-ref -m "resolve $merge_name: Fast-forward" \ HEAD "$merge" "$head" git diff-tree -p $head $merge | git apply --stat dropheads diff --git a/contrib/examples/git-revert.sh b/contrib/examples/git-revert.sh new file mode 100755 index 0000000000..6bf155cbdb --- /dev/null +++ b/contrib/examples/git-revert.sh @@ -0,0 +1,207 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# Copyright (c) 2005 Junio C Hamano +# + +case "$0" in +*-revert* ) + test -t 0 && edit=-e + replay= + me=revert + USAGE='[--edit | --no-edit] [-n] <commit-ish>' ;; +*-cherry-pick* ) + replay=t + edit= + me=cherry-pick + USAGE='[--edit] [-n] [-r] [-x] <commit-ish>' ;; +* ) + echo >&2 "What are you talking about?" + exit 1 ;; +esac + +SUBDIRECTORY_OK=Yes ;# we will cd up +. git-sh-setup +require_work_tree +cd_to_toplevel + +no_commit= +xopt= +while case "$#" in 0) break ;; esac +do + case "$1" in + -n|--n|--no|--no-|--no-c|--no-co|--no-com|--no-comm|\ + --no-commi|--no-commit) + no_commit=t + ;; + -e|--e|--ed|--edi|--edit) + edit=-e + ;; + --n|--no|--no-|--no-e|--no-ed|--no-edi|--no-edit) + edit= + ;; + -r) + : no-op ;; + -x|--i-really-want-to-expose-my-private-commit-object-name) + replay= + ;; + -X?*) + xopt="$xopt$(git rev-parse --sq-quote "--${1#-X}")" + ;; + --strategy-option=*) + xopt="$xopt$(git rev-parse --sq-quote "--${1#--strategy-option=}")" + ;; + -X|--strategy-option) + shift + xopt="$xopt$(git rev-parse --sq-quote "--$1")" + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +set_reflog_action "$me" + +test "$me,$replay" = "revert,t" && usage + +case "$no_commit" in +t) + # We do not intend to commit immediately. We just want to + # merge the differences in. + head=$(git-write-tree) || + die "Your index file is unmerged." + ;; +*) + head=$(git-rev-parse --verify HEAD) || + die "You do not have a valid HEAD" + files=$(git-diff-index --cached --name-only $head) || exit + if [ "$files" ]; then + die "Dirty index: cannot $me (dirty: $files)" + fi + ;; +esac + +rev=$(git-rev-parse --verify "$@") && +commit=$(git-rev-parse --verify "$rev^0") || + die "Not a single commit $@" +prev=$(git-rev-parse --verify "$commit^1" 2>/dev/null) || + die "Cannot run $me a root commit" +git-rev-parse --verify "$commit^2" >/dev/null 2>&1 && + die "Cannot run $me a multi-parent commit." + +encoding=$(git config i18n.commitencoding || echo UTF-8) + +# "commit" is an existing commit. We would want to apply +# the difference it introduces since its first parent "prev" +# on top of the current HEAD if we are cherry-pick. Or the +# reverse of it if we are revert. + +case "$me" in +revert) + git show -s --pretty=oneline --encoding="$encoding" $commit | + sed -e ' + s/^[^ ]* /Revert "/ + s/$/"/ + ' + echo + echo "This reverts commit $commit." + test "$rev" = "$commit" || + echo "(original 'git revert' arguments: $@)" + base=$commit next=$prev + ;; + +cherry-pick) + pick_author_script=' + /^author /{ + s/'\''/'\''\\'\'\''/g + h + s/^author \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_NAME='\''&'\''/p + + g + s/^author [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p + + g + s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_DATE='\''&'\''/p + + q + }' + + logmsg=`git show -s --pretty=raw --encoding="$encoding" "$commit"` + set_author_env=`echo "$logmsg" | + LANG=C LC_ALL=C sed -ne "$pick_author_script"` + eval "$set_author_env" + export GIT_AUTHOR_NAME + export GIT_AUTHOR_EMAIL + export GIT_AUTHOR_DATE + + echo "$logmsg" | + sed -e '1,/^$/d' -e 's/^ //' + case "$replay" in + '') + echo "(cherry picked from commit $commit)" + test "$rev" = "$commit" || + echo "(original 'git cherry-pick' arguments: $@)" + ;; + esac + base=$prev next=$commit + ;; + +esac >.msg + +eval GITHEAD_$head=HEAD +eval GITHEAD_$next='`git show -s \ + --pretty=oneline --encoding="$encoding" "$commit" | + sed -e "s/^[^ ]* //"`' +export GITHEAD_$head GITHEAD_$next + +# This three way merge is an interesting one. We are at +# $head, and would want to apply the change between $commit +# and $prev on top of us (when reverting), or the change between +# $prev and $commit on top of us (when cherry-picking or replaying). + +eval "git merge-recursive $xopt $base -- $head $next" && +result=$(git-write-tree 2>/dev/null) || { + mv -f .msg "$GIT_DIR/MERGE_MSG" + { + echo ' +Conflicts: +' + git ls-files --unmerged | + sed -e 's/^[^ ]* / /' | + uniq + } >>"$GIT_DIR/MERGE_MSG" + echo >&2 "Automatic $me failed. After resolving the conflicts," + echo >&2 "mark the corrected paths with 'git-add <paths>'" + echo >&2 "and commit the result." + case "$me" in + cherry-pick) + echo >&2 "You may choose to use the following when making" + echo >&2 "the commit:" + echo >&2 "$set_author_env" + esac + exit 1 +} + +# If we are cherry-pick, and if the merge did not result in +# hand-editing, we will hit this commit and inherit the original +# author date and name. +# If we are revert, or if our cherry-pick results in a hand merge, +# we had better say that the current user is responsible for that. + +case "$no_commit" in +'') + git-commit -n -F .msg $edit + rm -f .msg + ;; +esac diff --git a/contrib/examples/git-svnimport.perl b/contrib/examples/git-svnimport.perl new file mode 100755 index 0000000000..b09ff8f12f --- /dev/null +++ b/contrib/examples/git-svnimport.perl @@ -0,0 +1,976 @@ +#!/usr/bin/perl + +# This tool is copyright (c) 2005, Matthias Urlichs. +# It is released under the Gnu Public License, version 2. +# +# The basic idea is to pull and analyze SVN changes. +# +# Checking out the files is done by a single long-running SVN connection. +# +# The head revision is on branch "origin" by default. +# You can change that with the '-o' option. + +use strict; +use warnings; +use Getopt::Std; +use File::Copy; +use File::Spec; +use File::Temp qw(tempfile); +use File::Path qw(mkpath); +use File::Basename qw(basename dirname); +use Time::Local; +use IO::Pipe; +use POSIX qw(strftime dup2); +use IPC::Open2; +use SVN::Core; +use SVN::Ra; + +die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1"; + +$SIG{'PIPE'}="IGNORE"; +$ENV{'TZ'}="UTC"; + +our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T, + $opt_b,$opt_r,$opt_I,$opt_A,$opt_s,$opt_l,$opt_d,$opt_D,$opt_S,$opt_F, + $opt_P,$opt_R); + +sub usage() { + print STDERR <<END; +Usage: ${\basename $0} # fetch/update GIT from SVN + [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs] + [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname] + [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg] + [-m] [-M regex] [-A author_file] [-S] [-F] [-P project_name] [SVN_URL] +END + exit(1); +} + +getopts("A:b:C:dDFhiI:l:mM:o:rs:t:T:SP:R:uv") or usage(); +usage if $opt_h; + +my $tag_name = $opt_t || "tags"; +my $trunk_name = defined $opt_T ? $opt_T : "trunk"; +my $branch_name = $opt_b || "branches"; +my $project_name = $opt_P || ""; +$project_name = "/" . $project_name if ($project_name); +my $repack_after = $opt_R || 1000; +my $root_pool = SVN::Pool->new_default; + +@ARGV == 1 or @ARGV == 2 or usage(); + +$opt_o ||= "origin"; +$opt_s ||= 1; +my $git_tree = $opt_C; +$git_tree ||= "."; + +my $svn_url = $ARGV[0]; +my $svn_dir = $ARGV[1]; + +our @mergerx = (); +if ($opt_m) { + my $branch_esc = quotemeta ($branch_name); + my $trunk_esc = quotemeta ($trunk_name); + @mergerx = + ( + qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i, + qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i, + qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i + ); +} +if ($opt_M) { + unshift (@mergerx, qr/$opt_M/); +} + +# Absolutize filename now, since we will have chdir'ed by the time we +# get around to opening it. +$opt_A = File::Spec->rel2abs($opt_A) if $opt_A; + +our %users = (); +our $users_file = undef; +sub read_users($) { + $users_file = File::Spec->rel2abs(@_); + die "Cannot open $users_file\n" unless -f $users_file; + open(my $authors,$users_file); + while(<$authors>) { + chomp; + next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/; + (my $user,my $name,my $email) = ($1,$2,$3); + $users{$user} = [$name,$email]; + } + close($authors); +} + +select(STDERR); $|=1; select(STDOUT); + + +package SVNconn; +# Basic SVN connection. +# We're only interested in connecting and downloading, so ... + +use File::Spec; +use File::Temp qw(tempfile); +use POSIX qw(strftime dup2); +use Fcntl qw(SEEK_SET); + +sub new { + my($what,$repo) = @_; + $what=ref($what) if ref($what); + + my $self = {}; + $self->{'buffer'} = ""; + bless($self,$what); + + $repo =~ s#/+$##; + $self->{'fullrep'} = $repo; + $self->conn(); + + return $self; +} + +sub conn { + my $self = shift; + my $repo = $self->{'fullrep'}; + my $auth = SVN::Core::auth_open ([SVN::Client::get_simple_provider, + SVN::Client::get_ssl_server_trust_file_provider, + SVN::Client::get_username_provider]); + my $s = SVN::Ra->new(url => $repo, auth => $auth, pool => $root_pool); + die "SVN connection to $repo: $!\n" unless defined $s; + $self->{'svn'} = $s; + $self->{'repo'} = $repo; + $self->{'maxrev'} = $s->get_latest_revnum(); +} + +sub file { + my($self,$path,$rev) = @_; + + my ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), UNLINK => 1); + + print "... $rev $path ...\n" if $opt_v; + my (undef, $properties); + $path =~ s#^/*##; + my $subpool = SVN::Pool::new_default_sub; + eval { (undef, $properties) + = $self->{'svn'}->get_file($path,$rev,$fh); }; + if($@) { + return undef if $@ =~ /Attempted to get checksum/; + die $@; + } + my $mode; + if (exists $properties->{'svn:executable'}) { + $mode = '100755'; + } elsif (exists $properties->{'svn:special'}) { + my ($special_content, $filesize); + $filesize = tell $fh; + seek $fh, 0, SEEK_SET; + read $fh, $special_content, $filesize; + if ($special_content =~ s/^link //) { + $mode = '120000'; + seek $fh, 0, SEEK_SET; + truncate $fh, 0; + print $fh $special_content; + } else { + die "unexpected svn:special file encountered"; + } + } else { + $mode = '100644'; + } + close ($fh); + + return ($name, $mode); +} + +sub ignore { + my($self,$path,$rev) = @_; + + print "... $rev $path ...\n" if $opt_v; + $path =~ s#^/*##; + my $subpool = SVN::Pool::new_default_sub; + my (undef,undef,$properties) + = $self->{'svn'}->get_dir($path,$rev,undef); + if (exists $properties->{'svn:ignore'}) { + my ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), + UNLINK => 1); + print $fh $properties->{'svn:ignore'}; + close($fh); + return $name; + } else { + return undef; + } +} + +sub dir_list { + my($self,$path,$rev) = @_; + $path =~ s#^/*##; + my $subpool = SVN::Pool::new_default_sub; + my ($dirents,undef,$properties) + = $self->{'svn'}->get_dir($path,$rev,undef); + return $dirents; +} + +package main; +use URI; + +our $svn = $svn_url; +$svn .= "/$svn_dir" if defined $svn_dir; +my $svn2 = SVNconn->new($svn); +$svn = SVNconn->new($svn); + +my $lwp_ua; +if($opt_d or $opt_D) { + $svn_url = URI->new($svn_url)->canonical; + if($opt_D) { + $svn_dir =~ s#/*$#/#; + } else { + $svn_dir = ""; + } + if ($svn_url->scheme eq "http") { + use LWP::UserAgent; + $lwp_ua = LWP::UserAgent->new(keep_alive => 1, requests_redirectable => []); + } else { + print STDERR "Warning: not HTTP; turning off direct file access\n"; + $opt_d=0; + } +} + +sub pdate($) { + my($d) = @_; + $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)# + or die "Unparseable date: $d\n"; + my $y=$1; $y-=1900 if $y>1900; + return timegm($6||0,$5,$4,$3,$2-1,$y); +} + +sub getwd() { + my $pwd = `pwd`; + chomp $pwd; + return $pwd; +} + + +sub get_headref($$) { + my $name = shift; + my $git_dir = shift; + my $sha; + + if (open(C,"$git_dir/refs/heads/$name")) { + chomp($sha = <C>); + close(C); + length($sha) == 40 + or die "Cannot get head id for $name ($sha): $!\n"; + } + return $sha; +} + + +-d $git_tree + or mkdir($git_tree,0777) + or die "Could not create $git_tree: $!"; +chdir($git_tree); + +my $orig_branch = ""; +my $forward_master = 0; +my %branches; + +my $git_dir = $ENV{"GIT_DIR"} || ".git"; +$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#; +$ENV{"GIT_DIR"} = $git_dir; +my $orig_git_index; +$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE}; +my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx', + DIR => File::Spec->tmpdir()); +close ($git_ih); +$ENV{GIT_INDEX_FILE} = $git_index; +my $maxnum = 0; +my $last_rev = ""; +my $last_branch; +my $current_rev = $opt_s || 1; +unless(-d $git_dir) { + system("git init"); + die "Cannot init the GIT db at $git_tree: $?\n" if $?; + system("git read-tree --empty"); + die "Cannot init an empty tree: $?\n" if $?; + + $last_branch = $opt_o; + $orig_branch = ""; +} else { + -f "$git_dir/refs/heads/$opt_o" + or die "Branch '$opt_o' does not exist.\n". + "Either use the correct '-o branch' option,\n". + "or import to a new repository.\n"; + + -f "$git_dir/svn2git" + or die "'$git_dir/svn2git' does not exist.\n". + "You need that file for incremental imports.\n"; + open(F, "git symbolic-ref HEAD |") or + die "Cannot run git-symbolic-ref: $!\n"; + chomp ($last_branch = <F>); + $last_branch = basename($last_branch); + close(F); + unless($last_branch) { + warn "Cannot read the last branch name: $! -- assuming 'master'\n"; + $last_branch = "master"; + } + $orig_branch = $last_branch; + $last_rev = get_headref($orig_branch, $git_dir); + if (-f "$git_dir/SVN2GIT_HEAD") { + die <<EOM; +SVN2GIT_HEAD exists. +Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD. +You may need to run + + git-read-tree -m -u SVN2GIT_HEAD HEAD +EOM + } + system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD"); + + $forward_master = + $opt_o ne 'master' && -f "$git_dir/refs/heads/master" && + system('cmp', '-s', "$git_dir/refs/heads/master", + "$git_dir/refs/heads/$opt_o") == 0; + + # populate index + system('git', 'read-tree', $last_rev); + die "read-tree failed: $?\n" if $?; + + # Get the last import timestamps + open my $B,"<", "$git_dir/svn2git"; + while(<$B>) { + chomp; + my($num,$branch,$ref) = split; + $branches{$branch}{$num} = $ref; + $branches{$branch}{"LAST"} = $ref; + $current_rev = $num+1 if $current_rev <= $num; + } + close($B); +} +-d $git_dir + or die "Could not create git subdir ($git_dir).\n"; + +my $default_authors = "$git_dir/svn-authors"; +if ($opt_A) { + read_users($opt_A); + copy($opt_A,$default_authors) or die "Copy failed: $!"; +} else { + read_users($default_authors) if -f $default_authors; +} + +open BRANCHES,">>", "$git_dir/svn2git"; + +sub node_kind($$) { + my ($svnpath, $revision) = @_; + $svnpath =~ s#^/*##; + my $subpool = SVN::Pool::new_default_sub; + my $kind = $svn->{'svn'}->check_path($svnpath,$revision); + return $kind; +} + +sub get_file($$$) { + my($svnpath,$rev,$path) = @_; + + # now get it + my ($name,$mode); + if($opt_d) { + my($req,$res); + + # /svn/!svn/bc/2/django/trunk/django-docs/build.py + my $url=$svn_url->clone(); + $url->path($url->path."/!svn/bc/$rev/$svn_dir$svnpath"); + print "... $path...\n" if $opt_v; + $req = HTTP::Request->new(GET => $url); + $res = $lwp_ua->request($req); + if ($res->is_success) { + my $fh; + ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), UNLINK => 1); + print $fh $res->content; + close($fh) or die "Could not write $name: $!\n"; + } else { + return undef if $res->code == 301; # directory? + die $res->status_line." at $url\n"; + } + $mode = '0644'; # can't obtain mode via direct http request? + } else { + ($name,$mode) = $svn->file("$svnpath",$rev); + return undef unless defined $name; + } + + my $pid = open(my $F, '-|'); + die $! unless defined $pid; + if (!$pid) { + exec("git", "hash-object", "-w", $name) + or die "Cannot create object: $!\n"; + } + my $sha = <$F>; + chomp $sha; + close $F; + unlink $name; + return [$mode, $sha, $path]; +} + +sub get_ignore($$$$$) { + my($new,$old,$rev,$path,$svnpath) = @_; + + return unless $opt_I; + my $name = $svn->ignore("$svnpath",$rev); + if ($path eq '/') { + $path = $opt_I; + } else { + $path = File::Spec->catfile($path,$opt_I); + } + if (defined $name) { + my $pid = open(my $F, '-|'); + die $! unless defined $pid; + if (!$pid) { + exec("git", "hash-object", "-w", $name) + or die "Cannot create object: $!\n"; + } + my $sha = <$F>; + chomp $sha; + close $F; + unlink $name; + push(@$new,['0644',$sha,$path]); + } elsif (defined $old) { + push(@$old,$path); + } +} + +sub project_path($$) +{ + my ($path, $project) = @_; + + $path = "/".$path unless ($path =~ m#^\/#) ; + return $1 if ($path =~ m#^$project\/(.*)$#); + + $path =~ s#\.#\\\.#g; + $path =~ s#\+#\\\+#g; + return "/" if ($project =~ m#^$path.*$#); + + return undef; +} + +sub split_path($$) { + my($rev,$path) = @_; + my $branch; + + if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) { + $branch = "/$1"; + } elsif($path =~ s#^/\Q$trunk_name\E/?##) { + $branch = "/"; + } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) { + $branch = $1; + } else { + my %no_error = ( + "/" => 1, + "/$tag_name" => 1, + "/$branch_name" => 1 + ); + print STDERR "$rev: Unrecognized path: $path\n" unless (defined $no_error{$path}); + return () + } + if ($path eq "") { + $path = "/"; + } elsif ($project_name) { + $path = project_path($path, $project_name); + } + return ($branch,$path); +} + +sub branch_rev($$) { + + my ($srcbranch,$uptorev) = @_; + + my $bbranches = $branches{$srcbranch}; + my @revs = reverse sort { ($a eq 'LAST' ? 0 : $a) <=> ($b eq 'LAST' ? 0 : $b) } keys %$bbranches; + my $therev; + foreach my $arev(@revs) { + next if ($arev eq 'LAST'); + if ($arev <= $uptorev) { + $therev = $arev; + last; + } + } + return $therev; +} + +sub expand_svndir($$$); + +sub expand_svndir($$$) +{ + my ($svnpath, $rev, $path) = @_; + my @list; + get_ignore(\@list, undef, $rev, $path, $svnpath); + my $dirents = $svn->dir_list($svnpath, $rev); + foreach my $p(keys %$dirents) { + my $kind = node_kind($svnpath.'/'.$p, $rev); + if ($kind eq $SVN::Node::file) { + my $f = get_file($svnpath.'/'.$p, $rev, $path.'/'.$p); + push(@list, $f) if $f; + } elsif ($kind eq $SVN::Node::dir) { + push(@list, + expand_svndir($svnpath.'/'.$p, $rev, $path.'/'.$p)); + } + } + return @list; +} + +sub copy_path($$$$$$$$) { + # Somebody copied a whole subdirectory. + # We need to find the index entries from the old version which the + # SVN log entry points to, and add them to the new place. + + my($newrev,$newbranch,$path,$oldpath,$rev,$node_kind,$new,$parents) = @_; + + my($srcbranch,$srcpath) = split_path($rev,$oldpath); + unless(defined $srcbranch && defined $srcpath) { + print "Path not found when copying from $oldpath @ $rev.\n". + "Will try to copy from original SVN location...\n" + if $opt_v; + push (@$new, expand_svndir($oldpath, $rev, $path)); + return; + } + my $therev = branch_rev($srcbranch, $rev); + my $gitrev = $branches{$srcbranch}{$therev}; + unless($gitrev) { + print STDERR "$newrev:$newbranch: could not find $oldpath \@ $rev\n"; + return; + } + if ($srcbranch ne $newbranch) { + push(@$parents, $branches{$srcbranch}{'LAST'}); + } + print "$newrev:$newbranch:$path: copying from $srcbranch:$srcpath @ $rev\n" if $opt_v; + if ($node_kind eq $SVN::Node::dir) { + $srcpath =~ s#/*$#/#; + } + + my $pid = open my $f,'-|'; + die $! unless defined $pid; + if (!$pid) { + exec("git","ls-tree","-r","-z",$gitrev,$srcpath) + or die $!; + } + local $/ = "\0"; + while(<$f>) { + chomp; + my($m,$p) = split(/\t/,$_,2); + my($mode,$type,$sha1) = split(/ /,$m); + next if $type ne "blob"; + if ($node_kind eq $SVN::Node::dir) { + $p = $path . substr($p,length($srcpath)-1); + } else { + $p = $path; + } + push(@$new,[$mode,$sha1,$p]); + } + close($f) or + print STDERR "$newrev:$newbranch: could not list files in $oldpath \@ $rev\n"; +} + +sub commit { + my($branch, $changed_paths, $revision, $author, $date, $message) = @_; + my($committer_name,$committer_email,$dest); + my($author_name,$author_email); + my(@old,@new,@parents); + + if (not defined $author or $author eq "") { + $committer_name = $committer_email = "unknown"; + } elsif (defined $users_file) { + die "User $author is not listed in $users_file\n" + unless exists $users{$author}; + ($committer_name,$committer_email) = @{$users{$author}}; + } elsif ($author =~ /^(.*?)\s+<(.*)>$/) { + ($committer_name, $committer_email) = ($1, $2); + } else { + $author =~ s/^<(.*)>$/$1/; + $committer_name = $committer_email = $author; + } + + if ($opt_F && $message =~ /From:\s+(.*?)\s+<(.*)>\s*\n/) { + ($author_name, $author_email) = ($1, $2); + print "Author from From: $1 <$2>\n" if ($opt_v);; + } elsif ($opt_S && $message =~ /Signed-off-by:\s+(.*?)\s+<(.*)>\s*\n/) { + ($author_name, $author_email) = ($1, $2); + print "Author from Signed-off-by: $1 <$2>\n" if ($opt_v);; + } else { + $author_name = $committer_name; + $author_email = $committer_email; + } + + $date = pdate($date); + + my $tag; + my $parent; + if($branch eq "/") { # trunk + $parent = $opt_o; + } elsif($branch =~ m#^/(.+)#) { # tag + $tag = 1; + $parent = $1; + } else { # "normal" branch + # nothing to do + $parent = $branch; + } + $dest = $parent; + + my $prev = $changed_paths->{"/"}; + if($prev and $prev->[0] eq "A") { + delete $changed_paths->{"/"}; + my $oldpath = $prev->[1]; + my $rev; + if(defined $oldpath) { + my $p; + ($parent,$p) = split_path($revision,$oldpath); + if(defined $parent) { + if($parent eq "/") { + $parent = $opt_o; + } else { + $parent =~ s#^/##; # if it's a tag + } + } + } else { + $parent = undef; + } + } + + my $rev; + if($revision > $opt_s and defined $parent) { + open(H,'-|',"git","rev-parse","--verify",$parent); + $rev = <H>; + close(H) or do { + print STDERR "$revision: cannot find commit '$parent'!\n"; + return; + }; + chop $rev; + if(length($rev) != 40) { + print STDERR "$revision: cannot find commit '$parent'!\n"; + return; + } + $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"}; + if($revision != $opt_s and not $rev) { + print STDERR "$revision: do not know ancestor for '$parent'!\n"; + return; + } + } else { + $rev = undef; + } + +# if($prev and $prev->[0] eq "A") { +# if(not $tag) { +# unless(open(H,"> $git_dir/refs/heads/$branch")) { +# print STDERR "$revision: Could not create branch $branch: $!\n"; +# $state=11; +# next; +# } +# print H "$rev\n" +# or die "Could not write branch $branch: $!"; +# close(H) +# or die "Could not write branch $branch: $!"; +# } +# } + if(not defined $rev) { + unlink($git_index); + } elsif ($rev ne $last_rev) { + print "Switching from $last_rev to $rev ($branch)\n" if $opt_v; + system("git", "read-tree", $rev); + die "read-tree failed for $rev: $?\n" if $?; + $last_rev = $rev; + } + + push (@parents, $rev) if defined $rev; + + my $cid; + if($tag and not %$changed_paths) { + $cid = $rev; + } else { + my @paths = sort keys %$changed_paths; + foreach my $path(@paths) { + my $action = $changed_paths->{$path}; + + if ($action->[0] eq "R") { + # refer to a file/tree in an earlier commit + push(@old,$path); # remove any old stuff + } + if(($action->[0] eq "A") || ($action->[0] eq "R")) { + my $node_kind = node_kind($action->[3], $revision); + if ($node_kind eq $SVN::Node::file) { + my $f = get_file($action->[3], + $revision, $path); + if ($f) { + push(@new,$f) if $f; + } else { + my $opath = $action->[3]; + print STDERR "$revision: $branch: could not fetch '$opath'\n"; + } + } elsif ($node_kind eq $SVN::Node::dir) { + if($action->[1]) { + copy_path($revision, $branch, + $path, $action->[1], + $action->[2], $node_kind, + \@new, \@parents); + } else { + get_ignore(\@new, \@old, $revision, + $path, $action->[3]); + } + } + } elsif ($action->[0] eq "D") { + push(@old,$path); + } elsif ($action->[0] eq "M") { + my $node_kind = node_kind($action->[3], $revision); + if ($node_kind eq $SVN::Node::file) { + my $f = get_file($action->[3], + $revision, $path); + push(@new,$f) if $f; + } elsif ($node_kind eq $SVN::Node::dir) { + get_ignore(\@new, \@old, $revision, + $path, $action->[3]); + } + } else { + die "$revision: unknown action '".$action->[0]."' for $path\n"; + } + } + + while(@old) { + my @o1; + if(@old > 55) { + @o1 = splice(@old,0,50); + } else { + @o1 = @old; + @old = (); + } + my $pid = open my $F, "-|"; + die "$!" unless defined $pid; + if (!$pid) { + exec("git", "ls-files", "-z", @o1) or die $!; + } + @o1 = (); + local $/ = "\0"; + while(<$F>) { + chomp; + push(@o1,$_); + } + close($F); + + while(@o1) { + my @o2; + if(@o1 > 55) { + @o2 = splice(@o1,0,50); + } else { + @o2 = @o1; + @o1 = (); + } + system("git","update-index","--force-remove","--",@o2); + die "Cannot remove files: $?\n" if $?; + } + } + while(@new) { + my @n2; + if(@new > 12) { + @n2 = splice(@new,0,10); + } else { + @n2 = @new; + @new = (); + } + system("git","update-index","--add", + (map { ('--cacheinfo', @$_) } @n2)); + die "Cannot add files: $?\n" if $?; + } + + my $pid = open(C,"-|"); + die "Cannot fork: $!" unless defined $pid; + unless($pid) { + exec("git","write-tree"); + die "Cannot exec git-write-tree: $!\n"; + } + chomp(my $tree = <C>); + length($tree) == 40 + or die "Cannot get tree id ($tree): $!\n"; + close(C) + or die "Error running git-write-tree: $?\n"; + print "Tree ID $tree\n" if $opt_v; + + my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n"; + my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n"; + $pid = fork(); + die "Fork: $!\n" unless defined $pid; + unless($pid) { + $pr->writer(); + $pw->reader(); + open(OUT,">&STDOUT"); + dup2($pw->fileno(),0); + dup2($pr->fileno(),1); + $pr->close(); + $pw->close(); + + my @par = (); + + # loose detection of merges + # based on the commit msg + foreach my $rx (@mergerx) { + if ($message =~ $rx) { + my $mparent = $1; + if ($mparent eq 'HEAD') { $mparent = $opt_o }; + if ( -e "$git_dir/refs/heads/$mparent") { + $mparent = get_headref($mparent, $git_dir); + push (@parents, $mparent); + print OUT "Merge parent branch: $mparent\n" if $opt_v; + } + } + } + my %seen_parents = (); + my @unique_parents = grep { ! $seen_parents{$_} ++ } @parents; + foreach my $bparent (@unique_parents) { + push @par, '-p', $bparent; + print OUT "Merge parent branch: $bparent\n" if $opt_v; + } + + exec("env", + "GIT_AUTHOR_NAME=$author_name", + "GIT_AUTHOR_EMAIL=$author_email", + "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), + "GIT_COMMITTER_NAME=$committer_name", + "GIT_COMMITTER_EMAIL=$committer_email", + "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), + "git", "commit-tree", $tree,@par); + die "Cannot exec git-commit-tree: $!\n"; + } + $pw->writer(); + $pr->reader(); + + $message =~ s/[\s\n]+\z//; + $message = "r$revision: $message" if $opt_r; + + print $pw "$message\n" + or die "Error writing to git-commit-tree: $!\n"; + $pw->close(); + + print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v; + chomp($cid = <$pr>); + length($cid) == 40 + or die "Cannot get commit id ($cid): $!\n"; + print "Commit ID $cid\n" if $opt_v; + $pr->close(); + + waitpid($pid,0); + die "Error running git-commit-tree: $?\n" if $?; + } + + if (not defined $cid) { + $cid = $branches{"/"}{"LAST"}; + } + + if(not defined $dest) { + print "... no known parent\n" if $opt_v; + } elsif(not $tag) { + print "Writing to refs/heads/$dest\n" if $opt_v; + open(C,">$git_dir/refs/heads/$dest") and + print C ("$cid\n") and + close(C) + or die "Cannot write branch $dest for update: $!\n"; + } + + if ($tag) { + $last_rev = "-" if %$changed_paths; + # the tag was 'complex', i.e. did not refer to a "real" revision + + $dest =~ tr/_/\./ if $opt_u; + + system('git', 'tag', '-f', $dest, $cid) == 0 + or die "Cannot create tag $dest: $!\n"; + + print "Created tag '$dest' on '$branch'\n" if $opt_v; + } + $branches{$branch}{"LAST"} = $cid; + $branches{$branch}{$revision} = $cid; + $last_rev = $cid; + print BRANCHES "$revision $branch $cid\n"; + print "DONE: $revision $dest $cid\n" if $opt_v; +} + +sub commit_all { + # Recursive use of the SVN connection does not work + local $svn = $svn2; + + my ($changed_paths, $revision, $author, $date, $message) = @_; + my %p; + while(my($path,$action) = each %$changed_paths) { + $p{$path} = [ $action->action,$action->copyfrom_path, $action->copyfrom_rev, $path ]; + } + $changed_paths = \%p; + + my %done; + my @col; + my $pref; + my $branch; + + while(my($path,$action) = each %$changed_paths) { + ($branch,$path) = split_path($revision,$path); + next if not defined $branch; + next if not defined $path; + $done{$branch}{$path} = $action; + } + while(($branch,$changed_paths) = each %done) { + commit($branch, $changed_paths, $revision, $author, $date, $message); + } +} + +$opt_l = $svn->{'maxrev'} if not defined $opt_l or $opt_l > $svn->{'maxrev'}; + +if ($opt_l < $current_rev) { + print "Up to date: no new revisions to fetch!\n" if $opt_v; + unlink("$git_dir/SVN2GIT_HEAD"); + exit; +} + +print "Processing from $current_rev to $opt_l ...\n" if $opt_v; + +my $from_rev; +my $to_rev = $current_rev - 1; + +my $subpool = SVN::Pool::new_default_sub; +while ($to_rev < $opt_l) { + $subpool->clear; + $from_rev = $to_rev + 1; + $to_rev = $from_rev + $repack_after; + $to_rev = $opt_l if $opt_l < $to_rev; + print "Fetching from $from_rev to $to_rev ...\n" if $opt_v; + $svn->{'svn'}->get_log("",$from_rev,$to_rev,0,1,1,\&commit_all); + my $pid = fork(); + die "Fork: $!\n" unless defined $pid; + unless($pid) { + exec("git", "repack", "-d") + or die "Cannot repack: $!\n"; + } + waitpid($pid, 0); +} + + +unlink($git_index); + +if (defined $orig_git_index) { + $ENV{GIT_INDEX_FILE} = $orig_git_index; +} else { + delete $ENV{GIT_INDEX_FILE}; +} + +# Now switch back to the branch we were in before all of this happened +if($orig_branch) { + print "DONE\n" if $opt_v and (not defined $opt_l or $opt_l > 0); + system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") + if $forward_master; + unless ($opt_i) { + system('git', 'read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD'); + die "read-tree failed: $?\n" if $?; + } +} else { + $orig_branch = "master"; + print "DONE; creating $orig_branch branch\n" if $opt_v and (not defined $opt_l or $opt_l > 0); + system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") + unless -f "$git_dir/refs/heads/master"; + system('git', 'update-ref', 'HEAD', "$orig_branch"); + unless ($opt_i) { + system('git checkout'); + die "checkout failed: $?\n" if $?; + } +} +unlink("$git_dir/SVN2GIT_HEAD"); +close(BRANCHES); diff --git a/contrib/examples/git-svnimport.txt b/contrib/examples/git-svnimport.txt new file mode 100644 index 0000000000..3bb871e42f --- /dev/null +++ b/contrib/examples/git-svnimport.txt @@ -0,0 +1,179 @@ +git-svnimport(1) +================ +v0.1, July 2005 + +NAME +---- +git-svnimport - Import a SVN repository into git + + +SYNOPSIS +-------- +[verse] +'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ] + [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev] + [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ] + [ -s start_chg ] [ -m ] [ -r ] [ -M regex ] + [ -I <ignorefile_name> ] [ -A <author_file> ] + [ -R <repack_each_revs>] [ -P <path_from_trunk> ] + <SVN_repository_URL> [ <path> ] + + +DESCRIPTION +----------- +Imports a SVN repository into git. It will either create a new +repository, or incrementally import into an existing one. + +SVN access is done by the SVN::Perl module. + +git-svnimport assumes that SVN repositories are organized into one +"trunk" directory where the main development happens, "branches/FOO" +directories for branches, and "/tags/FOO" directories for tags. +Other subdirectories are ignored. + +git-svnimport creates a file ".git/svn2git", which is required for +incremental SVN imports. + +OPTIONS +------- +-C <target-dir>:: + The GIT repository to import to. If the directory doesn't + exist, it will be created. Default is the current directory. + +-s <start_rev>:: + Start importing at this SVN change number. The default is 1. ++ +When importing incrementally, you might need to edit the .git/svn2git file. + +-i:: + Import-only: don't perform a checkout after importing. This option + ensures the working directory and index remain untouched and will + not create them if they do not exist. + +-T <trunk_subdir>:: + Name the SVN trunk. Default "trunk". + +-t <tag_subdir>:: + Name the SVN subdirectory for tags. Default "tags". + +-b <branch_subdir>:: + Name the SVN subdirectory for branches. Default "branches". + +-o <branch-for-HEAD>:: + The 'trunk' branch from SVN is imported to the 'origin' branch within + the git repository. Use this option if you want to import into a + different branch. + +-r:: + Prepend 'rX: ' to commit messages, where X is the imported + subversion revision. + +-u:: + Replace underscores in tag names with periods. + +-I <ignorefile_name>:: + Import the svn:ignore directory property to files with this + name in each directory. (The Subversion and GIT ignore + syntaxes are similar enough that using the Subversion patterns + directly with "-I .gitignore" will almost always just work.) + +-A <author_file>:: + Read a file with lines on the form ++ +------ + username = User's Full Name <email@addr.es> + +------ ++ +and use "User's Full Name <email@addr.es>" as the GIT +author and committer for Subversion commits made by +"username". If encountering a commit made by a user not in the +list, abort. ++ +For convenience, this data is saved to $GIT_DIR/svn-authors +each time the -A option is provided, and read from that same +file each time git-svnimport is run with an existing GIT +repository without -A. + +-m:: + Attempt to detect merges based on the commit message. This option + will enable default regexes that try to capture the name source + branch name from the commit message. + +-M <regex>:: + Attempt to detect merges based on the commit message with a custom + regex. It can be used with -m to also see the default regexes. + You must escape forward slashes. + +-l <max_rev>:: + Specify a maximum revision number to pull. ++ +Formerly, this option controlled how many revisions to pull, +due to SVN memory leaks. (These have been worked around.) + +-R <repack_each_revs>:: + Specify how often git repository should be repacked. ++ +The default value is 1000. git-svnimport will do imports in chunks of 1000 +revisions, after each chunk the git repository will be repacked. To disable +this behavior specify some large value here which is greater than the number of +revisions to import. + +-P <path_from_trunk>:: + Partial import of the SVN tree. ++ +By default, the whole tree on the SVN trunk (/trunk) is imported. +'-P my/proj' will import starting only from '/trunk/my/proj'. +This option is useful when you want to import one project from a +svn repo which hosts multiple projects under the same trunk. + +-v:: + Verbosity: let 'svnimport' report what it is doing. + +-d:: + Use direct HTTP requests if possible. The "<path>" argument is used + only for retrieving the SVN logs; the path to the contents is + included in the SVN log. + +-D:: + Use direct HTTP requests if possible. The "<path>" argument is used + for retrieving the logs, as well as for the contents. ++ +There's no safe way to automatically find out which of these options to +use, so you need to try both. Usually, the one that's wrong will die +with a 40x error pretty quickly. + +<SVN_repository_URL>:: + The URL of the SVN module you want to import. For local + repositories, use "file:///absolute/path". ++ +If you're using the "-d" or "-D" option, this is the URL of the SVN +repository itself; it usually ends in "/svn". + +<path>:: + The path to the module you want to check out. + +-h:: + Print a short usage message and exit. + +OUTPUT +------ +If '-v' is specified, the script reports what it is doing. + +Otherwise, success is indicated the Unix way, i.e. by simply exiting with +a zero exit status. + +Author +------ +Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from +various participants of the git-list <git@vger.kernel.org>. + +Based on a cvs2git script by the same author. + +Documentation +-------------- +Documentation by Matthias Urlichs <smurf@smurf.noris.de>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/contrib/examples/git-tag.sh b/contrib/examples/git-tag.sh index 5ee3f50a3c..2c15bc955b 100755 --- a/contrib/examples/git-tag.sh +++ b/contrib/examples/git-tag.sh @@ -14,7 +14,7 @@ username= list= verify= LINES=0 -while case "$#" in 0) break ;; esac +while test $# != 0 do case "$1" in -a) @@ -164,10 +164,10 @@ git check-ref-format "tags/$name" || object=$(git rev-parse --verify --default HEAD "$@") || exit 1 type=$(git cat-file -t $object) || exit 1 -tagger=$(git-var GIT_COMMITTER_IDENT) || exit 1 +tagger=$(git var GIT_COMMITTER_IDENT) || exit 1 test -n "$username" || - username=$(git repo-config user.signingkey) || + username=$(git config user.signingkey) || username=$(expr "z$tagger" : 'z\(.*>\)') trap 'rm -f "$GIT_DIR"/TAG_TMP* "$GIT_DIR"/TAG_FINALMSG "$GIT_DIR"/TAG_EDITMSG' 0 diff --git a/contrib/examples/git-verify-tag.sh b/contrib/examples/git-verify-tag.sh index 37b0023b27..0902a5c21a 100755 --- a/contrib/examples/git-verify-tag.sh +++ b/contrib/examples/git-verify-tag.sh @@ -5,7 +5,7 @@ SUBDIRECTORY_OK='Yes' . git-sh-setup verbose= -while case $# in 0) break;; esac +while test $# != 0 do case "$1" in -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) diff --git a/contrib/fast-import/git-import.perl b/contrib/fast-import/git-import.perl new file mode 100755 index 0000000000..f9fef6db28 --- /dev/null +++ b/contrib/fast-import/git-import.perl @@ -0,0 +1,64 @@ +#!/usr/bin/perl +# +# Performs an initial import of a directory. This is the equivalent +# of doing 'git init; git add .; git commit'. It's a little slower, +# but is meant to be a simple fast-import example. + +use strict; +use File::Find; + +my $USAGE = 'Usage: git-import branch import-message'; +my $branch = shift or die "$USAGE\n"; +my $message = shift or die "$USAGE\n"; + +chomp(my $username = `git config user.name`); +chomp(my $email = `git config user.email`); +die 'You need to set user name and email' + unless $username && $email; + +system('git init'); +open(my $fi, '|-', qw(git fast-import --date-format=now)) + or die "unable to spawn fast-import: $!"; + +print $fi <<EOF; +commit refs/heads/$branch +committer $username <$email> now +data <<MSGEOF +$message +MSGEOF + +EOF + +find( + sub { + if($File::Find::name eq './.git') { + $File::Find::prune = 1; + return; + } + return unless -f $_; + + my $fn = $File::Find::name; + $fn =~ s#^.\/##; + + open(my $in, '<', $_) + or die "unable to open $fn: $!"; + my @st = stat($in) + or die "unable to stat $fn: $!"; + my $len = $st[7]; + + print $fi "M 644 inline $fn\n"; + print $fi "data $len\n"; + while($len > 0) { + my $r = read($in, my $buf, $len < 4096 ? $len : 4096); + defined($r) or die "read error from $fn: $!"; + $r > 0 or die "premature EOF from $fn: $!"; + print $fi $buf; + $len -= $r; + } + print $fi "\n"; + + }, '.' +); + +close($fi); +exit $?; diff --git a/contrib/fast-import/git-import.sh b/contrib/fast-import/git-import.sh new file mode 100755 index 0000000000..0ca7718d05 --- /dev/null +++ b/contrib/fast-import/git-import.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# +# Performs an initial import of a directory. This is the equivalent +# of doing 'git init; git add .; git commit'. It's a lot slower, +# but is meant to be a simple fast-import example. + +if [ -z "$1" -o -z "$2" ]; then + echo "Usage: git-import branch import-message" + exit 1 +fi + +USERNAME="$(git config user.name)" +EMAIL="$(git config user.email)" + +if [ -z "$USERNAME" -o -z "$EMAIL" ]; then + echo "You need to set user name and email" + exit 1 +fi + +git init + +( + cat <<EOF +commit refs/heads/$1 +committer $USERNAME <$EMAIL> now +data <<MSGEOF +$2 +MSGEOF + +EOF + find * -type f|while read i;do + echo "M 100644 inline $i" + echo data $(stat -c '%s' "$i") + cat "$i" + echo + done + echo +) | git fast-import --date-format=now diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 deleted file mode 100755 index 55778c5775..0000000000 --- a/contrib/fast-import/git-p4 +++ /dev/null @@ -1,1627 +0,0 @@ -#!/usr/bin/env python -# -# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git. -# -# Author: Simon Hausmann <simon@lst.de> -# Copyright: 2007 Simon Hausmann <simon@lst.de> -# 2007 Trolltech ASA -# License: MIT <http://www.opensource.org/licenses/mit-license.php> -# - -import optparse, sys, os, marshal, popen2, subprocess, shelve -import tempfile, getopt, sha, os.path, time, platform -import re - -from sets import Set; - -verbose = False - -def die(msg): - if verbose: - raise Exception(msg) - else: - sys.stderr.write(msg + "\n") - sys.exit(1) - -def write_pipe(c, str): - if verbose: - sys.stderr.write('Writing pipe: %s\n' % c) - - pipe = os.popen(c, 'w') - val = pipe.write(str) - if pipe.close(): - die('Command failed: %s' % c) - - return val - -def read_pipe(c, ignore_error=False): - if verbose: - sys.stderr.write('Reading pipe: %s\n' % c) - - pipe = os.popen(c, 'rb') - val = pipe.read() - if pipe.close() and not ignore_error: - die('Command failed: %s' % c) - - return val - - -def read_pipe_lines(c): - if verbose: - sys.stderr.write('Reading pipe: %s\n' % c) - ## todo: check return status - pipe = os.popen(c, 'rb') - val = pipe.readlines() - if pipe.close(): - die('Command failed: %s' % c) - - return val - -def system(cmd): - if verbose: - sys.stderr.write("executing %s\n" % cmd) - if os.system(cmd) != 0: - die("command failed: %s" % cmd) - -def p4CmdList(cmd, stdin=None, stdin_mode='w+b'): - cmd = "p4 -G %s" % cmd - if verbose: - sys.stderr.write("Opening pipe: %s\n" % cmd) - - # Use a temporary file to avoid deadlocks without - # subprocess.communicate(), which would put another copy - # of stdout into memory. - stdin_file = None - if stdin is not None: - stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode) - stdin_file.write(stdin) - stdin_file.flush() - stdin_file.seek(0) - - p4 = subprocess.Popen(cmd, shell=True, - stdin=stdin_file, - stdout=subprocess.PIPE) - - result = [] - try: - while True: - entry = marshal.load(p4.stdout) - result.append(entry) - except EOFError: - pass - exitCode = p4.wait() - if exitCode != 0: - entry = {} - entry["p4ExitCode"] = exitCode - result.append(entry) - - return result - -def p4Cmd(cmd): - list = p4CmdList(cmd) - result = {} - for entry in list: - result.update(entry) - return result; - -def p4Where(depotPath): - if not depotPath.endswith("/"): - depotPath += "/" - output = p4Cmd("where %s..." % depotPath) - if output["code"] == "error": - return "" - clientPath = "" - if "path" in output: - clientPath = output.get("path") - elif "data" in output: - data = output.get("data") - lastSpace = data.rfind(" ") - clientPath = data[lastSpace + 1:] - - if clientPath.endswith("..."): - clientPath = clientPath[:-3] - return clientPath - -def currentGitBranch(): - return read_pipe("git name-rev HEAD").split(" ")[1].strip() - -def isValidGitDir(path): - if (os.path.exists(path + "/HEAD") - and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")): - return True; - return False - -def parseRevision(ref): - return read_pipe("git rev-parse %s" % ref).strip() - -def extractLogMessageFromGitCommit(commit): - logMessage = "" - - ## fixme: title is first line of commit, not 1st paragraph. - foundTitle = False - for log in read_pipe_lines("git cat-file commit %s" % commit): - if not foundTitle: - if len(log) == 1: - foundTitle = True - continue - - logMessage += log - return logMessage - -def extractSettingsGitLog(log): - values = {} - for line in log.split("\n"): - line = line.strip() - m = re.search (r"^ *\[git-p4: (.*)\]$", line) - if not m: - continue - - assignments = m.group(1).split (':') - for a in assignments: - vals = a.split ('=') - key = vals[0].strip() - val = ('='.join (vals[1:])).strip() - if val.endswith ('\"') and val.startswith('"'): - val = val[1:-1] - - values[key] = val - - paths = values.get("depot-paths") - if not paths: - paths = values.get("depot-path") - if paths: - values['depot-paths'] = paths.split(',') - return values - -def gitBranchExists(branch): - proc = subprocess.Popen(["git", "rev-parse", branch], - stderr=subprocess.PIPE, stdout=subprocess.PIPE); - return proc.wait() == 0; - -def gitConfig(key): - return read_pipe("git config %s" % key, ignore_error=True).strip() - -def p4BranchesInGit(branchesAreInRemotes = True): - branches = {} - - cmdline = "git rev-parse --symbolic " - if branchesAreInRemotes: - cmdline += " --remotes" - else: - cmdline += " --branches" - - for line in read_pipe_lines(cmdline): - line = line.strip() - - ## only import to p4/ - if not line.startswith('p4/') or line == "p4/HEAD": - continue - branch = line - - # strip off p4 - branch = re.sub ("^p4/", "", line) - - branches[branch] = parseRevision(line) - return branches - -def findUpstreamBranchPoint(head = "HEAD"): - branches = p4BranchesInGit() - # map from depot-path to branch name - branchByDepotPath = {} - for branch in branches.keys(): - tip = branches[branch] - log = extractLogMessageFromGitCommit(tip) - settings = extractSettingsGitLog(log) - if settings.has_key("depot-paths"): - paths = ",".join(settings["depot-paths"]) - branchByDepotPath[paths] = "remotes/p4/" + branch - - settings = None - parent = 0 - while parent < 65535: - commit = head + "~%s" % parent - log = extractLogMessageFromGitCommit(commit) - settings = extractSettingsGitLog(log) - if settings.has_key("depot-paths"): - paths = ",".join(settings["depot-paths"]) - if branchByDepotPath.has_key(paths): - return [branchByDepotPath[paths], settings] - - parent = parent + 1 - - return ["", settings] - -def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True): - if not silent: - print ("Creating/updating branch(es) in %s based on origin branch(es)" - % localRefPrefix) - - originPrefix = "origin/p4/" - - for line in read_pipe_lines("git rev-parse --symbolic --remotes"): - line = line.strip() - if (not line.startswith(originPrefix)) or line.endswith("HEAD"): - continue - - headName = line[len(originPrefix):] - remoteHead = localRefPrefix + headName - originHead = line - - original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead)) - if (not original.has_key('depot-paths') - or not original.has_key('change')): - continue - - update = False - if not gitBranchExists(remoteHead): - if verbose: - print "creating %s" % remoteHead - update = True - else: - settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead)) - if settings.has_key('change') > 0: - if settings['depot-paths'] == original['depot-paths']: - originP4Change = int(original['change']) - p4Change = int(settings['change']) - if originP4Change > p4Change: - print ("%s (%s) is newer than %s (%s). " - "Updating p4 branch from origin." - % (originHead, originP4Change, - remoteHead, p4Change)) - update = True - else: - print ("Ignoring: %s was imported from %s while " - "%s was imported from %s" - % (originHead, ','.join(original['depot-paths']), - remoteHead, ','.join(settings['depot-paths']))) - - if update: - system("git update-ref %s %s" % (remoteHead, originHead)) - -def originP4BranchesExist(): - return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master") - -class Command: - def __init__(self): - self.usage = "usage: %prog [options]" - self.needsGit = True - -class P4Debug(Command): - def __init__(self): - Command.__init__(self) - self.options = [ - optparse.make_option("--verbose", dest="verbose", action="store_true", - default=False), - ] - self.description = "A tool to debug the output of p4 -G." - self.needsGit = False - self.verbose = False - - def run(self, args): - j = 0 - for output in p4CmdList(" ".join(args)): - print 'Element: %d' % j - j += 1 - print output - return True - -class P4RollBack(Command): - def __init__(self): - Command.__init__(self) - self.options = [ - optparse.make_option("--verbose", dest="verbose", action="store_true"), - optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true") - ] - self.description = "A tool to debug the multi-branch import. Don't use :)" - self.verbose = False - self.rollbackLocalBranches = False - - def run(self, args): - if len(args) != 1: - return False - maxChange = int(args[0]) - - if "p4ExitCode" in p4Cmd("changes -m 1"): - die("Problems executing p4"); - - if self.rollbackLocalBranches: - refPrefix = "refs/heads/" - lines = read_pipe_lines("git rev-parse --symbolic --branches") - else: - refPrefix = "refs/remotes/" - lines = read_pipe_lines("git rev-parse --symbolic --remotes") - - for line in lines: - if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"): - line = line.strip() - ref = refPrefix + line - log = extractLogMessageFromGitCommit(ref) - settings = extractSettingsGitLog(log) - - depotPaths = settings['depot-paths'] - change = settings['change'] - - changed = False - - if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange) - for p in depotPaths]))) == 0: - print "Branch %s did not exist at change %s, deleting." % (ref, maxChange) - system("git update-ref -d %s `git rev-parse %s`" % (ref, ref)) - continue - - while change and int(change) > maxChange: - changed = True - if self.verbose: - print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange) - system("git update-ref %s \"%s^\"" % (ref, ref)) - log = extractLogMessageFromGitCommit(ref) - settings = extractSettingsGitLog(log) - - - depotPaths = settings['depot-paths'] - change = settings['change'] - - if changed: - print "%s rewound to %s" % (ref, change) - - return True - -class P4Submit(Command): - def __init__(self): - Command.__init__(self) - self.options = [ - optparse.make_option("--continue", action="store_false", dest="firstTime"), - optparse.make_option("--verbose", dest="verbose", action="store_true"), - optparse.make_option("--origin", dest="origin"), - optparse.make_option("--reset", action="store_true", dest="reset"), - optparse.make_option("--log-substitutions", dest="substFile"), - optparse.make_option("--dry-run", action="store_true"), - optparse.make_option("--direct", dest="directSubmit", action="store_true"), - optparse.make_option("--trust-me-like-a-fool", dest="trustMeLikeAFool", action="store_true"), - ] - self.description = "Submit changes from git to the perforce depot." - self.usage += " [name of git branch to submit into perforce depot]" - self.firstTime = True - self.reset = False - self.interactive = True - self.dryRun = False - self.substFile = "" - self.firstTime = True - self.origin = "" - self.directSubmit = False - self.trustMeLikeAFool = False - self.verbose = False - self.isWindows = (platform.system() == "Windows") - - self.logSubstitutions = {} - self.logSubstitutions["<enter description here>"] = "%log%" - self.logSubstitutions["\tDetails:"] = "\tDetails: %log%" - - def check(self): - if len(p4CmdList("opened ...")) > 0: - die("You have files opened with perforce! Close them before starting the sync.") - - def start(self): - if len(self.config) > 0 and not self.reset: - die("Cannot start sync. Previous sync config found at %s\n" - "If you want to start submitting again from scratch " - "maybe you want to call git-p4 submit --reset" % self.configFile) - - commits = [] - if self.directSubmit: - commits.append("0") - else: - for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)): - commits.append(line.strip()) - commits.reverse() - - self.config["commits"] = commits - - def prepareLogMessage(self, template, message): - result = "" - - for line in template.split("\n"): - if line.startswith("#"): - result += line + "\n" - continue - - substituted = False - for key in self.logSubstitutions.keys(): - if line.find(key) != -1: - value = self.logSubstitutions[key] - value = value.replace("%log%", message) - if value != "@remove@": - result += line.replace(key, value) + "\n" - substituted = True - break - - if not substituted: - result += line + "\n" - - return result - - def prepareSubmitTemplate(self): - # remove lines in the Files section that show changes to files outside the depot path we're committing into - template = "" - inFilesSection = False - for line in read_pipe_lines("p4 change -o"): - if inFilesSection: - if line.startswith("\t"): - # path starts and ends with a tab - path = line[1:] - lastTab = path.rfind("\t") - if lastTab != -1: - path = path[:lastTab] - if not path.startswith(self.depotPath): - continue - else: - inFilesSection = False - else: - if line.startswith("Files:"): - inFilesSection = True - - template += line - - return template - - def applyCommit(self, id): - if self.directSubmit: - print "Applying local change in working directory/index" - diff = self.diffStatus - else: - print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) - diff = read_pipe_lines("git diff-tree -r --name-status \"%s^\" \"%s\"" % (id, id)) - filesToAdd = set() - filesToDelete = set() - editedFiles = set() - for line in diff: - modifier = line[0] - path = line[1:].strip() - if modifier == "M": - system("p4 edit \"%s\"" % path) - editedFiles.add(path) - elif modifier == "A": - filesToAdd.add(path) - if path in filesToDelete: - filesToDelete.remove(path) - elif modifier == "D": - filesToDelete.add(path) - if path in filesToAdd: - filesToAdd.remove(path) - else: - die("unknown modifier %s for %s" % (modifier, path)) - - if self.directSubmit: - diffcmd = "cat \"%s\"" % self.diffFile - else: - diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id) - patchcmd = diffcmd + " | git apply " - tryPatchCmd = patchcmd + "--check -" - applyPatchCmd = patchcmd + "--check --apply -" - - if os.system(tryPatchCmd) != 0: - print "Unfortunately applying the change failed!" - print "What do you want to do?" - response = "x" - while response != "s" and response != "a" and response != "w": - response = raw_input("[s]kip this patch / [a]pply the patch forcibly " - "and with .rej files / [w]rite the patch to a file (patch.txt) ") - if response == "s": - print "Skipping! Good luck with the next patches..." - return - elif response == "a": - os.system(applyPatchCmd) - if len(filesToAdd) > 0: - print "You may also want to call p4 add on the following files:" - print " ".join(filesToAdd) - if len(filesToDelete): - print "The following files should be scheduled for deletion with p4 delete:" - print " ".join(filesToDelete) - die("Please resolve and submit the conflict manually and " - + "continue afterwards with git-p4 submit --continue") - elif response == "w": - system(diffcmd + " > patch.txt") - print "Patch saved to patch.txt in %s !" % self.clientPath - die("Please resolve and submit the conflict manually and " - "continue afterwards with git-p4 submit --continue") - - system(applyPatchCmd) - - for f in filesToAdd: - system("p4 add \"%s\"" % f) - for f in filesToDelete: - system("p4 revert \"%s\"" % f) - system("p4 delete \"%s\"" % f) - - logMessage = "" - if not self.directSubmit: - logMessage = extractLogMessageFromGitCommit(id) - logMessage = logMessage.replace("\n", "\n\t") - if self.isWindows: - logMessage = logMessage.replace("\n", "\r\n") - logMessage = logMessage.strip() - - template = self.prepareSubmitTemplate() - - if self.interactive: - submitTemplate = self.prepareLogMessage(template, logMessage) - diff = read_pipe("p4 diff -du ...") - - for newFile in filesToAdd: - diff += "==== new file ====\n" - diff += "--- /dev/null\n" - diff += "+++ %s\n" % newFile - f = open(newFile, "r") - for line in f.readlines(): - diff += "+" + line - f.close() - - separatorLine = "######## everything below this line is just the diff #######" - if platform.system() == "Windows": - separatorLine += "\r" - separatorLine += "\n" - - response = "e" - if self.trustMeLikeAFool: - response = "y" - - firstIteration = True - while response == "e": - if not firstIteration: - response = raw_input("Do you want to submit this change? [y]es/[e]dit/[n]o/[s]kip ") - firstIteration = False - if response == "e": - [handle, fileName] = tempfile.mkstemp() - tmpFile = os.fdopen(handle, "w+") - tmpFile.write(submitTemplate + separatorLine + diff) - tmpFile.close() - defaultEditor = "vi" - if platform.system() == "Windows": - defaultEditor = "notepad" - editor = os.environ.get("EDITOR", defaultEditor); - system(editor + " " + fileName) - tmpFile = open(fileName, "rb") - message = tmpFile.read() - tmpFile.close() - os.remove(fileName) - submitTemplate = message[:message.index(separatorLine)] - if self.isWindows: - submitTemplate = submitTemplate.replace("\r\n", "\n") - - if response == "y" or response == "yes": - if self.dryRun: - print submitTemplate - raw_input("Press return to continue...") - else: - if self.directSubmit: - print "Submitting to git first" - os.chdir(self.oldWorkingDirectory) - write_pipe("git commit -a -F -", submitTemplate) - os.chdir(self.clientPath) - - write_pipe("p4 submit -i", submitTemplate) - elif response == "s": - for f in editedFiles: - system("p4 revert \"%s\"" % f); - for f in filesToAdd: - system("p4 revert \"%s\"" % f); - system("rm %s" %f) - for f in filesToDelete: - system("p4 delete \"%s\"" % f); - return - else: - print "Not submitting!" - self.interactive = False - else: - fileName = "submit.txt" - file = open(fileName, "w+") - file.write(self.prepareLogMessage(template, logMessage)) - file.close() - print ("Perforce submit template written as %s. " - + "Please review/edit and then use p4 submit -i < %s to submit directly!" - % (fileName, fileName)) - - def run(self, args): - if len(args) == 0: - self.master = currentGitBranch() - if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master): - die("Detecting current git branch failed!") - elif len(args) == 1: - self.master = args[0] - else: - return False - - [upstream, settings] = findUpstreamBranchPoint() - self.depotPath = settings['depot-paths'][0] - if len(self.origin) == 0: - self.origin = upstream - - if self.verbose: - print "Origin branch is " + self.origin - - if len(self.depotPath) == 0: - print "Internal error: cannot locate perforce depot path from existing branches" - sys.exit(128) - - self.clientPath = p4Where(self.depotPath) - - if len(self.clientPath) == 0: - print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath - sys.exit(128) - - print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath) - self.oldWorkingDirectory = os.getcwd() - - if self.directSubmit: - self.diffStatus = read_pipe_lines("git diff -r --name-status HEAD") - if len(self.diffStatus) == 0: - print "No changes in working directory to submit." - return True - patch = read_pipe("git diff -p --binary --diff-filter=ACMRTUXB HEAD") - self.diffFile = self.gitdir + "/p4-git-diff" - f = open(self.diffFile, "wb") - f.write(patch) - f.close(); - - os.chdir(self.clientPath) - response = raw_input("Do you want to sync %s with p4 sync? [y]es/[n]o " % self.clientPath) - if response == "y" or response == "yes": - system("p4 sync ...") - - if self.reset: - self.firstTime = True - - if len(self.substFile) > 0: - for line in open(self.substFile, "r").readlines(): - tokens = line.strip().split("=") - self.logSubstitutions[tokens[0]] = tokens[1] - - self.check() - self.configFile = self.gitdir + "/p4-git-sync.cfg" - self.config = shelve.open(self.configFile, writeback=True) - - if self.firstTime: - self.start() - - commits = self.config.get("commits", []) - - while len(commits) > 0: - self.firstTime = False - commit = commits[0] - commits = commits[1:] - self.config["commits"] = commits - self.applyCommit(commit) - if not self.interactive: - break - - self.config.close() - - if self.directSubmit: - os.remove(self.diffFile) - - if len(commits) == 0: - if self.firstTime: - print "No changes found to apply between %s and current HEAD" % self.origin - else: - print "All changes applied!" - os.chdir(self.oldWorkingDirectory) - response = raw_input("Do you want to sync from Perforce now using git-p4 rebase? [y]es/[n]o ") - if response == "y" or response == "yes": - rebase = P4Rebase() - rebase.run([]) - os.remove(self.configFile) - - return True - -class P4Sync(Command): - def __init__(self): - Command.__init__(self) - self.options = [ - optparse.make_option("--branch", dest="branch"), - optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"), - optparse.make_option("--changesfile", dest="changesFile"), - optparse.make_option("--silent", dest="silent", action="store_true"), - optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"), - optparse.make_option("--verbose", dest="verbose", action="store_true"), - optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false", - help="Import into refs/heads/ , not refs/remotes"), - optparse.make_option("--max-changes", dest="maxChanges"), - optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true', - help="Keep entire BRANCH/DIR/SUBDIR prefix during import") - ] - self.description = """Imports from Perforce into a git repository.\n - example: - //depot/my/project/ -- to import the current head - //depot/my/project/@all -- to import everything - //depot/my/project/@1,6 -- to import only from revision 1 to 6 - - (a ... is not needed in the path p4 specification, it's added implicitly)""" - - self.usage += " //depot/path[@revRange]" - self.silent = False - self.createdBranches = Set() - self.committedChanges = Set() - self.branch = "" - self.detectBranches = False - self.detectLabels = False - self.changesFile = "" - self.syncWithOrigin = True - self.verbose = False - self.importIntoRemotes = True - self.maxChanges = "" - self.isWindows = (platform.system() == "Windows") - self.keepRepoPath = False - self.depotPaths = None - self.p4BranchesInGit = [] - - if gitConfig("git-p4.syncFromOrigin") == "false": - self.syncWithOrigin = False - - def extractFilesFromCommit(self, commit): - files = [] - fnum = 0 - while commit.has_key("depotFile%s" % fnum): - path = commit["depotFile%s" % fnum] - - found = [p for p in self.depotPaths - if path.startswith (p)] - if not found: - fnum = fnum + 1 - continue - - file = {} - file["path"] = path - file["rev"] = commit["rev%s" % fnum] - file["action"] = commit["action%s" % fnum] - file["type"] = commit["type%s" % fnum] - files.append(file) - fnum = fnum + 1 - return files - - def stripRepoPath(self, path, prefixes): - if self.keepRepoPath: - prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])] - - for p in prefixes: - if path.startswith(p): - path = path[len(p):] - - return path - - def splitFilesIntoBranches(self, commit): - branches = {} - fnum = 0 - while commit.has_key("depotFile%s" % fnum): - path = commit["depotFile%s" % fnum] - found = [p for p in self.depotPaths - if path.startswith (p)] - if not found: - fnum = fnum + 1 - continue - - file = {} - file["path"] = path - file["rev"] = commit["rev%s" % fnum] - file["action"] = commit["action%s" % fnum] - file["type"] = commit["type%s" % fnum] - fnum = fnum + 1 - - relPath = self.stripRepoPath(path, self.depotPaths) - - for branch in self.knownBranches.keys(): - - # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2 - if relPath.startswith(branch + "/"): - if branch not in branches: - branches[branch] = [] - branches[branch].append(file) - break - - return branches - - ## Should move this out, doesn't use SELF. - def readP4Files(self, files): - files = [f for f in files - if f['action'] != 'delete'] - - if not files: - return - - filedata = p4CmdList('-x - print', - stdin='\n'.join(['%s#%s' % (f['path'], f['rev']) - for f in files]), - stdin_mode='w+') - if "p4ExitCode" in filedata[0]: - die("Problems executing p4. Error: [%d]." - % (filedata[0]['p4ExitCode'])); - - j = 0; - contents = {} - while j < len(filedata): - stat = filedata[j] - j += 1 - text = '' - while j < len(filedata) and filedata[j]['code'] in ('text', - 'binary'): - text += filedata[j]['data'] - j += 1 - - - if not stat.has_key('depotFile'): - sys.stderr.write("p4 print fails with: %s\n" % repr(stat)) - continue - - contents[stat['depotFile']] = text - - for f in files: - assert not f.has_key('data') - f['data'] = contents[f['path']] - - def commit(self, details, files, branch, branchPrefixes, parent = ""): - epoch = details["time"] - author = details["user"] - - if self.verbose: - print "commit into %s" % branch - - # start with reading files; if that fails, we should not - # create a commit. - new_files = [] - for f in files: - if [p for p in branchPrefixes if f['path'].startswith(p)]: - new_files.append (f) - else: - sys.stderr.write("Ignoring file outside of prefix: %s\n" % path) - files = new_files - self.readP4Files(files) - - - - - self.gitStream.write("commit %s\n" % branch) -# gitStream.write("mark :%s\n" % details["change"]) - self.committedChanges.add(int(details["change"])) - committer = "" - if author not in self.users: - self.getUserMapFromPerforceServer() - if author in self.users: - committer = "%s %s %s" % (self.users[author], epoch, self.tz) - else: - committer = "%s <a@b> %s %s" % (author, epoch, self.tz) - - self.gitStream.write("committer %s\n" % committer) - - self.gitStream.write("data <<EOT\n") - self.gitStream.write(details["desc"]) - self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" - % (','.join (branchPrefixes), details["change"])) - if len(details['options']) > 0: - self.gitStream.write(": options = %s" % details['options']) - self.gitStream.write("]\nEOT\n\n") - - if len(parent) > 0: - if self.verbose: - print "parent %s" % parent - self.gitStream.write("from %s\n" % parent) - - for file in files: - if file["type"] == "apple": - print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path'] - continue - - relPath = self.stripRepoPath(file['path'], branchPrefixes) - if file["action"] == "delete": - self.gitStream.write("D %s\n" % relPath) - else: - data = file['data'] - - mode = "644" - if file["type"].startswith("x"): - mode = "755" - elif file["type"] == "symlink": - mode = "120000" - # p4 print on a symlink contains "target\n", so strip it off - data = data[:-1] - - if self.isWindows and file["type"].endswith("text"): - data = data.replace("\r\n", "\n") - - self.gitStream.write("M %s inline %s\n" % (mode, relPath)) - self.gitStream.write("data %s\n" % len(data)) - self.gitStream.write(data) - self.gitStream.write("\n") - - self.gitStream.write("\n") - - change = int(details["change"]) - - if self.labels.has_key(change): - label = self.labels[change] - labelDetails = label[0] - labelRevisions = label[1] - if self.verbose: - print "Change %s is labelled %s" % (change, labelDetails) - - files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change) - for p in branchPrefixes])) - - if len(files) == len(labelRevisions): - - cleanedFiles = {} - for info in files: - if info["action"] == "delete": - continue - cleanedFiles[info["depotFile"]] = info["rev"] - - if cleanedFiles == labelRevisions: - self.gitStream.write("tag tag_%s\n" % labelDetails["label"]) - self.gitStream.write("from %s\n" % branch) - - owner = labelDetails["Owner"] - tagger = "" - if author in self.users: - tagger = "%s %s %s" % (self.users[owner], epoch, self.tz) - else: - tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz) - self.gitStream.write("tagger %s\n" % tagger) - self.gitStream.write("data <<EOT\n") - self.gitStream.write(labelDetails["Description"]) - self.gitStream.write("EOT\n\n") - - else: - if not self.silent: - print ("Tag %s does not match with change %s: files do not match." - % (labelDetails["label"], change)) - - else: - if not self.silent: - print ("Tag %s does not match with change %s: file count is different." - % (labelDetails["label"], change)) - - def getUserCacheFilename(self): - home = os.environ.get("HOME", os.environ.get("USERPROFILE")) - return home + "/.gitp4-usercache.txt" - - def getUserMapFromPerforceServer(self): - if self.userMapFromPerforceServer: - return - self.users = {} - - for output in p4CmdList("users"): - if not output.has_key("User"): - continue - self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" - - - s = '' - for (key, val) in self.users.items(): - s += "%s\t%s\n" % (key, val) - - open(self.getUserCacheFilename(), "wb").write(s) - self.userMapFromPerforceServer = True - - def loadUserMapFromCache(self): - self.users = {} - self.userMapFromPerforceServer = False - try: - cache = open(self.getUserCacheFilename(), "rb") - lines = cache.readlines() - cache.close() - for line in lines: - entry = line.strip().split("\t") - self.users[entry[0]] = entry[1] - except IOError: - self.getUserMapFromPerforceServer() - - def getLabels(self): - self.labels = {} - - l = p4CmdList("labels %s..." % ' '.join (self.depotPaths)) - if len(l) > 0 and not self.silent: - print "Finding files belonging to labels in %s" % `self.depotPath` - - for output in l: - label = output["label"] - revisions = {} - newestChange = 0 - if self.verbose: - print "Querying files for label %s" % label - for file in p4CmdList("files " - + ' '.join (["%s...@%s" % (p, label) - for p in self.depotPaths])): - revisions[file["depotFile"]] = file["rev"] - change = int(file["change"]) - if change > newestChange: - newestChange = change - - self.labels[newestChange] = [output, revisions] - - if self.verbose: - print "Label changes: %s" % self.labels.keys() - - def guessProjectName(self): - for p in self.depotPaths: - if p.endswith("/"): - p = p[:-1] - p = p[p.strip().rfind("/") + 1:] - if not p.endswith("/"): - p += "/" - return p - - def getBranchMapping(self): - lostAndFoundBranches = set() - - for info in p4CmdList("branches"): - details = p4Cmd("branch -o %s" % info["branch"]) - viewIdx = 0 - while details.has_key("View%s" % viewIdx): - paths = details["View%s" % viewIdx].split(" ") - viewIdx = viewIdx + 1 - # require standard //depot/foo/... //depot/bar/... mapping - if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."): - continue - source = paths[0] - destination = paths[1] - ## HACK - if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]): - source = source[len(self.depotPaths[0]):-4] - destination = destination[len(self.depotPaths[0]):-4] - - if destination in self.knownBranches: - if not self.silent: - print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination) - print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination) - continue - - self.knownBranches[destination] = source - - lostAndFoundBranches.discard(destination) - - if source not in self.knownBranches: - lostAndFoundBranches.add(source) - - - for branch in lostAndFoundBranches: - self.knownBranches[branch] = branch - - def listExistingP4GitBranches(self): - # branches holds mapping from name to commit - branches = p4BranchesInGit(self.importIntoRemotes) - self.p4BranchesInGit = branches.keys() - for branch in branches.keys(): - self.initialParents[self.refPrefix + branch] = branches[branch] - - def updateOptionDict(self, d): - option_keys = {} - if self.keepRepoPath: - option_keys['keepRepoPath'] = 1 - - d["options"] = ' '.join(sorted(option_keys.keys())) - - def readOptions(self, d): - self.keepRepoPath = (d.has_key('options') - and ('keepRepoPath' in d['options'])) - - def run(self, args): - self.depotPaths = [] - self.changeRange = "" - self.initialParent = "" - self.previousDepotPaths = [] - - # map from branch depot path to parent branch - self.knownBranches = {} - self.initialParents = {} - self.hasOrigin = originP4BranchesExist() - if not self.syncWithOrigin: - self.hasOrigin = False - - if self.importIntoRemotes: - self.refPrefix = "refs/remotes/p4/" - else: - self.refPrefix = "refs/heads/p4/" - - if self.syncWithOrigin and self.hasOrigin: - if not self.silent: - print "Syncing with origin first by calling git fetch origin" - system("git fetch origin") - - if len(self.branch) == 0: - self.branch = self.refPrefix + "master" - if gitBranchExists("refs/heads/p4") and self.importIntoRemotes: - system("git update-ref %s refs/heads/p4" % self.branch) - system("git branch -D p4"); - # create it /after/ importing, when master exists - if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch): - system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch)) - - # TODO: should always look at previous commits, - # merge with previous imports, if possible. - if args == []: - if self.hasOrigin: - createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent) - self.listExistingP4GitBranches() - - if len(self.p4BranchesInGit) > 1: - if not self.silent: - print "Importing from/into multiple branches" - self.detectBranches = True - - if self.verbose: - print "branches: %s" % self.p4BranchesInGit - - p4Change = 0 - for branch in self.p4BranchesInGit: - logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch) - - settings = extractSettingsGitLog(logMsg) - - self.readOptions(settings) - if (settings.has_key('depot-paths') - and settings.has_key ('change')): - change = int(settings['change']) + 1 - p4Change = max(p4Change, change) - - depotPaths = sorted(settings['depot-paths']) - if self.previousDepotPaths == []: - self.previousDepotPaths = depotPaths - else: - paths = [] - for (prev, cur) in zip(self.previousDepotPaths, depotPaths): - for i in range(0, min(len(cur), len(prev))): - if cur[i] <> prev[i]: - i = i - 1 - break - - paths.append (cur[:i + 1]) - - self.previousDepotPaths = paths - - if p4Change > 0: - self.depotPaths = sorted(self.previousDepotPaths) - self.changeRange = "@%s,#head" % p4Change - if not self.detectBranches: - self.initialParent = parseRevision(self.branch) - if not self.silent and not self.detectBranches: - print "Performing incremental import into %s git branch" % self.branch - - if not self.branch.startswith("refs/"): - self.branch = "refs/heads/" + self.branch - - if len(args) == 0 and self.depotPaths: - if not self.silent: - print "Depot paths: %s" % ' '.join(self.depotPaths) - else: - if self.depotPaths and self.depotPaths != args: - print ("previous import used depot path %s and now %s was specified. " - "This doesn't work!" % (' '.join (self.depotPaths), - ' '.join (args))) - sys.exit(1) - - self.depotPaths = sorted(args) - - self.revision = "" - self.users = {} - - newPaths = [] - for p in self.depotPaths: - if p.find("@") != -1: - atIdx = p.index("@") - self.changeRange = p[atIdx:] - if self.changeRange == "@all": - self.changeRange = "" - elif ',' not in self.changeRange: - self.revision = self.changeRange - self.changeRange = "" - p = p[:atIdx] - elif p.find("#") != -1: - hashIdx = p.index("#") - self.revision = p[hashIdx:] - p = p[:hashIdx] - elif self.previousDepotPaths == []: - self.revision = "#head" - - p = re.sub ("\.\.\.$", "", p) - if not p.endswith("/"): - p += "/" - - newPaths.append(p) - - self.depotPaths = newPaths - - - self.loadUserMapFromCache() - self.labels = {} - if self.detectLabels: - self.getLabels(); - - if self.detectBranches: - ## FIXME - what's a P4 projectName ? - self.projectName = self.guessProjectName() - - if not self.hasOrigin: - self.getBranchMapping(); - if self.verbose: - print "p4-git branches: %s" % self.p4BranchesInGit - print "initial parents: %s" % self.initialParents - for b in self.p4BranchesInGit: - if b != "master": - - ## FIXME - b = b[len(self.projectName):] - self.createdBranches.add(b) - - self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60)) - - importProcess = subprocess.Popen(["git", "fast-import"], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE); - self.gitOutput = importProcess.stdout - self.gitStream = importProcess.stdin - self.gitError = importProcess.stderr - - if self.revision: - print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), self.revision, self.branch) - - details = { "user" : "git perforce import user", "time" : int(time.time()) } - details["desc"] = ("Initial import of %s from the state at revision %s" - % (' '.join(self.depotPaths), self.revision)) - details["change"] = self.revision - newestRevision = 0 - - fileCnt = 0 - for info in p4CmdList("files " - + ' '.join(["%s...%s" - % (p, self.revision) - for p in self.depotPaths])): - - if info['code'] == 'error': - sys.stderr.write("p4 returned an error: %s\n" - % info['data']) - sys.exit(1) - - - change = int(info["change"]) - if change > newestRevision: - newestRevision = change - - if info["action"] == "delete": - # don't increase the file cnt, otherwise details["depotFile123"] will have gaps! - #fileCnt = fileCnt + 1 - continue - - for prop in ["depotFile", "rev", "action", "type" ]: - details["%s%s" % (prop, fileCnt)] = info[prop] - - fileCnt = fileCnt + 1 - - details["change"] = newestRevision - self.updateOptionDict(details) - try: - self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths) - except IOError: - print "IO error with git fast-import. Is your git version recent enough?" - print self.gitError.read() - - else: - changes = [] - - if len(self.changesFile) > 0: - output = open(self.changesFile).readlines() - changeSet = Set() - for line in output: - changeSet.add(int(line)) - - for change in changeSet: - changes.append(change) - - changes.sort() - else: - if self.verbose: - print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths), - self.changeRange) - assert self.depotPaths - output = read_pipe_lines("p4 changes " + ' '.join (["%s...%s" % (p, self.changeRange) - for p in self.depotPaths])) - - for line in output: - changeNum = line.split(" ")[1] - changes.append(int(changeNum)) - - changes.sort() - - if len(self.maxChanges) > 0: - changes = changes[:min(int(self.maxChanges), len(changes))] - - if len(changes) == 0: - if not self.silent: - print "No changes to import!" - return True - - if not self.silent and not self.detectBranches: - print "Import destination: %s" % self.branch - - self.updatedBranches = set() - - cnt = 1 - for change in changes: - description = p4Cmd("describe %s" % change) - self.updateOptionDict(description) - - if not self.silent: - sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes))) - sys.stdout.flush() - cnt = cnt + 1 - - try: - if self.detectBranches: - branches = self.splitFilesIntoBranches(description) - for branch in branches.keys(): - ## HACK --hwn - branchPrefix = self.depotPaths[0] + branch + "/" - - parent = "" - - filesForCommit = branches[branch] - - if self.verbose: - print "branch is %s" % branch - - self.updatedBranches.add(branch) - - if branch not in self.createdBranches: - self.createdBranches.add(branch) - parent = self.knownBranches[branch] - if parent == branch: - parent = "" - elif self.verbose: - print "parent determined through known branches: %s" % parent - - # main branch? use master - if branch == "main": - branch = "master" - else: - - ## FIXME - branch = self.projectName + branch - - if parent == "main": - parent = "master" - elif len(parent) > 0: - ## FIXME - parent = self.projectName + parent - - branch = self.refPrefix + branch - if len(parent) > 0: - parent = self.refPrefix + parent - - if self.verbose: - print "looking for initial parent for %s; current parent is %s" % (branch, parent) - - if len(parent) == 0 and branch in self.initialParents: - parent = self.initialParents[branch] - del self.initialParents[branch] - - self.commit(description, filesForCommit, branch, [branchPrefix], parent) - else: - files = self.extractFilesFromCommit(description) - self.commit(description, files, self.branch, self.depotPaths, - self.initialParent) - self.initialParent = "" - except IOError: - print self.gitError.read() - sys.exit(1) - - if not self.silent: - print "" - if len(self.updatedBranches) > 0: - sys.stdout.write("Updated branches: ") - for b in self.updatedBranches: - sys.stdout.write("%s " % b) - sys.stdout.write("\n") - - - self.gitStream.close() - if importProcess.wait() != 0: - die("fast-import failed: %s" % self.gitError.read()) - self.gitOutput.close() - self.gitError.close() - - return True - -class P4Rebase(Command): - def __init__(self): - Command.__init__(self) - self.options = [ ] - self.description = ("Fetches the latest revision from perforce and " - + "rebases the current work (branch) against it") - self.verbose = False - - def run(self, args): - sync = P4Sync() - sync.run([]) - - [upstream, settings] = findUpstreamBranchPoint() - if len(upstream) == 0: - die("Cannot find upstream branchpoint for rebase") - - # the branchpoint may be p4/foo~3, so strip off the parent - upstream = re.sub("~[0-9]+$", "", upstream) - - print "Rebasing the current branch onto %s" % upstream - oldHead = read_pipe("git rev-parse HEAD").strip() - system("git rebase %s" % upstream) - system("git diff-tree --stat --summary -M %s HEAD" % oldHead) - return True - -class P4Clone(P4Sync): - def __init__(self): - P4Sync.__init__(self) - self.description = "Creates a new git repository and imports from Perforce into it" - self.usage = "usage: %prog [options] //depot/path[@revRange]" - self.options.append( - optparse.make_option("--destination", dest="cloneDestination", - action='store', default=None, - help="where to leave result of the clone")) - self.cloneDestination = None - self.needsGit = False - - def defaultDestination(self, args): - ## TODO: use common prefix of args? - depotPath = args[0] - depotDir = re.sub("(@[^@]*)$", "", depotPath) - depotDir = re.sub("(#[^#]*)$", "", depotDir) - depotDir = re.sub(r"\.\.\.$,", "", depotDir) - depotDir = re.sub(r"/$", "", depotDir) - return os.path.split(depotDir)[1] - - def run(self, args): - if len(args) < 1: - return False - - if self.keepRepoPath and not self.cloneDestination: - sys.stderr.write("Must specify destination for --keep-path\n") - sys.exit(1) - - depotPaths = args - - if not self.cloneDestination and len(depotPaths) > 1: - self.cloneDestination = depotPaths[-1] - depotPaths = depotPaths[:-1] - - for p in depotPaths: - if not p.startswith("//"): - return False - - if not self.cloneDestination: - self.cloneDestination = self.defaultDestination(args) - - print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination) - if not os.path.exists(self.cloneDestination): - os.makedirs(self.cloneDestination) - os.chdir(self.cloneDestination) - system("git init") - self.gitdir = os.getcwd() + "/.git" - if not P4Sync.run(self, depotPaths): - return False - if self.branch != "master": - if gitBranchExists("refs/remotes/p4/master"): - system("git branch master refs/remotes/p4/master") - system("git checkout -f") - else: - print "Could not detect main branch. No checkout/master branch created." - - return True - -class P4Branches(Command): - def __init__(self): - Command.__init__(self) - self.options = [ ] - self.description = ("Shows the git branches that hold imports and their " - + "corresponding perforce depot paths") - self.verbose = False - - def run(self, args): - if originP4BranchesExist(): - createOrUpdateBranchesFromOrigin() - - cmdline = "git rev-parse --symbolic " - cmdline += " --remotes" - - for line in read_pipe_lines(cmdline): - line = line.strip() - - if not line.startswith('p4/') or line == "p4/HEAD": - continue - branch = line - - log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch) - settings = extractSettingsGitLog(log) - - print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]) - return True - -class HelpFormatter(optparse.IndentedHelpFormatter): - def __init__(self): - optparse.IndentedHelpFormatter.__init__(self) - - def format_description(self, description): - if description: - return description + "\n" - else: - return "" - -def printUsage(commands): - print "usage: %s <command> [options]" % sys.argv[0] - print "" - print "valid commands: %s" % ", ".join(commands) - print "" - print "Try %s <command> --help for command specific help." % sys.argv[0] - print "" - -commands = { - "debug" : P4Debug, - "submit" : P4Submit, - "sync" : P4Sync, - "rebase" : P4Rebase, - "clone" : P4Clone, - "rollback" : P4RollBack, - "branches" : P4Branches -} - - -def main(): - if len(sys.argv[1:]) == 0: - printUsage(commands.keys()) - sys.exit(2) - - cmd = "" - cmdName = sys.argv[1] - try: - klass = commands[cmdName] - cmd = klass() - except KeyError: - print "unknown command %s" % cmdName - print "" - printUsage(commands.keys()) - sys.exit(2) - - options = cmd.options - cmd.gitdir = os.environ.get("GIT_DIR", None) - - args = sys.argv[2:] - - if len(options) > 0: - options.append(optparse.make_option("--git-dir", dest="gitdir")) - - parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName), - options, - description = cmd.description, - formatter = HelpFormatter()) - - (cmd, args) = parser.parse_args(sys.argv[2:], cmd); - global verbose - verbose = cmd.verbose - if cmd.needsGit: - if cmd.gitdir == None: - cmd.gitdir = os.path.abspath(".git") - if not isValidGitDir(cmd.gitdir): - cmd.gitdir = read_pipe("git rev-parse --git-dir").strip() - if os.path.exists(cmd.gitdir): - cdup = read_pipe("git rev-parse --show-cdup").strip() - if len(cdup) > 0: - os.chdir(cdup); - - if not isValidGitDir(cmd.gitdir): - if isValidGitDir(cmd.gitdir + "/.git"): - cmd.gitdir += "/.git" - else: - die("fatal: cannot locate git repository at %s" % cmd.gitdir) - - os.environ["GIT_DIR"] = cmd.gitdir - - if not cmd.run(args): - parser.print_help() - - -if __name__ == '__main__': - main() diff --git a/contrib/fast-import/git-p4.README b/contrib/fast-import/git-p4.README new file mode 100644 index 0000000000..cec5ecfa7c --- /dev/null +++ b/contrib/fast-import/git-p4.README @@ -0,0 +1,12 @@ +The git-p4 script moved to the top-level of the git source directory. + +Invoke it as any other git command, like "git p4 clone", for instance. + +Note that the top-level git-p4.py script is now the source. It is +built using make to git-p4, which will be installed. + +Windows users can copy the git-p4.py source script directly, possibly +invoking it through a batch file called "git-p4.bat" in the same folder. +It should contain just one line: + + @python "%~d0%~p0git-p4.py" %* diff --git a/contrib/fast-import/git-p4.bat b/contrib/fast-import/git-p4.bat deleted file mode 100644 index 9f97e884f5..0000000000 --- a/contrib/fast-import/git-p4.bat +++ /dev/null @@ -1 +0,0 @@ -@python "%~d0%~p0git-p4" %* diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt deleted file mode 100644 index b16a8384bc..0000000000 --- a/contrib/fast-import/git-p4.txt +++ /dev/null @@ -1,159 +0,0 @@ -git-p4 - Perforce <-> Git converter using git-fast-import - -Usage -===== - -git-p4 supports two main modes: Importing from Perforce to a Git repository is -done using "git-p4 sync" or "git-p4 rebase". Submitting changes from Git back -to Perforce is done using "git-p4 submit". - -Importing -========= - -You can simply start with - - git-p4 clone //depot/path/project - -or - - git-p4 clone //depot/path/project myproject - -This will create an empty git repository in a subdirectory called "project" (or -"myproject" with the second command), import the head revision from the -specified perforce path into a git "p4" branch (remotes/p4 actually), create a -master branch off it and check it out. If you want the entire history (not just -the head revision) then you can simply append a "@all" to the depot path: - - git-p4 clone //depot/project/main@all myproject - - - -If you want more control you can also use the git-p4 sync command directly: - - mkdir repo-git - cd repo-git - git init - git-p4 sync //path/in/your/perforce/depot - -This will import the current head revision of the specified depot path into a -"remotes/p4/master" branch of your git repository. You can use the ---branch=mybranch option to use a different branch. - -If you want to import the entire history of a given depot path just use - - git-p4 sync //path/in/depot@all - -To achieve optimal compression you may want to run 'git repack -a -d -f' after -a big import. This may take a while. - -Support for Perforce integrations is still work in progress. Don't bother -trying it unless you want to hack on it :) - -Incremental Imports -=================== - -After an initial import you can easily synchronize your git repository with -newer changes from the Perforce depot by just calling - - git-p4 sync - -in your git repository. By default the "remotes/p4/master" branch is updated. - -It is recommended to run 'git repack -a -d -f' from time to time when using -incremental imports to optimally combine the individual git packs that each -incremental import creates through the use of git-fast-import. - - -A useful setup may be that you have a periodically updated git repository -somewhere that contains a complete import of a Perforce project. That git -repository can be used to clone the working repository from and one would -import from Perforce directly after cloning using git-p4. If the connection to -the Perforce server is slow and the working repository hasn't been synced for a -while it may be desirable to fetch changes from the origin git repository using -the efficient git protocol. git-p4 supports this setup by calling "git fetch origin" -by default if there is an origin branch. You can disable this using - - git config git-p4.syncFromOrigin false - -Updating -======== - -A common working pattern is to fetch the latest changes from the Perforce depot -and merge them with local uncommitted changes. The recommended way is to use -git's rebase mechanism to preserve linear history. git-p4 provides a convenient - - git-p4 rebase - -command that calls git-p4 sync followed by git rebase to rebase the current -working branch. - -Submitting -========== - -git-p4 has support for submitting changes from a git repository back to the -Perforce depot. This requires a Perforce checkout separate to your git -repository. To submit all changes that are in the current git branch but not in -the "p4" branch (or "origin" if "p4" doesn't exist) simply call - - git-p4 submit - -in your git repository. If you want to submit changes in a specific branch that -is not your current git branch you can also pass that as an argument: - - git-p4 submit mytopicbranch - -You can override the reference branch with the --origin=mysourcebranch option. - -If a submit fails you may have to "p4 resolve" and submit manually. You can -continue importing the remaining changes with - - git-p4 submit --continue - -After submitting you should sync your perforce import branch ("p4" or "origin") -from Perforce using git-p4's sync command. - -If you have changes in your working directory that you haven't committed into -git yet but that you want to commit to Perforce directly ("quick fixes") then -you do not have to go through the intermediate step of creating a git commit -first but you can just call - - git-p4 submit --direct - - -Example -======= - -# Clone a repository - git-p4 clone //depot/path/project -# Enter the newly cloned directory - cd project -# Do some work... - vi foo.h -# ... and commit locally to gi - git commit foo.h -# In the meantime somebody submitted changes to the Perforce depot. Rebase your latest -# changes against the latest changes in Perforce: - git-p4 rebase -# Submit your locally committed changes back to Perforce - git-p4 submit -# ... and synchronize with Perforce - git-p4 rebase - - -Implementation Details... -========================= - -* Changesets from Perforce are imported using git fast-import. -* The import does not require anything from the Perforce client view as it just uses - "p4 print //depot/path/file#revision" to get the actual file contents. -* Every imported changeset has a special [git-p4...] line at the - end of the log message that gives information about the corresponding - Perforce change number and is also used by git-p4 itself to find out - where to continue importing when doing incremental imports. - Basically when syncing it extracts the perforce change number of the - latest commit in the "p4" branch and uses "p4 changes //depot/path/...@changenum,#head" - to find out which changes need to be imported. -* git-p4 submit uses "git rev-list" to pick the commits between the "p4" branch - and the current branch. - The commits themselves are applied using git diff/format-patch ... | git apply - diff --git a/contrib/fast-import/import-directories.perl b/contrib/fast-import/import-directories.perl new file mode 100755 index 0000000000..7f3afa5ac4 --- /dev/null +++ b/contrib/fast-import/import-directories.perl @@ -0,0 +1,417 @@ +#!/usr/bin/perl +# +# Copyright 2008-2009 Peter Krefting <peter@softwolves.pp.se> +# +# ------------------------------------------------------------------------ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# ------------------------------------------------------------------------ + +=pod + +=head1 NAME + +import-directories - Import bits and pieces to Git. + +=head1 SYNOPSIS + +B<import-directories.perl> F<configfile> F<outputfile> + +=head1 DESCRIPTION + +Script to import arbitrary projects version controlled by the "copy the +source directory to a new location and edit it there"-version controlled +projects into version control. Handles projects with arbitrary branching +and version trees, taking a file describing the inputs and generating a +file compatible with the L<git-fast-import(1)> format. + +=head1 CONFIGURATION FILE + +=head2 Format + +The configuration file is based on the standard I<.ini> format. + + ; Comments start with semi-colons + [section] + key=value + +Please see below for information on how to escape special characters. + +=head2 Global configuration + +Global configuration is done in the B<[config]> section, which should be +the first section in the file. Configuration can be changed by +repeating configuration sections later on. + + [config] + ; configure conversion of CRLFs. "convert" means that all CRLFs + ; should be converted into LFs (suitable for the core.autocrlf + ; setting set to true in Git). "none" means that all data is + ; treated as binary. + crlf=convert + +=head2 Revision configuration + +Each revision that is to be imported is described in three +sections. Revisions should be defined in topological order, so +that a revision's parent has always been defined when a new revision +is introduced. All the sections for one revision must be defined +before defining the next revision. + +Each revision is assigned a unique numerical identifier. The +numbers do not need to be consecutive, nor monotonically +increasing. + +For instance, if your configuration file contains only the two +revisions 4711 and 42, where 4711 is the initial commit, the +only requirement is that 4711 is completely defined before 42. + +=pod + +=head3 Revision description section + +A section whose section name is just an integer gives meta-data +about the revision. + + [3] + ; author sets the author of the revisions + author=Peter Krefting <peter@softwolves.pp.se> + ; branch sets the branch that the revision should be committed to + branch=master + ; parent describes the revision that is the parent of this commit + ; (optional) + parent=1 + ; merges describes a revision that is merged into this commit + ; (optional; can be repeated) + merges=2 + ; selects one file to take the timestamp from + ; (optional; if unspecified, the most recent file from the .files + ; section is used) + timestamp=3/source.c + +=head3 Revision contents section + +A section whose section name is an integer followed by B<.files> +describe all the files included in this revision. If a file that +was available previously is not included in this revision, it will +be removed. + +If an on-disk revision is incomplete, you can point to files from +a previous revision. There are no restriction as to where the source +files are located, nor to the names of them. + + [3.files] + ; the key is the path inside the repository, the value is the path + ; as seen from the importer script. + source.c=ver-3.00/source.c + source.h=ver-2.99/source.h + readme.txt=ver-3.00/introduction to the project.txt + +File names are treated as byte strings (but please see below on +quoting rules), and should be stored in the configuration file in +the encoding that should be used in the generated repository. + +=head3 Revision commit message section + +A section whose section name is an integer followed by B<.message> +gives the commit message. This section is read verbatim, up until +the beginning of the next section. As such, a commit message may not +contain a line that begins with an opening square bracket ("[") and +ends with a closing square bracket ("]"), unless they are surrounded +by whitespace or other characters. + + [3.message] + Implement foobar. + ; trailing blank lines are ignored. + +=cut + +# Globals +use strict; +use warnings; +use integer; +my $crlfmode = 0; +my @revs; +my (%revmap, %message, %files, %author, %branch, %parent, %merges, %time, %timesource); +my $sectiontype = 0; +my $rev = 0; +my $mark = 1; + +# Check command line +if ($#ARGV < 1 || $ARGV[0] =~ /^--?h/) +{ + exec('perldoc', $0); + exit 1; +} + +# Open configuration +my $config = $ARGV[0]; +open CFG, '<', $config or die "Cannot open configuration file \"$config\": "; + +# Open output +my $output = $ARGV[1]; +open OUT, '>', $output or die "Cannot create output file \"$output\": "; +binmode OUT; + +LINE: while (my $line = <CFG>) +{ + $line =~ s/\r?\n$//; + next LINE if $sectiontype != 4 && $line eq ''; + next LINE if $line =~ /^;/; + my $oldsectiontype = $sectiontype; + my $oldrev = $rev; + + # Sections + if ($line =~ m"^\[(config|(\d+)(|\.files|\.message))\]$") + { + if ($1 eq 'config') + { + $sectiontype = 1; + } + elsif ($3 eq '') + { + $sectiontype = 2; + $rev = $2; + # Create a new revision + die "Duplicate rev: $line\n " if defined $revmap{$rev}; + print "Reading revision $rev\n"; + push @revs, $rev; + $revmap{$rev} = $mark ++; + $time{$revmap{$rev}} = 0; + } + elsif ($3 eq '.files') + { + $sectiontype = 3; + $rev = $2; + die "Revision mismatch: $line\n " unless $rev == $oldrev; + } + elsif ($3 eq '.message') + { + $sectiontype = 4; + $rev = $2; + die "Revision mismatch: $line\n " unless $rev == $oldrev; + } + else + { + die "Internal parse error: $line\n "; + } + next LINE; + } + + # Parse data + if ($sectiontype != 4) + { + # Key and value + if ($line =~ m"^\s*([^\s].*=.*[^\s])\s*$") + { + my ($key, $value) = &parsekeyvaluepair($1); + # Global configuration + if (1 == $sectiontype) + { + if ($key eq 'crlf') + { + $crlfmode = 1, next LINE if $value eq 'convert'; + $crlfmode = 0, next LINE if $value eq 'none'; + } + die "Unknown configuration option: $line\n "; + } + # Revision specification + if (2 == $sectiontype) + { + my $current = $revmap{$rev}; + $author{$current} = $value, next LINE if $key eq 'author'; + $branch{$current} = $value, next LINE if $key eq 'branch'; + $parent{$current} = $value, next LINE if $key eq 'parent'; + $timesource{$current} = $value, next LINE if $key eq 'timestamp'; + push(@{$merges{$current}}, $value), next LINE if $key eq 'merges'; + die "Unknown revision option: $line\n "; + } + # Filespecs + if (3 == $sectiontype) + { + # Add the file and create a marker + die "File not found: $line\n " unless -f $value; + my $current = $revmap{$rev}; + ${$files{$current}}{$key} = $mark; + my $time = &fileblob($value, $crlfmode, $mark ++); + + # Update revision timestamp if more recent than other + # files seen, or if this is the file we have selected + # to take the time stamp from using the "timestamp" + # directive. + if ((defined $timesource{$current} && $timesource{$current} eq $value) + || $time > $time{$current}) + { + $time{$current} = $time; + } + } + } + else + { + die "Parse error: $line\n "; + } + } + else + { + # Commit message + my $current = $revmap{$rev}; + if (defined $message{$current}) + { + $message{$current} .= "\n"; + } + $message{$current} .= $line; + } +} +close CFG; + +# Start spewing out data for git-fast-import +foreach my $commit (@revs) +{ + # Progress + print OUT "progress Creating revision $commit\n"; + + # Create commit header + my $mark = $revmap{$commit}; + + # Branch and commit id + print OUT "commit refs/heads/", $branch{$mark}, "\nmark :", $mark, "\n"; + + # Author and timestamp + die "No timestamp defined for $commit (no files?)\n" unless defined $time{$mark}; + print OUT "committer ", $author{$mark}, " ", $time{$mark}, " +0100\n"; + + # Commit message + die "No message defined for $commit\n" unless defined $message{$mark}; + my $message = $message{$mark}; + $message =~ s/\n$//; # Kill trailing empty line + print OUT "data ", length($message), "\n", $message, "\n"; + + # Parent and any merges + print OUT "from :", $revmap{$parent{$mark}}, "\n" if defined $parent{$mark}; + if (defined $merges{$mark}) + { + foreach my $merge (@{$merges{$mark}}) + { + print OUT "merge :", $revmap{$merge}, "\n"; + } + } + + # Output file marks + print OUT "deleteall\n"; # start from scratch + foreach my $file (sort keys %{$files{$mark}}) + { + print OUT "M 644 :", ${$files{$mark}}{$file}, " $file\n"; + } + print OUT "\n"; +} + +# Create one file blob +sub fileblob +{ + my ($filename, $crlfmode, $mark) = @_; + + # Import the file + print OUT "progress Importing $filename\nblob\nmark :$mark\n"; + open FILE, '<', $filename or die "Cannot read $filename\n "; + binmode FILE; + my ($size, $mtime) = (stat(FILE))[7,9]; + my $file; + read FILE, $file, $size; + close FILE; + $file =~ s/\r\n/\n/g if $crlfmode; + print OUT "data ", length($file), "\n", $file, "\n"; + + return $mtime; +} + +# Parse a key=value pair +sub parsekeyvaluepair +{ +=pod + +=head2 Escaping special characters + +Key and value strings may be enclosed in quotes, in which case +whitespace inside the quotes is preserved. Additionally, an equal +sign may be included in the key by preceding it with a backslash. +For example: + + "key1 "=value1 + key2=" value2" + key\=3=value3 + key4=value=4 + "key5""=value5 + +Here the first key is "key1 " (note the trailing white-space) and the +second value is " value2" (note the leading white-space). The third +key contains an equal sign "key=3" and so does the fourth value, which +does not need to be escaped. The fifth key contains a trailing quote, +which does not need to be escaped since it is inside a surrounding +quote. + +=cut + my $pair = shift; + + # Separate key and value by the first non-quoted equal sign + my ($key, $value); + if ($pair =~ /^(.*[^\\])=(.*)$/) + { + ($key, $value) = ($1, $2) + } + else + { + die "Parse error: $pair\n "; + } + + # Unquote and unescape the key and value separately + return (&unescape($key), &unescape($value)); +} + +# Unquote and unescape +sub unescape +{ + my $string = shift; + + # First remove enclosing quotes. Backslash before the trailing + # quote leaves both. + if ($string =~ /^"(.*[^\\])"$/) + { + $string = $1; + } + + # Second remove any backslashes inside the unquoted string. + # For later: Handle special sequences like \t ? + $string =~ s/\\(.)/$1/g; + + return $string; +} + +__END__ + +=pod + +=head1 EXAMPLES + +B<import-directories.perl> F<project.import> + +=head1 AUTHOR + +Copyright 2008-2009 Peter Krefting E<lt>peter@softwolves.pp.se> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation. + +=cut diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl index 23aeb257b9..95438e1ed4 100755 --- a/contrib/fast-import/import-tars.perl +++ b/contrib/fast-import/import-tars.perl @@ -8,19 +8,35 @@ ## perl import-tars.perl *.tar.bz2 ## git whatchanged import-tars ## +## Use --metainfo to specify the extension for a meta data file, where +## import-tars can read the commit message and optionally author and +## committer information. +## +## echo 'This is the commit message' > myfile.tar.bz2.msg +## perl import-tars.perl --metainfo=msg myfile.tar.bz2 use strict; -die "usage: import-tars *.tar.{gz,bz2,Z}\n" unless @ARGV; +use Getopt::Long; + +my $metaext = ''; + +die "usage: import-tars [--metainfo=extension] *.tar.{gz,bz2,lzma,xz,Z}\n" + unless GetOptions('metainfo=s' => \$metaext) && @ARGV; my $branch_name = 'import-tars'; my $branch_ref = "refs/heads/$branch_name"; -my $committer_name = 'T Ar Creator'; -my $committer_email = 'tar@example.com'; +my $author_name = $ENV{'GIT_AUTHOR_NAME'} || 'T Ar Creator'; +my $author_email = $ENV{'GIT_AUTHOR_EMAIL'} || 'tar@example.com'; +my $committer_name = $ENV{'GIT_COMMITTER_NAME'} || `git config --get user.name`; +my $committer_email = $ENV{'GIT_COMMITTER_EMAIL'} || `git config --get user.email`; + +chomp($committer_name, $committer_email); open(FI, '|-', 'git', 'fast-import', '--quiet') or die "Unable to start git fast-import: $!\n"; foreach my $tar_file (@ARGV) { + my $commit_time = time; $tar_file =~ m,([^/]+)$,; my $tar_name = $1; @@ -33,13 +49,16 @@ foreach my $tar_file (@ARGV) } elsif ($tar_name =~ s/\.tar\.Z$//) { open(I, '-|', 'uncompress', '-c', $tar_file) or die "Unable to uncompress -c $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.(tar\.(lzma|xz)|(tlz|txz))$//) { + open(I, '-|', 'xz', '-dc', $tar_file) + or die "Unable to xz -dc $tar_file: $!\n"; } elsif ($tar_name =~ s/\.tar$//) { open(I, $tar_file) or die "Unable to open $tar_file: $!\n"; } else { die "Unrecognized compression format: $tar_file\n"; } - my $commit_time = 0; + my $author_time = 0; my $next_mark = 1; my $have_top_dir = 1; my ($top_dir, %files); @@ -77,10 +96,16 @@ foreach my $tar_file (@ARGV) $mtime = oct $mtime; next if $typeflag == 5; # directory - print FI "blob\n", "mark :$next_mark\n", "data $size\n"; - while ($size > 0 && read(I, $_, 512) == 512) { - print FI substr($_, 0, $size); - $size -= 512; + print FI "blob\n", "mark :$next_mark\n"; + if ($typeflag == 2) { # symbolic link + print FI "data ", length($linkname), "\n", $linkname; + $mode = 0120000; + } else { + print FI "data $size\n"; + while ($size > 0 && read(I, $_, 512) == 512) { + print FI substr($_, 0, $size); + $size -= 512; + } } print FI "\n"; @@ -92,17 +117,49 @@ foreach my $tar_file (@ARGV) } $files{$path} = [$next_mark++, $mode]; - $commit_time = $mtime if $mtime > $commit_time; + $author_time = $mtime if $mtime > $author_time; $path =~ m,^([^/]+)/,; $top_dir = $1 unless $top_dir; $have_top_dir = 0 if $top_dir ne $1; } + my $commit_msg = "Imported from $tar_file."; + my $this_committer_name = $committer_name; + my $this_committer_email = $committer_email; + my $this_author_name = $author_name; + my $this_author_email = $author_email; + if ($metaext ne '') { + # Optionally read a commit message from <filename.tar>.msg + # Add a line on the form "Committer: name <e-mail>" to override + # the committer and "Author: name <e-mail>" to override the + # author for this tar ball. + if (open MSG, '<', "${tar_file}.${metaext}") { + my $header_done = 0; + $commit_msg = ''; + while (<MSG>) { + if (!$header_done && /^Committer:\s+([^<>]*)\s+<(.*)>\s*$/i) { + $this_committer_name = $1; + $this_committer_email = $2; + } elsif (!$header_done && /^Author:\s+([^<>]*)\s+<(.*)>\s*$/i) { + $this_author_name = $1; + $this_author_email = $2; + } elsif (!$header_done && /^$/) { # empty line ends header. + $header_done = 1; + } else { + $commit_msg .= $_; + $header_done = 1; + } + } + close MSG; + } + } + print FI <<EOF; commit $branch_ref -committer $committer_name <$committer_email> $commit_time +0000 +author $this_author_name <$this_author_email> $author_time +0000 +committer $this_committer_name <$this_committer_email> $commit_time +0000 data <<END_OF_COMMIT_MESSAGE -Imported from $tar_file. +$commit_msg END_OF_COMMIT_MESSAGE deleteall @@ -112,14 +169,15 @@ EOF { my ($mark, $mode) = @{$files{$path}}; $path =~ s,^([^/]+)/,, if $have_top_dir; - printf FI "M %o :%i %s\n", $mode & 0111 ? 0755 : 0644, $mark, $path; + $mode = $mode & 0111 ? 0755 : 0644 unless $mode == 0120000; + printf FI "M %o :%i %s\n", $mode, $mark, $path; } print FI "\n"; print FI <<EOF; tag $tar_name from $branch_ref -tagger $committer_name <$committer_email> $commit_time +0000 +tagger $author_name <$author_email> $author_time +0000 data <<END_OF_TAG_MESSAGE Package $tar_name END_OF_TAG_MESSAGE diff --git a/contrib/fast-import/import-zips.py b/contrib/fast-import/import-zips.py new file mode 100755 index 0000000000..82f5ed3ddc --- /dev/null +++ b/contrib/fast-import/import-zips.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +## zip archive frontend for git-fast-import +## +## For example: +## +## mkdir project; cd project; git init +## python import-zips.py *.zip +## git log --stat import-zips + +from os import popen, path +from sys import argv, exit +from time import mktime +from zipfile import ZipFile + +if len(argv) < 2: + print 'Usage:', argv[0], '<zipfile>...' + exit(1) + +branch_ref = 'refs/heads/import-zips' +committer_name = 'Z Ip Creator' +committer_email = 'zip@example.com' + +fast_import = popen('git fast-import --quiet', 'w') +def printlines(list): + for str in list: + fast_import.write(str + "\n") + +for zipfile in argv[1:]: + commit_time = 0 + next_mark = 1 + common_prefix = None + mark = dict() + + zip = ZipFile(zipfile, 'r') + for name in zip.namelist(): + if name.endswith('/'): + continue + info = zip.getinfo(name) + + if commit_time < info.date_time: + commit_time = info.date_time + if common_prefix == None: + common_prefix = name[:name.rfind('/') + 1] + else: + while not name.startswith(common_prefix): + last_slash = common_prefix[:-1].rfind('/') + 1 + common_prefix = common_prefix[:last_slash] + + mark[name] = ':' + str(next_mark) + next_mark += 1 + + printlines(('blob', 'mark ' + mark[name], \ + 'data ' + str(info.file_size))) + fast_import.write(zip.read(name) + "\n") + + committer = committer_name + ' <' + committer_email + '> %d +0000' % \ + mktime(commit_time + (0, 0, 0)) + + printlines(('commit ' + branch_ref, 'committer ' + committer, \ + 'data <<EOM', 'Imported from ' + zipfile + '.', 'EOM', \ + '', 'deleteall')) + + for name in mark.keys(): + fast_import.write('M 100644 ' + mark[name] + ' ' + + name[len(common_prefix):] + "\n") + + printlines(('', 'tag ' + path.basename(zipfile), \ + 'from ' + branch_ref, 'tagger ' + committer, \ + 'data <<EOM', 'Package ' + zipfile, 'EOM', '')) + +if fast_import.close(): + exit(1) diff --git a/contrib/git-jump/README b/contrib/git-jump/README new file mode 100644 index 0000000000..1cebc328cb --- /dev/null +++ b/contrib/git-jump/README @@ -0,0 +1,92 @@ +git-jump +======== + +Git-jump is a script for helping you jump to "interesting" parts of your +project in your editor. It works by outputting a set of interesting +spots in the "quickfix" format, which editors like vim can use as a +queue of places to visit (this feature is usually used to jump to errors +produced by a compiler). For example, given a diff like this: + +------------------------------------ +diff --git a/foo.c b/foo.c +index a655540..5a59044 100644 +--- a/foo.c ++++ b/foo.c +@@ -1,3 +1,3 @@ + int main(void) { +- printf("hello word!\n"); ++ printf("hello world!\n"); + } +----------------------------------- + +git-jump will feed this to the editor: + +----------------------------------- +foo.c:2: printf("hello word!\n"); +----------------------------------- + +Obviously this trivial case isn't that interesting; you could just open +`foo.c` yourself. But when you have many changes scattered across a +project, you can use the editor's support to "jump" from point to point. + +Git-jump can generate three types of interesting lists: + + 1. The beginning of any diff hunks. + + 2. The beginning of any merge conflict markers. + + 3. Any grep matches. + + +Using git-jump +-------------- + +To use it, just drop git-jump in your PATH, and then invoke it like +this: + +-------------------------------------------------- +# jump to changes not yet staged for commit +git jump diff + +# jump to changes that are staged for commit; you can give +# arbitrary diff options +git jump diff --cached + +# jump to merge conflicts +git jump merge + +# jump to all instances of foo_bar +git jump grep foo_bar + +# same as above, but case-insensitive; you can give +# arbitrary grep options +git jump grep -i foo_bar +-------------------------------------------------- + + +Related Programs +---------------- + +You can accomplish some of the same things with individual tools. For +example, you can use `git mergetool` to start vimdiff on each unmerged +file. `git jump merge` is for the vim-wielding luddite who just wants to +jump straight to the conflict text with no fanfare. + +As of git v1.7.2, `git grep` knows the `--open-files-in-pager` option, +which does something similar to `git jump grep`. However, it is limited +to positioning the cursor to the correct line in only the first file, +leaving you to locate subsequent hits in that file or other files using +the editor or pager. By contrast, git-jump provides the editor with a +complete list of files and line numbers for each match. + + +Limitations +----------- + +This scripts was written and tested with vim. Given that the quickfix +format is the same as what gcc produces, I expect emacs users have a +similar feature for iterating through the list, but I know nothing about +how to activate it. + +The shell snippets to generate the quickfix lines will almost certainly +choke on filenames with exotic characters (like newlines). diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump new file mode 100755 index 0000000000..a33674e47a --- /dev/null +++ b/contrib/git-jump/git-jump @@ -0,0 +1,69 @@ +#!/bin/sh + +usage() { + cat <<\EOF +usage: git jump <mode> [<args>] + +Jump to interesting elements in an editor. +The <mode> parameter is one of: + +diff: elements are diff hunks. Arguments are given to diff. + +merge: elements are merge conflicts. Arguments are ignored. + +grep: elements are grep hits. Arguments are given to grep. +EOF +} + +open_editor() { + editor=`git var GIT_EDITOR` + eval "$editor -q \$1" +} + +mode_diff() { + git diff --relative "$@" | + perl -ne ' + if (m{^\+\+\+ b/(.*)}) { $file = $1; next } + defined($file) or next; + if (m/^@@ .*\+(\d+)/) { $line = $1; next } + defined($line) or next; + if (/^ /) { $line++; next } + if (/^[-+]\s*(.*)/) { + print "$file:$line: $1\n"; + $line = undef; + } + ' +} + +mode_merge() { + git ls-files -u | + perl -pe 's/^.*?\t//' | + sort -u | + while IFS= read fn; do + grep -Hn '^<<<<<<<' "$fn" + done +} + +# Grep -n generates nice quickfix-looking lines by itself, +# but let's clean up extra whitespace, so they look better if the +# editor shows them to us in the status bar. +mode_grep() { + git grep -n "$@" | + perl -pe ' + s/[ \t]+/ /g; + s/^ *//; + ' +} + +if test $# -lt 1; then + usage >&2 + exit 1 +fi +mode=$1; shift + +trap 'rm -f "$tmp"' 0 1 2 3 15 +tmp=`mktemp -t git-jump.XXXXXX` || exit 1 +type "mode_$mode" >/dev/null 2>&1 || { usage >&2; exit 1; } +"mode_$mode" "$@" >"$tmp" +test -s "$tmp" || exit 0 +open_editor "$tmp" diff --git a/contrib/git-resurrect.sh b/contrib/git-resurrect.sh new file mode 100755 index 0000000000..a4ed4c3c62 --- /dev/null +++ b/contrib/git-resurrect.sh @@ -0,0 +1,181 @@ +#!/bin/sh + +USAGE="[-a] [-r] [-m] [-t] [-n] [-b <newname>] <name>" +LONG_USAGE="git-resurrect attempts to find traces of a branch tip +called <name>, and tries to resurrect it. Currently, the reflog is +searched for checkout messages, and with -r also merge messages. With +-m and -t, the history of all refs is scanned for Merge <name> into +other/Merge <other> into <name> (respectively) commit subjects, which +is rather slow but allows you to resurrect other people's topic +branches." + +OPTIONS_KEEPDASHDASH= +OPTIONS_SPEC="\ +git resurrect $USAGE +-- +b,branch= save branch as <newname> instead of <name> +a,all same as -l -r -m -t +k,keep-going full rev-list scan (instead of first match) +l,reflog scan reflog for checkouts (enabled by default) +r,reflog-merges scan for merges recorded in reflog +m,merges scan for merges into other branches (slow) +t,merge-targets scan for merges of other branches into <name> +n,dry-run don't recreate the branch" + +. git-sh-setup + +search_reflog () { + sed -ne 's~^\([^ ]*\) .*\tcheckout: moving from '"$1"' .*~\1~p' \ + < "$GIT_DIR"/logs/HEAD +} + +search_reflog_merges () { + git rev-parse $( + sed -ne 's~^[^ ]* \([^ ]*\) .*\tmerge '"$1"':.*~\1^2~p' \ + < "$GIT_DIR"/logs/HEAD + ) +} + +_x40="[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]" +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" + +search_merges () { + git rev-list --all --grep="Merge branch '$1'" \ + --pretty=tformat:"%P %s" | + sed -ne "/^$_x40 \($_x40\) Merge .*/ {s//\1/p;$early_exit}" +} + +search_merge_targets () { + git rev-list --all --grep="Merge branch '[^']*' into $branch\$" \ + --pretty=tformat:"%H %s" --all | + sed -ne "/^\($_x40\) Merge .*/ {s//\1/p;$early_exit} " +} + +dry_run= +early_exit=q +scan_reflog=t +scan_reflog_merges= +scan_merges= +scan_merge_targets= +new_name= + +while test "$#" != 0; do + case "$1" in + -b|--branch) + shift + new_name="$1" + ;; + -n|--dry-run) + dry_run=t + ;; + --no-dry-run) + dry_run= + ;; + -k|--keep-going) + early_exit= + ;; + --no-keep-going) + early_exit=q + ;; + -m|--merges) + scan_merges=t + ;; + --no-merges) + scan_merges= + ;; + -l|--reflog) + scan_reflog=t + ;; + --no-reflog) + scan_reflog= + ;; + -r|--reflog_merges) + scan_reflog_merges=t + ;; + --no-reflog_merges) + scan_reflog_merges= + ;; + -t|--merge-targets) + scan_merge_targets=t + ;; + --no-merge-targets) + scan_merge_targets= + ;; + -a|--all) + scan_reflog=t + scan_reflog_merges=t + scan_merges=t + scan_merge_targets=t + ;; + --) + shift + break + ;; + *) + usage + ;; + esac + shift +done + +test "$#" = 1 || usage + +all_strategies="$scan_reflog$scan_reflog_merges$scan_merges$scan_merge_targets" +if test -z "$all_strategies"; then + die "must enable at least one of -lrmt" +fi + +branch="$1" +test -z "$new_name" && new_name="$branch" + +if test ! -z "$scan_reflog"; then + if test -r "$GIT_DIR"/logs/HEAD; then + candidates="$(search_reflog $branch)" + else + die 'reflog scanning requested, but' \ + '$GIT_DIR/logs/HEAD not readable' + fi +fi +if test ! -z "$scan_reflog_merges"; then + if test -r "$GIT_DIR"/logs/HEAD; then + candidates="$candidates $(search_reflog_merges $branch)" + else + die 'reflog scanning requested, but' \ + '$GIT_DIR/logs/HEAD not readable' + fi +fi +if test ! -z "$scan_merges"; then + candidates="$candidates $(search_merges $branch)" +fi +if test ! -z "$scan_merge_targets"; then + candidates="$candidates $(search_merge_targets $branch)" +fi + +candidates="$(git rev-parse $candidates | sort -u)" + +if test -z "$candidates"; then + hint= + test "z$all_strategies" != "ztttt" \ + && hint=" (maybe try again with -a)" + die "no candidates for $branch found$hint" +fi + +echo "** Candidates for $branch **" +for cmt in $candidates; do + git --no-pager log --pretty=tformat:"%ct:%h [%cr] %s" --abbrev-commit -1 $cmt +done \ +| sort -n | cut -d: -f2- + +newest="$(git rev-list -1 $candidates)" +if test ! -z "$dry_run"; then + printf "** Most recent: " + git --no-pager log -1 --pretty=tformat:"%h %s" $newest +elif ! git rev-parse --verify --quiet $new_name >/dev/null; then + printf "** Restoring $new_name to " + git --no-pager log -1 --pretty=tformat:"%h %s" $newest + git branch $new_name $newest +else + printf "Most recent: " + git --no-pager log -1 --pretty=tformat:"%h %s" $newest + echo "** $new_name already exists, doing nothing" +fi diff --git a/contrib/git-shell-commands/README b/contrib/git-shell-commands/README new file mode 100644 index 0000000000..438463b160 --- /dev/null +++ b/contrib/git-shell-commands/README @@ -0,0 +1,18 @@ +Sample programs callable through git-shell. Place a directory named +'git-shell-commands' in the home directory of a user whose shell is +git-shell. Then anyone logging in as that user will be able to run +executables in the 'git-shell-commands' directory. + +Provided commands: + +help: Prints out the names of available commands. When run +interactively, git-shell will automatically run 'help' on startup, +provided it exists. + +list: Displays any bare repository whose name ends with ".git" under +user's home directory. No other git repositories are visible, +although they might be clonable through git-shell. 'list' is designed +to minimize the number of calls to git that must be made in finding +available repositories; if your setup has additional repositories that +should be user-discoverable, you may wish to modify 'list' +accordingly. diff --git a/contrib/git-shell-commands/help b/contrib/git-shell-commands/help new file mode 100755 index 0000000000..535770c6ec --- /dev/null +++ b/contrib/git-shell-commands/help @@ -0,0 +1,18 @@ +#!/bin/sh + +if tty -s +then + echo "Run 'help' for help, or 'exit' to leave. Available commands:" +else + echo "Run 'help' for help. Available commands:" +fi + +cd "$(dirname "$0")" + +for cmd in * +do + case "$cmd" in + help) ;; + *) [ -f "$cmd" ] && [ -x "$cmd" ] && echo "$cmd" ;; + esac +done diff --git a/contrib/git-shell-commands/list b/contrib/git-shell-commands/list new file mode 100755 index 0000000000..6f89938821 --- /dev/null +++ b/contrib/git-shell-commands/list @@ -0,0 +1,10 @@ +#!/bin/sh + +print_if_bare_repo=' + if "$(git --git-dir="$1" rev-parse --is-bare-repository)" = true + then + printf "%s\n" "${1#./}" + fi +' + +find -type d -name "*.git" -exec sh -c "$print_if_bare_repo" -- \{} \; -prune 2>/dev/null diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview index 5931766620..4c99dfb903 100755 --- a/contrib/gitview/gitview +++ b/contrib/gitview/gitview @@ -27,12 +27,17 @@ import math import string import fcntl +have_gtksourceview2 = False +have_gtksourceview = False try: - import gtksourceview - have_gtksourceview = True + import gtksourceview2 + have_gtksourceview2 = True except ImportError: - have_gtksourceview = False - print "Running without gtksourceview module" + try: + import gtksourceview + have_gtksourceview = True + except ImportError: + print "Running without gtksourceview2 or gtksourceview module" re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})') @@ -58,6 +63,26 @@ def show_date(epoch, tz): return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs)) +def get_source_buffer_and_view(): + if have_gtksourceview2: + buffer = gtksourceview2.Buffer() + slm = gtksourceview2.LanguageManager() + gsl = slm.get_language("diff") + buffer.set_highlight_syntax(True) + buffer.set_language(gsl) + view = gtksourceview2.View(buffer) + elif have_gtksourceview: + buffer = gtksourceview.SourceBuffer() + slm = gtksourceview.SourceLanguagesManager() + gsl = slm.get_language_from_mime_type("text/x-patch") + buffer.set_highlight(True) + buffer.set_language(gsl) + view = gtksourceview.SourceView(buffer) + else: + buffer = gtk.TextBuffer() + view = gtk.TextView(buffer) + return (buffer, view) + class CellRendererGraph(gtk.GenericCellRenderer): """Cell renderer for directed graph. @@ -582,17 +607,7 @@ class DiffWindow(object): hpan.pack1(scrollwin, True, True) scrollwin.show() - if have_gtksourceview: - self.buffer = gtksourceview.SourceBuffer() - slm = gtksourceview.SourceLanguagesManager() - gsl = slm.get_language_from_mime_type("text/x-patch") - self.buffer.set_highlight(True) - self.buffer.set_language(gsl) - sourceview = gtksourceview.SourceView(self.buffer) - else: - self.buffer = gtk.TextBuffer() - sourceview = gtk.TextView(self.buffer) - + (self.buffer, sourceview) = get_source_buffer_and_view() sourceview.set_editable(False) sourceview.modify_font(pango.FontDescription("Monospace")) @@ -956,16 +971,7 @@ class GitView(object): vbox.pack_start(scrollwin, expand=True, fill=True) scrollwin.show() - if have_gtksourceview: - self.message_buffer = gtksourceview.SourceBuffer() - slm = gtksourceview.SourceLanguagesManager() - gsl = slm.get_language_from_mime_type("text/x-patch") - self.message_buffer.set_highlight(True) - self.message_buffer.set_language(gsl) - sourceview = gtksourceview.SourceView(self.message_buffer) - else: - self.message_buffer = gtk.TextBuffer() - sourceview = gtk.TextView(self.message_buffer) + (self.message_buffer, sourceview) = get_source_buffer_and_view() sourceview.set_editable(False) sourceview.modify_font(pango.FontDescription("Monospace")) diff --git a/contrib/gitview/gitview.txt b/contrib/gitview/gitview.txt index 77c29de305..9e12f97842 100644 --- a/contrib/gitview/gitview.txt +++ b/contrib/gitview/gitview.txt @@ -7,6 +7,7 @@ gitview - A GTK based repository browser for git SYNOPSIS -------- +[verse] 'gitview' [options] [args] DESCRIPTION diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py index 37337ff01f..046cb2b268 100755 --- a/contrib/hg-to-git/hg-to-git.py +++ b/contrib/hg-to-git/hg-to-git.py @@ -1,6 +1,6 @@ -#! /usr/bin/python +#!/usr/bin/env python -""" hg-to-svn.py - A Mercurial to GIT converter +""" hg-to-git.py - A Mercurial to GIT converter Copyright (C)2007 Stelian Pop <stelian@popies.net> @@ -20,15 +20,19 @@ """ import os, os.path, sys -import tempfile, popen2, pickle, getopt +import tempfile, pickle, getopt import re # Maps hg version -> git version hgvers = {} # List of children for each hg revision hgchildren = {} +# List of parents for each hg revision +hgparents = {} # Current branch for each hg revision hgbranch = {} +# Number of new changesets converted from hg +hgnewcsets = 0 #------------------------------------------------------------------------------ @@ -40,6 +44,9 @@ def usage(): options: -s, --gitstate=FILE: name of the state to be saved/read for incrementals + -n, --nrepack=INT: number of changesets that will trigger + a repack (default=0, -1 to deactivate) + -v, --verbose: be verbose required: hgprj: name of the HG project to import (directory) @@ -52,14 +59,14 @@ def getgitenv(user, date): elems = re.compile('(.*?)\s+<(.*)>').match(user) if elems: env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1) - env += 'export GIT_COMMITER_NAME="%s" ;' % elems.group(1) + env += 'export GIT_COMMITTER_NAME="%s" ;' % elems.group(1) env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2) - env += 'export GIT_COMMITER_EMAIL="%s" ;' % elems.group(2) + env += 'export GIT_COMMITTER_EMAIL="%s" ;' % elems.group(2) else: env += 'export GIT_AUTHOR_NAME="%s" ;' % user - env += 'export GIT_COMMITER_NAME="%s" ;' % user + env += 'export GIT_COMMITTER_NAME="%s" ;' % user env += 'export GIT_AUTHOR_EMAIL= ;' - env += 'export GIT_COMMITER_EMAIL= ;' + env += 'export GIT_COMMITTER_EMAIL= ;' env += 'export GIT_AUTHOR_DATE="%s" ;' % date env += 'export GIT_COMMITTER_DATE="%s" ;' % date @@ -68,16 +75,21 @@ def getgitenv(user, date): #------------------------------------------------------------------------------ state = '' +opt_nrepack = 0 +verbose = False try: - opts, args = getopt.getopt(sys.argv[1:], 's:t:', ['gitstate=', 'tempdir=']) + opts, args = getopt.getopt(sys.argv[1:], 's:t:n:v', ['gitstate=', 'tempdir=', 'nrepack=', 'verbose']) for o, a in opts: if o in ('-s', '--gitstate'): state = a state = os.path.abspath(state) - + if o in ('-n', '--nrepack'): + opt_nrepack = int(a) + if o in ('-v', '--verbose'): + verbose = True if len(args) != 1: - raise('params') + raise Exception('params') except: usage() sys.exit(1) @@ -87,23 +99,31 @@ os.chdir(hgprj) if state: if os.path.exists(state): - print 'State does exist, reading' + if verbose: + print 'State does exist, reading' f = open(state, 'r') hgvers = pickle.load(f) else: print 'State does not exist, first run' -tip = os.popen('hg tip | head -1 | cut -f 2 -d :').read().strip() -print 'tip is', tip +sock = os.popen('hg tip --template "{rev}"') +tip = sock.read() +if sock.close(): + sys.exit(1) +if verbose: + print 'tip is', tip # Calculate the branches -print 'analysing the branches...' +if verbose: + print 'analysing the branches...' hgchildren["0"] = () +hgparents["0"] = (None, None) hgbranch["0"] = "master" for cset in range(1, int(tip) + 1): hgchildren[str(cset)] = () - prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines() - if len(prnts) > 0: + prnts = os.popen('hg log -r %d --template "{parents}"' % cset).read().strip().split(' ') + prnts = map(lambda x: x[:x.find(':')], prnts) + if prnts[0] != '': parent = prnts[0].strip() else: parent = str(cset - 1) @@ -114,6 +134,8 @@ for cset in range(1, int(tip) + 1): else: mparent = None + hgparents[str(cset)] = (parent, mparent) + if mparent: # For merge changesets, take either one, preferably the 'master' branch if hgbranch[mparent] == 'master': @@ -130,7 +152,7 @@ for cset in range(1, int(tip) + 1): if not hgvers.has_key("0"): print 'creating repository' - os.system('git-init-db') + os.system('git init') # loop through every hg changeset for cset in range(int(tip) + 1): @@ -138,36 +160,30 @@ for cset in range(int(tip) + 1): # incremental, already seen if hgvers.has_key(str(cset)): continue + hgnewcsets += 1 # get info - prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines() - if len(prnts) > 0: - parent = prnts[0].strip() - else: - parent = str(cset - 1) - if len(prnts) > 1: - mparent = prnts[1].strip() - else: - mparent = None - + log_data = os.popen('hg log -r %d --template "{tags}\n{date|date}\n{author}\n"' % cset).readlines() + tag = log_data[0].strip() + date = log_data[1].strip() + user = log_data[2].strip() + parent = hgparents[str(cset)][0] + mparent = hgparents[str(cset)][1] + + #get comment (fdcomment, filecomment) = tempfile.mkstemp() - csetcomment = os.popen('hg log -r %d -v | grep -v ^changeset: | grep -v ^parent: | grep -v ^user: | grep -v ^date | grep -v ^files: | grep -v ^description: | grep -v ^tag:' % cset).read().strip() + csetcomment = os.popen('hg log -r %d --template "{desc}"' % cset).read().strip() os.write(fdcomment, csetcomment) os.close(fdcomment) - date = os.popen('hg log -r %d | grep ^date: | cut -f 2- -d :' % cset).read().strip() - - tag = os.popen('hg log -r %d | grep ^tag: | cut -f 2- -d :' % cset).read().strip() - - user = os.popen('hg log -r %d | grep ^user: | cut -f 2- -d :' % cset).read().strip() - print '-----------------------------------------' print 'cset:', cset print 'branch:', hgbranch[str(cset)] print 'user:', user print 'date:', date print 'comment:', csetcomment - print 'parent:', parent + if parent: + print 'parent:', parent if mparent: print 'mparent:', mparent if tag: @@ -178,10 +194,10 @@ for cset in range(int(tip) + 1): if cset != 0: if hgbranch[str(cset)] == "branch-" + str(cset): print 'creating new branch', hgbranch[str(cset)] - os.system('git-checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent])) + os.system('git checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent])) else: print 'checking out branch', hgbranch[str(cset)] - os.system('git-checkout %s' % hgbranch[str(cset)]) + os.system('git checkout %s' % hgbranch[str(cset)]) # merge if mparent: @@ -190,7 +206,7 @@ for cset in range(int(tip) + 1): else: otherbranch = hgbranch[parent] print 'merging', otherbranch, 'into', hgbranch[str(cset)] - os.system(getgitenv(user, date) + 'git-merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch)) + os.system(getgitenv(user, date) + 'git merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch)) # remove everything except .git and .hg directories os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf') @@ -199,34 +215,35 @@ for cset in range(int(tip) + 1): os.system('hg update -C %d' % cset) # add new files - os.system('git-ls-files -x .hg --others | git-update-index --add --stdin') + os.system('git ls-files -x .hg --others | git update-index --add --stdin') # delete removed files - os.system('git-ls-files -x .hg --deleted | git-update-index --remove --stdin') + os.system('git ls-files -x .hg --deleted | git update-index --remove --stdin') # commit - os.system(getgitenv(user, date) + 'git-commit -a -F %s' % filecomment) + os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment) os.unlink(filecomment) # tag if tag and tag != 'tip': - os.system(getgitenv(user, date) + 'git-tag %s' % tag) + os.system(getgitenv(user, date) + 'git tag %s' % tag) # delete branch if not used anymore... if mparent and len(hgchildren[str(cset)]): print "Deleting unused branch:", otherbranch - os.system('git-branch -d %s' % otherbranch) + os.system('git branch -d %s' % otherbranch) # retrieve and record the version - vvv = os.popen('git-show | head -1').read() - vvv = vvv[vvv.index(' ') + 1 : ].strip() + vvv = os.popen('git show --quiet --pretty=format:%H').read() print 'record', cset, '->', vvv hgvers[str(cset)] = vvv -os.system('git-repack -a -d') +if hgnewcsets >= opt_nrepack and opt_nrepack != -1: + os.system('git repack -a -d') # write the state for incrementals if state: - print 'Writing state' + if verbose: + print 'Writing state' f = open(state, 'w') pickle.dump(hgvers, f) diff --git a/contrib/hooks/post-receive-email b/contrib/hooks/post-receive-email index c589a39a0c..01af9df15e 100644..100755 --- a/contrib/hooks/post-receive-email +++ b/contrib/hooks/post-receive-email @@ -2,24 +2,33 @@ # # Copyright (c) 2007 Andy Parkins # -# An example hook script to mail out commit update information. This hook sends emails -# listing new revisions to the repository introduced by the change being reported. The -# rule is that (for branch updates) each commit will appear on one email and one email -# only. +# An example hook script to mail out commit update information. This hook +# sends emails listing new revisions to the repository introduced by the +# change being reported. The rule is that (for branch updates) each commit +# will appear on one email and one email only. # -# This hook is stored in the contrib/hooks directory. Your distribution will have put -# this somewhere standard. You should make this script executable then link to it in -# the repository you would like to use it in. For example, on debian the hook is stored -# in /usr/share/doc/git-core/contrib/hooks/post-receive-email: +# This hook is stored in the contrib/hooks directory. Your distribution +# will have put this somewhere standard. You should make this script +# executable then link to it in the repository you would like to use it in. +# For example, on debian the hook is stored in +# /usr/share/git-core/contrib/hooks/post-receive-email: # # chmod a+x post-receive-email # cd /path/to/your/repository.git -# ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive +# ln -sf /usr/share/git-core/contrib/hooks/post-receive-email hooks/post-receive # -# This hook script assumes it is enabled on the central repository of a project, with -# all users pushing only to it and not between each other. It will still work if you -# don't operate in that style, but it would become possible for the email to be from -# someone other than the person doing the push. +# This hook script assumes it is enabled on the central repository of a +# project, with all users pushing only to it and not between each other. It +# will still work if you don't operate in that style, but it would become +# possible for the email to be from someone other than the person doing the +# push. +# +# To help with debugging and use on pre-v1.5.1 git servers, this script will +# also obey the interface of hooks/update, taking its arguments on the +# command line. Unfortunately, hooks/update is called once for each ref. +# To avoid firing one email per ref, this script just prints its output to +# the screen when used in this mode. The output can then be redirected if +# wanted. # # Config # ------ @@ -28,15 +37,37 @@ # emails for every ref update. # hooks.announcelist # This is the list that all pushes of annotated tags will go to. Leave it -# blank to default to the mailinglist field. The announce emails lists the -# short log summary of the changes since the last annotated tag. -# hook.envelopesender -# If set then the -f option is passed to sendmail to allow the envelope sender -# address to be set +# blank to default to the mailinglist field. The announce emails lists +# the short log summary of the changes since the last annotated tag. +# hooks.envelopesender +# If set then the -f option is passed to sendmail to allow the envelope +# sender address to be set +# hooks.emailprefix +# All emails have their subjects prefixed with this prefix, or "[SCM]" +# if emailprefix is unset, to aid filtering +# hooks.showrev +# The shell command used to format each revision in the email, with +# "%s" replaced with the commit id. Defaults to "git rev-list -1 +# --pretty %s", displaying the commit id, author, date and log +# message. To list full patches separated by a blank line, you +# could set this to "git show -C %s; echo". +# To list a gitweb/cgit URL *and* a full patch for each change set, use this: +# "t=%s; printf 'http://.../?id=%%s' \$t; echo;echo; git show -C \$t; echo" +# Be careful if "..." contains things that will be expanded by shell "eval" +# or printf. +# hooks.emailmaxlines +# The maximum number of lines that should be included in the generated +# email body. If not specified, there is no limit. +# Lines beyond the limit are suppressed and counted, and a final +# line is added indicating the number of suppressed lines. +# hooks.diffopts +# Alternate options for the git diff-tree invocation that shows changes. +# Default is "--stat --summary --find-copies-harder". Add -p to those +# options to include a unified diff of changes in addition to the usual +# summary output. # # Notes # ----- -# All emails have their subjects prefixed with "[SCM]" to aid filtering. # All emails include the headers "X-Git-Refname", "X-Git-Oldrev", # "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and # give information for debugging. @@ -45,19 +76,10 @@ # ---------------------------- Functions # -# Top level email generation function. This decides what type of update -# this is and calls the appropriate body-generation routine after outputting -# the common header +# Function to prepare for email generation. This decides what type +# of update this is and whether an email should even be generated. # -# Note this function doesn't actually generate any email output, that is taken -# care of by the functions it calls: -# - generate_email_header -# - generate_create_XXXX_email -# - generate_update_XXXX_email -# - generate_delete_XXXX_email -# - generate_email_footer -# -generate_email() +prep_for_email() { # --- Arguments oldrev=$(git rev-parse $1) @@ -126,28 +148,55 @@ generate_email() short_refname=${refname##refs/remotes/} echo >&2 "*** Push-update of tracking branch, $refname" echo >&2 "*** - no email generated." - exit 0 + return 1 ;; *) # Anything else (is there anything else?) echo >&2 "*** Unknown type of update to $refname ($rev_type)" echo >&2 "*** - no email generated" - exit 1 + return 1 ;; esac # Check if we've got anyone to send to if [ -z "$recipients" ]; then - echo >&2 "*** hooks.recipients is not set so no email will be sent" + case "$refname_type" in + "annotated tag") + config_name="hooks.announcelist" + ;; + *) + config_name="hooks.mailinglist" + ;; + esac + echo >&2 "*** $config_name is not set so no email will be sent" echo >&2 "*** for $refname update $oldrev->$newrev" - exit 0 + return 1 fi + return 0 +} + +# +# Top level email generation function. This calls the appropriate +# body-generation routine after outputting the common header. +# +# Note this function doesn't actually generate any email output, that is +# taken care of by the functions it calls: +# - generate_email_header +# - generate_create_XXXX_email +# - generate_update_XXXX_email +# - generate_delete_XXXX_email +# - generate_email_footer +# +# Note also that this function cannot 'exit' from the script; when this +# function is running (in hook script mode), the send_mail() function +# is already executing in another process, connected via a pipe, and +# if this function exits without, whatever has been generated to that +# point will be sent as an email... even if nothing has been generated. +# +generate_email() +{ # Email parameters - # The committer will be obtained from the latest existing rev; so - # for a deletion it will be the oldrev, for the others, then newrev - committer=$(git show --pretty=full -s $rev | sed -ne "s/^Commit: //p" | - sed -ne 's/\(.*\) </"\1" </p') # The email subject will contain the best description of the ref # that we can build from the parameters describe=$(git describe $rev 2>/dev/null) @@ -167,7 +216,12 @@ generate_email() fn_name=atag ;; esac - generate_${change_type}_${fn_name}_email + + if [ -z "$maxlines" ]; then + generate_${change_type}_${fn_name}_email + else + generate_${change_type}_${fn_name}_email | limit_lines $maxlines + fi generate_email_footer } @@ -177,9 +231,8 @@ generate_email_header() # --- Email (all stdout will be the email) # Generate header cat <<-EOF - From: $committer To: $recipients - Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname, ${change_type}d. $describe + Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe X-Git-Refname: $refname X-Git-Reftype: $refname_type X-Git-Oldrev: $oldrev @@ -195,11 +248,12 @@ generate_email_header() generate_email_footer() { + SPACE=" " cat <<-EOF hooks/post-receive - -- + --${SPACE} $projectdesc EOF } @@ -216,12 +270,7 @@ generate_create_branch_email() echo "" echo $LOGBEGIN - # This shows all log entries that are not already covered by - # another ref - i.e. commits that are now accessible from this - # ref that were previously not accessible (see generate_update_branch_email - # for the explanation of this command) - git rev-parse --not --branches | grep -v $(git rev-parse $refname) | - git rev-list --pretty --stdin $newrev + show_new_revisions echo $LOGEND } @@ -240,35 +289,37 @@ generate_update_branch_email() # In this case we want to issue an email containing only revisions # 3, 4, and N. Given (almost) by # - # git-rev-list N ^O --not --all + # git rev-list N ^O --not --all # # The reason for the "almost", is that the "--not --all" will take # precedence over the "N", and effectively will translate to # - # git-rev-list N ^O ^X ^N + # git rev-list N ^O ^X ^N # - # So, we need to build up the list more carefully. git-rev-parse will - # generate a list of revs that may be fed into git-rev-list. We can get - # it to make the "--not --all" part and then filter out the "^N" with: + # So, we need to build up the list more carefully. git rev-parse + # will generate a list of revs that may be fed into git rev-list. + # We can get it to make the "--not --all" part and then filter out + # the "^N" with: # - # git-rev-parse --not --all | grep -v N + # git rev-parse --not --all | grep -v N # - # Then, using the --stdin switch to git-rev-list we have effectively + # Then, using the --stdin switch to git rev-list we have effectively # manufactured # - # git-rev-list N ^O ^X + # git rev-list N ^O ^X # # This leaves a problem when someone else updates the repository - # while this script is running. Their new value of the ref we're working - # on would be included in the "--not --all" output; and as our $newrev - # would be an ancestor of that commit, it would exclude all of our - # commits. What we really want is to exclude the current value of - # $refname from the --not list, rather than N itself. So: + # while this script is running. Their new value of the ref we're + # working on would be included in the "--not --all" output; and as + # our $newrev would be an ancestor of that commit, it would exclude + # all of our commits. What we really want is to exclude the current + # value of $refname from the --not list, rather than N itself. So: # - # git-rev-parse --not --all | grep -v $(git-rev-parse $refname) + # git rev-parse --not --all | grep -v $(git rev-parse $refname) # - # Get's us to something pretty safe (apart from the small time between - # refname being read, and git-rev-parse running - for that, I give up) + # Get's us to something pretty safe (apart from the small time + # between refname being read, and git rev-parse running - for that, + # I give up) # # # Next problem, consider this: @@ -276,18 +327,18 @@ generate_update_branch_email() # \ # * --- X --- * --- N ($newrev) # - # That is to say, there is no guarantee that oldrev is a strict subset of - # newrev (it would have required a --force, but that's allowed). So, we - # can't simply say rev-list $oldrev..$newrev. Instead we find the common - # base of the two revs and list from there. + # That is to say, there is no guarantee that oldrev is a strict + # subset of newrev (it would have required a --force, but that's + # allowed). So, we can't simply say rev-list $oldrev..$newrev. + # Instead we find the common base of the two revs and list from + # there. # - # As above, we need to take into account the presence of X; if another - # branch is already in the repository and points at some of the revisions - # that we are about to output - we don't want them. The solution is as - # before: git-rev-parse output filtered. + # As above, we need to take into account the presence of X; if + # another branch is already in the repository and points at some of + # the revisions that we are about to output - we don't want them. + # The solution is as before: git rev-parse output filtered. # - # Finally, tags: - # 1 --- 2 --- O --- T --- 3 --- 4 --- N + # Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N # # Tags pushed into the repository generate nice shortlog emails that # summarise the commits between them and the previous tag. However, @@ -295,13 +346,14 @@ generate_update_branch_email() # for a branch update. Therefore we still want to output revisions # that have been output on a tag email. # - # Luckily, git-rev-parse includes just the tool. Instead of using "--all" - # we use "--branches"; this has the added benefit that "remotes/" will - # be ignored as well. - - # List all of the revisions that were removed by this update, in a fast forward - # update, this list will be empty, because rev-list O ^N is empty. For a non - # fast forward, O ^N is the list of removed revisions + # Luckily, git rev-parse includes just the tool. Instead of using + # "--all" we use "--branches"; this has the added benefit that + # "remotes/" will be ignored as well. + + # List all of the revisions that were removed by this update, in a + # fast-forward update, this list will be empty, because rev-list O + # ^N is empty. For a non-fast-forward, O ^N is the list of removed + # revisions fast_forward="" rev="" for rev in $(git rev-list $newrev..$oldrev) @@ -314,32 +366,33 @@ generate_update_branch_email() fi # List all the revisions from baserev to newrev in a kind of - # "table-of-contents"; note this list can include revisions that have - # already had notification emails and is present to show the full detail - # of the change from rolling back the old revision to the base revision and - # then forward to the new revision + # "table-of-contents"; note this list can include revisions that + # have already had notification emails and is present to show the + # full detail of the change from rolling back the old revision to + # the base revision and then forward to the new revision for rev in $(git rev-list $oldrev..$newrev) do revtype=$(git cat-file -t "$rev") echo " via $rev ($revtype)" done - if [ -z "$fastforward" ]; then + if [ "$fast_forward" ]; then echo " from $oldrev ($oldrev_type)" else - # 1. Existing revisions were removed. In this case newrev is a - # subset of oldrev - this is the reverse of a fast-forward, - # a rewind - # 2. New revisions were added on top of an old revision, this is - # a rewind and addition. + # 1. Existing revisions were removed. In this case newrev + # is a subset of oldrev - this is the reverse of a + # fast-forward, a rewind + # 2. New revisions were added on top of an old revision, + # this is a rewind and addition. - # (1) certainly happened, (2) possibly. When (2) hasn't happened, - # we set a flag to indicate that no log printout is required. + # (1) certainly happened, (2) possibly. When (2) hasn't + # happened, we set a flag to indicate that no log printout + # is required. echo "" - # Find the common ancestor of the old and new revisions and compare - # it with newrev + # Find the common ancestor of the old and new revisions and + # compare it with newrev baserev=$(git merge-base $oldrev $newrev) rewind_only="" if [ "$baserev" = "$newrev" ]; then @@ -377,27 +430,27 @@ generate_update_branch_email() echo "" echo $LOGBEGIN - git rev-parse --not --branches | grep -v $(git rev-parse $refname) | - git rev-list --pretty --stdin $oldrev..$newrev + show_new_revisions - # XXX: Need a way of detecting whether git rev-list actually outputted - # anything, so that we can issue a "no new revisions added by this - # update" message + # XXX: Need a way of detecting whether git rev-list actually + # outputted anything, so that we can issue a "no new + # revisions added by this update" message echo $LOGEND else echo "No new revisions were added by this update." fi - # The diffstat is shown from the old revision to the new revision. This - # is to show the truth of what happened in this change. There's no point - # showing the stat from the base to the new revision because the base - # is effectively a random revision at this point - the user will be - # interested in what this revision changed - including the undoing of - # previous revisions in the case of non-fast forward updates. + # The diffstat is shown from the old revision to the new revision. + # This is to show the truth of what happened in this change. + # There's no point showing the stat from the base to the new + # revision because the base is effectively a random revision at this + # point - the user will be interested in what this revision changed + # - including the undoing of previous revisions in the case of + # non-fast-forward updates. echo "" echo "Summary of changes:" - git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev + git diff-tree $diffopts $oldrev..$newrev } # @@ -407,7 +460,7 @@ generate_delete_branch_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } @@ -441,7 +494,8 @@ generate_update_atag_email() # generate_atag_email() { - # Use git-for-each-ref to pull out the individual fields from the tag + # Use git for-each-ref to pull out the individual fields from the + # tag eval $(git for-each-ref --shell --format=' tagobject=%(*objectname) tagtype=%(*objecttype) @@ -452,8 +506,10 @@ generate_atag_email() echo " tagging $tagobject ($tagtype)" case "$tagtype" in commit) + # If the tagged object is a commit, then we assume this is a - # release, and so we calculate which tag this tag is replacing + # release, and so we calculate which tag this tag is + # replacing prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) if [ -n "$prevtag" ]; then @@ -470,25 +526,27 @@ generate_atag_email() echo "" echo $LOGBEGIN - # Show the content of the tag message; this might contain a change log - # or release notes so is worth displaying. + # Show the content of the tag message; this might contain a change + # log or release notes so is worth displaying. git cat-file tag $newrev | sed -e '1,/^$/d' echo "" case "$tagtype" in commit) - # Only commit tags make sense to have rev-list operations performed - # on them + # Only commit tags make sense to have rev-list operations + # performed on them if [ -n "$prevtag" ]; then # Show changes since the previous release git rev-list --pretty=short "$prevtag..$newrev" | git shortlog else - # No previous tag, show all the changes since time began + # No previous tag, show all the changes since time + # began git rev-list --pretty=short $newrev | git shortlog fi ;; *) - # XXX: Is there anything useful we can do for non-commit objects? + # XXX: Is there anything useful we can do for non-commit + # objects? ;; esac @@ -502,7 +560,7 @@ generate_delete_atag_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } @@ -537,24 +595,25 @@ generate_update_general_email() # generate_general_email() { - # Unannotated tags are more about marking a point than releasing a version; - # therefore we don't do the shortlog summary that we do for annotated tags - # above - we simply show that the point has been marked, and print the log - # message for the marked point for reference purposes + # Unannotated tags are more about marking a point than releasing a + # version; therefore we don't do the shortlog summary that we do for + # annotated tags above - we simply show that the point has been + # marked, and print the log message for the marked point for + # reference purposes # - # Note this section also catches any other reference type (although there - # aren't any) and deals with them in the same way. + # Note this section also catches any other reference type (although + # there aren't any) and deals with them in the same way. echo "" if [ "$newrev_type" = "commit" ]; then echo $LOGBEGIN - git show --no-color --root -s $newrev + git show --no-color --root -s --pretty=medium $newrev echo $LOGEND else - # What can we do here? The tag marks an object that is not a commit, - # so there is no log for us to display. It's probably not wise to - # output git-cat-file as it could be a binary blob. We'll just say how - # big it is + # What can we do here? The tag marks an object that is not + # a commit, so there is no log for us to display. It's + # probably not wise to output git cat-file as it could be a + # binary blob. We'll just say how big it is echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." fi } @@ -566,15 +625,82 @@ generate_delete_general_email() { echo " was $oldrev" echo "" - echo $LOGEND + echo $LOGBEGIN git show -s --pretty=oneline $oldrev echo $LOGEND } + +# --------------- Miscellaneous utilities + +# +# Show new revisions as the user would like to see them in the email. +# +show_new_revisions() +{ + # This shows all log entries that are not already covered by + # another ref - i.e. commits that are now accessible from this + # ref that were previously not accessible + # (see generate_update_branch_email for the explanation of this + # command) + + # Revision range passed to rev-list differs for new vs. updated + # branches. + if [ "$change_type" = create ] + then + # Show all revisions exclusive to this (new) branch. + revspec=$newrev + else + # Branch update; show revisions not part of $oldrev. + revspec=$oldrev..$newrev + fi + + other_branches=$(git for-each-ref --format='%(refname)' refs/heads/ | + grep -F -v $refname) + git rev-parse --not $other_branches | + if [ -z "$custom_showrev" ] + then + git rev-list --pretty --stdin $revspec + else + git rev-list --stdin $revspec | + while read onerev + do + eval $(printf "$custom_showrev" $onerev) + done + fi +} + + +limit_lines() +{ + lines=0 + skipped=0 + while IFS="" read -r line; do + lines=$((lines + 1)) + if [ $lines -gt $1 ]; then + skipped=$((skipped + 1)) + else + printf "%s\n" "$line" + fi + done + if [ $skipped -ne 0 ]; then + echo "... $skipped lines suppressed ..." + fi +} + + +send_mail() +{ + if [ -n "$envelopesender" ]; then + /usr/sbin/sendmail -t -f "$envelopesender" + else + /usr/sbin/sendmail -t + fi +} + # ---------------------------- main() # --- Constants -EMAILPREFIX="[SCM] " LOGBEGIN="- Log -----------------------------------------------------------------" LOGEND="-----------------------------------------------------------------------" @@ -587,33 +713,35 @@ if [ -z "$GIT_DIR" ]; then exit 1 fi -projectdesc=$(sed -ne '1p' "$GIT_DIR/description") -# Check if the description is unchanged from it's default, and shorten it to a -# more manageable length if it is +projectdesc=$(sed -ne '1p' "$GIT_DIR/description" 2>/dev/null) +# Check if the description is unchanged from it's default, and shorten it to +# a more manageable length if it is if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null then projectdesc="UNNAMED PROJECT" fi -recipients=$(git repo-config hooks.mailinglist) -announcerecipients=$(git repo-config hooks.announcelist) -envelopesender=$(git-repo-config hooks.envelopesender) +recipients=$(git config hooks.mailinglist) +announcerecipients=$(git config hooks.announcelist) +envelopesender=$(git config hooks.envelopesender) +emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') +custom_showrev=$(git config hooks.showrev) +maxlines=$(git config hooks.emailmaxlines) +diffopts=$(git config hooks.diffopts) +: ${diffopts:="--stat --summary --find-copies-harder"} # --- Main loop -# Allow dual mode: run from the command line just like the update hook, or if -# no arguments are given then run as a hook script +# Allow dual mode: run from the command line just like the update hook, or +# if no arguments are given then run as a hook script if [ -n "$1" -a -n "$2" -a -n "$3" ]; then # Output to the terminal in command line mode - if someone wanted to - # resend an email; they could redirect the output to sendmail themselves - PAGER= generate_email $2 $3 $1 + # resend an email; they could redirect the output to sendmail + # themselves + prep_for_email $2 $3 $1 && PAGER= generate_email else - if [ -n "$envelopesender" ]; then - envelopesender="-f '$envelopesender'" - fi - while read oldrev newrev refname do - generate_email $oldrev $newrev $refname | - /usr/sbin/sendmail -t $envelopesender + prep_for_email $oldrev $newrev $refname || continue + generate_email $maxlines | send_mail done fi diff --git a/contrib/hooks/pre-auto-gc-battery b/contrib/hooks/pre-auto-gc-battery new file mode 100644 index 0000000000..1f914c94aa --- /dev/null +++ b/contrib/hooks/pre-auto-gc-battery @@ -0,0 +1,43 @@ +#!/bin/sh +# +# An example hook script to verify if you are on battery, in case you +# are running Linux or OS X. Called by git-gc --auto with no arguments. +# The hook should exit with non-zero status after issuing an appropriate +# message if it wants to stop the auto repacking. +# +# This hook is stored in the contrib/hooks directory. Your distribution +# may have put this somewhere else. If you want to use this hook, you +# should make this script executable then link to it in the repository +# you would like to use it in. +# +# For example, if the hook is stored in +# /usr/share/git-core/contrib/hooks/pre-auto-gc-battery: +# +# chmod a+x pre-auto-gc-battery +# cd /path/to/your/repository.git +# ln -sf /usr/share/git-core/contrib/hooks/pre-auto-gc-battery \ +# hooks/pre-auto-gc + +if test -x /sbin/on_ac_power && /sbin/on_ac_power +then + exit 0 +elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1 +then + exit 0 +elif grep -q 'on-line' /proc/acpi/ac_adapter/AC/state 2>/dev/null +then + exit 0 +elif grep -q '0x01$' /proc/apm 2>/dev/null +then + exit 0 +elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null +then + exit 0 +elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt | + grep -q "Currently drawing from 'AC Power'" +then + exit 0 +fi + +echo "Auto packing deferred; not on AC" +exit 1 diff --git a/contrib/hooks/setgitperms.perl b/contrib/hooks/setgitperms.perl new file mode 100644 index 0000000000..a577ad095f --- /dev/null +++ b/contrib/hooks/setgitperms.perl @@ -0,0 +1,214 @@ +#!/usr/bin/perl +# +# Copyright (c) 2006 Josh England +# +# This script can be used to save/restore full permissions and ownership data +# within a git working tree. +# +# To save permissions/ownership data, place this script in your .git/hooks +# directory and enable a `pre-commit` hook with the following lines: +# #!/bin/sh +# SUBDIRECTORY_OK=1 . git-sh-setup +# $GIT_DIR/hooks/setgitperms.perl -r +# +# To restore permissions/ownership data, place this script in your .git/hooks +# directory and enable a `post-merge` and `post-checkout` hook with the +# following lines: +# #!/bin/sh +# SUBDIRECTORY_OK=1 . git-sh-setup +# $GIT_DIR/hooks/setgitperms.perl -w +# +use strict; +use Getopt::Long; +use File::Find; +use File::Basename; + +my $usage = +"Usage: setgitperms.perl [OPTION]... <--read|--write> +This program uses a file `.gitmeta` to store/restore permissions and uid/gid +info for all files/dirs tracked by git in the repository. + +---------------------------------Read Mode------------------------------------- +-r, --read Reads perms/etc from working dir into a .gitmeta file +-s, --stdout Output to stdout instead of .gitmeta +-d, --diff Show unified diff of perms file (XOR with --stdout) + +---------------------------------Write Mode------------------------------------ +-w, --write Modify perms/etc in working dir to match the .gitmeta file +-v, --verbose Be verbose + +\n"; + +my ($stdout, $showdiff, $verbose, $read_mode, $write_mode); + +if ((@ARGV < 0) || !GetOptions( + "stdout", \$stdout, + "diff", \$showdiff, + "read", \$read_mode, + "write", \$write_mode, + "verbose", \$verbose, + )) { die $usage; } +die $usage unless ($read_mode xor $write_mode); + +my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir; +my $gitdir = $topdir . '.git'; +my $gitmeta = $topdir . '.gitmeta'; + +if ($write_mode) { + # Update the working dir permissions/ownership based on data from .gitmeta + open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n"; + while (defined ($_ = <IN>)) { + chomp; + if (/^(.*) mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) { + # Compare recorded perms to actual perms in the working dir + my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4); + my $fullpath = $topdir . $path; + my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath); + $wmode = sprintf "%04o", $wmode & 07777; + if ($mode ne $wmode) { + $verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n"; + chmod oct($mode), $fullpath; + } + if ($uid != $wuid || $gid != $wgid) { + if ($verbose) { + # Print out user/group names instead of uid/gid + my $pwname = getpwuid($uid); + my $grpname = getgrgid($gid); + my $wpwname = getpwuid($wuid); + my $wgrpname = getgrgid($wgid); + $pwname = $uid if !defined $pwname; + $grpname = $gid if !defined $grpname; + $wpwname = $wuid if !defined $wpwname; + $wgrpname = $wgid if !defined $wgrpname; + + print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n"; + } + chown $uid, $gid, $fullpath; + } + } + else { + warn "Invalid input format in $gitmeta:\n\t$_\n"; + } + } + close IN; +} +elsif ($read_mode) { + # Handle merge conflicts in the .gitperms file + if (-e "$gitdir/MERGE_MSG") { + if (`grep ====== $gitmeta`) { + # Conflict not resolved -- abort the commit + print "PERMISSIONS/OWNERSHIP CONFLICT\n"; + print " Resolve the conflict in the $gitmeta file and then run\n"; + print " `.git/hooks/setgitperms.perl --write` to reconcile.\n"; + exit 1; + } + elsif (`grep $gitmeta $gitdir/MERGE_MSG`) { + # A conflict in .gitmeta has been manually resolved. Verify that + # the working dir perms matches the current .gitmeta perms for + # each file/dir that conflicted. + # This is here because a `setgitperms.perl --write` was not + # performed due to a merge conflict, so permissions/ownership + # may not be consistent with the manually merged .gitmeta file. + my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`; + my @conflict_files; + my $metadiff = 0; + + # Build a list of files that conflicted from the .gitmeta diff + foreach my $line (@conflict_diff) { + if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) { + $metadiff = 1; + } + elsif ($line =~ /^diff --git/) { + $metadiff = 0; + } + elsif ($metadiff && $line =~ /^\+(.*) mode=/) { + push @conflict_files, $1; + } + } + + # Verify that each conflict file now has permissions consistent + # with the .gitmeta file + foreach my $file (@conflict_files) { + my $absfile = $topdir . $file; + my $gm_entry = `grep "^$file mode=" $gitmeta`; + if ($gm_entry =~ /mode=(\d+) uid=(\d+) gid=(\d+)/) { + my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3); + my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile"); + $mode = sprintf("%04o", $mode & 07777); + if (($gm_mode ne $mode) || ($gm_uid != $uid) + || ($gm_gid != $gid)) { + print "PERMISSIONS/OWNERSHIP CONFLICT\n"; + print " Mismatch found for file: $file\n"; + print " Run `.git/hooks/setgitperms.perl --write` to reconcile.\n"; + exit 1; + } + } + else { + print "Warning! Permissions/ownership no longer being tracked for file: $file\n"; + } + } + } + } + + # No merge conflicts -- write out perms/ownership data to .gitmeta file + unless ($stdout) { + open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n"; + } + + my @files = `git ls-files`; + my %dirs; + + foreach my $path (@files) { + chomp $path; + # We have to manually add stats for parent directories + my $parent = dirname($path); + while (!exists $dirs{$parent}) { + $dirs{$parent} = 1; + next if $parent eq '.'; + printstats($parent); + $parent = dirname($parent); + } + # Now the git-tracked file + printstats($path); + } + + # diff the temporary metadata file to see if anything has changed + # If no metadata has changed, don't overwrite the real file + # This is just so `git commit -a` doesn't try to commit a bogus update + unless ($stdout) { + if (! -e $gitmeta) { + rename "$gitmeta.tmp", $gitmeta; + } + else { + my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`; + if ($diff ne '') { + rename "$gitmeta.tmp", $gitmeta; + } + else { + unlink "$gitmeta.tmp"; + } + if ($showdiff) { + print $diff; + } + } + close OUT; + } + # Make sure the .gitmeta file is tracked + system("git add $gitmeta"); +} + + +sub printstats { + my $path = $_[0]; + $path =~ s/@/\@/g; + my (undef,undef,$mode,undef,$uid,$gid) = lstat($path); + $path =~ s/%/\%/g; + if ($stdout) { + print $path; + printf " mode=%04o uid=$uid gid=$gid\n", $mode & 07777; + } + else { + print OUT $path; + printf OUT " mode=%04o uid=$uid gid=$gid\n", $mode & 07777; + } +} diff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid index 068fa37083..d18b317b2f 100644 --- a/contrib/hooks/update-paranoid +++ b/contrib/hooks/update-paranoid @@ -136,6 +136,7 @@ sub parse_config ($$$$) { local $ENV{GIT_DIR} = shift; my $br = shift; my $fn = shift; + return unless git_value('rev-list','--max-count=1',$br,'--',$fn); info "Loading $br:$fn"; open(I,'-|','git','cat-file','blob',"$br:$fn"); my $section = ''; @@ -225,14 +226,12 @@ sub load_diff ($) { local $/ = "\0"; my %this_diff; if ($base =~ /^0{40}$/) { - open(T,'-|','git','ls-tree', - '-r','--name-only','-z', - $new) or return undef; - while (<T>) { - chop; - $this_diff{$_} = 'A'; - } - close T or return undef; + # Don't load the diff at all; we are making the + # branch and have no base to compare to in this + # case. A file level ACL makes no sense in this + # context. Having an empty diff will allow the + # branch creation. + # } else { open(T,'-|','git','diff-tree', '-r','--name-status','-z', @@ -260,6 +259,7 @@ deny "Refusing funny ref $ref" unless $ref =~ s,^refs/,,; deny "Bad old value $old" unless $old =~ /^[a-z0-9]{40}$/; deny "Bad new value $new" unless $new =~ /^[a-z0-9]{40}$/; deny "Cannot determine who you are." unless $this_user; +grant "No change requested." if $old eq $new; $repository_name = File::Spec->rel2abs($git_dir); $repository_name =~ m,/([^/]+)(?:\.git|/\.git)$,; diff --git a/contrib/mw-to-git/Makefile b/contrib/mw-to-git/Makefile new file mode 100644 index 0000000000..3ed728b0ef --- /dev/null +++ b/contrib/mw-to-git/Makefile @@ -0,0 +1,47 @@ +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +## Build git-remote-mediawiki + +-include ../../config.mak.autogen +-include ../../config.mak + +ifndef PERL_PATH + PERL_PATH = /usr/bin/perl +endif +ifndef gitexecdir + gitexecdir = $(shell git --exec-path) +endif + +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) +gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) +SCRIPT = git-remote-mediawiki + +.PHONY: install help doc test clean + +help: + @echo 'This is the help target of the Makefile. Current configuration:' + @echo ' gitexecdir = $(gitexecdir_SQ)' + @echo ' PERL_PATH = $(PERL_PATH_SQ)' + @echo 'Run "$(MAKE) install" to install $(SCRIPT) in gitexecdir' + @echo 'Run "$(MAKE) test" to run the testsuite' + +install: + sed -e '1s|#!.*/perl|#!$(PERL_PATH_SQ)|' $(SCRIPT) \ + > '$(gitexecdir_SQ)/$(SCRIPT)' + chmod +x '$(gitexecdir)/$(SCRIPT)' + +doc: + @echo 'Sorry, "make doc" is not implemented yet for $(SCRIPT)' + +test: + $(MAKE) -C t/ test + +clean: + $(RM) '$(gitexecdir)/$(SCRIPT)' + $(MAKE) -C t/ clean diff --git a/contrib/mw-to-git/git-remote-mediawiki b/contrib/mw-to-git/git-remote-mediawiki new file mode 100755 index 0000000000..accd70a94c --- /dev/null +++ b/contrib/mw-to-git/git-remote-mediawiki @@ -0,0 +1,1323 @@ +#! /usr/bin/perl + +# Copyright (C) 2011 +# Jérémie Nikaes <jeremie.nikaes@ensimag.imag.fr> +# Arnaud Lacurie <arnaud.lacurie@ensimag.imag.fr> +# Claire Fousse <claire.fousse@ensimag.imag.fr> +# David Amouyal <david.amouyal@ensimag.imag.fr> +# Matthieu Moy <matthieu.moy@grenoble-inp.fr> +# License: GPL v2 or later + +# Gateway between Git and MediaWiki. +# https://github.com/Bibzball/Git-Mediawiki/wiki +# +# Known limitations: +# +# - Several strategies are provided to fetch modifications from the +# wiki, but no automatic heuristics is provided, the user has +# to understand and chose which strategy is appropriate for him. +# +# - Git renames could be turned into MediaWiki renames (see TODO +# below) +# +# - No way to import "one page, and all pages included in it" +# +# - Multiple remote MediaWikis have not been very well tested. + +use strict; +use MediaWiki::API; +use DateTime::Format::ISO8601; + +# By default, use UTF-8 to communicate with Git and the user +binmode STDERR, ":utf8"; +binmode STDOUT, ":utf8"; + +use URI::Escape; +use IPC::Open2; + +use warnings; + +# Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced +use constant SLASH_REPLACEMENT => "%2F"; + +# It's not always possible to delete pages (may require some +# priviledges). Deleted pages are replaced with this content. +use constant DELETED_CONTENT => "[[Category:Deleted]]\n"; + +# It's not possible to create empty pages. New empty files in Git are +# sent with this content instead. +use constant EMPTY_CONTENT => "<!-- empty page -->\n"; + +# used to reflect file creation or deletion in diff. +use constant NULL_SHA1 => "0000000000000000000000000000000000000000"; + +my $remotename = $ARGV[0]; +my $url = $ARGV[1]; + +# Accept both space-separated and multiple keys in config file. +# Spaces should be written as _ anyway because we'll use chomp. +my @tracked_pages = split(/[ \n]/, run_git("config --get-all remote.". $remotename .".pages")); +chomp(@tracked_pages); + +# Just like @tracked_pages, but for MediaWiki categories. +my @tracked_categories = split(/[ \n]/, run_git("config --get-all remote.". $remotename .".categories")); +chomp(@tracked_categories); + +# Import media files too. +my $import_media = run_git("config --get --bool remote.". $remotename .".mediaimport"); +chomp($import_media); +$import_media = ($import_media eq "true"); + +my $wiki_login = run_git("config --get remote.". $remotename .".mwLogin"); +# Note: mwPassword is discourraged. Use the credential system instead. +my $wiki_passwd = run_git("config --get remote.". $remotename .".mwPassword"); +my $wiki_domain = run_git("config --get remote.". $remotename .".mwDomain"); +chomp($wiki_login); +chomp($wiki_passwd); +chomp($wiki_domain); + +# Import only last revisions (both for clone and fetch) +my $shallow_import = run_git("config --get --bool remote.". $remotename .".shallow"); +chomp($shallow_import); +$shallow_import = ($shallow_import eq "true"); + +# Fetch (clone and pull) by revisions instead of by pages. This behavior +# is more efficient when we have a wiki with lots of pages and we fetch +# the revisions quite often so that they concern only few pages. +# Possible values: +# - by_rev: perform one query per new revision on the remote wiki +# - by_page: query each tracked page for new revision +my $fetch_strategy = run_git("config --get remote.$remotename.fetchStrategy"); +unless ($fetch_strategy) { + $fetch_strategy = run_git("config --get mediawiki.fetchStrategy"); +} +chomp($fetch_strategy); +unless ($fetch_strategy) { + $fetch_strategy = "by_page"; +} + +# Dumb push: don't update notes and mediawiki ref to reflect the last push. +# +# Configurable with mediawiki.dumbPush, or per-remote with +# remote.<remotename>.dumbPush. +# +# This means the user will have to re-import the just-pushed +# revisions. On the other hand, this means that the Git revisions +# corresponding to MediaWiki revisions are all imported from the wiki, +# regardless of whether they were initially created in Git or from the +# web interface, hence all users will get the same history (i.e. if +# the push from Git to MediaWiki loses some information, everybody +# will get the history with information lost). If the import is +# deterministic, this means everybody gets the same sha1 for each +# MediaWiki revision. +my $dumb_push = run_git("config --get --bool remote.$remotename.dumbPush"); +unless ($dumb_push) { + $dumb_push = run_git("config --get --bool mediawiki.dumbPush"); +} +chomp($dumb_push); +$dumb_push = ($dumb_push eq "true"); + +my $wiki_name = $url; +$wiki_name =~ s/[^\/]*:\/\///; +# If URL is like http://user:password@example.com/, we clearly don't +# want the password in $wiki_name. While we're there, also remove user +# and '@' sign, to avoid author like MWUser@HTTPUser@host.com +$wiki_name =~ s/^.*@//; + +# Commands parser +my $entry; +my @cmd; +while (<STDIN>) { + chomp; + @cmd = split(/ /); + if (defined($cmd[0])) { + # Line not blank + if ($cmd[0] eq "capabilities") { + die("Too many arguments for capabilities") unless (!defined($cmd[1])); + mw_capabilities(); + } elsif ($cmd[0] eq "list") { + die("Too many arguments for list") unless (!defined($cmd[2])); + mw_list($cmd[1]); + } elsif ($cmd[0] eq "import") { + die("Invalid arguments for import") unless ($cmd[1] ne "" && !defined($cmd[2])); + mw_import($cmd[1]); + } elsif ($cmd[0] eq "option") { + die("Too many arguments for option") unless ($cmd[1] ne "" && $cmd[2] ne "" && !defined($cmd[3])); + mw_option($cmd[1],$cmd[2]); + } elsif ($cmd[0] eq "push") { + mw_push($cmd[1]); + } else { + print STDERR "Unknown command. Aborting...\n"; + last; + } + } else { + # blank line: we should terminate + last; + } + + BEGIN { $| = 1 } # flush STDOUT, to make sure the previous + # command is fully processed. +} + +########################## Functions ############################## + +## credential API management (generic functions) + +sub credential_from_url { + my $url = shift; + my $parsed = URI->new($url); + my %credential; + + if ($parsed->scheme) { + $credential{protocol} = $parsed->scheme; + } + if ($parsed->host) { + $credential{host} = $parsed->host; + } + if ($parsed->path) { + $credential{path} = $parsed->path; + } + if ($parsed->userinfo) { + if ($parsed->userinfo =~ /([^:]*):(.*)/) { + $credential{username} = $1; + $credential{password} = $2; + } else { + $credential{username} = $parsed->userinfo; + } + } + + return %credential; +} + +sub credential_read { + my %credential; + my $reader = shift; + my $op = shift; + while (<$reader>) { + my ($key, $value) = /([^=]*)=(.*)/; + if (not defined $key) { + die "ERROR receiving response from git credential $op:\n$_\n"; + } + $credential{$key} = $value; + } + return %credential; +} + +sub credential_write { + my $credential = shift; + my $writer = shift; + while (my ($key, $value) = each(%$credential) ) { + if ($value) { + print $writer "$key=$value\n"; + } + } +} + +sub credential_run { + my $op = shift; + my $credential = shift; + my $pid = open2(my $reader, my $writer, "git credential $op"); + credential_write($credential, $writer); + print $writer "\n"; + close($writer); + + if ($op eq "fill") { + %$credential = credential_read($reader, $op); + } else { + if (<$reader>) { + die "ERROR while running git credential $op:\n$_"; + } + } + close($reader); + waitpid($pid, 0); + my $child_exit_status = $? >> 8; + if ($child_exit_status != 0) { + die "'git credential $op' failed with code $child_exit_status."; + } +} + +# MediaWiki API instance, created lazily. +my $mediawiki; + +sub mw_connect_maybe { + if ($mediawiki) { + return; + } + $mediawiki = MediaWiki::API->new; + $mediawiki->{config}->{api_url} = "$url/api.php"; + if ($wiki_login) { + my %credential = credential_from_url($url); + $credential{username} = $wiki_login; + $credential{password} = $wiki_passwd; + credential_run("fill", \%credential); + my $request = {lgname => $credential{username}, + lgpassword => $credential{password}, + lgdomain => $wiki_domain}; + if ($mediawiki->login($request)) { + credential_run("approve", \%credential); + print STDERR "Logged in mediawiki user \"$credential{username}\".\n"; + } else { + print STDERR "Failed to log in mediawiki user \"$credential{username}\" on $url\n"; + print STDERR " (error " . + $mediawiki->{error}->{code} . ': ' . + $mediawiki->{error}->{details} . ")\n"; + credential_run("reject", \%credential); + exit 1; + } + } +} + +## Functions for listing pages on the remote wiki +sub get_mw_tracked_pages { + my $pages = shift; + get_mw_page_list(\@tracked_pages, $pages); +} + +sub get_mw_page_list { + my $page_list = shift; + my $pages = shift; + my @some_pages = @$page_list; + while (@some_pages) { + my $last = 50; + if ($#some_pages < $last) { + $last = $#some_pages; + } + my @slice = @some_pages[0..$last]; + get_mw_first_pages(\@slice, $pages); + @some_pages = @some_pages[51..$#some_pages]; + } +} + +sub get_mw_tracked_categories { + my $pages = shift; + foreach my $category (@tracked_categories) { + if (index($category, ':') < 0) { + # Mediawiki requires the Category + # prefix, but let's not force the user + # to specify it. + $category = "Category:" . $category; + } + my $mw_pages = $mediawiki->list( { + action => 'query', + list => 'categorymembers', + cmtitle => $category, + cmlimit => 'max' } ) + || die $mediawiki->{error}->{code} . ': ' + . $mediawiki->{error}->{details}; + foreach my $page (@{$mw_pages}) { + $pages->{$page->{title}} = $page; + } + } +} + +sub get_mw_all_pages { + my $pages = shift; + # No user-provided list, get the list of pages from the API. + my $mw_pages = $mediawiki->list({ + action => 'query', + list => 'allpages', + aplimit => 'max' + }); + if (!defined($mw_pages)) { + print STDERR "fatal: could not get the list of wiki pages.\n"; + print STDERR "fatal: '$url' does not appear to be a mediawiki\n"; + print STDERR "fatal: make sure '$url/api.php' is a valid page.\n"; + exit 1; + } + foreach my $page (@{$mw_pages}) { + $pages->{$page->{title}} = $page; + } +} + +# queries the wiki for a set of pages. Meant to be used within a loop +# querying the wiki for slices of page list. +sub get_mw_first_pages { + my $some_pages = shift; + my @some_pages = @{$some_pages}; + + my $pages = shift; + + # pattern 'page1|page2|...' required by the API + my $titles = join('|', @some_pages); + + my $mw_pages = $mediawiki->api({ + action => 'query', + titles => $titles, + }); + if (!defined($mw_pages)) { + print STDERR "fatal: could not query the list of wiki pages.\n"; + print STDERR "fatal: '$url' does not appear to be a mediawiki\n"; + print STDERR "fatal: make sure '$url/api.php' is a valid page.\n"; + exit 1; + } + while (my ($id, $page) = each(%{$mw_pages->{query}->{pages}})) { + if ($id < 0) { + print STDERR "Warning: page $page->{title} not found on wiki\n"; + } else { + $pages->{$page->{title}} = $page; + } + } +} + +# Get the list of pages to be fetched according to configuration. +sub get_mw_pages { + mw_connect_maybe(); + + my %pages; # hash on page titles to avoid duplicates + my $user_defined; + if (@tracked_pages) { + $user_defined = 1; + # The user provided a list of pages titles, but we + # still need to query the API to get the page IDs. + get_mw_tracked_pages(\%pages); + } + if (@tracked_categories) { + $user_defined = 1; + get_mw_tracked_categories(\%pages); + } + if (!$user_defined) { + get_mw_all_pages(\%pages); + } + if ($import_media) { + print STDERR "Getting media files for selected pages...\n"; + if ($user_defined) { + get_linked_mediafiles(\%pages); + } else { + get_all_mediafiles(\%pages); + } + } + return %pages; +} + +# usage: $out = run_git("command args"); +# $out = run_git("command args", "raw"); # don't interpret output as UTF-8. +sub run_git { + my $args = shift; + my $encoding = (shift || "encoding(UTF-8)"); + open(my $git, "-|:$encoding", "git " . $args); + my $res = do { local $/; <$git> }; + close($git); + + return $res; +} + + +sub get_all_mediafiles { + my $pages = shift; + # Attach list of all pages for media files from the API, + # they are in a different namespace, only one namespace + # can be queried at the same moment + my $mw_pages = $mediawiki->list({ + action => 'query', + list => 'allpages', + apnamespace => get_mw_namespace_id("File"), + aplimit => 'max' + }); + if (!defined($mw_pages)) { + print STDERR "fatal: could not get the list of pages for media files.\n"; + print STDERR "fatal: '$url' does not appear to be a mediawiki\n"; + print STDERR "fatal: make sure '$url/api.php' is a valid page.\n"; + exit 1; + } + foreach my $page (@{$mw_pages}) { + $pages->{$page->{title}} = $page; + } +} + +sub get_linked_mediafiles { + my $pages = shift; + my @titles = map $_->{title}, values(%{$pages}); + + # The query is split in small batches because of the MW API limit of + # the number of links to be returned (500 links max). + my $batch = 10; + while (@titles) { + if ($#titles < $batch) { + $batch = $#titles; + } + my @slice = @titles[0..$batch]; + + # pattern 'page1|page2|...' required by the API + my $mw_titles = join('|', @slice); + + # Media files could be included or linked from + # a page, get all related + my $query = { + action => 'query', + prop => 'links|images', + titles => $mw_titles, + plnamespace => get_mw_namespace_id("File"), + pllimit => 'max' + }; + my $result = $mediawiki->api($query); + + while (my ($id, $page) = each(%{$result->{query}->{pages}})) { + my @media_titles; + if (defined($page->{links})) { + my @link_titles = map $_->{title}, @{$page->{links}}; + push(@media_titles, @link_titles); + } + if (defined($page->{images})) { + my @image_titles = map $_->{title}, @{$page->{images}}; + push(@media_titles, @image_titles); + } + if (@media_titles) { + get_mw_page_list(\@media_titles, $pages); + } + } + + @titles = @titles[($batch+1)..$#titles]; + } +} + +sub get_mw_mediafile_for_page_revision { + # Name of the file on Wiki, with the prefix. + my $filename = shift; + my $timestamp = shift; + my %mediafile; + + # Search if on a media file with given timestamp exists on + # MediaWiki. In that case download the file. + my $query = { + action => 'query', + prop => 'imageinfo', + titles => "File:" . $filename, + iistart => $timestamp, + iiend => $timestamp, + iiprop => 'timestamp|archivename|url', + iilimit => 1 + }; + my $result = $mediawiki->api($query); + + my ($fileid, $file) = each( %{$result->{query}->{pages}} ); + # If not defined it means there is no revision of the file for + # given timestamp. + if (defined($file->{imageinfo})) { + $mediafile{title} = $filename; + + my $fileinfo = pop(@{$file->{imageinfo}}); + $mediafile{timestamp} = $fileinfo->{timestamp}; + # Mediawiki::API's download function doesn't support https URLs + # and can't download old versions of files. + print STDERR "\tDownloading file $mediafile{title}, version $mediafile{timestamp}\n"; + $mediafile{content} = download_mw_mediafile($fileinfo->{url}); + } + return %mediafile; +} + +sub download_mw_mediafile { + my $url = shift; + + my $response = $mediawiki->{ua}->get($url); + if ($response->code == 200) { + return $response->decoded_content; + } else { + print STDERR "Error downloading mediafile from :\n"; + print STDERR "URL: $url\n"; + print STDERR "Server response: " . $response->code . " " . $response->message . "\n"; + exit 1; + } +} + +sub get_last_local_revision { + # Get note regarding last mediawiki revision + my $note = run_git("notes --ref=$remotename/mediawiki show refs/mediawiki/$remotename/master 2>/dev/null"); + my @note_info = split(/ /, $note); + + my $lastrevision_number; + if (!(defined($note_info[0]) && $note_info[0] eq "mediawiki_revision:")) { + print STDERR "No previous mediawiki revision found"; + $lastrevision_number = 0; + } else { + # Notes are formatted : mediawiki_revision: #number + $lastrevision_number = $note_info[1]; + chomp($lastrevision_number); + print STDERR "Last local mediawiki revision found is $lastrevision_number"; + } + return $lastrevision_number; +} + +# Remember the timestamp corresponding to a revision id. +my %basetimestamps; + +# Get the last remote revision without taking in account which pages are +# tracked or not. This function makes a single request to the wiki thus +# avoid a loop onto all tracked pages. This is useful for the fetch-by-rev +# option. +sub get_last_global_remote_rev { + mw_connect_maybe(); + + my $query = { + action => 'query', + list => 'recentchanges', + prop => 'revisions', + rclimit => '1', + rcdir => 'older', + }; + my $result = $mediawiki->api($query); + return $result->{query}->{recentchanges}[0]->{revid}; +} + +# Get the last remote revision concerning the tracked pages and the tracked +# categories. +sub get_last_remote_revision { + mw_connect_maybe(); + + my %pages_hash = get_mw_pages(); + my @pages = values(%pages_hash); + + my $max_rev_num = 0; + + foreach my $page (@pages) { + my $id = $page->{pageid}; + + my $query = { + action => 'query', + prop => 'revisions', + rvprop => 'ids|timestamp', + pageids => $id, + }; + + my $result = $mediawiki->api($query); + + my $lastrev = pop(@{$result->{query}->{pages}->{$id}->{revisions}}); + + $basetimestamps{$lastrev->{revid}} = $lastrev->{timestamp}; + + $max_rev_num = ($lastrev->{revid} > $max_rev_num ? $lastrev->{revid} : $max_rev_num); + } + + print STDERR "Last remote revision found is $max_rev_num.\n"; + return $max_rev_num; +} + +# Clean content before sending it to MediaWiki +sub mediawiki_clean { + my $string = shift; + my $page_created = shift; + # Mediawiki does not allow blank space at the end of a page and ends with a single \n. + # This function right trims a string and adds a \n at the end to follow this rule + $string =~ s/\s+$//; + if ($string eq "" && $page_created) { + # Creating empty pages is forbidden. + $string = EMPTY_CONTENT; + } + return $string."\n"; +} + +# Filter applied on MediaWiki data before adding them to Git +sub mediawiki_smudge { + my $string = shift; + if ($string eq EMPTY_CONTENT) { + $string = ""; + } + # This \n is important. This is due to mediawiki's way to handle end of files. + return $string."\n"; +} + +sub mediawiki_clean_filename { + my $filename = shift; + $filename =~ s/@{[SLASH_REPLACEMENT]}/\//g; + # [, ], |, {, and } are forbidden by MediaWiki, even URL-encoded. + # Do a variant of URL-encoding, i.e. looks like URL-encoding, + # but with _ added to prevent MediaWiki from thinking this is + # an actual special character. + $filename =~ s/[\[\]\{\}\|]/sprintf("_%%_%x", ord($&))/ge; + # If we use the uri escape before + # we should unescape here, before anything + + return $filename; +} + +sub mediawiki_smudge_filename { + my $filename = shift; + $filename =~ s/\//@{[SLASH_REPLACEMENT]}/g; + $filename =~ s/ /_/g; + # Decode forbidden characters encoded in mediawiki_clean_filename + $filename =~ s/_%_([0-9a-fA-F][0-9a-fA-F])/sprintf("%c", hex($1))/ge; + return $filename; +} + +sub literal_data { + my ($content) = @_; + print STDOUT "data ", bytes::length($content), "\n", $content; +} + +sub literal_data_raw { + # Output possibly binary content. + my ($content) = @_; + # Avoid confusion between size in bytes and in characters + utf8::downgrade($content); + binmode STDOUT, ":raw"; + print STDOUT "data ", bytes::length($content), "\n", $content; + binmode STDOUT, ":utf8"; +} + +sub mw_capabilities { + # Revisions are imported to the private namespace + # refs/mediawiki/$remotename/ by the helper and fetched into + # refs/remotes/$remotename later by fetch. + print STDOUT "refspec refs/heads/*:refs/mediawiki/$remotename/*\n"; + print STDOUT "import\n"; + print STDOUT "list\n"; + print STDOUT "push\n"; + print STDOUT "\n"; +} + +sub mw_list { + # MediaWiki do not have branches, we consider one branch arbitrarily + # called master, and HEAD pointing to it. + print STDOUT "? refs/heads/master\n"; + print STDOUT "\@refs/heads/master HEAD\n"; + print STDOUT "\n"; +} + +sub mw_option { + print STDERR "remote-helper command 'option $_[0]' not yet implemented\n"; + print STDOUT "unsupported\n"; +} + +sub fetch_mw_revisions_for_page { + my $page = shift; + my $id = shift; + my $fetch_from = shift; + my @page_revs = (); + my $query = { + action => 'query', + prop => 'revisions', + rvprop => 'ids', + rvdir => 'newer', + rvstartid => $fetch_from, + rvlimit => 500, + pageids => $id, + }; + + my $revnum = 0; + # Get 500 revisions at a time due to the mediawiki api limit + while (1) { + my $result = $mediawiki->api($query); + + # Parse each of those 500 revisions + foreach my $revision (@{$result->{query}->{pages}->{$id}->{revisions}}) { + my $page_rev_ids; + $page_rev_ids->{pageid} = $page->{pageid}; + $page_rev_ids->{revid} = $revision->{revid}; + push(@page_revs, $page_rev_ids); + $revnum++; + } + last unless $result->{'query-continue'}; + $query->{rvstartid} = $result->{'query-continue'}->{revisions}->{rvstartid}; + } + if ($shallow_import && @page_revs) { + print STDERR " Found 1 revision (shallow import).\n"; + @page_revs = sort {$b->{revid} <=> $a->{revid}} (@page_revs); + return $page_revs[0]; + } + print STDERR " Found ", $revnum, " revision(s).\n"; + return @page_revs; +} + +sub fetch_mw_revisions { + my $pages = shift; my @pages = @{$pages}; + my $fetch_from = shift; + + my @revisions = (); + my $n = 1; + foreach my $page (@pages) { + my $id = $page->{pageid}; + + print STDERR "page $n/", scalar(@pages), ": ". $page->{title} ."\n"; + $n++; + my @page_revs = fetch_mw_revisions_for_page($page, $id, $fetch_from); + @revisions = (@page_revs, @revisions); + } + + return ($n, @revisions); +} + +sub import_file_revision { + my $commit = shift; + my %commit = %{$commit}; + my $full_import = shift; + my $n = shift; + my $mediafile = shift; + my %mediafile; + if ($mediafile) { + %mediafile = %{$mediafile}; + } + + my $title = $commit{title}; + my $comment = $commit{comment}; + my $content = $commit{content}; + my $author = $commit{author}; + my $date = $commit{date}; + + print STDOUT "commit refs/mediawiki/$remotename/master\n"; + print STDOUT "mark :$n\n"; + print STDOUT "committer $author <$author\@$wiki_name> ", $date->epoch, " +0000\n"; + literal_data($comment); + + # If it's not a clone, we need to know where to start from + if (!$full_import && $n == 1) { + print STDOUT "from refs/mediawiki/$remotename/master^0\n"; + } + if ($content ne DELETED_CONTENT) { + print STDOUT "M 644 inline $title.mw\n"; + literal_data($content); + if (%mediafile) { + print STDOUT "M 644 inline $mediafile{title}\n"; + literal_data_raw($mediafile{content}); + } + print STDOUT "\n\n"; + } else { + print STDOUT "D $title.mw\n"; + } + + # mediawiki revision number in the git note + if ($full_import && $n == 1) { + print STDOUT "reset refs/notes/$remotename/mediawiki\n"; + } + print STDOUT "commit refs/notes/$remotename/mediawiki\n"; + print STDOUT "committer $author <$author\@$wiki_name> ", $date->epoch, " +0000\n"; + literal_data("Note added by git-mediawiki during import"); + if (!$full_import && $n == 1) { + print STDOUT "from refs/notes/$remotename/mediawiki^0\n"; + } + print STDOUT "N inline :$n\n"; + literal_data("mediawiki_revision: " . $commit{mw_revision}); + print STDOUT "\n\n"; +} + +# parse a sequence of +# <cmd> <arg1> +# <cmd> <arg2> +# \n +# (like batch sequence of import and sequence of push statements) +sub get_more_refs { + my $cmd = shift; + my @refs; + while (1) { + my $line = <STDIN>; + if ($line =~ m/^$cmd (.*)$/) { + push(@refs, $1); + } elsif ($line eq "\n") { + return @refs; + } else { + die("Invalid command in a '$cmd' batch: ". $_); + } + } +} + +sub mw_import { + # multiple import commands can follow each other. + my @refs = (shift, get_more_refs("import")); + foreach my $ref (@refs) { + mw_import_ref($ref); + } + print STDOUT "done\n"; +} + +sub mw_import_ref { + my $ref = shift; + # The remote helper will call "import HEAD" and + # "import refs/heads/master". + # Since HEAD is a symbolic ref to master (by convention, + # followed by the output of the command "list" that we gave), + # we don't need to do anything in this case. + if ($ref eq "HEAD") { + return; + } + + mw_connect_maybe(); + + print STDERR "Searching revisions...\n"; + my $last_local = get_last_local_revision(); + my $fetch_from = $last_local + 1; + if ($fetch_from == 1) { + print STDERR ", fetching from beginning.\n"; + } else { + print STDERR ", fetching from here.\n"; + } + + my $n = 0; + if ($fetch_strategy eq "by_rev") { + print STDERR "Fetching & writing export data by revs...\n"; + $n = mw_import_ref_by_revs($fetch_from); + } elsif ($fetch_strategy eq "by_page") { + print STDERR "Fetching & writing export data by pages...\n"; + $n = mw_import_ref_by_pages($fetch_from); + } else { + print STDERR "fatal: invalid fetch strategy \"$fetch_strategy\".\n"; + print STDERR "Check your configuration variables remote.$remotename.fetchStrategy and mediawiki.fetchStrategy\n"; + exit 1; + } + + if ($fetch_from == 1 && $n == 0) { + print STDERR "You appear to have cloned an empty MediaWiki.\n"; + # Something has to be done remote-helper side. If nothing is done, an error is + # thrown saying that HEAD is refering to unknown object 0000000000000000000 + # and the clone fails. + } +} + +sub mw_import_ref_by_pages { + + my $fetch_from = shift; + my %pages_hash = get_mw_pages(); + my @pages = values(%pages_hash); + + my ($n, @revisions) = fetch_mw_revisions(\@pages, $fetch_from); + + @revisions = sort {$a->{revid} <=> $b->{revid}} @revisions; + my @revision_ids = map $_->{revid}, @revisions; + + return mw_import_revids($fetch_from, \@revision_ids, \%pages_hash); +} + +sub mw_import_ref_by_revs { + + my $fetch_from = shift; + my %pages_hash = get_mw_pages(); + + my $last_remote = get_last_global_remote_rev(); + my @revision_ids = $fetch_from..$last_remote; + return mw_import_revids($fetch_from, \@revision_ids, \%pages_hash); +} + +# Import revisions given in second argument (array of integers). +# Only pages appearing in the third argument (hash indexed by page titles) +# will be imported. +sub mw_import_revids { + my $fetch_from = shift; + my $revision_ids = shift; + my $pages = shift; + + my $n = 0; + my $n_actual = 0; + my $last_timestamp = 0; # Placeholer in case $rev->timestamp is undefined + + foreach my $pagerevid (@$revision_ids) { + # fetch the content of the pages + my $query = { + action => 'query', + prop => 'revisions', + rvprop => 'content|timestamp|comment|user|ids', + revids => $pagerevid, + }; + + my $result = $mediawiki->api($query); + + if (!$result) { + die "Failed to retrieve modified page for revision $pagerevid"; + } + + if (!defined($result->{query}->{pages})) { + die "Invalid revision $pagerevid."; + } + + my @result_pages = values(%{$result->{query}->{pages}}); + my $result_page = $result_pages[0]; + my $rev = $result_pages[0]->{revisions}->[0]; + + # Count page even if we skip it, since we display + # $n/$total and $total includes skipped pages. + $n++; + + my $page_title = $result_page->{title}; + + if (!exists($pages->{$page_title})) { + print STDERR "$n/", scalar(@$revision_ids), + ": Skipping revision #$rev->{revid} of $page_title\n"; + next; + } + + $n_actual++; + + my %commit; + $commit{author} = $rev->{user} || 'Anonymous'; + $commit{comment} = $rev->{comment} || '*Empty MediaWiki Message*'; + $commit{title} = mediawiki_smudge_filename($page_title); + $commit{mw_revision} = $rev->{revid}; + $commit{content} = mediawiki_smudge($rev->{'*'}); + + if (!defined($rev->{timestamp})) { + $last_timestamp++; + } else { + $last_timestamp = $rev->{timestamp}; + } + $commit{date} = DateTime::Format::ISO8601->parse_datetime($last_timestamp); + + # Differentiates classic pages and media files. + my ($namespace, $filename) = $page_title =~ /^([^:]*):(.*)$/; + my %mediafile; + if ($namespace && get_mw_namespace_id($namespace) == get_mw_namespace_id("File")) { + %mediafile = get_mw_mediafile_for_page_revision($filename, $rev->{timestamp}); + } + # If this is a revision of the media page for new version + # of a file do one common commit for both file and media page. + # Else do commit only for that page. + print STDERR "$n/", scalar(@$revision_ids), ": Revision #$rev->{revid} of $commit{title}\n"; + import_file_revision(\%commit, ($fetch_from == 1), $n_actual, \%mediafile); + } + + return $n_actual; +} + +sub error_non_fast_forward { + my $advice = run_git("config --bool advice.pushNonFastForward"); + chomp($advice); + if ($advice ne "false") { + # Native git-push would show this after the summary. + # We can't ask it to display it cleanly, so print it + # ourselves before. + print STDERR "To prevent you from losing history, non-fast-forward updates were rejected\n"; + print STDERR "Merge the remote changes (e.g. 'git pull') before pushing again. See the\n"; + print STDERR "'Note about fast-forwards' section of 'git push --help' for details.\n"; + } + print STDOUT "error $_[0] \"non-fast-forward\"\n"; + return 0; +} + +sub mw_upload_file { + my $complete_file_name = shift; + my $new_sha1 = shift; + my $extension = shift; + my $file_deleted = shift; + my $summary = shift; + my $newrevid; + my $path = "File:" . $complete_file_name; + my %hashFiles = get_allowed_file_extensions(); + if (!exists($hashFiles{$extension})) { + print STDERR "$complete_file_name is not a permitted file on this wiki.\n"; + print STDERR "Check the configuration of file uploads in your mediawiki.\n"; + return $newrevid; + } + # Deleting and uploading a file requires a priviledged user + if ($file_deleted) { + mw_connect_maybe(); + my $query = { + action => 'delete', + title => $path, + reason => $summary + }; + if (!$mediawiki->edit($query)) { + print STDERR "Failed to delete file on remote wiki\n"; + print STDERR "Check your permissions on the remote site. Error code:\n"; + print STDERR $mediawiki->{error}->{code} . ':' . $mediawiki->{error}->{details}; + exit 1; + } + } else { + # Don't let perl try to interpret file content as UTF-8 => use "raw" + my $content = run_git("cat-file blob $new_sha1", "raw"); + if ($content ne "") { + mw_connect_maybe(); + $mediawiki->{config}->{upload_url} = + "$url/index.php/Special:Upload"; + $mediawiki->edit({ + action => 'upload', + filename => $complete_file_name, + comment => $summary, + file => [undef, + $complete_file_name, + Content => $content], + ignorewarnings => 1, + }, { + skip_encoding => 1 + } ) || die $mediawiki->{error}->{code} . ':' + . $mediawiki->{error}->{details}; + my $last_file_page = $mediawiki->get_page({title => $path}); + $newrevid = $last_file_page->{revid}; + print STDERR "Pushed file: $new_sha1 - $complete_file_name.\n"; + } else { + print STDERR "Empty file $complete_file_name not pushed.\n"; + } + } + return $newrevid; +} + +sub mw_push_file { + my $diff_info = shift; + # $diff_info contains a string in this format: + # 100644 100644 <sha1_of_blob_before_commit> <sha1_of_blob_now> <status> + my @diff_info_split = split(/[ \t]/, $diff_info); + + # Filename, including .mw extension + my $complete_file_name = shift; + # Commit message + my $summary = shift; + # MediaWiki revision number. Keep the previous one by default, + # in case there's no edit to perform. + my $oldrevid = shift; + my $newrevid; + + my $new_sha1 = $diff_info_split[3]; + my $old_sha1 = $diff_info_split[2]; + my $page_created = ($old_sha1 eq NULL_SHA1); + my $page_deleted = ($new_sha1 eq NULL_SHA1); + $complete_file_name = mediawiki_clean_filename($complete_file_name); + + my ($title, $extension) = $complete_file_name =~ /^(.*)\.([^\.]*)$/; + if (!defined($extension)) { + $extension = ""; + } + if ($extension eq "mw") { + my $file_content; + if ($page_deleted) { + # Deleting a page usually requires + # special priviledges. A common + # convention is to replace the page + # with this content instead: + $file_content = DELETED_CONTENT; + } else { + $file_content = run_git("cat-file blob $new_sha1"); + } + + mw_connect_maybe(); + + my $result = $mediawiki->edit( { + action => 'edit', + summary => $summary, + title => $title, + basetimestamp => $basetimestamps{$oldrevid}, + text => mediawiki_clean($file_content, $page_created), + }, { + skip_encoding => 1 # Helps with names with accentuated characters + }); + if (!$result) { + if ($mediawiki->{error}->{code} == 3) { + # edit conflicts, considered as non-fast-forward + print STDERR 'Warning: Error ' . + $mediawiki->{error}->{code} . + ' from mediwiki: ' . $mediawiki->{error}->{details} . + ".\n"; + return ($oldrevid, "non-fast-forward"); + } else { + # Other errors. Shouldn't happen => just die() + die 'Fatal: Error ' . + $mediawiki->{error}->{code} . + ' from mediwiki: ' . $mediawiki->{error}->{details}; + } + } + $newrevid = $result->{edit}->{newrevid}; + print STDERR "Pushed file: $new_sha1 - $title\n"; + } else { + $newrevid = mw_upload_file($complete_file_name, $new_sha1, + $extension, $page_deleted, + $summary); + } + $newrevid = ($newrevid or $oldrevid); + return ($newrevid, "ok"); +} + +sub mw_push { + # multiple push statements can follow each other + my @refsspecs = (shift, get_more_refs("push")); + my $pushed; + for my $refspec (@refsspecs) { + my ($force, $local, $remote) = $refspec =~ /^(\+)?([^:]*):([^:]*)$/ + or die("Invalid refspec for push. Expected <src>:<dst> or +<src>:<dst>"); + if ($force) { + print STDERR "Warning: forced push not allowed on a MediaWiki.\n"; + } + if ($local eq "") { + print STDERR "Cannot delete remote branch on a MediaWiki\n"; + print STDOUT "error $remote cannot delete\n"; + next; + } + if ($remote ne "refs/heads/master") { + print STDERR "Only push to the branch 'master' is supported on a MediaWiki\n"; + print STDOUT "error $remote only master allowed\n"; + next; + } + if (mw_push_revision($local, $remote)) { + $pushed = 1; + } + } + + # Notify Git that the push is done + print STDOUT "\n"; + + if ($pushed && $dumb_push) { + print STDERR "Just pushed some revisions to MediaWiki.\n"; + print STDERR "The pushed revisions now have to be re-imported, and your current branch\n"; + print STDERR "needs to be updated with these re-imported commits. You can do this with\n"; + print STDERR "\n"; + print STDERR " git pull --rebase\n"; + print STDERR "\n"; + } +} + +sub mw_push_revision { + my $local = shift; + my $remote = shift; # actually, this has to be "refs/heads/master" at this point. + my $last_local_revid = get_last_local_revision(); + print STDERR ".\n"; # Finish sentence started by get_last_local_revision() + my $last_remote_revid = get_last_remote_revision(); + my $mw_revision = $last_remote_revid; + + # Get sha1 of commit pointed by local HEAD + my $HEAD_sha1 = run_git("rev-parse $local 2>/dev/null"); chomp($HEAD_sha1); + # Get sha1 of commit pointed by remotes/$remotename/master + my $remoteorigin_sha1 = run_git("rev-parse refs/remotes/$remotename/master 2>/dev/null"); + chomp($remoteorigin_sha1); + + if ($last_local_revid > 0 && + $last_local_revid < $last_remote_revid) { + return error_non_fast_forward($remote); + } + + if ($HEAD_sha1 eq $remoteorigin_sha1) { + # nothing to push + return 0; + } + + # Get every commit in between HEAD and refs/remotes/origin/master, + # including HEAD and refs/remotes/origin/master + my @commit_pairs = (); + if ($last_local_revid > 0) { + my $parsed_sha1 = $remoteorigin_sha1; + # Find a path from last MediaWiki commit to pushed commit + while ($parsed_sha1 ne $HEAD_sha1) { + my @commit_info = grep(/^$parsed_sha1/, split(/\n/, run_git("rev-list --children $local"))); + if (!@commit_info) { + return error_non_fast_forward($remote); + } + my @commit_info_split = split(/ |\n/, $commit_info[0]); + # $commit_info_split[1] is the sha1 of the commit to export + # $commit_info_split[0] is the sha1 of its direct child + push(@commit_pairs, \@commit_info_split); + $parsed_sha1 = $commit_info_split[1]; + } + } else { + # No remote mediawiki revision. Export the whole + # history (linearized with --first-parent) + print STDERR "Warning: no common ancestor, pushing complete history\n"; + my $history = run_git("rev-list --first-parent --children $local"); + my @history = split('\n', $history); + @history = @history[1..$#history]; + foreach my $line (reverse @history) { + my @commit_info_split = split(/ |\n/, $line); + push(@commit_pairs, \@commit_info_split); + } + } + + foreach my $commit_info_split (@commit_pairs) { + my $sha1_child = @{$commit_info_split}[0]; + my $sha1_commit = @{$commit_info_split}[1]; + my $diff_infos = run_git("diff-tree -r --raw -z $sha1_child $sha1_commit"); + # TODO: we could detect rename, and encode them with a #redirect on the wiki. + # TODO: for now, it's just a delete+add + my @diff_info_list = split(/\0/, $diff_infos); + # Keep the subject line of the commit message as mediawiki comment for the revision + my $commit_msg = run_git("log --no-walk --format=\"%s\" $sha1_commit"); + chomp($commit_msg); + # Push every blob + while (@diff_info_list) { + my $status; + # git diff-tree -z gives an output like + # <metadata>\0<filename1>\0 + # <metadata>\0<filename2>\0 + # and we've split on \0. + my $info = shift(@diff_info_list); + my $file = shift(@diff_info_list); + ($mw_revision, $status) = mw_push_file($info, $file, $commit_msg, $mw_revision); + if ($status eq "non-fast-forward") { + # we may already have sent part of the + # commit to MediaWiki, but it's too + # late to cancel it. Stop the push in + # the middle, but still give an + # accurate error message. + return error_non_fast_forward($remote); + } + if ($status ne "ok") { + die("Unknown error from mw_push_file()"); + } + } + unless ($dumb_push) { + run_git("notes --ref=$remotename/mediawiki add -m \"mediawiki_revision: $mw_revision\" $sha1_commit"); + run_git("update-ref -m \"Git-MediaWiki push\" refs/mediawiki/$remotename/master $sha1_commit $sha1_child"); + } + } + + print STDOUT "ok $remote\n"; + return 1; +} + +sub get_allowed_file_extensions { + mw_connect_maybe(); + + my $query = { + action => 'query', + meta => 'siteinfo', + siprop => 'fileextensions' + }; + my $result = $mediawiki->api($query); + my @file_extensions= map $_->{ext},@{$result->{query}->{fileextensions}}; + my %hashFile = map {$_ => 1}@file_extensions; + + return %hashFile; +} + +# In memory cache for MediaWiki namespace ids. +my %namespace_id; + +# Namespaces whose id is cached in the configuration file +# (to avoid duplicates) +my %cached_mw_namespace_id; + +# Return MediaWiki id for a canonical namespace name. +# Ex.: "File", "Project". +sub get_mw_namespace_id { + mw_connect_maybe(); + my $name = shift; + + if (!exists $namespace_id{$name}) { + # Look at configuration file, if the record for that namespace is + # already cached. Namespaces are stored in form: + # "Name_of_namespace:Id_namespace", ex.: "File:6". + my @temp = split(/[ \n]/, run_git("config --get-all remote." + . $remotename .".namespaceCache")); + chomp(@temp); + foreach my $ns (@temp) { + my ($n, $id) = split(/:/, $ns); + $namespace_id{$n} = $id; + $cached_mw_namespace_id{$n} = 1; + } + } + + if (!exists $namespace_id{$name}) { + print STDERR "Namespace $name not found in cache, querying the wiki ...\n"; + # NS not found => get namespace id from MW and store it in + # configuration file. + my $query = { + action => 'query', + meta => 'siteinfo', + siprop => 'namespaces' + }; + my $result = $mediawiki->api($query); + + while (my ($id, $ns) = each(%{$result->{query}->{namespaces}})) { + if (defined($ns->{id}) && defined($ns->{canonical})) { + $namespace_id{$ns->{canonical}} = $ns->{id}; + if ($ns->{'*'}) { + # alias (e.g. french Fichier: as alias for canonical File:) + $namespace_id{$ns->{'*'}} = $ns->{id}; + } + } + } + } + + my $id = $namespace_id{$name}; + + if (defined $id) { + # Store explicitely requested namespaces on disk + if (!exists $cached_mw_namespace_id{$name}) { + run_git("config --add remote.". $remotename + .".namespaceCache \"". $name .":". $id ."\""); + $cached_mw_namespace_id{$name} = 1; + } + return $id; + } else { + die "No such namespace $name on MediaWiki."; + } +} diff --git a/contrib/mw-to-git/git-remote-mediawiki.txt b/contrib/mw-to-git/git-remote-mediawiki.txt new file mode 100644 index 0000000000..4d211f5b81 --- /dev/null +++ b/contrib/mw-to-git/git-remote-mediawiki.txt @@ -0,0 +1,7 @@ +Git-Mediawiki is a project which aims the creation of a gate +between git and mediawiki, allowing git users to push and pull +objects from mediawiki just as one would do with a classic git +repository thanks to remote-helpers. + +For more information, visit the wiki at +https://github.com/Bibzball/Git-Mediawiki/wiki diff --git a/contrib/mw-to-git/t/.gitignore b/contrib/mw-to-git/t/.gitignore new file mode 100644 index 0000000000..a7a40b4964 --- /dev/null +++ b/contrib/mw-to-git/t/.gitignore @@ -0,0 +1,4 @@ +WEB/ +wiki/ +trash directory.t*/ +test-results/ diff --git a/contrib/mw-to-git/t/Makefile b/contrib/mw-to-git/t/Makefile new file mode 100644 index 0000000000..f422203fa0 --- /dev/null +++ b/contrib/mw-to-git/t/Makefile @@ -0,0 +1,31 @@ +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +## Test git-remote-mediawiki + +all: test + +-include ../../../config.mak.autogen +-include ../../../config.mak + +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) + +.PHONY: help test clean all + +help: + @echo 'Run "$(MAKE) test" to launch test scripts' + @echo 'Run "$(MAKE) clean" to remove trash folders' + +test: + @for t in $(T); do \ + echo "$$t"; \ + "./$$t" || exit 1; \ + done + +clean: + $(RM) -r 'trash directory'.* diff --git a/contrib/mw-to-git/t/README b/contrib/mw-to-git/t/README new file mode 100644 index 0000000000..96e97390cf --- /dev/null +++ b/contrib/mw-to-git/t/README @@ -0,0 +1,124 @@ +Tests for Mediawiki-to-Git +========================== + +Introduction +------------ +This manual describes how to install the git-remote-mediawiki test +environment on a machine with git installed on it. + +Prerequisite +------------ + +In order to run this test environment correctly, you will need to +install the following packages (Debian/Ubuntu names, may need to be +adapted for another distribution): + +* lighttpd +* php5 +* php5-cgi +* php5-cli +* php5-curl +* php5-sqlite + +Principles and Technical Choices +-------------------------------- + +The test environment makes it easy to install and manipulate one or +several MediaWiki instances. To allow developers to run the testsuite +easily, the environment does not require root priviledge (except to +install the required packages if needed). It starts a webserver +instance on the user's account (using lighttpd greatly helps for +that), and does not need a separate database daemon (thanks to the use +of sqlite). + +Run the test environment +------------------------ + +Install a new wiki +~~~~~~~~~~~~~~~~~~ + +Once you have all the prerequisite, you need to install a MediaWiki +instance on your machine. If you already have one, it is still +strongly recommended to install one with the script provided. Here's +how to work it: + +a. change directory to contrib/mw-to-git/t/ +b. if needed, edit test.config to choose your installation parameters +c. run `./install-wiki.sh install` +d. check on your favourite web browser if your wiki is correctly + installed. + +Remove an existing wiki +~~~~~~~~~~~~~~~~~~~~~~~ + +Edit the file test.config to fit the wiki you want to delete, and then +execute the command `./install-wiki.sh delete` from the +contrib/mw-to-git/t directory. + +Run the existing tests +~~~~~~~~~~~~~~~~~~~~~~ + +The provided tests are currently in the `contrib/mw-to-git/t` directory. +The files are all the t936[0-9]-*.sh shell scripts. + +a. Run all tests: +To do so, run "make test" from the contrib/mw-to-git/ directory. + +b. Run a specific test: +To run a given test <test_name>, run ./<test_name> from the +contrib/mw-to-git/t directory. + +How to create new tests +----------------------- + +Available functions +~~~~~~~~~~~~~~~~~~~ + +The test environment of git-remote-mediawiki provides some functions +useful to test its behaviour. for more details about the functions' +parameters, please refer to the `test-gitmw-lib.sh` and +`test-gitmw.pl` files. + +** `test_check_wiki_precond`: +Check if the tests must be skipped or not. Please use this function +at the beggining of each new test file. + +** `wiki_getpage`: +Fetch a given page from the wiki and puts its content in the +directory in parameter. + +** `wiki_delete_page`: +Delete a given page from the wiki. + +** `wiki_edit_page`: +Create or modify a given page in the wiki. You can specify several +parameters like a summary for the page edition, or add the page to a +given category. +See test-gitmw.pl for more details. + +** `wiki_getallpage`: +Fetch all pages from the wiki into a given directory. The directory +is created if it does not exists. + +** `test_diff_directories`: +Compare the content of two directories. The content must be the same. +Use this function to compare the content of a git directory and a wiki +one created by wiki_getallpage. + +** `test_contains_N_files`: +Check if the given directory contains a given number of file. + +** `wiki_page_exists`: +Tests if a given page exists on the wiki. + +** `wiki_reset`: +Reset the wiki, i.e. flush the database. Use this function at the +begining of each new test, except if the test re-uses the same wiki +(and history) as the previous test. + +How to write a new test +~~~~~~~~~~~~~~~~~~~~~~~ + +Please, follow the standards given by git. See git/t/README. +New file should be named as t936[0-9]-*.sh. +Be sure to reset your wiki regulary with the function `wiki_reset`. diff --git a/contrib/mw-to-git/t/install-wiki.sh b/contrib/mw-to-git/t/install-wiki.sh new file mode 100755 index 0000000000..c6d6fa3aef --- /dev/null +++ b/contrib/mw-to-git/t/install-wiki.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +# This script installs or deletes a MediaWiki on your computer. +# It requires a web server with PHP and SQLite running. In addition, if you +# do not have MediaWiki sources on your computer, the option 'install' +# downloads them for you. +# Please set the CONFIGURATION VARIABLES in ./test-gitmw-lib.sh + +WIKI_TEST_DIR=$(cd "$(dirname "$0")" && pwd) + +if test -z "$WIKI_TEST_DIR" +then + WIKI_TEST_DIR=. +fi + +. "$WIKI_TEST_DIR"/test-gitmw-lib.sh +usage () { + echo "Usage: " + echo " ./install-wiki.sh <install | delete | --help>" + echo " install | -i : Install a wiki on your computer." + echo " delete | -d : Delete the wiki and all its pages and " + echo " content." +} + + +# Argument: install, delete, --help | -h +case "$1" in + "install" | "-i") + wiki_install + exit 0 + ;; + "delete" | "-d") + wiki_delete + exit 0 + ;; + "--help" | "-h") + usage + exit 0 + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; +esac diff --git a/contrib/mw-to-git/t/install-wiki/.gitignore b/contrib/mw-to-git/t/install-wiki/.gitignore new file mode 100644 index 0000000000..b5a2a4408c --- /dev/null +++ b/contrib/mw-to-git/t/install-wiki/.gitignore @@ -0,0 +1 @@ +wikidb.sqlite diff --git a/contrib/mw-to-git/t/install-wiki/LocalSettings.php b/contrib/mw-to-git/t/install-wiki/LocalSettings.php new file mode 100644 index 0000000000..29f125116b --- /dev/null +++ b/contrib/mw-to-git/t/install-wiki/LocalSettings.php @@ -0,0 +1,129 @@ +<?php +# This file was automatically generated by the MediaWiki 1.19.0 +# installer. If you make manual changes, please keep track in case you +# need to recreate them later. +# +# See includes/DefaultSettings.php for all configurable settings +# and their default values, but don't forget to make changes in _this_ +# file, not there. +# +# Further documentation for configuration settings may be found at: +# http://www.mediawiki.org/wiki/Manual:Configuration_settings + +# Protect against web entry +if ( !defined( 'MEDIAWIKI' ) ) { + exit; +} + +## Uncomment this to disable output compression +# $wgDisableOutputCompression = true; + +$wgSitename = "Git-MediaWiki-Test"; +$wgMetaNamespace = "Git-MediaWiki-Test"; + +## The URL base path to the directory containing the wiki; +## defaults for all runtime URL paths are based off of this. +## For more information on customizing the URLs please see: +## http://www.mediawiki.org/wiki/Manual:Short_URL +$wgScriptPath = "@WG_SCRIPT_PATH@"; +$wgScriptExtension = ".php"; + +## The protocol and server name to use in fully-qualified URLs +$wgServer = "@WG_SERVER@"; + +## The relative URL path to the skins directory +$wgStylePath = "$wgScriptPath/skins"; + +## The relative URL path to the logo. Make sure you change this from the default, +## or else you'll overwrite your logo when you upgrade! +$wgLogo = "$wgStylePath/common/images/wiki.png"; + +## UPO means: this is also a user preference option + +$wgEnableEmail = true; +$wgEnableUserEmail = true; # UPO + +$wgEmergencyContact = "apache@localhost"; +$wgPasswordSender = "apache@localhost"; + +$wgEnotifUserTalk = false; # UPO +$wgEnotifWatchlist = false; # UPO +$wgEmailAuthentication = true; + +## Database settings +$wgDBtype = "sqlite"; +$wgDBserver = ""; +$wgDBname = "@WG_SQLITE_DATAFILE@"; +$wgDBuser = ""; +$wgDBpassword = ""; + +# SQLite-specific settings +$wgSQLiteDataDir = "@WG_SQLITE_DATADIR@"; + + +## Shared memory settings +$wgMainCacheType = CACHE_NONE; +$wgMemCachedServers = array(); + +## To enable image uploads, make sure the 'images' directory +## is writable, then set this to true: +$wgEnableUploads = true; +$wgUseImageMagick = true; +$wgImageMagickConvertCommand ="@CONVERT@"; +$wgFileExtensions[] = 'txt'; + +# InstantCommons allows wiki to use images from http://commons.wikimedia.org +$wgUseInstantCommons = false; + +## If you use ImageMagick (or any other shell command) on a +## Linux server, this will need to be set to the name of an +## available UTF-8 locale +$wgShellLocale = "en_US.utf8"; + +## If you want to use image uploads under safe mode, +## create the directories images/archive, images/thumb and +## images/temp, and make them all writable. Then uncomment +## this, if it's not already uncommented: +#$wgHashedUploadDirectory = false; + +## Set $wgCacheDirectory to a writable directory on the web server +## to make your wiki go slightly faster. The directory should not +## be publically accessible from the web. +#$wgCacheDirectory = "$IP/cache"; + +# Site language code, should be one of the list in ./languages/Names.php +$wgLanguageCode = "en"; + +$wgSecretKey = "1c912bfe3519fb70f5dc523ecc698111cd43d81a11c585b3eefb28f29c2699b7"; +#$wgSecretKey = "@SECRETKEY@"; + + +# Site upgrade key. Must be set to a string (default provided) to turn on the +# web installer while LocalSettings.php is in place +$wgUpgradeKey = "ddae7dc87cd0a645"; + +## Default skin: you can change the default skin. Use the internal symbolic +## names, ie 'standard', 'nostalgia', 'cologneblue', 'monobook', 'vector': +$wgDefaultSkin = "vector"; + +## For attaching licensing metadata to pages, and displaying an +## appropriate copyright notice / icon. GNU Free Documentation +## License and Creative Commons licenses are supported so far. +$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright +$wgRightsUrl = ""; +$wgRightsText = ""; +$wgRightsIcon = ""; + +# Path to the GNU diff3 utility. Used for conflict resolution. +$wgDiff3 = "/usr/bin/diff3"; + +# Query string length limit for ResourceLoader. You should only set this if +# your web server has a query string length limit (then set it to that limit), +# or if you have suhosin.get.max_value_length set in php.ini (then set it to +# that value) +$wgResourceLoaderMaxQueryLength = -1; + + + +# End of automatically generated settings. +# Add more configuration options below. diff --git a/contrib/mw-to-git/t/install-wiki/db_install.php b/contrib/mw-to-git/t/install-wiki/db_install.php new file mode 100644 index 0000000000..0f3f4e018a --- /dev/null +++ b/contrib/mw-to-git/t/install-wiki/db_install.php @@ -0,0 +1,120 @@ +<?php +/** + * This script generates a SQLite database for a MediaWiki version 1.19.0 + * You must specify the login of the admin (argument 1) and its + * password (argument 2) and the folder where the database file + * is located (absolute path in argument 3). + * It is used by the script install-wiki.sh in order to make easy the + * installation of a MediaWiki. + * + * In order to generate a SQLite database file, MediaWiki ask the user + * to submit some forms in its web browser. This script simulates this + * behavior though the functions <get> and <submit> + * + */ +$argc = $_SERVER['argc']; +$argv = $_SERVER['argv']; + +$login = $argv[2]; +$pass = $argv[3]; +$tmp = $argv[4]; +$port = $argv[5]; + +$url = 'http://localhost:'.$port.'/wiki/mw-config/index.php'; +$db_dir = urlencode($tmp); +$tmp_cookie = tempnam($tmp, "COOKIE_"); +/* + * Fetchs a page with cURL. + */ +function get($page_name = "") { + $curl = curl_init(); + $page_name_add = ""; + if ($page_name != "") { + $page_name_add = '?page='.$page_name; + } + $url = $GLOBALS['url'].$page_name_add; + $tmp_cookie = $GLOBALS['tmp_cookie']; + curl_setopt($curl, CURLOPT_COOKIEJAR, $tmp_cookie); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_COOKIEFILE, $tmp_cookie); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_URL, $url); + + $page = curl_exec($curl); + if (!$page) { + die("Could not get page: $url\n"); + } + curl_close($curl); + return $page; +} + +/* + * Submits a form with cURL. + */ +function submit($page_name, $option = "") { + $curl = curl_init(); + $datapost = 'submit-continue=Continue+%E2%86%92'; + if ($option != "") { + $datapost = $option.'&'.$datapost; + } + $url = $GLOBALS['url'].'?page='.$page_name; + $tmp_cookie = $GLOBALS['tmp_cookie']; + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $datapost); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_COOKIEJAR, $tmp_cookie); + curl_setopt($curl, CURLOPT_COOKIEFILE, $tmp_cookie); + + $page = curl_exec($curl); + if (!$page) { + die("Could not get page: $url\n"); + } + curl_close($curl); + return "$page"; +} + +/* + * Here starts this script: simulates the behavior of the user + * submitting forms to generates the database file. + * Note this simulation was made for the MediaWiki version 1.19.0, + * we can't assume it works with other versions. + * + */ + +$page = get(); +if (!preg_match('/input type="hidden" value="([0-9]+)" name="LanguageRequestTime"/', + $page, $matches)) { + echo "Unexpected content for page downloaded:\n"; + echo "$page"; + die; +}; +$timestamp = $matches[1]; +$language = "LanguageRequestTime=$timestamp&uselang=en&ContLang=en"; +$page = submit('Language', $language); + +submit('Welcome'); + +$db_config = 'DBType=sqlite'; +$db_config = $db_config.'&sqlite_wgSQLiteDataDir='.$db_dir; +$db_config = $db_config.'&sqlite_wgDBname='.$argv[1]; +submit('DBConnect', $db_config); + +$wiki_config = 'config_wgSitename=TEST'; +$wiki_config = $wiki_config.'&config__NamespaceType=site-name'; +$wiki_config = $wiki_config.'&config_wgMetaNamespace=MyWiki'; +$wiki_config = $wiki_config.'&config__AdminName='.$login; + +$wiki_config = $wiki_config.'&config__AdminPassword='.$pass; +$wiki_config = $wiki_config.'&config__AdminPassword2='.$pass; + +$wiki_config = $wiki_config.'&wiki__configEmail=email%40email.org'; +$wiki_config = $wiki_config.'&config__SkipOptional=skip'; +submit('Name', $wiki_config); +submit('Install'); +submit('Install'); + +unlink($tmp_cookie); +?> diff --git a/contrib/mw-to-git/t/push-pull-tests.sh b/contrib/mw-to-git/t/push-pull-tests.sh new file mode 100644 index 0000000000..6692a0f40f --- /dev/null +++ b/contrib/mw-to-git/t/push-pull-tests.sh @@ -0,0 +1,144 @@ +test_push_pull () { + + test_expect_success 'Git pull works after adding a new wiki page' ' + wiki_reset && + + git clone mediawiki::'"$WIKI_URL"' mw_dir_1 && + wiki_editpage Foo "page created after the git clone" false && + + ( + cd mw_dir_1 && + git pull + ) && + + wiki_getallpage ref_page_1 && + test_diff_directories mw_dir_1 ref_page_1 + ' + + test_expect_success 'Git pull works after editing a wiki page' ' + wiki_reset && + + wiki_editpage Foo "page created before the git clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_2 && + wiki_editpage Foo "new line added on the wiki" true && + + ( + cd mw_dir_2 && + git pull + ) && + + wiki_getallpage ref_page_2 && + test_diff_directories mw_dir_2 ref_page_2 + ' + + test_expect_success 'git pull works on conflict handled by auto-merge' ' + wiki_reset && + + wiki_editpage Foo "1 init +3 +5 + " false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_3 && + + wiki_editpage Foo "1 init +2 content added on wiki after clone +3 +5 + " false && + + ( + cd mw_dir_3 && + echo "1 init +3 +4 content added on git after clone +5 +" >Foo.mw && + git commit -am "conflicting change on foo" && + git pull && + git push + ) + ' + + test_expect_success 'Git push works after adding a file .mw' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_4 && + wiki_getallpage ref_page_4 && + ( + cd mw_dir_4 && + test_path_is_missing Foo.mw && + touch Foo.mw && + echo "hello world" >>Foo.mw && + git add Foo.mw && + git commit -m "Foo" && + git push + ) && + wiki_getallpage ref_page_4 && + test_diff_directories mw_dir_4 ref_page_4 + ' + + test_expect_success 'Git push works after editing a file .mw' ' + wiki_reset && + wiki_editpage "Foo" "page created before the git clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_5 && + + ( + cd mw_dir_5 && + echo "new line added in the file Foo.mw" >>Foo.mw && + git commit -am "edit file Foo.mw" && + git push + ) && + + wiki_getallpage ref_page_5 && + test_diff_directories mw_dir_5 ref_page_5 + ' + + test_expect_failure 'Git push works after deleting a file' ' + wiki_reset && + wiki_editpage Foo "wiki page added before git clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_6 && + + ( + cd mw_dir_6 && + git rm Foo.mw && + git commit -am "page Foo.mw deleted" && + git push + ) && + + test ! wiki_page_exist Foo + ' + + test_expect_success 'Merge conflict expected and solving it' ' + wiki_reset && + + git clone mediawiki::'"$WIKI_URL"' mw_dir_7 && + wiki_editpage Foo "1 conflict +3 wiki +4" false && + + ( + cd mw_dir_7 && + echo "1 conflict +2 git +4" >Foo.mw && + git add Foo.mw && + git commit -m "conflict created" && + test_must_fail git pull && + "$PERL_PATH" -pi -e "s/[<=>].*//g" Foo.mw && + git commit -am "merge conflict solved" && + git push + ) + ' + + test_expect_failure 'git pull works after deleting a wiki page' ' + wiki_reset && + wiki_editpage Foo "wiki page added before the git clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_8 && + + wiki_delete_page Foo && + ( + cd mw_dir_8 && + git pull && + test_path_is_missing Foo.mw + ) + ' +} diff --git a/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh new file mode 100755 index 0000000000..811a90c9ae --- /dev/null +++ b/contrib/mw-to-git/t/t9360-mw-to-git-clone.sh @@ -0,0 +1,257 @@ +#!/bin/sh +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +# License: GPL v2 or later + + +test_description='Test the Git Mediawiki remote helper: git clone' + +. ./test-gitmw-lib.sh +. $TEST_DIRECTORY/test-lib.sh + + +test_check_precond + + +test_expect_success 'Git clone creates the expected git log with one file' ' + wiki_reset && + wiki_editpage foo "this is not important" false -c cat -s "this must be the same" && + git clone mediawiki::'"$WIKI_URL"' mw_dir_1 && + ( + cd mw_dir_1 && + git log --format=%s HEAD^..HEAD >log.tmp + ) && + echo "this must be the same" >msg.tmp && + diff -b mw_dir_1/log.tmp msg.tmp +' + + +test_expect_success 'Git clone creates the expected git log with multiple files' ' + wiki_reset && + wiki_editpage daddy "this is not important" false -s="this must be the same" && + wiki_editpage daddy "neither is this" true -s="this must also be the same" && + wiki_editpage daddy "neither is this" true -s="same same same" && + wiki_editpage dj "dont care" false -s="identical" && + wiki_editpage dj "dont care either" true -s="identical too" && + git clone mediawiki::'"$WIKI_URL"' mw_dir_2 && + ( + cd mw_dir_2 && + git log --format=%s Daddy.mw >logDaddy.tmp && + git log --format=%s Dj.mw >logDj.tmp + ) && + echo "same same same" >msgDaddy.tmp && + echo "this must also be the same" >>msgDaddy.tmp && + echo "this must be the same" >>msgDaddy.tmp && + echo "identical too" >msgDj.tmp && + echo "identical" >>msgDj.tmp && + diff -b mw_dir_2/logDaddy.tmp msgDaddy.tmp && + diff -b mw_dir_2/logDj.tmp msgDj.tmp +' + + +test_expect_success 'Git clone creates only Main_Page.mw with an empty wiki' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_3 && + test_contains_N_files mw_dir_3 1 && + test_path_is_file mw_dir_3/Main_Page.mw +' + +test_expect_success 'Git clone does not fetch a deleted page' ' + wiki_reset && + wiki_editpage foo "this page must be deleted before the clone" false && + wiki_delete_page foo && + git clone mediawiki::'"$WIKI_URL"' mw_dir_4 && + test_contains_N_files mw_dir_4 1 && + test_path_is_file mw_dir_4/Main_Page.mw && + test_path_is_missing mw_dir_4/Foo.mw +' + +test_expect_success 'Git clone works with page added' ' + wiki_reset && + wiki_editpage foo " I will be cloned" false && + wiki_editpage bar "I will be cloned" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_5 && + wiki_getallpage ref_page_5 && + test_diff_directories mw_dir_5 ref_page_5 && + wiki_delete_page foo && + wiki_delete_page bar +' + +test_expect_success 'Git clone works with an edited page ' ' + wiki_reset && + wiki_editpage foo "this page will be edited" \ + false -s "first edition of page foo"&& + wiki_editpage foo "this page has been edited and must be on the clone " true && + git clone mediawiki::'"$WIKI_URL"' mw_dir_6 && + test_path_is_file mw_dir_6/Foo.mw && + test_path_is_file mw_dir_6/Main_Page.mw && + wiki_getallpage mw_dir_6/page_ref_6 && + test_diff_directories mw_dir_6 mw_dir_6/page_ref_6 && + ( + cd mw_dir_6 && + git log --format=%s HEAD^ Foo.mw > ../Foo.log + ) && + echo "first edition of page foo" > FooExpect.log && + diff FooExpect.log Foo.log +' + + +test_expect_success 'Git clone works with several pages and some deleted ' ' + wiki_reset && + wiki_editpage foo "this page will not be deleted" false && + wiki_editpage bar "I must not be erased" false && + wiki_editpage namnam "I will not be there at the end" false && + wiki_editpage nyancat "nyan nyan nyan delete me" false && + wiki_delete_page namnam && + wiki_delete_page nyancat && + git clone mediawiki::'"$WIKI_URL"' mw_dir_7 && + test_path_is_file mw_dir_7/Foo.mw && + test_path_is_file mw_dir_7/Bar.mw && + test_path_is_missing mw_dir_7/Namnam.mw && + test_path_is_missing mw_dir_7/Nyancat.mw && + wiki_getallpage mw_dir_7/page_ref_7 && + test_diff_directories mw_dir_7 mw_dir_7/page_ref_7 +' + + +test_expect_success 'Git clone works with one specific page cloned ' ' + wiki_reset && + wiki_editpage foo "I will not be cloned" false && + wiki_editpage bar "Do not clone me" false && + wiki_editpage namnam "I will be cloned :)" false -s="this log must stay" && + wiki_editpage nyancat "nyan nyan nyan you cant clone me" false && + git clone -c remote.origin.pages=namnam \ + mediawiki::'"$WIKI_URL"' mw_dir_8 && + test_contains_N_files mw_dir_8 1 && + test_path_is_file mw_dir_8/Namnam.mw && + test_path_is_missing mw_dir_8/Main_Page.mw && + ( + cd mw_dir_8 && + echo "this log must stay" >msg.tmp && + git log --format=%s >log.tmp && + diff -b msg.tmp log.tmp + ) && + wiki_check_content mw_dir_8/Namnam.mw Namnam +' + +test_expect_success 'Git clone works with multiple specific page cloned ' ' + wiki_reset && + wiki_editpage foo "I will be there" false && + wiki_editpage bar "I will not disapear" false && + wiki_editpage namnam "I be erased" false && + wiki_editpage nyancat "nyan nyan nyan you will not erase me" false && + wiki_delete_page namnam && + git clone -c remote.origin.pages="foo bar nyancat namnam" \ + mediawiki::'"$WIKI_URL"' mw_dir_9 && + test_contains_N_files mw_dir_9 3 && + test_path_is_missing mw_dir_9/Namnam.mw && + test_path_is_file mw_dir_9/Foo.mw && + test_path_is_file mw_dir_9/Nyancat.mw && + test_path_is_file mw_dir_9/Bar.mw && + wiki_check_content mw_dir_9/Foo.mw Foo && + wiki_check_content mw_dir_9/Bar.mw Bar && + wiki_check_content mw_dir_9/Nyancat.mw Nyancat +' + +test_expect_success 'Mediawiki-clone of several specific pages on wiki' ' + wiki_reset && + wiki_editpage foo "foo 1" false && + wiki_editpage bar "bar 1" false && + wiki_editpage dummy "dummy 1" false && + wiki_editpage cloned_1 "cloned_1 1" false && + wiki_editpage cloned_2 "cloned_2 2" false && + wiki_editpage cloned_3 "cloned_3 3" false && + mkdir -p ref_page_10 && + wiki_getpage cloned_1 ref_page_10 && + wiki_getpage cloned_2 ref_page_10 && + wiki_getpage cloned_3 ref_page_10 && + git clone -c remote.origin.pages="cloned_1 cloned_2 cloned_3" \ + mediawiki::'"$WIKI_URL"' mw_dir_10 && + test_diff_directories mw_dir_10 ref_page_10 +' + +test_expect_success 'Git clone works with the shallow option' ' + wiki_reset && + wiki_editpage foo "1st revision, should be cloned" false && + wiki_editpage bar "1st revision, should be cloned" false && + wiki_editpage nyan "1st revision, should not be cloned" false && + wiki_editpage nyan "2nd revision, should be cloned" false && + git -c remote.origin.shallow=true clone \ + mediawiki::'"$WIKI_URL"' mw_dir_11 && + test_contains_N_files mw_dir_11 4 && + test_path_is_file mw_dir_11/Nyan.mw && + test_path_is_file mw_dir_11/Foo.mw && + test_path_is_file mw_dir_11/Bar.mw && + test_path_is_file mw_dir_11/Main_Page.mw && + ( + cd mw_dir_11 && + test `git log --oneline Nyan.mw | wc -l` -eq 1 && + test `git log --oneline Foo.mw | wc -l` -eq 1 && + test `git log --oneline Bar.mw | wc -l` -eq 1 && + test `git log --oneline Main_Page.mw | wc -l ` -eq 1 + ) && + wiki_check_content mw_dir_11/Nyan.mw Nyan && + wiki_check_content mw_dir_11/Foo.mw Foo && + wiki_check_content mw_dir_11/Bar.mw Bar && + wiki_check_content mw_dir_11/Main_Page.mw Main_Page +' + +test_expect_success 'Git clone works with the shallow option with a delete page' ' + wiki_reset && + wiki_editpage foo "1st revision, will be deleted" false && + wiki_editpage bar "1st revision, should be cloned" false && + wiki_editpage nyan "1st revision, should not be cloned" false && + wiki_editpage nyan "2nd revision, should be cloned" false && + wiki_delete_page foo && + git -c remote.origin.shallow=true clone \ + mediawiki::'"$WIKI_URL"' mw_dir_12 && + test_contains_N_files mw_dir_12 3 && + test_path_is_file mw_dir_12/Nyan.mw && + test_path_is_missing mw_dir_12/Foo.mw && + test_path_is_file mw_dir_12/Bar.mw && + test_path_is_file mw_dir_12/Main_Page.mw && + ( + cd mw_dir_12 && + test `git log --oneline Nyan.mw | wc -l` -eq 1 && + test `git log --oneline Bar.mw | wc -l` -eq 1 && + test `git log --oneline Main_Page.mw | wc -l ` -eq 1 + ) && + wiki_check_content mw_dir_12/Nyan.mw Nyan && + wiki_check_content mw_dir_12/Bar.mw Bar && + wiki_check_content mw_dir_12/Main_Page.mw Main_Page +' + +test_expect_success 'Test of fetching a category' ' + wiki_reset && + wiki_editpage Foo "I will be cloned" false -c=Category && + wiki_editpage Bar "Meet me on the repository" false -c=Category && + wiki_editpage Dummy "I will not come" false && + wiki_editpage BarWrong "I will stay online only" false -c=NotCategory && + git clone -c remote.origin.categories="Category" \ + mediawiki::'"$WIKI_URL"' mw_dir_13 && + wiki_getallpage ref_page_13 Category && + test_diff_directories mw_dir_13 ref_page_13 +' + +test_expect_success 'Test of resistance to modification of category on wiki for clone' ' + wiki_reset && + wiki_editpage Tobedeleted "this page will be deleted" false -c=Catone && + wiki_editpage Tobeedited "this page will be modified" false -c=Catone && + wiki_editpage Normalone "this page wont be modified and will be on git" false -c=Catone && + wiki_editpage Notconsidered "this page will not appear on local" false && + wiki_editpage Othercategory "this page will not appear on local" false -c=Cattwo && + wiki_editpage Tobeedited "this page have been modified" true -c=Catone && + wiki_delete_page Tobedeleted + git clone -c remote.origin.categories="Catone" \ + mediawiki::'"$WIKI_URL"' mw_dir_14 && + wiki_getallpage ref_page_14 Catone && + test_diff_directories mw_dir_14 ref_page_14 +' + +test_done diff --git a/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh b/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh new file mode 100755 index 0000000000..9ea201459b --- /dev/null +++ b/contrib/mw-to-git/t/t9361-mw-to-git-push-pull.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +# License: GPL v2 or later + +# tests for git-remote-mediawiki + +test_description='Test the Git Mediawiki remote helper: git push and git pull simple test cases' + +. ./test-gitmw-lib.sh +. ./push-pull-tests.sh +. $TEST_DIRECTORY/test-lib.sh + +test_check_precond + +test_push_pull + +test_done diff --git a/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh new file mode 100755 index 0000000000..8635878452 --- /dev/null +++ b/contrib/mw-to-git/t/t9362-mw-to-git-utf8.sh @@ -0,0 +1,301 @@ +#!/bin/sh +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +# License: GPL v2 or later + +# tests for git-remote-mediawiki + +test_description='Test git-mediawiki with special characters in filenames' + +. ./test-gitmw-lib.sh +. $TEST_DIRECTORY/test-lib.sh + + +test_check_precond + + +test_expect_success 'Git clone works for a wiki with accents in the page names' ' + wiki_reset && + wiki_editpage féé "This page must be délétéd before clone" false && + wiki_editpage kèè "This page must be deleted before clone" false && + wiki_editpage hà à "This page must be deleted before clone" false && + wiki_editpage kîî "This page must be deleted before clone" false && + wiki_editpage foo "This page must be deleted before clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_1 && + wiki_getallpage ref_page_1 && + test_diff_directories mw_dir_1 ref_page_1 +' + + +test_expect_success 'Git pull works with a wiki with accents in the pages names' ' + wiki_reset && + wiki_editpage kîî "this page must be cloned" false && + wiki_editpage foo "this page must be cloned" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_2 && + wiki_editpage éà îôû "This page must be pulled" false && + ( + cd mw_dir_2 && + git pull + ) && + wiki_getallpage ref_page_2 && + test_diff_directories mw_dir_2 ref_page_2 +' + + +test_expect_success 'Cloning a chosen page works with accents' ' + wiki_reset && + wiki_editpage kîî "this page must be cloned" false && + git clone -c remote.origin.pages=kîî \ + mediawiki::'"$WIKI_URL"' mw_dir_3 && + wiki_check_content mw_dir_3/Kîî.mw Kîî && + test_path_is_file mw_dir_3/Kîî.mw && + rm -rf mw_dir_3 +' + + +test_expect_success 'The shallow option works with accents' ' + wiki_reset && + wiki_editpage néoà "1st revision, should not be cloned" false && + wiki_editpage néoà "2nd revision, should be cloned" false && + git -c remote.origin.shallow=true clone \ + mediawiki::'"$WIKI_URL"' mw_dir_4 && + test_contains_N_files mw_dir_4 2 && + test_path_is_file mw_dir_4/Néoà .mw && + test_path_is_file mw_dir_4/Main_Page.mw && + ( + cd mw_dir_4 && + test `git log --oneline Néoà .mw | wc -l` -eq 1 && + test `git log --oneline Main_Page.mw | wc -l ` -eq 1 + ) && + wiki_check_content mw_dir_4/Néoà .mw Néoà && + wiki_check_content mw_dir_4/Main_Page.mw Main_Page +' + + +test_expect_success 'Cloning works when page name first letter has an accent' ' + wiki_reset && + wiki_editpage îî "this page must be cloned" false && + git clone -c remote.origin.pages=îî \ + mediawiki::'"$WIKI_URL"' mw_dir_5 && + test_path_is_file mw_dir_5/Îî.mw && + wiki_check_content mw_dir_5/Îî.mw Îî +' + + +test_expect_success 'Git push works with a wiki with accents' ' + wiki_reset && + wiki_editpage féé "lots of accents : éèà Ö" false && + wiki_editpage foo "this page must be cloned" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_6 && + ( + cd mw_dir_6 && + echo "A wild Pîkächû appears on the wiki" >Pîkächû.mw && + git add Pîkächû.mw && + git commit -m "A new page appears" && + git push + ) && + wiki_getallpage ref_page_6 && + test_diff_directories mw_dir_6 ref_page_6 +' + +test_expect_success 'Git clone works with accentsand spaces' ' + wiki_reset && + wiki_editpage "é à î" "this page must be délété before the clone" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_7 && + wiki_getallpage ref_page_7 && + test_diff_directories mw_dir_7 ref_page_7 +' + +test_expect_success 'character $ in page name (mw -> git)' ' + wiki_reset && + wiki_editpage file_\$_foo "expect to be called file_$_foo" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_8 && + test_path_is_file mw_dir_8/File_\$_foo.mw && + wiki_getallpage ref_page_8 && + test_diff_directories mw_dir_8 ref_page_8 +' + + + +test_expect_success 'character $ in file name (git -> mw) ' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_9 && + ( + cd mw_dir_9 && + echo "this file is called File_\$_foo.mw" >File_\$_foo.mw && + git add . && + git commit -am "file File_\$_foo.mw" && + git pull && + git push + ) && + wiki_getallpage ref_page_9 && + test_diff_directories mw_dir_9 ref_page_9 +' + + +test_expect_failure 'capital at the begining of file names' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_10 && + ( + cd mw_dir_10 && + echo "my new file foo" >foo.mw && + echo "my new file Foo... Finger crossed" >Foo.mw && + git add . && + git commit -am "file foo.mw" && + git pull && + git push + ) && + wiki_getallpage ref_page_10 && + test_diff_directories mw_dir_10 ref_page_10 +' + + +test_expect_failure 'special character at the begining of file name from mw to git' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_11 && + wiki_editpage {char_1 "expect to be renamed {char_1" false && + wiki_editpage [char_2 "expect to be renamed [char_2" false && + ( + cd mw_dir_11 && + git pull + ) && + test_path_is_file mw_dir_11/{char_1 && + test_path_is_file mw_dir_11/[char_2 +' + +test_expect_success 'test of correct formating for file name from mw to git' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_12 && + wiki_editpage char_%_7b_1 "expect to be renamed char{_1" false && + wiki_editpage char_%_5b_2 "expect to be renamed char{_2" false && + ( + cd mw_dir_12 && + git pull + ) && + test_path_is_file mw_dir_12/Char\{_1.mw && + test_path_is_file mw_dir_12/Char\[_2.mw && + wiki_getallpage ref_page_12 && + mv ref_page_12/Char_%_7b_1.mw ref_page_12/Char\{_1.mw && + mv ref_page_12/Char_%_5b_2.mw ref_page_12/Char\[_2.mw && + test_diff_directories mw_dir_12 ref_page_12 +' + + +test_expect_failure 'test of correct formating for file name begining with special character' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_13 && + ( + cd mw_dir_13 && + echo "my new file {char_1" >\{char_1.mw && + echo "my new file [char_2" >\[char_2.mw && + git add . && + git commit -am "commiting some exotic file name..." && + git push && + git pull + ) && + wiki_getallpage ref_page_13 && + test_path_is_file ref_page_13/{char_1.mw && + test_path_is_file ref_page_13/[char_2.mw && + test_diff_directories mw_dir_13 ref_page_13 +' + + +test_expect_success 'test of correct formating for file name from git to mw' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_14 && + ( + cd mw_dir_14 && + echo "my new file char{_1" >Char\{_1.mw && + echo "my new file char[_2" >Char\[_2.mw && + git add . && + git commit -m "commiting some exotic file name..." && + git push + ) && + wiki_getallpage ref_page_14 && + mv mw_dir_14/Char\{_1.mw mw_dir_14/Char_%_7b_1.mw && + mv mw_dir_14/Char\[_2.mw mw_dir_14/Char_%_5b_2.mw && + test_diff_directories mw_dir_14 ref_page_14 +' + + +test_expect_success 'git clone with /' ' + wiki_reset && + wiki_editpage \/fo\/o "this is not important" false -c=Deleted && + git clone mediawiki::'"$WIKI_URL"' mw_dir_15 && + test_path_is_file mw_dir_15/%2Ffo%2Fo.mw && + wiki_check_content mw_dir_15/%2Ffo%2Fo.mw \/fo\/o +' + + +test_expect_success 'git push with /' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_16 && + echo "I will be on the wiki" >mw_dir_16/%2Ffo%2Fo.mw && + ( + cd mw_dir_16 && + git add %2Ffo%2Fo.mw && + git commit -m " %2Ffo%2Fo added" && + git push + ) && + wiki_page_exist \/fo\/o && + wiki_check_content mw_dir_16/%2Ffo%2Fo.mw \/fo\/o + +' + + +test_expect_success 'git clone with \' ' + wiki_reset && + wiki_editpage \\ko\\o "this is not important" false -c=Deleted && + git clone mediawiki::'"$WIKI_URL"' mw_dir_17 && + test_path_is_file mw_dir_17/\\ko\\o.mw && + wiki_check_content mw_dir_17/\\ko\\o.mw \\ko\\o +' + + +test_expect_success 'git push with \' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_18 && + echo "I will be on the wiki" >mw_dir_18/\\ko\\o.mw && + ( + cd mw_dir_18 && + git add \\ko\\o.mw && + git commit -m " \\ko\\o added" && + git push + )&& + wiki_page_exist \\ko\\o && + wiki_check_content mw_dir_18/\\ko\\o.mw \\ko\\o + +' + +test_expect_success 'git clone with \ in format control' ' + wiki_reset && + wiki_editpage \\no\\o "this is not important" false && + git clone mediawiki::'"$WIKI_URL"' mw_dir_19 && + test_path_is_file mw_dir_19/\\no\\o.mw && + wiki_check_content mw_dir_19/\\no\\o.mw \\no\\o +' + + +test_expect_success 'git push with \ in format control' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir_20 && + echo "I will be on the wiki" >mw_dir_20/\\fo\\o.mw && + ( + cd mw_dir_20 && + git add \\fo\\o.mw && + git commit -m " \\fo\\o added" && + git push + )&& + wiki_page_exist \\fo\\o && + wiki_check_content mw_dir_20/\\fo\\o.mw \\fo\\o + +' + + +test_done diff --git a/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh new file mode 100755 index 0000000000..5a0373935f --- /dev/null +++ b/contrib/mw-to-git/t/t9363-mw-to-git-export-import.sh @@ -0,0 +1,198 @@ +#!/bin/sh +# +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# +# License: GPL v2 or later + +# tests for git-remote-mediawiki + +test_description='Test the Git Mediawiki remote helper: git push and git pull simple test cases' + +. ./test-gitmw-lib.sh +. $TEST_DIRECTORY/test-lib.sh + + +test_check_precond + + +test_git_reimport () { + git -c remote.origin.dumbPush=true push && + git -c remote.origin.mediaImport=true pull --rebase +} + +# Don't bother with permissions, be administrator by default +test_expect_success 'setup config' ' + git config --global remote.origin.mwLogin WikiAdmin && + git config --global remote.origin.mwPassword AdminPass && + test_might_fail git config --global --unset remote.origin.mediaImport +' + +test_expect_success 'git push can upload media (File:) files' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + ( + cd mw_dir && + echo "hello world" >Foo.txt && + git add Foo.txt && + git commit -m "add a text file" && + git push && + "$PERL_PATH" -e "print STDOUT \"binary content: \".chr(255);" >Foo.txt && + git add Foo.txt && + git commit -m "add a text file with binary content" && + git push + ) +' + +test_expect_success 'git clone works on previously created wiki with media files' ' + test_when_finished "rm -rf mw_dir mw_dir_clone" && + git clone -c remote.origin.mediaimport=true \ + mediawiki::'"$WIKI_URL"' mw_dir_clone && + test_cmp mw_dir_clone/Foo.txt mw_dir/Foo.txt && + (cd mw_dir_clone && git checkout HEAD^) && + (cd mw_dir && git checkout HEAD^) && + test_cmp mw_dir_clone/Foo.txt mw_dir/Foo.txt +' + +test_expect_success 'git push & pull work with locally renamed media files' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + test_when_finished "rm -fr mw_dir" && + ( + cd mw_dir && + echo "A File" >Foo.txt && + git add Foo.txt && + git commit -m "add a file" && + git mv Foo.txt Bar.txt && + git commit -m "Rename a file" && + test_git_reimport && + echo "A File" >expect && + test_cmp expect Bar.txt && + test_path_is_missing Foo.txt + ) +' + +test_expect_success 'git push can propagate local page deletion' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + test_when_finished "rm -fr mw_dir" && + ( + cd mw_dir && + test_path_is_missing Foo.mw && + echo "hello world" >Foo.mw && + git add Foo.mw && + git commit -m "Add the page Foo" && + git push && + rm -f Foo.mw && + git commit -am "Delete the page Foo" && + test_git_reimport && + test_path_is_missing Foo.mw + ) +' + +test_expect_success 'git push can propagate local media file deletion' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + test_when_finished "rm -fr mw_dir" && + ( + cd mw_dir && + echo "hello world" >Foo.txt && + git add Foo.txt && + git commit -m "Add the text file Foo" && + git rm Foo.txt && + git commit -m "Delete the file Foo" && + test_git_reimport && + test_path_is_missing Foo.txt + ) +' + +# test failure: the file is correctly uploaded, and then deleted but +# as no page link to it, the import (which looks at page revisions) +# doesn't notice the file deletion on the wiki. We fetch the list of +# files from the wiki, but as the file is deleted, it doesn't appear. +test_expect_failure 'git pull correctly imports media file deletion when no page link to it' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + test_when_finished "rm -fr mw_dir" && + ( + cd mw_dir && + echo "hello world" >Foo.txt && + git add Foo.txt && + git commit -m "Add the text file Foo" && + git push && + git rm Foo.txt && + git commit -m "Delete the file Foo" && + test_git_reimport && + test_path_is_missing Foo.txt + ) +' + +test_expect_success 'git push properly warns about insufficient permissions' ' + wiki_reset && + git clone mediawiki::'"$WIKI_URL"' mw_dir && + test_when_finished "rm -fr mw_dir" && + ( + cd mw_dir && + echo "A File" >foo.forbidden && + git add foo.forbidden && + git commit -m "add a file" && + git push 2>actual && + test_i18ngrep "foo.forbidden is not a permitted file" actual + ) +' + +test_expect_success 'setup a repository with media files' ' + wiki_reset && + wiki_editpage testpage "I am linking a file [[File:File.txt]]" false && + echo "File content" >File.txt && + wiki_upload_file File.txt && + echo "Another file content" >AnotherFile.txt && + wiki_upload_file AnotherFile.txt +' + +test_expect_success 'git clone works with one specific page cloned and mediaimport=true' ' + git clone -c remote.origin.pages=testpage \ + -c remote.origin.mediaimport=true \ + mediawiki::'"$WIKI_URL"' mw_dir_15 && + test_when_finished "rm -rf mw_dir_15" && + test_contains_N_files mw_dir_15 3 && + test_path_is_file mw_dir_15/Testpage.mw && + test_path_is_file mw_dir_15/File:File.txt.mw && + test_path_is_file mw_dir_15/File.txt && + test_path_is_missing mw_dir_15/Main_Page.mw && + test_path_is_missing mw_dir_15/File:AnotherFile.txt.mw && + test_path_is_missing mw_dir_15/AnothetFile.txt && + wiki_check_content mw_dir_15/Testpage.mw Testpage && + test_cmp mw_dir_15/File.txt File.txt +' + +test_expect_success 'git clone works with one specific page cloned and mediaimport=false' ' + test_when_finished "rm -rf mw_dir_16" && + git clone -c remote.origin.pages=testpage \ + mediawiki::'"$WIKI_URL"' mw_dir_16 && + test_contains_N_files mw_dir_16 1 && + test_path_is_file mw_dir_16/Testpage.mw && + test_path_is_missing mw_dir_16/File:File.txt.mw && + test_path_is_missing mw_dir_16/File.txt && + test_path_is_missing mw_dir_16/Main_Page.mw && + wiki_check_content mw_dir_16/Testpage.mw Testpage +' + +# should behave like mediaimport=false +test_expect_success 'git clone works with one specific page cloned and mediaimport unset' ' + test_when_finished "rm -fr mw_dir_17" && + git clone -c remote.origin.pages=testpage \ + mediawiki::'"$WIKI_URL"' mw_dir_17 && + test_contains_N_files mw_dir_17 1 && + test_path_is_file mw_dir_17/Testpage.mw && + test_path_is_missing mw_dir_17/File:File.txt.mw && + test_path_is_missing mw_dir_17/File.txt && + test_path_is_missing mw_dir_17/Main_Page.mw && + wiki_check_content mw_dir_17/Testpage.mw Testpage +' + +test_done diff --git a/contrib/mw-to-git/t/t9364-pull-by-rev.sh b/contrib/mw-to-git/t/t9364-pull-by-rev.sh new file mode 100755 index 0000000000..5c22457a0b --- /dev/null +++ b/contrib/mw-to-git/t/t9364-pull-by-rev.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +test_description='Test the Git Mediawiki remote helper: git pull by revision' + +. ./test-gitmw-lib.sh +. ./push-pull-tests.sh +. $TEST_DIRECTORY/test-lib.sh + +test_check_precond + +test_expect_success 'configuration' ' + git config --global mediawiki.fetchStrategy by_rev +' + +test_push_pull + +test_done diff --git a/contrib/mw-to-git/t/test-gitmw-lib.sh b/contrib/mw-to-git/t/test-gitmw-lib.sh new file mode 100755 index 0000000000..3b2cfacf51 --- /dev/null +++ b/contrib/mw-to-git/t/test-gitmw-lib.sh @@ -0,0 +1,435 @@ +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# License: GPL v2 or later + +# +# CONFIGURATION VARIABLES +# You might want to change these ones +# + +. ./test.config + +WIKI_URL=http://"$SERVER_ADDR:$PORT/$WIKI_DIR_NAME" +CURR_DIR=$(pwd) +TEST_OUTPUT_DIRECTORY=$(pwd) +TEST_DIRECTORY="$CURR_DIR"/../../../t + +export TEST_OUTPUT_DIRECTORY TEST_DIRECTORY CURR_DIR + +if test "$LIGHTTPD" = "false" ; then + PORT=80 +else + WIKI_DIR_INST="$CURR_DIR/$WEB_WWW" +fi + +wiki_upload_file () { + "$CURR_DIR"/test-gitmw.pl upload_file "$@" +} + +wiki_getpage () { + "$CURR_DIR"/test-gitmw.pl get_page "$@" +} + +wiki_delete_page () { + "$CURR_DIR"/test-gitmw.pl delete_page "$@" +} + +wiki_editpage () { + "$CURR_DIR"/test-gitmw.pl edit_page "$@" +} + +die () { + die_with_status 1 "$@" +} + +die_with_status () { + status=$1 + shift + echo >&2 "$*" + exit "$status" +} + + +# Check the preconditions to run git-remote-mediawiki's tests +test_check_precond () { + if ! test_have_prereq PERL + then + skip_all='skipping gateway git-mw tests, perl not available' + test_done + fi + + if [ ! -f "$GIT_BUILD_DIR"/git-remote-mediawiki ]; + then + echo "No remote mediawiki for git found. Copying it in git" + echo "cp $GIT_BUILD_DIR/contrib/mw-to-git/git-remote-mediawiki $GIT_BUILD_DIR/" + ln -s "$GIT_BUILD_DIR"/contrib/mw-to-git/git-remote-mediawiki "$GIT_BUILD_DIR" + fi + + if [ ! -d "$WIKI_DIR_INST/$WIKI_DIR_NAME" ]; + then + skip_all='skipping gateway git-mw tests, no mediawiki found' + test_done + fi +} + +# test_diff_directories <dir_git> <dir_wiki> +# +# Compare the contents of directories <dir_git> and <dir_wiki> with diff +# and errors if they do not match. The program will +# not look into .git in the process. +# Warning: the first argument MUST be the directory containing the git data +test_diff_directories () { + rm -rf "$1_tmp" + mkdir -p "$1_tmp" + cp "$1"/*.mw "$1_tmp" + diff -r -b "$1_tmp" "$2" +} + +# $1=<dir> +# $2=<N> +# +# Check that <dir> contains exactly <N> files +test_contains_N_files () { + if test `ls -- "$1" | wc -l` -ne "$2"; then + echo "directory $1 sould contain $2 files" + echo "it contains these files:" + ls "$1" + false + fi +} + + +# wiki_check_content <file_name> <page_name> +# +# Compares the contents of the file <file_name> and the wiki page +# <page_name> and exits with error 1 if they do not match. +wiki_check_content () { + mkdir -p wiki_tmp + wiki_getpage "$2" wiki_tmp + # replacement of forbidden character in file name + page_name=$(printf "%s\n" "$2" | sed -e "s/\//%2F/g") + + diff -b "$1" wiki_tmp/"$page_name".mw + if test $? -ne 0 + then + rm -rf wiki_tmp + error "ERROR: file $2 not found on wiki" + fi + rm -rf wiki_tmp +} + +# wiki_page_exist <page_name> +# +# Check the existence of the page <page_name> on the wiki and exits +# with error if it is absent from it. +wiki_page_exist () { + mkdir -p wiki_tmp + wiki_getpage "$1" wiki_tmp + page_name=$(printf "%s\n" "$1" | sed "s/\//%2F/g") + if test -f wiki_tmp/"$page_name".mw ; then + rm -rf wiki_tmp + else + rm -rf wiki_tmp + error "test failed: file $1 not found on wiki" + fi +} + +# wiki_getallpagename +# +# Fetch the name of each page on the wiki. +wiki_getallpagename () { + "$CURR_DIR"/test-gitmw.pl getallpagename +} + +# wiki_getallpagecategory <category> +# +# Fetch the name of each page belonging to <category> on the wiki. +wiki_getallpagecategory () { + "$CURR_DIR"/test-gitmw.pl getallpagename "$@" +} + +# wiki_getallpage <dest_dir> [<category>] +# +# Fetch all the pages from the wiki and place them in the directory +# <dest_dir>. +# If <category> is define, then wiki_getallpage fetch the pages included +# in <category>. +wiki_getallpage () { + if test -z "$2"; + then + wiki_getallpagename + else + wiki_getallpagecategory "$2" + fi + mkdir -p "$1" + while read -r line; do + wiki_getpage "$line" $1; + done < all.txt +} + +# ================= Install part ================= + +error () { + echo "$@" >&2 + exit 1 +} + +# config_lighttpd +# +# Create the configuration files and the folders necessary to start lighttpd. +# Overwrite any existing file. +config_lighttpd () { + mkdir -p $WEB + mkdir -p $WEB_TMP + mkdir -p $WEB_WWW + cat > $WEB/lighttpd.conf <<EOF + server.document-root = "$CURR_DIR/$WEB_WWW" + server.port = $PORT + server.pid-file = "$CURR_DIR/$WEB_TMP/pid" + + server.modules = ( + "mod_rewrite", + "mod_redirect", + "mod_access", + "mod_accesslog", + "mod_fastcgi" + ) + + index-file.names = ("index.php" , "index.html") + + mimetype.assign = ( + ".pdf" => "application/pdf", + ".sig" => "application/pgp-signature", + ".spl" => "application/futuresplash", + ".class" => "application/octet-stream", + ".ps" => "application/postscript", + ".torrent" => "application/x-bittorrent", + ".dvi" => "application/x-dvi", + ".gz" => "application/x-gzip", + ".pac" => "application/x-ns-proxy-autoconfig", + ".swf" => "application/x-shockwave-flash", + ".tar.gz" => "application/x-tgz", + ".tgz" => "application/x-tgz", + ".tar" => "application/x-tar", + ".zip" => "application/zip", + ".mp3" => "audio/mpeg", + ".m3u" => "audio/x-mpegurl", + ".wma" => "audio/x-ms-wma", + ".wax" => "audio/x-ms-wax", + ".ogg" => "application/ogg", + ".wav" => "audio/x-wav", + ".gif" => "image/gif", + ".jpg" => "image/jpeg", + ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".xbm" => "image/x-xbitmap", + ".xpm" => "image/x-xpixmap", + ".xwd" => "image/x-xwindowdump", + ".css" => "text/css", + ".html" => "text/html", + ".htm" => "text/html", + ".js" => "text/javascript", + ".asc" => "text/plain", + ".c" => "text/plain", + ".cpp" => "text/plain", + ".log" => "text/plain", + ".conf" => "text/plain", + ".text" => "text/plain", + ".txt" => "text/plain", + ".dtd" => "text/xml", + ".xml" => "text/xml", + ".mpeg" => "video/mpeg", + ".mpg" => "video/mpeg", + ".mov" => "video/quicktime", + ".qt" => "video/quicktime", + ".avi" => "video/x-msvideo", + ".asf" => "video/x-ms-asf", + ".asx" => "video/x-ms-asf", + ".wmv" => "video/x-ms-wmv", + ".bz2" => "application/x-bzip", + ".tbz" => "application/x-bzip-compressed-tar", + ".tar.bz2" => "application/x-bzip-compressed-tar", + "" => "text/plain" + ) + + fastcgi.server = ( ".php" => + ("localhost" => + ( "socket" => "$CURR_DIR/$WEB_TMP/php.socket", + "bin-path" => "$PHP_DIR/php-cgi -c $CURR_DIR/$WEB/php.ini" + + ) + ) + ) +EOF + + cat > $WEB/php.ini <<EOF + session.save_path ='$CURR_DIR/$WEB_TMP' +EOF +} + +# start_lighttpd +# +# Start or restart daemon lighttpd. If restart, rewrite configuration files. +start_lighttpd () { + if test -f "$WEB_TMP/pid"; then + echo "Instance already running. Restarting..." + stop_lighttpd + fi + config_lighttpd + "$LIGHTTPD_DIR"/lighttpd -f "$WEB"/lighttpd.conf + + if test $? -ne 0 ; then + echo "Could not execute http deamon lighttpd" + exit 1 + fi +} + +# stop_lighttpd +# +# Kill daemon lighttpd and removes files and folders associated. +stop_lighttpd () { + test -f "$WEB_TMP/pid" && kill $(cat "$WEB_TMP/pid") + rm -rf "$WEB" +} + +# Create the SQLite database of the MediaWiki. If the database file already +# exists, it will be deleted. +# This script should be runned from the directory where $FILES_FOLDER is +# located. +create_db () { + rm -f "$TMP/$DB_FILE" + + echo "Generating the SQLite database file. It can take some time ..." + # Run the php script to generate the SQLite database file + # with cURL calls. + php "$FILES_FOLDER/$DB_INSTALL_SCRIPT" $(basename "$DB_FILE" .sqlite) \ + "$WIKI_ADMIN" "$WIKI_PASSW" "$TMP" "$PORT" + + if [ ! -f "$TMP/$DB_FILE" ] ; then + error "Can't create database file $TMP/$DB_FILE. Try to run ./install-wiki.sh delete first." + fi + + # Copy the generated database file into the directory the + # user indicated. + cp "$TMP/$DB_FILE" "$FILES_FOLDER" || + error "Unable to copy $TMP/$DB_FILE to $FILES_FOLDER" +} + +# Install a wiki in your web server directory. +wiki_install () { + if test $LIGHTTPD = "true" ; then + start_lighttpd + fi + + SERVER_ADDR=$SERVER_ADDR:$PORT + # In this part, we change directory to $TMP in order to download, + # unpack and copy the files of MediaWiki + ( + mkdir -p "$WIKI_DIR_INST/$WIKI_DIR_NAME" + if [ ! -d "$WIKI_DIR_INST/$WIKI_DIR_NAME" ] ; then + error "Folder $WIKI_DIR_INST/$WIKI_DIR_NAME doesn't exist. + Please create it and launch the script again." + fi + + # Fetch MediaWiki's archive if not already present in the TMP directory + cd "$TMP" + if [ ! -f "$MW_VERSION.tar.gz" ] ; then + echo "Downloading $MW_VERSION sources ..." + wget "http://download.wikimedia.org/mediawiki/1.19/mediawiki-1.19.0.tar.gz" || + error "Unable to download "\ + "http://download.wikimedia.org/mediawiki/1.19/"\ + "mediawiki-1.19.0.tar.gz. "\ + "Please fix your connection and launch the script again." + echo "$MW_VERSION.tar.gz downloaded in `pwd`. "\ + "You can delete it later if you want." + else + echo "Reusing existing $MW_VERSION.tar.gz downloaded in `pwd`." + fi + archive_abs_path=$(pwd)/"$MW_VERSION.tar.gz" + cd "$WIKI_DIR_INST/$WIKI_DIR_NAME/" || + error "can't cd to $WIKI_DIR_INST/$WIKI_DIR_NAME/" + tar xzf "$archive_abs_path" --strip-components=1 || + error "Unable to extract WikiMedia's files from $archive_abs_path to "\ + "$WIKI_DIR_INST/$WIKI_DIR_NAME" + ) || exit 1 + + create_db + + # Copy the generic LocalSettings.php in the web server's directory + # And modify parameters according to the ones set at the top + # of this script. + # Note that LocalSettings.php is never modified. + if [ ! -f "$FILES_FOLDER/LocalSettings.php" ] ; then + error "Can't find $FILES_FOLDER/LocalSettings.php " \ + "in the current folder. "\ + "Please run the script inside its folder." + fi + cp "$FILES_FOLDER/LocalSettings.php" \ + "$FILES_FOLDER/LocalSettings-tmp.php" || + error "Unable to copy $FILES_FOLDER/LocalSettings.php " \ + "to $FILES_FOLDER/LocalSettings-tmp.php" + + # Parse and set the LocalSettings file of the user according to the + # CONFIGURATION VARIABLES section at the beginning of this script + file_swap="$FILES_FOLDER/LocalSettings-swap.php" + sed "s,@WG_SCRIPT_PATH@,/$WIKI_DIR_NAME," \ + "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap" + mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php" + sed "s,@WG_SERVER@,http://$SERVER_ADDR," \ + "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap" + mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php" + sed "s,@WG_SQLITE_DATADIR@,$TMP," \ + "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap" + mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php" + sed "s,@WG_SQLITE_DATAFILE@,$( basename $DB_FILE .sqlite)," \ + "$FILES_FOLDER/LocalSettings-tmp.php" > "$file_swap" + mv "$file_swap" "$FILES_FOLDER/LocalSettings-tmp.php" + + mv "$FILES_FOLDER/LocalSettings-tmp.php" \ + "$WIKI_DIR_INST/$WIKI_DIR_NAME/LocalSettings.php" || + error "Unable to move $FILES_FOLDER/LocalSettings-tmp.php" \ + "in $WIKI_DIR_INST/$WIKI_DIR_NAME" + echo "File $FILES_FOLDER/LocalSettings.php is set in" \ + " $WIKI_DIR_INST/$WIKI_DIR_NAME" + + echo "Your wiki has been installed. You can check it at + http://$SERVER_ADDR/$WIKI_DIR_NAME" +} + +# Reset the database of the wiki and the password of the admin +# +# Warning: This function must be called only in a subdirectory of t/ directory +wiki_reset () { + # Copy initial database of the wiki + if [ ! -f "../$FILES_FOLDER/$DB_FILE" ] ; then + error "Can't find ../$FILES_FOLDER/$DB_FILE in the current folder." + fi + cp "../$FILES_FOLDER/$DB_FILE" "$TMP" || + error "Can't copy ../$FILES_FOLDER/$DB_FILE in $TMP" + echo "File $FILES_FOLDER/$DB_FILE is set in $TMP" +} + +# Delete the wiki created in the web server's directory and all its content +# saved in the database. +wiki_delete () { + if test $LIGHTTPD = "true"; then + stop_lighttpd + else + # Delete the wiki's directory. + rm -rf "$WIKI_DIR_INST/$WIKI_DIR_NAME" || + error "Wiki's directory $WIKI_DIR_INST/" \ + "$WIKI_DIR_NAME could not be deleted" + # Delete the wiki's SQLite database. + rm -f "$TMP/$DB_FILE" || + error "Database $TMP/$DB_FILE could not be deleted." + fi + + # Delete the wiki's SQLite database + rm -f "$TMP/$DB_FILE" || error "Database $TMP/$DB_FILE could not be deleted." + rm -f "$FILES_FOLDER/$DB_FILE" + rm -rf "$TMP/$MW_VERSION" +} diff --git a/contrib/mw-to-git/t/test-gitmw.pl b/contrib/mw-to-git/t/test-gitmw.pl new file mode 100755 index 0000000000..0ff76259fa --- /dev/null +++ b/contrib/mw-to-git/t/test-gitmw.pl @@ -0,0 +1,225 @@ +#!/usr/bin/perl -w -s +# Copyright (C) 2012 +# Charles Roussel <charles.roussel@ensimag.imag.fr> +# Simon Cathebras <simon.cathebras@ensimag.imag.fr> +# Julien Khayat <julien.khayat@ensimag.imag.fr> +# Guillaume Sasdy <guillaume.sasdy@ensimag.imag.fr> +# Simon Perrat <simon.perrat@ensimag.imag.fr> +# License: GPL v2 or later + +# Usage: +# ./test-gitmw.pl <command> [argument]* +# Execute in terminal using the name of the function to call as first +# parameter, and the function's arguments as following parameters +# +# Example: +# ./test-gitmw.pl "get_page" foo . +# will call <wiki_getpage> with arguments <foo> and <.> +# +# Available functions are: +# "get_page" +# "delete_page" +# "edit_page" +# "getallpagename" + +use MediaWiki::API; +use Getopt::Long; +use encoding 'utf8'; +use DateTime::Format::ISO8601; +use open ':encoding(utf8)'; +use constant SLASH_REPLACEMENT => "%2F"; + +#Parsing of the config file + +my $configfile = "$ENV{'CURR_DIR'}/test.config"; +my %config; +open my $CONFIG, "<", $configfile or die "can't open $configfile: $!"; +while (<$CONFIG>) +{ + chomp; + s/#.*//; + s/^\s+//; + s/\s+$//; + next unless length; + my ($key, $value) = split (/\s*=\s*/,$_, 2); + $config{$key} = $value; + last if ($key eq 'LIGHTTPD' and $value eq 'false'); + last if ($key eq 'PORT'); +} +close $CONFIG or die "can't close $configfile: $!"; + +my $wiki_address = "http://$config{'SERVER_ADDR'}".":"."$config{'PORT'}"; +my $wiki_url = "$wiki_address/$config{'WIKI_DIR_NAME'}/api.php"; +my $wiki_admin = "$config{'WIKI_ADMIN'}"; +my $wiki_admin_pass = "$config{'WIKI_PASSW'}"; +my $mw = MediaWiki::API->new; +$mw->{config}->{api_url} = $wiki_url; + + +# wiki_login <name> <password> +# +# Logs the user with <name> and <password> in the global variable +# of the mediawiki $mw +sub wiki_login { + $mw->login( { lgname => "$_[0]",lgpassword => "$_[1]" } ) + || die "getpage: login failed"; +} + +# wiki_getpage <wiki_page> <dest_path> +# +# fetch a page <wiki_page> from the wiki referenced in the global variable +# $mw and copies its content in directory dest_path +sub wiki_getpage { + my $pagename = $_[0]; + my $destdir = $_[1]; + + my $page = $mw->get_page( { title => $pagename } ); + if (!defined($page)) { + die "getpage: wiki does not exist"; + } + + my $content = $page->{'*'}; + if (!defined($content)) { + die "getpage: page does not exist"; + } + + $pagename=$page->{'title'}; + # Replace spaces by underscore in the page name + $pagename =~ s/ /_/g; + $pagename =~ s/\//%2F/g; + open(my $file, ">$destdir/$pagename.mw"); + print $file "$content"; + close ($file); + +} + +# wiki_delete_page <page_name> +# +# delete the page with name <page_name> from the wiki referenced +# in the global variable $mw +sub wiki_delete_page { + my $pagename = $_[0]; + + my $exist=$mw->get_page({title => $pagename}); + + if (defined($exist->{'*'})){ + $mw->edit({ action => 'delete', + title => $pagename}) + || die $mw->{error}->{code} . ": " . $mw->{error}->{details}; + } else { + die "no page with such name found: $pagename\n"; + } +} + +# wiki_editpage <wiki_page> <wiki_content> <wiki_append> [-c=<category>] [-s=<summary>] +# +# Edit a page named <wiki_page> with content <wiki_content> on the wiki +# referenced with the global variable $mw +# If <wiki_append> == true : append <wiki_content> at the end of the actual +# content of the page <wiki_page> +# If <wik_page> doesn't exist, that page is created with the <wiki_content> +sub wiki_editpage { + my $wiki_page = $_[0]; + my $wiki_content = $_[1]; + my $wiki_append = $_[2]; + my $summary = ""; + my ($summ, $cat) = (); + GetOptions('s=s' => \$summ, 'c=s' => \$cat); + + my $append = 0; + if (defined($wiki_append) && $wiki_append eq 'true') { + $append=1; + } + + my $previous_text =""; + + if ($append) { + my $ref = $mw->get_page( { title => $wiki_page } ); + $previous_text = $ref->{'*'}; + } + + my $text = $wiki_content; + if (defined($previous_text)) { + $text="$previous_text$text"; + } + + # Eventually, add this page to a category. + if (defined($cat)) { + my $category_name="[[Category:$cat]]"; + $text="$text\n $category_name"; + } + if(defined($summ)){ + $summary=$summ; + } + + $mw->edit( { action => 'edit', title => $wiki_page, summary => $summary, text => "$text"} ); +} + +# wiki_getallpagename [<category>] +# +# Fetch all pages of the wiki referenced by the global variable $mw +# and print the names of each one in the file all.txt with a new line +# ("\n") between these. +# If the argument <category> is defined, then this function get only the pages +# belonging to <category>. +sub wiki_getallpagename { + # fetch the pages of the wiki + if (defined($_[0])) { + my $mw_pages = $mw->list ( { action => 'query', + list => 'categorymembers', + cmtitle => "Category:$_[0]", + cmnamespace => 0, + cmlimit => 500 }, + ) + || die $mw->{error}->{code}.": ".$mw->{error}->{details}; + open(my $file, ">all.txt"); + foreach my $page (@{$mw_pages}) { + print $file "$page->{title}\n"; + } + close ($file); + + } else { + my $mw_pages = $mw->list({ + action => 'query', + list => 'allpages', + aplimit => 500, + }) + || die $mw->{error}->{code}.": ".$mw->{error}->{details}; + open(my $file, ">all.txt"); + foreach my $page (@{$mw_pages}) { + print $file "$page->{title}\n"; + } + close ($file); + } +} + +sub wiki_upload_file { + my $file_name = $_[0]; + my $resultat = $mw->edit ( { + action => 'upload', + filename => $file_name, + comment => 'upload a file', + file => [ $file_name ], + ignorewarnings=>1, + }, { + skip_encoding => 1 + } ) || die $mw->{error}->{code} . ' : ' . $mw->{error}->{details}; +} + + + +# Main part of this script: parse the command line arguments +# and select which function to execute +my $fct_to_call = shift; + +wiki_login($wiki_admin, $wiki_admin_pass); + +my %functions_to_call = qw( + upload_file wiki_upload_file + get_page wiki_getpage + delete_page wiki_delete_page + edit_page wiki_editpage + getallpagename wiki_getallpagename +); +die "$0 ERROR: wrong argument" unless exists $functions_to_call{$fct_to_call}; +&{$functions_to_call{$fct_to_call}}(@ARGV); diff --git a/contrib/mw-to-git/t/test.config b/contrib/mw-to-git/t/test.config new file mode 100644 index 0000000000..958b37b4a7 --- /dev/null +++ b/contrib/mw-to-git/t/test.config @@ -0,0 +1,35 @@ +# Name of the web server's directory dedicated to the wiki is WIKI_DIR_NAME +WIKI_DIR_NAME=wiki + +# Login and password of the wiki's admin +WIKI_ADMIN=WikiAdmin +WIKI_PASSW=AdminPass + +# Address of the web server +SERVER_ADDR=localhost + +# SQLite database of the wiki, named DB_FILE, is located in TMP +TMP=/tmp +DB_FILE=wikidb.sqlite + +# If LIGHTTPD is not set to true, the script will use the defaut +# web server running in WIKI_DIR_INST. +WIKI_DIR_INST=/var/www + +# If LIGHTTPD is set to true, the script will use Lighttpd to run +# the wiki. +LIGHTTPD=true + +# The variables below are useful only if LIGHTTPD is set to true. +PORT=1234 +PHP_DIR=/usr/bin +LIGHTTPD_DIR=/usr/sbin +WEB=WEB +WEB_TMP=$WEB/tmp +WEB_WWW=$WEB/www + +# The variables below are used by the script to install a wiki. +# You should not modify these unless you are modifying the script itself. +MW_VERSION=mediawiki-1.19.0 +FILES_FOLDER=install-wiki +DB_INSTALL_SCRIPT=db_install.php diff --git a/contrib/p4import/git-p4import.py b/contrib/p4import/git-p4import.py index 0f3d97b67e..b6e534b65b 100644 --- a/contrib/p4import/git-p4import.py +++ b/contrib/p4import/git-p4import.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # This tool is copyright (c) 2006, Sean Estabrooks. # It is released under the Gnu Public License, version 2. diff --git a/contrib/persistent-https/LICENSE b/contrib/persistent-https/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/contrib/persistent-https/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contrib/persistent-https/Makefile b/contrib/persistent-https/Makefile new file mode 100644 index 0000000000..92baa3beee --- /dev/null +++ b/contrib/persistent-https/Makefile @@ -0,0 +1,38 @@ +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +BUILD_LABEL=$(shell date +"%s") +TAR_OUT=$(shell go env GOOS)_$(shell go env GOARCH).tar.gz + +all: git-remote-persistent-https git-remote-persistent-https--proxy \ + git-remote-persistent-http + +git-remote-persistent-https--proxy: git-remote-persistent-https + ln -f -s git-remote-persistent-https git-remote-persistent-https--proxy + +git-remote-persistent-http: git-remote-persistent-https + ln -f -s git-remote-persistent-https git-remote-persistent-http + +git-remote-persistent-https: + go build -o git-remote-persistent-https \ + -ldflags "-X main._BUILD_EMBED_LABEL $(BUILD_LABEL)" + +clean: + rm -f git-remote-persistent-http* *.tar.gz + +tar: clean all + @chmod 555 git-remote-persistent-https + @tar -czf $(TAR_OUT) git-remote-persistent-http* README LICENSE + @echo + @echo "Created $(TAR_OUT)" diff --git a/contrib/persistent-https/README b/contrib/persistent-https/README new file mode 100644 index 0000000000..f784dd2e66 --- /dev/null +++ b/contrib/persistent-https/README @@ -0,0 +1,62 @@ +git-remote-persistent-https + +The git-remote-persistent-https binary speeds up SSL operations +by running a daemon job (git-remote-persistent-https--proxy) that +keeps a connection open to a server. + + +PRE-BUILT BINARIES + +Darwin amd64: +https://commondatastorage.googleapis.com/git-remote-persistent-https/darwin_amd64.tar.gz + +Linux amd64: +https://commondatastorage.googleapis.com/git-remote-persistent-https/linux_amd64.tar.gz + + +INSTALLING + +Move all of the git-remote-persistent-http* binaries to a directory +in PATH. + + +USAGE + +HTTPS requests can be delegated to the proxy by using the +"persistent-https" scheme, e.g. + +git clone persistent-https://kernel.googlesource.com/pub/scm/git/git + +Likewise, .gitconfig can be updated as follows to rewrite https urls +to use persistent-https: + +[url "persistent-https"] + insteadof = https +[url "persistent-http"] + insteadof = http + + +##################################################################### +# BUILDING FROM SOURCE +##################################################################### + +LOCATION + +The source is available in the contrib/persistent-https directory of +the Git source repository. The Git source repository is available at +git://git.kernel.org/pub/scm/git/git.git/ +https://kernel.googlesource.com/pub/scm/git/git + + +PREREQUISITES + +The code is written in Go (http://golang.org/) and the Go compiler is +required. Currently, the compiler must be built and installed from tip +of source, in order to include a fix in the reverse http proxy: +http://code.google.com/p/go/source/detail?r=a615b796570a2cd8591884767a7d67ede74f6648 + + +BUILDING + +Run "make" to build the binaries. See the section on +INSTALLING above. diff --git a/contrib/persistent-https/client.go b/contrib/persistent-https/client.go new file mode 100644 index 0000000000..71125b5832 --- /dev/null +++ b/contrib/persistent-https/client.go @@ -0,0 +1,189 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "errors" + "fmt" + "net" + "net/url" + "os" + "os/exec" + "strings" + "syscall" + "time" +) + +type Client struct { + ProxyBin string + Args []string + + insecure bool +} + +func (c *Client) Run() error { + if err := c.resolveArgs(); err != nil { + return fmt.Errorf("resolveArgs() got error: %v", err) + } + + // Connect to the proxy. + uconn, hconn, addr, err := c.connect() + if err != nil { + return fmt.Errorf("connect() got error: %v", err) + } + // Keep the unix socket connection open for the duration of the request. + defer uconn.Close() + // Keep a connection to the HTTP server open, so no other user can + // bind on the same address so long as the process is running. + defer hconn.Close() + + // Start the git-remote-http subprocess. + cargs := []string{"-c", fmt.Sprintf("http.proxy=%v", addr), "remote-http"} + cargs = append(cargs, c.Args...) + cmd := exec.Command("git", cargs...) + + for _, v := range os.Environ() { + if !strings.HasPrefix(v, "GIT_PERSISTENT_HTTPS_SECURE=") { + cmd.Env = append(cmd.Env, v) + } + } + // Set the GIT_PERSISTENT_HTTPS_SECURE environment variable when + // the proxy is using a SSL connection. This allows credential helpers + // to identify secure proxy connections, despite being passed an HTTP + // scheme. + if !c.insecure { + cmd.Env = append(cmd.Env, "GIT_PERSISTENT_HTTPS_SECURE=1") + } + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + if stat, ok := eerr.ProcessState.Sys().(syscall.WaitStatus); ok && stat.ExitStatus() != 0 { + os.Exit(stat.ExitStatus()) + } + } + return fmt.Errorf("git-remote-http subprocess got error: %v", err) + } + return nil +} + +func (c *Client) connect() (uconn net.Conn, hconn net.Conn, addr string, err error) { + uconn, err = DefaultSocket.Dial() + if err != nil { + if e, ok := err.(*net.OpError); ok && (os.IsNotExist(e.Err) || e.Err == syscall.ECONNREFUSED) { + if err = c.startProxy(); err == nil { + uconn, err = DefaultSocket.Dial() + } + } + if err != nil { + return + } + } + + if addr, err = c.readAddr(uconn); err != nil { + return + } + + // Open a tcp connection to the proxy. + if hconn, err = net.Dial("tcp", addr); err != nil { + return + } + + // Verify the address hasn't changed ownership. + var addr2 string + if addr2, err = c.readAddr(uconn); err != nil { + return + } else if addr != addr2 { + err = fmt.Errorf("address changed after connect. got %q, want %q", addr2, addr) + return + } + return +} + +func (c *Client) readAddr(conn net.Conn) (string, error) { + conn.SetDeadline(time.Now().Add(5 * time.Second)) + data := make([]byte, 100) + n, err := conn.Read(data) + if err != nil { + return "", fmt.Errorf("error reading unix socket: %v", err) + } else if n == 0 { + return "", errors.New("empty data response") + } + conn.Write([]byte{1}) // Ack + + var addr string + if addrs := strings.Split(string(data[:n]), "\n"); len(addrs) != 2 { + return "", fmt.Errorf("got %q, wanted 2 addresses", data[:n]) + } else if c.insecure { + addr = addrs[1] + } else { + addr = addrs[0] + } + return addr, nil +} + +func (c *Client) startProxy() error { + cmd := exec.Command(c.ProxyBin) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + defer stdout.Close() + if err := cmd.Start(); err != nil { + return err + } + result := make(chan error) + go func() { + bytes, _, err := bufio.NewReader(stdout).ReadLine() + if line := string(bytes); err == nil && line != "OK" { + err = fmt.Errorf("proxy returned %q, want \"OK\"", line) + } + result <- err + }() + select { + case err := <-result: + return err + case <-time.After(5 * time.Second): + return errors.New("timeout waiting for proxy to start") + } + panic("not reachable") +} + +func (c *Client) resolveArgs() error { + if nargs := len(c.Args); nargs == 0 { + return errors.New("remote needed") + } else if nargs > 2 { + return fmt.Errorf("want at most 2 args, got %v", c.Args) + } + + // Rewrite the url scheme to be http. + idx := len(c.Args) - 1 + rawurl := c.Args[idx] + rurl, err := url.Parse(rawurl) + if err != nil { + return fmt.Errorf("invalid remote: %v", err) + } + c.insecure = rurl.Scheme == "persistent-http" + rurl.Scheme = "http" + c.Args[idx] = rurl.String() + if idx != 0 && c.Args[0] == rawurl { + c.Args[0] = c.Args[idx] + } + return nil +} diff --git a/contrib/persistent-https/main.go b/contrib/persistent-https/main.go new file mode 100644 index 0000000000..fd1b107743 --- /dev/null +++ b/contrib/persistent-https/main.go @@ -0,0 +1,82 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The git-remote-persistent-https binary speeds up SSL operations by running +// a daemon job that keeps a connection open to a Git server. This ensures the +// git-remote-persistent-https--proxy is running and delegating execution +// to the git-remote-http binary with the http_proxy set to the daemon job. +// A unix socket is used to authenticate the proxy and discover the +// HTTP address. Note, both the client and proxy are included in the same +// binary. +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strings" + "time" +) + +var ( + forceProxy = flag.Bool("proxy", false, "Whether to start the binary in proxy mode") + proxyBin = flag.String("proxy_bin", "git-remote-persistent-https--proxy", "Path to the proxy binary") + printLabel = flag.Bool("print_label", false, "Prints the build label for the binary") + + // Variable that should be defined through the -X linker flag. + _BUILD_EMBED_LABEL string +) + +const ( + defaultMaxIdleDuration = 24 * time.Hour + defaultPollUpdateInterval = 15 * time.Minute +) + +func main() { + flag.Parse() + if *printLabel { + // Short circuit execution to print the build label + fmt.Println(buildLabel()) + return + } + + var err error + if *forceProxy || strings.HasSuffix(os.Args[0], "--proxy") { + log.SetPrefix("git-remote-persistent-https--proxy: ") + proxy := &Proxy{ + BuildLabel: buildLabel(), + MaxIdleDuration: defaultMaxIdleDuration, + PollUpdateInterval: defaultPollUpdateInterval, + } + err = proxy.Run() + } else { + log.SetPrefix("git-remote-persistent-https: ") + client := &Client{ + ProxyBin: *proxyBin, + Args: flag.Args(), + } + err = client.Run() + } + if err != nil { + log.Fatalln(err) + } +} + +func buildLabel() string { + if _BUILD_EMBED_LABEL == "" { + log.Println(`unlabeled build; build with "make" to label`) + } + return _BUILD_EMBED_LABEL +} diff --git a/contrib/persistent-https/proxy.go b/contrib/persistent-https/proxy.go new file mode 100644 index 0000000000..bb0cdba386 --- /dev/null +++ b/contrib/persistent-https/proxy.go @@ -0,0 +1,190 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "os" + "os/exec" + "os/signal" + "sync" + "syscall" + "time" +) + +type Proxy struct { + BuildLabel string + MaxIdleDuration time.Duration + PollUpdateInterval time.Duration + + ul net.Listener + httpAddr string + httpsAddr string +} + +func (p *Proxy) Run() error { + hl, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("http listen failed: %v", err) + } + defer hl.Close() + + hsl, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("https listen failed: %v", err) + } + defer hsl.Close() + + p.ul, err = DefaultSocket.Listen() + if err != nil { + c, derr := DefaultSocket.Dial() + if derr == nil { + c.Close() + fmt.Println("OK\nA proxy is already running... exiting") + return nil + } else if e, ok := derr.(*net.OpError); ok && e.Err == syscall.ECONNREFUSED { + // Nothing is listening on the socket, unlink it and try again. + syscall.Unlink(DefaultSocket.Path()) + p.ul, err = DefaultSocket.Listen() + } + if err != nil { + return fmt.Errorf("unix listen failed on %v: %v", DefaultSocket.Path(), err) + } + } + defer p.ul.Close() + go p.closeOnSignal() + go p.closeOnUpdate() + + p.httpAddr = hl.Addr().String() + p.httpsAddr = hsl.Addr().String() + fmt.Printf("OK\nListening on unix socket=%v http=%v https=%v\n", + p.ul.Addr(), p.httpAddr, p.httpsAddr) + + result := make(chan error, 2) + go p.serveUnix(result) + go func() { + result <- http.Serve(hl, &httputil.ReverseProxy{ + FlushInterval: 500 * time.Millisecond, + Director: func(r *http.Request) {}, + }) + }() + go func() { + result <- http.Serve(hsl, &httputil.ReverseProxy{ + FlushInterval: 500 * time.Millisecond, + Director: func(r *http.Request) { + r.URL.Scheme = "https" + }, + }) + }() + return <-result +} + +type socketContext struct { + sync.WaitGroup + mutex sync.Mutex + last time.Time +} + +func (sc *socketContext) Done() { + sc.mutex.Lock() + defer sc.mutex.Unlock() + sc.last = time.Now() + sc.WaitGroup.Done() +} + +func (p *Proxy) serveUnix(result chan<- error) { + sockCtx := &socketContext{} + go p.closeOnIdle(sockCtx) + + var err error + for { + var uconn net.Conn + uconn, err = p.ul.Accept() + if err != nil { + err = fmt.Errorf("accept failed: %v", err) + break + } + sockCtx.Add(1) + go p.handleUnixConn(sockCtx, uconn) + } + sockCtx.Wait() + result <- err +} + +func (p *Proxy) handleUnixConn(sockCtx *socketContext, uconn net.Conn) { + defer sockCtx.Done() + defer uconn.Close() + data := []byte(fmt.Sprintf("%v\n%v", p.httpsAddr, p.httpAddr)) + uconn.SetDeadline(time.Now().Add(5 * time.Second)) + for i := 0; i < 2; i++ { + if n, err := uconn.Write(data); err != nil { + log.Printf("error sending http addresses: %+v\n", err) + return + } else if n != len(data) { + log.Printf("sent %d data bytes, wanted %d\n", n, len(data)) + return + } + if _, err := uconn.Read([]byte{0, 0, 0, 0}); err != nil { + log.Printf("error waiting for Ack: %+v\n", err) + return + } + } + // Wait without a deadline for the client to finish via EOF + uconn.SetDeadline(time.Time{}) + uconn.Read([]byte{0, 0, 0, 0}) +} + +func (p *Proxy) closeOnIdle(sockCtx *socketContext) { + for d := p.MaxIdleDuration; d > 0; { + time.Sleep(d) + sockCtx.Wait() + sockCtx.mutex.Lock() + if d = sockCtx.last.Add(p.MaxIdleDuration).Sub(time.Now()); d <= 0 { + log.Println("graceful shutdown from idle timeout") + p.ul.Close() + } + sockCtx.mutex.Unlock() + } +} + +func (p *Proxy) closeOnUpdate() { + for { + time.Sleep(p.PollUpdateInterval) + if out, err := exec.Command(os.Args[0], "--print_label").Output(); err != nil { + log.Printf("error polling for updated binary: %v\n", err) + } else if s := string(out[:len(out)-1]); p.BuildLabel != s { + log.Printf("graceful shutdown from updated binary: %q --> %q\n", p.BuildLabel, s) + p.ul.Close() + break + } + } +} + +func (p *Proxy) closeOnSignal() { + ch := make(chan os.Signal, 10) + signal.Notify(ch, os.Interrupt, os.Kill, os.Signal(syscall.SIGTERM), os.Signal(syscall.SIGHUP)) + sig := <-ch + p.ul.Close() + switch sig { + case os.Signal(syscall.SIGHUP): + log.Printf("graceful shutdown from signal: %v\n", sig) + default: + log.Fatalf("exiting from signal: %v\n", sig) + } +} diff --git a/contrib/persistent-https/socket.go b/contrib/persistent-https/socket.go new file mode 100644 index 0000000000..193b911dd1 --- /dev/null +++ b/contrib/persistent-https/socket.go @@ -0,0 +1,97 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "net" + "os" + "path/filepath" + "syscall" +) + +// A Socket is a wrapper around a Unix socket that verifies directory +// permissions. +type Socket struct { + Dir string +} + +func defaultDir() string { + sockPath := ".git-credential-cache" + if home := os.Getenv("HOME"); home != "" { + return filepath.Join(home, sockPath) + } + log.Printf("socket: cannot find HOME path. using relative directory %q for socket", sockPath) + return sockPath +} + +// DefaultSocket is a Socket in the $HOME/.git-credential-cache directory. +var DefaultSocket = Socket{Dir: defaultDir()} + +// Listen announces the local network address of the unix socket. The +// permissions on the socket directory are verified before attempting +// the actual listen. +func (s Socket) Listen() (net.Listener, error) { + network, addr := "unix", s.Path() + if err := s.mkdir(); err != nil { + return nil, &net.OpError{Op: "listen", Net: network, Addr: &net.UnixAddr{Name: addr, Net: network}, Err: err} + } + return net.Listen(network, addr) +} + +// Dial connects to the unix socket. The permissions on the socket directory +// are verified before attempting the actual dial. +func (s Socket) Dial() (net.Conn, error) { + network, addr := "unix", s.Path() + if err := s.checkPermissions(); err != nil { + return nil, &net.OpError{Op: "dial", Net: network, Addr: &net.UnixAddr{Name: addr, Net: network}, Err: err} + } + return net.Dial(network, addr) +} + +// Path returns the fully specified file name of the unix socket. +func (s Socket) Path() string { + return filepath.Join(s.Dir, "persistent-https-proxy-socket") +} + +func (s Socket) mkdir() error { + if err := s.checkPermissions(); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + if err := os.MkdirAll(s.Dir, 0700); err != nil { + return err + } + return s.checkPermissions() +} + +func (s Socket) checkPermissions() error { + fi, err := os.Stat(s.Dir) + if err != nil { + return err + } + if !fi.IsDir() { + return fmt.Errorf("socket: got file, want directory for %q", s.Dir) + } + if fi.Mode().Perm() != 0700 { + return fmt.Errorf("socket: got perm %o, want 700 for %q", fi.Mode().Perm(), s.Dir) + } + if st := fi.Sys().(*syscall.Stat_t); int(st.Uid) != os.Getuid() { + return fmt.Errorf("socket: got uid %d, want %d for %q", st.Uid, os.Getuid(), s.Dir) + } + return nil +} diff --git a/contrib/remotes2config.sh b/contrib/remotes2config.sh index 5838b3ab05..1cda19f66a 100644..100755 --- a/contrib/remotes2config.sh +++ b/contrib/remotes2config.sh @@ -11,11 +11,11 @@ if [ -d "$GIT_DIR"/remotes ]; then { cd "$GIT_DIR"/remotes ls | while read f; do - name=$(printf "$f" | tr -c "A-Za-z0-9" ".") + name=$(printf "$f" | tr -c "A-Za-z0-9-" ".") sed -n \ - -e "s/^URL: \(.*\)$/remote.$name.url \1 ./p" \ - -e "s/^Pull: \(.*\)$/remote.$name.fetch \1 ^$ /p" \ - -e "s/^Push: \(.*\)$/remote.$name.push \1 ^$ /p" \ + -e "s/^URL:[ ]*\(.*\)$/remote.$name.url \1 ./p" \ + -e "s/^Pull:[ ]*\(.*\)$/remote.$name.fetch \1 ^$ /p" \ + -e "s/^Push:[ ]*\(.*\)$/remote.$name.push \1 ^$ /p" \ < "$f" done echo done diff --git a/contrib/rerere-train.sh b/contrib/rerere-train.sh new file mode 100755 index 0000000000..36b6feebe0 --- /dev/null +++ b/contrib/rerere-train.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Copyright (c) 2008, Nanako Shiraishi +# Prime rerere database from existing merge commits + +me=rerere-train +USAGE="$me rev-list-args" + +SUBDIRECTORY_OK=Yes +OPTIONS_SPEC= +. $(git --exec-path)/git-sh-setup +require_work_tree +cd_to_toplevel + +# Remember original branch +branch=$(git symbolic-ref -q HEAD) || +original_HEAD=$(git rev-parse --verify HEAD) || { + echo >&2 "Not on any branch and no commit yet?" + exit 1 +} + +mkdir -p "$GIT_DIR/rr-cache" || exit + +git rev-list --parents "$@" | +while read commit parent1 other_parents +do + if test -z "$other_parents" + then + # Skip non-merges + continue + fi + git checkout -q "$parent1^0" + if git merge $other_parents >/dev/null 2>&1 + then + # Cleanly merges + continue + fi + if test -s "$GIT_DIR/MERGE_RR" + then + git show -s --pretty=format:"Learning from %h %s" "$commit" + git rerere + git checkout -q $commit -- . + git rerere + fi + git reset -q --hard +done + +if test -z "$branch" +then + git checkout "$original_HEAD" +else + git checkout "${branch#refs/heads/}" +fi diff --git a/contrib/stats/packinfo.pl b/contrib/stats/packinfo.pl index aab501ea08..be188c0f11 100755 --- a/contrib/stats/packinfo.pl +++ b/contrib/stats/packinfo.pl @@ -1,9 +1,9 @@ #!/usr/bin/perl # # This tool will print vaguely pretty information about a pack. It -# expects the output of "git-verify-pack -v" as input on stdin. +# expects the output of "git verify-pack -v" as input on stdin. # -# $ git-verify-pack -v | packinfo.pl +# $ git verify-pack -v | packinfo.pl # # This prints some full-pack statistics; currently "all sizes", "all # path sizes", "tree sizes", "tree path sizes", and "depths". @@ -20,7 +20,7 @@ # # When run as: # -# $ git-verify-pack -v | packinfo.pl -tree +# $ git verify-pack -v | packinfo.pl -tree # # the trees of objects are output along with the stats. This looks # like: @@ -43,7 +43,7 @@ # # When run as: # -# $ git-verify-pack -v | packinfo.pl -tree -filenames +# $ git verify-pack -v | packinfo.pl -tree -filenames # # it adds filenames to the tree. Getting this information is slow: # @@ -58,7 +58,7 @@ # # When run as: # -# $ git-verify-pack -v | packinfo.pl -dump +# $ git verify-pack -v | packinfo.pl -dump # # it prints out "sha1 size pathsize depth" for each sha1 in lexical # order. @@ -93,7 +93,7 @@ my %depths; my @depths; while (<STDIN>) { - my ($sha1, $type, $size, $offset, $depth, $parent) = split(/\s+/, $_); + my ($sha1, $type, $size, $space, $offset, $depth, $parent) = split(/\s+/, $_); next unless ($sha1 =~ /^[0-9a-f]{40}$/); $depths{$sha1} = $depth || 0; push(@depths, $depth || 0); @@ -106,7 +106,7 @@ while (<STDIN>) { } if ($filenames && ($tree || $dump)) { - open(NAMES, "git-name-rev --all|"); + open(NAMES, "git name-rev --all|"); while (<NAMES>) { if (/^(\S+)\s+(.*)$/) { my ($sha1, $name) = ($1, $2); @@ -117,7 +117,7 @@ if ($filenames && ($tree || $dump)) { for my $commit (@commits) { my $name = $names{$commit}; - open(TREE, "git-ls-tree -t -r $commit|"); + open(TREE, "git ls-tree -t -r $commit|"); print STDERR "Plumbing tree $name\n"; while (<TREE>) { if (/^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/) { diff --git a/contrib/subtree/.gitignore b/contrib/subtree/.gitignore new file mode 100644 index 0000000000..7e77c9d022 --- /dev/null +++ b/contrib/subtree/.gitignore @@ -0,0 +1,5 @@ +*~ +git-subtree.xml +git-subtree.1 +mainline +subproj diff --git a/contrib/subtree/COPYING b/contrib/subtree/COPYING new file mode 100644 index 0000000000..d511905c16 --- /dev/null +++ b/contrib/subtree/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/contrib/subtree/INSTALL b/contrib/subtree/INSTALL new file mode 100644 index 0000000000..7ab0cf4509 --- /dev/null +++ b/contrib/subtree/INSTALL @@ -0,0 +1,28 @@ +HOW TO INSTALL git-subtree +========================== + +First, build from the top source directory. + +Then, in contrib/subtree, run: + + make + make install + make install-doc + +If you used configure to do the main build the git-subtree build will +pick up those settings. If not, you will likely have to provide a +value for prefix: + + make prefix=<some dir> + make prefix=<some dir> install + make prefix=<some dir> install-doc + +To run tests first copy git-subtree to the main build area so the +newly-built git can find it: + + cp git-subtree ../.. + +Then: + + make test + diff --git a/contrib/subtree/Makefile b/contrib/subtree/Makefile new file mode 100644 index 0000000000..05cdd5c9b2 --- /dev/null +++ b/contrib/subtree/Makefile @@ -0,0 +1,52 @@ +-include ../../config.mak.autogen +-include ../../config.mak + +prefix ?= /usr/local +mandir ?= $(prefix)/share/man +libexecdir ?= $(prefix)/libexec/git-core +gitdir ?= $(shell git --exec-path) +man1dir ?= $(mandir)/man1 + +gitver ?= $(word 3,$(shell git --version)) + +# this should be set to a 'standard' bsd-type install program +INSTALL ?= install + +ASCIIDOC_CONF = ../../Documentation/asciidoc.conf +MANPAGE_NORMAL_XSL = ../../Documentation/manpage-normal.xsl + +GIT_SUBTREE_SH := git-subtree.sh +GIT_SUBTREE := git-subtree + +GIT_SUBTREE_DOC := git-subtree.1 +GIT_SUBTREE_XML := git-subtree.xml +GIT_SUBTREE_TXT := git-subtree.txt + +all: $(GIT_SUBTREE) + +$(GIT_SUBTREE): $(GIT_SUBTREE_SH) + cp $< $@ && chmod +x $@ + +doc: $(GIT_SUBTREE_DOC) + +install: $(GIT_SUBTREE) + $(INSTALL) -m 755 $(GIT_SUBTREE) $(libexecdir) + +install-doc: install-man + +install-man: $(GIT_SUBTREE_DOC) + $(INSTALL) -m 644 $^ $(man1dir) + +$(GIT_SUBTREE_DOC): $(GIT_SUBTREE_XML) + xmlto -m $(MANPAGE_NORMAL_XSL) man $^ + +$(GIT_SUBTREE_XML): $(GIT_SUBTREE_TXT) + asciidoc -b docbook -d manpage -f $(ASCIIDOC_CONF) \ + -agit_version=$(gitver) $^ + +test: + $(MAKE) -C t/ test + +clean: + rm -f *~ *.xml *.html *.1 + rm -rf subproj mainline diff --git a/contrib/subtree/README b/contrib/subtree/README new file mode 100644 index 0000000000..c686b4a69b --- /dev/null +++ b/contrib/subtree/README @@ -0,0 +1,8 @@ + +Please read git-subtree.txt for documentation. + +Please don't contact me using github mail; it's slow, ugly, and worst of +all, redundant. Email me instead at apenwarr@gmail.com and I'll be happy to +help. + +Avery diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh new file mode 100755 index 0000000000..920c664bb7 --- /dev/null +++ b/contrib/subtree/git-subtree.sh @@ -0,0 +1,712 @@ +#!/bin/bash +# +# git-subtree.sh: split/join git repositories in subdirectories of this one +# +# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com> +# +if [ $# -eq 0 ]; then + set -- -h +fi +OPTS_SPEC="\ +git subtree add --prefix=<prefix> <commit> +git subtree merge --prefix=<prefix> <commit> +git subtree pull --prefix=<prefix> <repository> <refspec...> +git subtree push --prefix=<prefix> <repository> <refspec...> +git subtree split --prefix=<prefix> <commit...> +-- +h,help show the help +q quiet +d show debug messages +P,prefix= the name of the subdir to split out +m,message= use the given message as the commit message for the merge commit + options for 'split' +annotate= add a prefix to commit message of new commits +b,branch= create a new branch from the split subtree +ignore-joins ignore prior --rejoin commits +onto= try connecting new tree to an existing one +rejoin merge the new branch back into HEAD + options for 'add', 'merge', 'pull' and 'push' +squash merge subtree changes as a single commit +" +eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" + +PATH=$PATH:$(git --exec-path) +. git-sh-setup + +require_work_tree + +quiet= +branch= +debug= +command= +onto= +rejoin= +ignore_joins= +annotate= +squash= +message= + +debug() +{ + if [ -n "$debug" ]; then + echo "$@" >&2 + fi +} + +say() +{ + if [ -z "$quiet" ]; then + echo "$@" >&2 + fi +} + +assert() +{ + if "$@"; then + : + else + die "assertion failed: " "$@" + fi +} + + +#echo "Options: $*" + +while [ $# -gt 0 ]; do + opt="$1" + shift + case "$opt" in + -q) quiet=1 ;; + -d) debug=1 ;; + --annotate) annotate="$1"; shift ;; + --no-annotate) annotate= ;; + -b) branch="$1"; shift ;; + -P) prefix="$1"; shift ;; + -m) message="$1"; shift ;; + --no-prefix) prefix= ;; + --onto) onto="$1"; shift ;; + --no-onto) onto= ;; + --rejoin) rejoin=1 ;; + --no-rejoin) rejoin= ;; + --ignore-joins) ignore_joins=1 ;; + --no-ignore-joins) ignore_joins= ;; + --squash) squash=1 ;; + --no-squash) squash= ;; + --) break ;; + *) die "Unexpected option: $opt" ;; + esac +done + +command="$1" +shift +case "$command" in + add|merge|pull) default= ;; + split|push) default="--default HEAD" ;; + *) die "Unknown command '$command'" ;; +esac + +if [ -z "$prefix" ]; then + die "You must provide the --prefix option." +fi + +case "$command" in + add) [ -e "$prefix" ] && + die "prefix '$prefix' already exists." ;; + *) [ -e "$prefix" ] || + die "'$prefix' does not exist; use 'git subtree add'" ;; +esac + +dir="$(dirname "$prefix/.")" + +if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then + revs=$(git rev-parse $default --revs-only "$@") || exit $? + dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? + if [ -n "$dirs" ]; then + die "Error: Use --prefix instead of bare filenames." + fi +fi + +debug "command: {$command}" +debug "quiet: {$quiet}" +debug "revs: {$revs}" +debug "dir: {$dir}" +debug "opts: {$*}" +debug + +cache_setup() +{ + cachedir="$GIT_DIR/subtree-cache/$$" + rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" + mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" + mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree" + debug "Using cachedir: $cachedir" >&2 +} + +cache_get() +{ + for oldrev in $*; do + if [ -r "$cachedir/$oldrev" ]; then + read newrev <"$cachedir/$oldrev" + echo $newrev + fi + done +} + +cache_miss() +{ + for oldrev in $*; do + if [ ! -r "$cachedir/$oldrev" ]; then + echo $oldrev + fi + done +} + +check_parents() +{ + missed=$(cache_miss $*) + for miss in $missed; do + if [ ! -r "$cachedir/notree/$miss" ]; then + debug " incorrect order: $miss" + fi + done +} + +set_notree() +{ + echo "1" > "$cachedir/notree/$1" +} + +cache_set() +{ + oldrev="$1" + newrev="$2" + if [ "$oldrev" != "latest_old" \ + -a "$oldrev" != "latest_new" \ + -a -e "$cachedir/$oldrev" ]; then + die "cache for $oldrev already exists!" + fi + echo "$newrev" >"$cachedir/$oldrev" +} + +rev_exists() +{ + if git rev-parse "$1" >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +rev_is_descendant_of_branch() +{ + newrev="$1" + branch="$2" + branch_hash=$(git rev-parse $branch) + match=$(git rev-list -1 $branch_hash ^$newrev) + + if [ -z "$match" ]; then + return 0 + else + return 1 + fi +} + +# if a commit doesn't have a parent, this might not work. But we only want +# to remove the parent from the rev-list, and since it doesn't exist, it won't +# be there anyway, so do nothing in that case. +try_remove_previous() +{ + if rev_exists "$1^"; then + echo "^$1^" + fi +} + +find_latest_squash() +{ + debug "Looking for latest squash ($dir)..." + dir="$1" + sq= + main= + sub= + git log --grep="^git-subtree-dir: $dir/*\$" \ + --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD | + while read a b junk; do + debug "$a $b $junk" + debug "{{$sq/$main/$sub}}" + case "$a" in + START) sq="$b" ;; + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + END) + if [ -n "$sub" ]; then + if [ -n "$main" ]; then + # a rejoin commit? + # Pretend its sub was a squash. + sq="$sub" + fi + debug "Squash found: $sq $sub" + echo "$sq" "$sub" + break + fi + sq= + main= + sub= + ;; + esac + done +} + +find_existing_splits() +{ + debug "Looking for prior splits..." + dir="$1" + revs="$2" + main= + sub= + git log --grep="^git-subtree-dir: $dir/*\$" \ + --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs | + while read a b junk; do + case "$a" in + START) sq="$b" ;; + git-subtree-mainline:) main="$b" ;; + git-subtree-split:) sub="$b" ;; + END) + debug " Main is: '$main'" + if [ -z "$main" -a -n "$sub" ]; then + # squash commits refer to a subtree + debug " Squash: $sq from $sub" + cache_set "$sq" "$sub" + fi + if [ -n "$main" -a -n "$sub" ]; then + debug " Prior: $main -> $sub" + cache_set $main $sub + cache_set $sub $sub + try_remove_previous "$main" + try_remove_previous "$sub" + fi + main= + sub= + ;; + esac + done +} + +copy_commit() +{ + # We're going to set some environment vars here, so + # do it in a subshell to get rid of them safely later + debug copy_commit "{$1}" "{$2}" "{$3}" + git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | + ( + read GIT_AUTHOR_NAME + read GIT_AUTHOR_EMAIL + read GIT_AUTHOR_DATE + read GIT_COMMITTER_NAME + read GIT_COMMITTER_EMAIL + read GIT_COMMITTER_DATE + export GIT_AUTHOR_NAME \ + GIT_AUTHOR_EMAIL \ + GIT_AUTHOR_DATE \ + GIT_COMMITTER_NAME \ + GIT_COMMITTER_EMAIL \ + GIT_COMMITTER_DATE + (echo -n "$annotate"; cat ) | + git commit-tree "$2" $3 # reads the rest of stdin + ) || die "Can't copy commit $1" +} + +add_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Add '$dir/' from commit '$latest_new'" + fi + cat <<-EOF + $commit_message + + git-subtree-dir: $dir + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new + EOF +} + +add_squashed_msg() +{ + if [ -n "$message" ]; then + echo "$message" + else + echo "Merge commit '$1' as '$2'" + fi +} + +rejoin_msg() +{ + dir="$1" + latest_old="$2" + latest_new="$3" + if [ -n "$message" ]; then + commit_message="$message" + else + commit_message="Split '$dir/' into commit '$latest_new'" + fi + cat <<-EOF + $commit_message + + git-subtree-dir: $dir + git-subtree-mainline: $latest_old + git-subtree-split: $latest_new + EOF +} + +squash_msg() +{ + dir="$1" + oldsub="$2" + newsub="$3" + newsub_short=$(git rev-parse --short "$newsub") + + if [ -n "$oldsub" ]; then + oldsub_short=$(git rev-parse --short "$oldsub") + echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short" + echo + git log --pretty=tformat:'%h %s' "$oldsub..$newsub" + git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub" + else + echo "Squashed '$dir/' content from commit $newsub_short" + fi + + echo + echo "git-subtree-dir: $dir" + echo "git-subtree-split: $newsub" +} + +toptree_for_commit() +{ + commit="$1" + git log -1 --pretty=format:'%T' "$commit" -- || exit $? +} + +subtree_for_commit() +{ + commit="$1" + dir="$2" + git ls-tree "$commit" -- "$dir" | + while read mode type tree name; do + assert [ "$name" = "$dir" ] + assert [ "$type" = "tree" -o "$type" = "commit" ] + [ "$type" = "commit" ] && continue # ignore submodules + echo $tree + break + done +} + +tree_changed() +{ + tree=$1 + shift + if [ $# -ne 1 ]; then + return 0 # weird parents, consider it changed + else + ptree=$(toptree_for_commit $1) + if [ "$ptree" != "$tree" ]; then + return 0 # changed + else + return 1 # not changed + fi + fi +} + +new_squash_commit() +{ + old="$1" + oldsub="$2" + newsub="$3" + tree=$(toptree_for_commit $newsub) || exit $? + if [ -n "$old" ]; then + squash_msg "$dir" "$oldsub" "$newsub" | + git commit-tree "$tree" -p "$old" || exit $? + else + squash_msg "$dir" "" "$newsub" | + git commit-tree "$tree" || exit $? + fi +} + +copy_or_skip() +{ + rev="$1" + tree="$2" + newparents="$3" + assert [ -n "$tree" ] + + identical= + nonidentical= + p= + gotparents= + for parent in $newparents; do + ptree=$(toptree_for_commit $parent) || exit $? + [ -z "$ptree" ] && continue + if [ "$ptree" = "$tree" ]; then + # an identical parent could be used in place of this rev. + identical="$parent" + else + nonidentical="$parent" + fi + + # sometimes both old parents map to the same newparent; + # eliminate duplicates + is_new=1 + for gp in $gotparents; do + if [ "$gp" = "$parent" ]; then + is_new= + break + fi + done + if [ -n "$is_new" ]; then + gotparents="$gotparents $parent" + p="$p -p $parent" + fi + done + + if [ -n "$identical" ]; then + echo $identical + else + copy_commit $rev $tree "$p" || exit $? + fi +} + +ensure_clean() +{ + if ! git diff-index HEAD --exit-code --quiet 2>&1; then + die "Working tree has modifications. Cannot add." + fi + if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then + die "Index has modifications. Cannot add." + fi +} + +cmd_add() +{ + if [ -e "$dir" ]; then + die "'$dir' already exists. Cannot add." + fi + + ensure_clean + + if [ $# -eq 1 ]; then + "cmd_add_commit" "$@" + elif [ $# -eq 2 ]; then + "cmd_add_repository" "$@" + else + say "error: parameters were '$@'" + die "Provide either a refspec or a repository and refspec." + fi +} + +cmd_add_repository() +{ + echo "git fetch" "$@" + repository=$1 + refspec=$2 + git fetch "$@" || exit $? + revs=FETCH_HEAD + set -- $revs + cmd_add_commit "$@" +} + +cmd_add_commit() +{ + revs=$(git rev-parse $default --revs-only "$@") || exit $? + set -- $revs + rev="$1" + + debug "Adding $dir as '$rev'..." + git read-tree --prefix="$dir" $rev || exit $? + git checkout -- "$dir" || exit $? + tree=$(git write-tree) || exit $? + + headrev=$(git rev-parse HEAD) || exit $? + if [ -n "$headrev" -a "$headrev" != "$rev" ]; then + headp="-p $headrev" + else + headp= + fi + + if [ -n "$squash" ]; then + rev=$(new_squash_commit "" "" "$rev") || exit $? + commit=$(add_squashed_msg "$rev" "$dir" | + git commit-tree $tree $headp -p "$rev") || exit $? + else + commit=$(add_msg "$dir" "$headrev" "$rev" | + git commit-tree $tree $headp -p "$rev") || exit $? + fi + git reset "$commit" || exit $? + + say "Added dir '$dir'" +} + +cmd_split() +{ + debug "Splitting $dir..." + cache_setup || exit $? + + if [ -n "$onto" ]; then + debug "Reading history for --onto=$onto..." + git rev-list $onto | + while read rev; do + # the 'onto' history is already just the subdir, so + # any parent we find there can be used verbatim + debug " cache: $rev" + cache_set $rev $rev + done + fi + + if [ -n "$ignore_joins" ]; then + unrevs= + else + unrevs="$(find_existing_splits "$dir" "$revs")" + fi + + # We can't restrict rev-list to only $dir here, because some of our + # parents have the $dir contents the root, and those won't match. + # (and rev-list --follow doesn't seem to solve this) + grl='git rev-list --topo-order --reverse --parents $revs $unrevs' + revmax=$(eval "$grl" | wc -l) + revcount=0 + createcount=0 + eval "$grl" | + while read rev parents; do + revcount=$(($revcount + 1)) + say -n "$revcount/$revmax ($createcount)
" + debug "Processing commit: $rev" + exists=$(cache_get $rev) + if [ -n "$exists" ]; then + debug " prior: $exists" + continue + fi + createcount=$(($createcount + 1)) + debug " parents: $parents" + newparents=$(cache_get $parents) + debug " newparents: $newparents" + + tree=$(subtree_for_commit $rev "$dir") + debug " tree is: $tree" + + check_parents $parents + + # ugly. is there no better way to tell if this is a subtree + # vs. a mainline commit? Does it matter? + if [ -z $tree ]; then + set_notree $rev + if [ -n "$newparents" ]; then + cache_set $rev $rev + fi + continue + fi + + newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? + debug " newrev is: $newrev" + cache_set $rev $newrev + cache_set latest_new $newrev + cache_set latest_old $rev + done || exit $? + latest_new=$(cache_get latest_new) + if [ -z "$latest_new" ]; then + die "No new revisions were found" + fi + + if [ -n "$rejoin" ]; then + debug "Merging split branch into HEAD..." + latest_old=$(cache_get latest_old) + git merge -s ours \ + -m "$(rejoin_msg $dir $latest_old $latest_new)" \ + $latest_new >&2 || exit $? + fi + if [ -n "$branch" ]; then + if rev_exists "refs/heads/$branch"; then + if ! rev_is_descendant_of_branch $latest_new $branch; then + die "Branch '$branch' is not an ancestor of commit '$latest_new'." + fi + action='Updated' + else + action='Created' + fi + git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $? + say "$action branch '$branch'" + fi + echo $latest_new + exit 0 +} + +cmd_merge() +{ + revs=$(git rev-parse $default --revs-only "$@") || exit $? + ensure_clean + + set -- $revs + if [ $# -ne 1 ]; then + die "You must provide exactly one revision. Got: '$revs'" + fi + rev="$1" + + if [ -n "$squash" ]; then + first_split="$(find_latest_squash "$dir")" + if [ -z "$first_split" ]; then + die "Can't squash-merge: '$dir' was never added." + fi + set $first_split + old=$1 + sub=$2 + if [ "$sub" = "$rev" ]; then + say "Subtree is already at commit $rev." + exit 0 + fi + new=$(new_squash_commit "$old" "$sub" "$rev") || exit $? + debug "New squash commit: $new" + rev="$new" + fi + + version=$(git version) + if [ "$version" \< "git version 1.7" ]; then + if [ -n "$message" ]; then + git merge -s subtree --message="$message" $rev + else + git merge -s subtree $rev + fi + else + if [ -n "$message" ]; then + git merge -Xsubtree="$prefix" --message="$message" $rev + else + git merge -Xsubtree="$prefix" $rev + fi + fi +} + +cmd_pull() +{ + ensure_clean + git fetch "$@" || exit $? + revs=FETCH_HEAD + set -- $revs + cmd_merge "$@" +} + +cmd_push() +{ + if [ $# -ne 2 ]; then + die "You must provide <repository> <refspec>" + fi + if [ -e "$dir" ]; then + repository=$1 + refspec=$2 + echo "git push using: " $repository $refspec + git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec + else + die "'$dir' must already exist. Try 'git subtree add'." + fi +} + +"cmd_$command" "$@" diff --git a/contrib/subtree/git-subtree.txt b/contrib/subtree/git-subtree.txt new file mode 100644 index 0000000000..0c44fda011 --- /dev/null +++ b/contrib/subtree/git-subtree.txt @@ -0,0 +1,366 @@ +git-subtree(1) +============== + +NAME +---- +git-subtree - Merge subtrees together and split repository into subtrees + + +SYNOPSIS +-------- +[verse] +'git subtree' add -P <prefix> <commit> +'git subtree' pull -P <prefix> <repository> <refspec...> +'git subtree' push -P <prefix> <repository> <refspec...> +'git subtree' merge -P <prefix> <commit> +'git subtree' split -P <prefix> [OPTIONS] [<commit>] + + +DESCRIPTION +----------- +Subtrees allow subprojects to be included within a subdirectory +of the main project, optionally including the subproject's +entire history. + +For example, you could include the source code for a library +as a subdirectory of your application. + +Subtrees are not to be confused with submodules, which are meant for +the same task. Unlike submodules, subtrees do not need any special +constructions (like .gitmodule files or gitlinks) be present in +your repository, and do not force end-users of your +repository to do anything special or to understand how subtrees +work. A subtree is just a subdirectory that can be +committed to, branched, and merged along with your project in +any way you want. + +They are also not to be confused with using the subtree merge +strategy. The main difference is that, besides merging +the other project as a subdirectory, you can also extract the +entire history of a subdirectory from your project and make it +into a standalone project. Unlike the subtree merge strategy +you can alternate back and forth between these +two operations. If the standalone library gets updated, you can +automatically merge the changes into your project; if you +update the library inside your project, you can "split" the +changes back out again and merge them back into the library +project. + +For example, if a library you made for one application ends up being +useful elsewhere, you can extract its entire history and publish +that as its own git repository, without accidentally +intermingling the history of your application project. + +[TIP] +In order to keep your commit messages clean, we recommend that +people split their commits between the subtrees and the main +project as much as possible. That is, if you make a change that +affects both the library and the main application, commit it in +two pieces. That way, when you split the library commits out +later, their descriptions will still make sense. But if this +isn't important to you, it's not *necessary*. git subtree will +simply leave out the non-library-related parts of the commit +when it splits it out into the subproject later. + + +COMMANDS +-------- +add:: + Create the <prefix> subtree by importing its contents + from the given <refspec> or <repository> and remote <refspec>. + A new commit is created automatically, joining the imported + project's history with your own. With '--squash', imports + only a single commit from the subproject, rather than its + entire history. + +merge:: + Merge recent changes up to <commit> into the <prefix> + subtree. As with normal 'git merge', this doesn't + remove your own local changes; it just merges those + changes into the latest <commit>. With '--squash', + creates only one commit that contains all the changes, + rather than merging in the entire history. + + If you use '--squash', the merge direction doesn't + always have to be forward; you can use this command to + go back in time from v2.5 to v2.4, for example. If your + merge introduces a conflict, you can resolve it in the + usual ways. + +pull:: + Exactly like 'merge', but parallels 'git pull' in that + it fetches the given commit from the specified remote + repository. + +push:: + Does a 'split' (see above) using the <prefix> supplied + and then does a 'git push' to push the result to the + repository and refspec. This can be used to push your + subtree to different branches of the remote repository. + +split:: + Extract a new, synthetic project history from the + history of the <prefix> subtree. The new history + includes only the commits (including merges) that + affected <prefix>, and each of those commits now has the + contents of <prefix> at the root of the project instead + of in a subdirectory. Thus, the newly created history + is suitable for export as a separate git repository. + + After splitting successfully, a single commit id is + printed to stdout. This corresponds to the HEAD of the + newly created tree, which you can manipulate however you + want. + + Repeated splits of exactly the same history are + guaranteed to be identical (ie. to produce the same + commit ids). Because of this, if you add new commits + and then re-split, the new commits will be attached as + commits on top of the history you generated last time, + so 'git merge' and friends will work as expected. + + Note that if you use '--squash' when you merge, you + should usually not just '--rejoin' when you split. + + +OPTIONS +------- +-q:: +--quiet:: + Suppress unnecessary output messages on stderr. + +-d:: +--debug:: + Produce even more unnecessary output messages on stderr. + +-P <prefix>:: +--prefix=<prefix>:: + Specify the path in the repository to the subtree you + want to manipulate. This option is mandatory + for all commands. + +-m <message>:: +--message=<message>:: + This option is only valid for add, merge and pull (unsure). + Specify <message> as the commit message for the merge commit. + + +OPTIONS FOR add, merge, push, pull +---------------------------------- +--squash:: + This option is only valid for add, merge, push and pull + commands. + + Instead of merging the entire history from the subtree + project, produce only a single commit that contains all + the differences you want to merge, and then merge that + new commit into your project. + + Using this option helps to reduce log clutter. People + rarely want to see every change that happened between + v1.0 and v1.1 of the library they're using, since none of the + interim versions were ever included in their application. + + Using '--squash' also helps avoid problems when the same + subproject is included multiple times in the same + project, or is removed and then re-added. In such a + case, it doesn't make sense to combine the histories + anyway, since it's unclear which part of the history + belongs to which subtree. + + Furthermore, with '--squash', you can switch back and + forth between different versions of a subtree, rather + than strictly forward. 'git subtree merge --squash' + always adjusts the subtree to match the exactly + specified commit, even if getting to that commit would + require undoing some changes that were added earlier. + + Whether or not you use '--squash', changes made in your + local repository remain intact and can be later split + and send upstream to the subproject. + + +OPTIONS FOR split +----------------- +--annotate=<annotation>:: + This option is only valid for the split command. + + When generating synthetic history, add <annotation> as a + prefix to each commit message. Since we're creating new + commits with the same commit message, but possibly + different content, from the original commits, this can help + to differentiate them and avoid confusion. + + Whenever you split, you need to use the same + <annotation>, or else you don't have a guarantee that + the new re-created history will be identical to the old + one. That will prevent merging from working correctly. + git subtree tries to make it work anyway, particularly + if you use --rejoin, but it may not always be effective. + +-b <branch>:: +--branch=<branch>:: + This option is only valid for the split command. + + After generating the synthetic history, create a new + branch called <branch> that contains the new history. + This is suitable for immediate pushing upstream. + <branch> must not already exist. + +--ignore-joins:: + This option is only valid for the split command. + + If you use '--rejoin', git subtree attempts to optimize + its history reconstruction to generate only the new + commits since the last '--rejoin'. '--ignore-join' + disables this behaviour, forcing it to regenerate the + entire history. In a large project, this can take a + long time. + +--onto=<onto>:: + This option is only valid for the split command. + + If your subtree was originally imported using something + other than git subtree, its history may not match what + git subtree is expecting. In that case, you can specify + the commit id <onto> that corresponds to the first + revision of the subproject's history that was imported + into your project, and git subtree will attempt to build + its history from there. + + If you used 'git subtree add', you should never need + this option. + +--rejoin:: + This option is only valid for the split command. + + After splitting, merge the newly created synthetic + history back into your main project. That way, future + splits can search only the part of history that has + been added since the most recent --rejoin. + + If your split commits end up merged into the upstream + subproject, and then you want to get the latest upstream + version, this will allow git's merge algorithm to more + intelligently avoid conflicts (since it knows these + synthetic commits are already part of the upstream + repository). + + Unfortunately, using this option results in 'git log' + showing an extra copy of every new commit that was + created (the original, and the synthetic one). + + If you do all your merges with '--squash', don't use + '--rejoin' when you split, because you don't want the + subproject's history to be part of your project anyway. + + +EXAMPLE 1. Add command +---------------------- +Let's assume that you have a local repository that you would like +to add an external vendor library to. In this case we will add the +git-subtree repository as a subdirectory of your already existing +git-extensions repository in ~/git-extensions/: + + $ git subtree add --prefix=git-subtree --squash \ + git://github.com/apenwarr/git-subtree.git master + +'master' needs to be a valid remote ref and can be a different branch +name + +You can omit the --squash flag, but doing so will increase the number +of commits that are incldued in your local repository. + +We now have a ~/git-extensions/git-subtree directory containing code +from the master branch of git://github.com/apenwarr/git-subtree.git +in our git-extensions repository. + +EXAMPLE 2. Extract a subtree using commit, merge and pull +--------------------------------------------------------- +Let's use the repository for the git source code as an example. +First, get your own copy of the git.git repository: + + $ git clone git://git.kernel.org/pub/scm/git/git.git test-git + $ cd test-git + +gitweb (commit 1130ef3) was merged into git as of commit +0a8f4f0, after which it was no longer maintained separately. +But imagine it had been maintained separately, and we wanted to +extract git's changes to gitweb since that time, to share with +the upstream. You could do this: + + $ git subtree split --prefix=gitweb --annotate='(split) ' \ + 0a8f4f0^.. --onto=1130ef3 --rejoin \ + --branch gitweb-latest + $ gitk gitweb-latest + $ git push git@github.com:whatever/gitweb.git gitweb-latest:master + +(We use '0a8f4f0^..' because that means "all the changes from +0a8f4f0 to the current version, including 0a8f4f0 itself.") + +If gitweb had originally been merged using 'git subtree add' (or +a previous split had already been done with --rejoin specified) +then you can do all your splits without having to remember any +weird commit ids: + + $ git subtree split --prefix=gitweb --annotate='(split) ' --rejoin \ + --branch gitweb-latest2 + +And you can merge changes back in from the upstream project just +as easily: + + $ git subtree pull --prefix=gitweb \ + git@github.com:whatever/gitweb.git master + +Or, using '--squash', you can actually rewind to an earlier +version of gitweb: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest~10 + +Then make some changes: + + $ date >gitweb/myfile + $ git add gitweb/myfile + $ git commit -m 'created myfile' + +And fast forward again: + + $ git subtree merge --prefix=gitweb --squash gitweb-latest + +And notice that your change is still intact: + + $ ls -l gitweb/myfile + +And you can split it out and look at your changes versus +the standard gitweb: + + git log gitweb-latest..$(git subtree split --prefix=gitweb) + +EXAMPLE 3. Extract a subtree using branch +----------------------------------------- +Suppose you have a source directory with many files and +subdirectories, and you want to extract the lib directory to its own +git project. Here's a short way to do it: + +First, make the new repository wherever you want: + + $ <go to the new location> + $ git init --bare + +Back in your original directory: + + $ git subtree split --prefix=lib --annotate="(split)" -b split + +Then push the new branch onto the new empty repository: + + $ git push <new-repo> split:master + + +AUTHOR +------ +Written by Avery Pennarun <apenwarr@gmail.com> + + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/contrib/subtree/t/Makefile b/contrib/subtree/t/Makefile new file mode 100644 index 0000000000..c864810389 --- /dev/null +++ b/contrib/subtree/t/Makefile @@ -0,0 +1,69 @@ +# Run tests +# +# Copyright (c) 2005 Junio C Hamano +# + +-include ../../../config.mak.autogen +-include ../../../config.mak + +#GIT_TEST_OPTS=--verbose --debug +SHELL_PATH ?= $(SHELL) +PERL_PATH ?= /usr/bin/perl +TAR ?= $(TAR) +RM ?= rm -f +PROVE ?= prove +DEFAULT_TEST_TARGET ?= test + +# Shell quote; +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) + +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) + +all: $(DEFAULT_TEST_TARGET) + +test: pre-clean $(TEST_LINT) + $(MAKE) aggregate-results-and-cleanup + +prove: pre-clean $(TEST_LINT) + @echo "*** prove ***"; GIT_CONFIG=.git/config $(PROVE) --exec '$(SHELL_PATH_SQ)' $(GIT_PROVE_OPTS) $(T) :: $(GIT_TEST_OPTS) + $(MAKE) clean + +$(T): + @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) + +pre-clean: + $(RM) -r test-results + +clean: + $(RM) -r 'trash directory'.* test-results + $(RM) -r valgrind/bin + $(RM) .prove + +test-lint: test-lint-duplicates test-lint-executable + +test-lint-duplicates: + @dups=`echo $(T) | tr ' ' '\n' | sed 's/-.*//' | sort | uniq -d` && \ + test -z "$$dups" || { \ + echo >&2 "duplicate test numbers:" $$dups; exit 1; } + +test-lint-executable: + @bad=`for i in $(T); do test -x "$$i" || echo $$i; done` && \ + test -z "$$bad" || { \ + echo >&2 "non-executable tests:" $$bad; exit 1; } + +aggregate-results-and-cleanup: $(T) + $(MAKE) aggregate-results + $(MAKE) clean + +aggregate-results: + for f in ../../../t/test-results/t*-*.counts; do \ + echo "$$f"; \ + done | '$(SHELL_PATH_SQ)' ../../../t/aggregate-results.sh + +valgrind: + $(MAKE) GIT_TEST_OPTS="$(GIT_TEST_OPTS) --valgrind" + +test-results: + mkdir -p test-results + +.PHONY: pre-clean $(T) aggregate-results clean valgrind diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh new file mode 100755 index 0000000000..bc2eeb0944 --- /dev/null +++ b/contrib/subtree/t/t7900-subtree.sh @@ -0,0 +1,508 @@ +#!/bin/sh +# +# Copyright (c) 2012 Avery Pennaraum +# +test_description='Basic porcelain support for subtrees + +This test verifies the basic operation of the merge, pull, add +and split subcommands of git subtree. +' + +export TEST_DIRECTORY=$(pwd)/../../../t + +. ../../../t/test-lib.sh + +create() +{ + echo "$1" >"$1" + git add "$1" +} + + +check_equal() +{ + test_debug 'echo' + test_debug "echo \"check a:\" \"{$1}\"" + test_debug "echo \" b:\" \"{$2}\"" + if [ "$1" = "$2" ]; then + return 0 + else + return 1 + fi +} + +fixnl() +{ + t="" + while read x; do + t="$t$x " + done + echo $t +} + +multiline() +{ + while read x; do + set -- $x + for d in "$@"; do + echo "$d" + done + done +} + +undo() +{ + git reset --hard HEAD~ +} + +last_commit_message() +{ + git log --pretty=format:%s -1 +} + +# 1 +test_expect_success 'init subproj' ' + test_create_repo subproj +' + +# To the subproject! +cd subproj + +# 2 +test_expect_success 'add sub1' ' + create sub1 && + git commit -m "sub1" && + git branch sub1 && + git branch -m master subproj +' + +# 3 +test_expect_success 'add sub2' ' + create sub2 && + git commit -m "sub2" && + git branch sub2 +' + +# 4 +test_expect_success 'add sub3' ' + create sub3 && + git commit -m "sub3" && + git branch sub3 +' + +# Back to mainline +cd .. + +# 5 +test_expect_success 'add main4' ' + create main4 && + git commit -m "main4" && + git branch -m master mainline && + git branch subdir +' + +# 6 +test_expect_success 'fetch subproj history' ' + git fetch ./subproj sub1 && + git branch sub1 FETCH_HEAD +' + +# 7 +test_expect_success 'no subtree exists in main tree' ' + test_must_fail git subtree merge --prefix=subdir sub1 +' + +# 8 +test_expect_success 'no pull from non-existant subtree' ' + test_must_fail git subtree pull --prefix=subdir ./subproj sub1 +' + +# 9 +test_expect_success 'check if --message works for add' ' + git subtree add --prefix=subdir --message="Added subproject" sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject" && + undo +' + +# 10 +test_expect_success 'check if --message works as -m and --prefix as -P' ' + git subtree add -P subdir -m "Added subproject using git subtree" sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject using git subtree" && + undo +' + +# 11 +test_expect_success 'check if --message works with squash too' ' + git subtree add -P subdir -m "Added subproject with squash" --squash sub1 && + check_equal ''"$(last_commit_message)"'' "Added subproject with squash" && + undo +' + +# 12 +test_expect_success 'add subproj to mainline' ' + git subtree add --prefix=subdir/ FETCH_HEAD && + check_equal ''"$(last_commit_message)"'' "Add '"'subdir/'"' from commit '"'"'''"$(git rev-parse sub1)"'''"'"'" +' + +# 13 +# this shouldn't actually do anything, since FETCH_HEAD is already a parent +test_expect_success 'merge fetched subproj' ' + git merge -m "merge -s -ours" -s ours FETCH_HEAD +' + +# 14 +test_expect_success 'add main-sub5' ' + create subdir/main-sub5 && + git commit -m "main-sub5" +' + +# 15 +test_expect_success 'add main6' ' + create main6 && + git commit -m "main6 boring" +' + +# 16 +test_expect_success 'add main-sub7' ' + create subdir/main-sub7 && + git commit -m "main-sub7" +' + +# 17 +test_expect_success 'fetch new subproj history' ' + git fetch ./subproj sub2 && + git branch sub2 FETCH_HEAD +' + +# 18 +test_expect_success 'check if --message works for merge' ' + git subtree merge --prefix=subdir -m "Merged changes from subproject" sub2 && + check_equal ''"$(last_commit_message)"'' "Merged changes from subproject" && + undo +' + +# 19 +test_expect_success 'check if --message for merge works with squash too' ' + git subtree merge --prefix subdir -m "Merged changes from subproject using squash" --squash sub2 && + check_equal ''"$(last_commit_message)"'' "Merged changes from subproject using squash" && + undo +' + +# 20 +test_expect_success 'merge new subproj history into subdir' ' + git subtree merge --prefix=subdir FETCH_HEAD && + git branch pre-split && + check_equal ''"$(last_commit_message)"'' "Merge commit '"'"'"$(git rev-parse sub2)"'"'"' into mainline" +' + +# 21 +test_expect_success 'Check that prefix argument is required for split' ' + echo "You must provide the --prefix option." > expected && + test_must_fail git subtree split > actual 2>&1 && + test_debug "echo -n expected: " && + test_debug "cat expected" && + test_debug "echo -n actual: " && + test_debug "cat actual" && + test_cmp expected actual && + rm -f expected actual +' + +# 22 +test_expect_success 'Check that the <prefix> exists for a split' ' + echo "'"'"'non-existent-directory'"'"'" does not exist\; use "'"'"'git subtree add'"'"'" > expected && + test_must_fail git subtree split --prefix=non-existent-directory > actual 2>&1 && + test_debug "echo -n expected: " && + test_debug "cat expected" && + test_debug "echo -n actual: " && + test_debug "cat actual" && + test_cmp expected actual +# rm -f expected actual +' + +# 23 +test_expect_success 'check if --message works for split+rejoin' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + git branch spl1 "$spl1" && + check_equal ''"$(last_commit_message)"'' "Split & rejoin" && + undo +' + +# 24 +test_expect_success 'check split with --branch' ' + spl1=$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin) && + undo && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr1 && + check_equal ''"$(git rev-parse splitbr1)"'' "$spl1" +' + +# 25 +test_expect_success 'check split with --branch for an existing branch' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + undo && + git branch splitbr2 sub1 && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --branch splitbr2 && + check_equal ''"$(git rev-parse splitbr2)"'' "$spl1" +' + +# 26 +test_expect_success 'check split with --branch for an incompatible branch' ' + test_must_fail git subtree split --prefix subdir --onto FETCH_HEAD --branch subdir +' + + +# 27 +test_expect_success 'check split+rejoin' ' + spl1=''"$(git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --message "Split & rejoin" --rejoin)"'' && + undo && + git subtree split --annotate='"'*'"' --prefix subdir --onto FETCH_HEAD --rejoin && + check_equal ''"$(last_commit_message)"'' "Split '"'"'subdir/'"'"' into commit '"'"'"$spl1"'"'"'" +' + +# 28 +test_expect_success 'add main-sub8' ' + create subdir/main-sub8 && + git commit -m "main-sub8" +' + +# To the subproject! +cd ./subproj + +# 29 +test_expect_success 'merge split into subproj' ' + git fetch .. spl1 && + git branch spl1 FETCH_HEAD && + git merge FETCH_HEAD +' + +# 30 +test_expect_success 'add sub9' ' + create sub9 && + git commit -m "sub9" +' + +# Back to mainline +cd .. + +# 31 +test_expect_success 'split for sub8' ' + split2=''"$(git subtree split --annotate='"'*'"' --prefix subdir/ --rejoin)"'' + git branch split2 "$split2" +' + +# 32 +test_expect_success 'add main-sub10' ' + create subdir/main-sub10 && + git commit -m "main-sub10" +' + +# 33 +test_expect_success 'split for sub10' ' + spl3=''"$(git subtree split --annotate='"'*'"' --prefix subdir --rejoin)"'' && + git branch spl3 "$spl3" +' + +# To the subproject! +cd ./subproj + +# 34 +test_expect_success 'merge split into subproj' ' + git fetch .. spl3 && + git branch spl3 FETCH_HEAD && + git merge FETCH_HEAD && + git branch subproj-merge-spl3 +' + +chkm="main4 main6" +chkms="main-sub10 main-sub5 main-sub7 main-sub8" +chkms_sub=$(echo $chkms | multiline | sed 's,^,subdir/,' | fixnl) +chks="sub1 sub2 sub3 sub9" +chks_sub=$(echo $chks | multiline | sed 's,^,subdir/,' | fixnl) + +# 35 +test_expect_success 'make sure exactly the right set of files ends up in the subproj' ' + subfiles=''"$(git ls-files | fixnl)"'' && + check_equal "$subfiles" "$chkms $chks" +' + +# 36 +test_expect_success 'make sure the subproj history *only* contains commits that affect the subdir' ' + allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' && + check_equal "$allchanges" "$chkms $chks" +' + +# Back to mainline +cd .. + +# 37 +test_expect_success 'pull from subproj' ' + git fetch ./subproj subproj-merge-spl3 && + git branch subproj-merge-spl3 FETCH_HEAD && + git subtree pull --prefix=subdir ./subproj subproj-merge-spl3 +' + +# 38 +test_expect_success 'make sure exactly the right set of files ends up in the mainline' ' + mainfiles=''"$(git ls-files | fixnl)"'' && + check_equal "$mainfiles" "$chkm $chkms_sub $chks_sub" +' + +# 39 +test_expect_success 'make sure each filename changed exactly once in the entire history' ' + # main-sub?? and /subdir/main-sub?? both change, because those are the + # changes that were split into their own history. And subdir/sub?? never + # change, since they were *only* changed in the subtree branch. + allchanges=''"$(git log --name-only --pretty=format:'"''"' | sort | fixnl)"'' && + check_equal "$allchanges" ''"$(echo $chkms $chkm $chks $chkms_sub | multiline | sort | fixnl)"'' +' + +# 40 +test_expect_success 'make sure the --rejoin commits never make it into subproj' ' + check_equal ''"$(git log --pretty=format:'"'%s'"' HEAD^2 | grep -i split)"'' "" +' + +# 41 +test_expect_success 'make sure no "git subtree" tagged commits make it into subproj' ' + # They are meaningless to subproj since one side of the merge refers to the mainline + check_equal ''"$(git log --pretty=format:'"'%s%n%b'"' HEAD^2 | grep "git-subtree.*:")"'' "" +' + +# prepare second pair of repositories +mkdir test2 +cd test2 + +# 42 +test_expect_success 'init main' ' + test_create_repo main +' + +cd main + +# 43 +test_expect_success 'add main1' ' + create main1 && + git commit -m "main1" +' + +cd .. + +# 44 +test_expect_success 'init sub' ' + test_create_repo sub +' + +cd sub + +# 45 +test_expect_success 'add sub2' ' + create sub2 && + git commit -m "sub2" +' + +cd ../main + +# check if split can find proper base without --onto + +# 46 +test_expect_success 'add sub as subdir in main' ' + git fetch ../sub master && + git branch sub2 FETCH_HEAD && + git subtree add --prefix subdir sub2 +' + +cd ../sub + +# 47 +test_expect_success 'add sub3' ' + create sub3 && + git commit -m "sub3" +' + +cd ../main + +# 48 +test_expect_success 'merge from sub' ' + git fetch ../sub master && + git branch sub3 FETCH_HEAD && + git subtree merge --prefix subdir sub3 +' + +# 49 +test_expect_success 'add main-sub4' ' + create subdir/main-sub4 && + git commit -m "main-sub4" +' + +# 50 +test_expect_success 'split for main-sub4 without --onto' ' + git subtree split --prefix subdir --branch mainsub4 +' + +# at this point, the new commit parent should be sub3 if it is not, +# something went wrong (the "newparent" of "master~" commit should +# have been sub3, but it was not, because its cache was not set to +# itself) + +# 51 +test_expect_success 'check that the commit parent is sub3' ' + check_equal ''"$(git log --pretty=format:%P -1 mainsub4)"'' ''"$(git rev-parse sub3)"'' +' + +# 52 +test_expect_success 'add main-sub5' ' + mkdir subdir2 && + create subdir2/main-sub5 && + git commit -m "main-sub5" +' + +# 53 +test_expect_success 'split for main-sub5 without --onto' ' + # also test that we still can split out an entirely new subtree + # if the parent of the first commit in the tree is not empty, + # then the new subtree has accidently been attached to something + git subtree split --prefix subdir2 --branch mainsub5 && + check_equal ''"$(git log --pretty=format:%P -1 mainsub5)"'' "" +' + +# make sure no patch changes more than one file. The original set of commits +# changed only one file each. A multi-file change would imply that we pruned +# commits too aggressively. +joincommits() +{ + commit= + all= + while read x y; do + #echo "{$x}" >&2 + if [ -z "$x" ]; then + continue + elif [ "$x" = "commit:" ]; then + if [ -n "$commit" ]; then + echo "$commit $all" + all= + fi + commit="$y" + else + all="$all $y" + fi + done + echo "$commit $all" +} + +# 54 +test_expect_success 'verify one file change per commit' ' + x= && + list=''"$(git log --pretty=format:'"'commit: %H'"' | joincommits)"'' && +# test_debug "echo HERE" && +# test_debug "echo ''"$list"''" && + (git log --pretty=format:'"'commit: %H'"' | joincommits | + ( while read commit a b; do + test_debug "echo Verifying commit "''"$commit"'' + test_debug "echo a: "''"$a"'' + test_debug "echo b: "''"$b"'' + check_equal "$b" "" + x=1 + done + check_equal "$x" 1 + )) +' + +test_done diff --git a/contrib/subtree/todo b/contrib/subtree/todo new file mode 100644 index 0000000000..7e44b0024f --- /dev/null +++ b/contrib/subtree/todo @@ -0,0 +1,50 @@ + + delete tempdir + + 'git subtree rejoin' option to do the same as --rejoin, eg. after a + rebase + + --prefix doesn't force the subtree correctly in merge/pull: + "-s subtree" should be given an explicit subtree option? + There doesn't seem to be a way to do this. We'd have to + patch git-merge-subtree. Ugh. + (but we could avoid this problem by generating squashes with + exactly the right subtree structure, rather than using + subtree merge...) + + add a 'push' subcommand to parallel 'pull' + + add a 'log' subcommand to see what's new in a subtree? + + add to-submodule and from-submodule commands + + automated tests for --squash stuff + + "add" command non-obviously requires a commitid; would be easier if + it had a "pull" sort of mode instead + + "pull" and "merge" commands should fail if you've never merged + that --prefix before + + docs should provide an example of "add" + + note that the initial split doesn't *have* to have a commitid + specified... that's just an optimization + + if you try to add (or maybe merge?) with an invalid commitid, you + get a misleading "prefix must end with /" message from + one of the other git tools that git-subtree calls. Should + detect this situation and print the *real* problem. + + "pull --squash" should do fetch-synthesize-merge, but instead just + does "pull" directly, which doesn't work at all. + + make a 'force-update' that does what 'add' does even if the subtree + already exists. That way we can help people who imported + subtrees "incorrectly" (eg. by just copying in the files) in + the past. + + guess --prefix automatically if possible based on pwd + + make a 'git subtree grafts' that automatically expands --squash'd + commits so you can see the full history if you want it. diff --git a/contrib/svn-fe/.gitignore b/contrib/svn-fe/.gitignore new file mode 100644 index 0000000000..02a7791585 --- /dev/null +++ b/contrib/svn-fe/.gitignore @@ -0,0 +1,4 @@ +/*.xml +/*.1 +/*.html +/svn-fe diff --git a/contrib/svn-fe/Makefile b/contrib/svn-fe/Makefile new file mode 100644 index 0000000000..360d8da417 --- /dev/null +++ b/contrib/svn-fe/Makefile @@ -0,0 +1,63 @@ +all:: svn-fe$X + +CC = gcc +RM = rm -f +MV = mv + +CFLAGS = -g -O2 -Wall +LDFLAGS = +ALL_CFLAGS = $(CFLAGS) +ALL_LDFLAGS = $(LDFLAGS) +EXTLIBS = + +GIT_LIB = ../../libgit.a +VCSSVN_LIB = ../../vcs-svn/lib.a +LIBS = $(VCSSVN_LIB) $(GIT_LIB) $(EXTLIBS) + +QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir +QUIET_SUBDIR1 = + +ifneq ($(findstring $(MAKEFLAGS),w),w) +PRINT_DIR = --no-print-directory +else # "make -w" +NO_SUBDIR = : +endif + +ifneq ($(findstring $(MAKEFLAGS),s),s) +ifndef V + QUIET_CC = @echo ' ' CC $@; + QUIET_LINK = @echo ' ' LINK $@; + QUIET_SUBDIR0 = +@subdir= + QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ + $(MAKE) $(PRINT_DIR) -C $$subdir +endif +endif + +svn-fe$X: svn-fe.o $(VCSSVN_LIB) $(GIT_LIB) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ svn-fe.o \ + $(ALL_LDFLAGS) $(LIBS) + +svn-fe.o: svn-fe.c ../../vcs-svn/svndump.h + $(QUIET_CC)$(CC) -I../../vcs-svn -o $*.o -c $(ALL_CFLAGS) $< + +svn-fe.html: svn-fe.txt + $(QUIET_SUBDIR0)../../Documentation $(QUIET_SUBDIR1) \ + MAN_TXT=../contrib/svn-fe/svn-fe.txt \ + ../contrib/svn-fe/$@ + +svn-fe.1: svn-fe.txt + $(QUIET_SUBDIR0)../../Documentation $(QUIET_SUBDIR1) \ + MAN_TXT=../contrib/svn-fe/svn-fe.txt \ + ../contrib/svn-fe/$@ + $(MV) ../../Documentation/svn-fe.1 . + +../../vcs-svn/lib.a: FORCE + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) vcs-svn/lib.a + +../../libgit.a: FORCE + $(QUIET_SUBDIR0)../.. $(QUIET_SUBDIR1) libgit.a + +clean: + $(RM) svn-fe$X svn-fe.o svn-fe.html svn-fe.xml svn-fe.1 + +.PHONY: all clean FORCE diff --git a/contrib/svn-fe/svn-fe.c b/contrib/svn-fe/svn-fe.c new file mode 100644 index 0000000000..35db24f5ea --- /dev/null +++ b/contrib/svn-fe/svn-fe.c @@ -0,0 +1,17 @@ +/* + * This file is in the public domain. + * You may freely use, modify, distribute, and relicense it. + */ + +#include <stdlib.h> +#include "svndump.h" + +int main(int argc, char **argv) +{ + if (svndump_init(NULL)) + return 1; + svndump_read((argc > 1) ? argv[1] : NULL); + svndump_deinit(); + svndump_reset(); + return 0; +} diff --git a/contrib/svn-fe/svn-fe.txt b/contrib/svn-fe/svn-fe.txt new file mode 100644 index 0000000000..1128ab2ce4 --- /dev/null +++ b/contrib/svn-fe/svn-fe.txt @@ -0,0 +1,71 @@ +svn-fe(1) +========= + +NAME +---- +svn-fe - convert an SVN "dumpfile" to a fast-import stream + +SYNOPSIS +-------- +[verse] +mkfifo backchannel && +svnadmin dump --deltas REPO | + svn-fe [url] 3<backchannel | + git fast-import --cat-blob-fd=3 3>backchannel + +DESCRIPTION +----------- + +Converts a Subversion dumpfile into input suitable for +git-fast-import(1) and similar importers. REPO is a path to a +Subversion repository mirrored on the local disk. Remote Subversion +repositories can be mirrored on local disk using the `svnsync` +command. + +Note: this tool is very young. The details of its commandline +interface may change in backward incompatible ways. + +INPUT FORMAT +------------ +Subversion's repository dump format is documented in full in +`notes/dump-load-format.txt` from the Subversion source tree. +Files in this format can be generated using the 'svnadmin dump' or +'svk admin dump' command. + +OUTPUT FORMAT +------------- +The fast-import format is documented by the git-fast-import(1) +manual page. + +NOTES +----- +Subversion dumps do not record a separate author and committer for +each revision, nor a separate display name and email address for +each author. Like git-svn(1), 'svn-fe' will use the name + +--------- +user <user@UUID> +--------- + +as committer, where 'user' is the value of the `svn:author` property +and 'UUID' the repository's identifier. + +To support incremental imports, 'svn-fe' puts a `git-svn-id` line at +the end of each commit log message if passed a URL on the command +line. This line has the form `git-svn-id: URL@REVNO UUID`. + +The resulting repository will generally require further processing +to put each project in its own repository and to separate the history +of each branch. The 'git filter-branch --subdirectory-filter' command +may be useful for this purpose. + +BUGS +---- +Empty directories and unknown properties are silently discarded. + +The exit status does not reflect whether an error was detected. + +SEE ALSO +-------- +git-svn(1), svn2git(1), svk(1), git-filter-branch(1), git-fast-import(1), +https://svn.apache.org/repos/asf/subversion/trunk/notes/dump-load-format.txt diff --git a/contrib/thunderbird-patch-inline/README b/contrib/thunderbird-patch-inline/README new file mode 100644 index 0000000000..000147bbe4 --- /dev/null +++ b/contrib/thunderbird-patch-inline/README @@ -0,0 +1,20 @@ +appp.sh is a script that is supposed to be used together with ExternalEditor +for Mozilla Thunderbird. It will let you include patches inline in e-mails +in an easy way. + +Usage: +- Generate the patch with git format-patch. +- Start writing a new e-mail in Thunderbird. +- Press the external editor button (or Ctrl-E) to run appp.sh +- Select the previously generated patch file. +- Finish editing the e-mail. + +Any text that is entered into the message editor before appp.sh is called +will be moved to the section between the --- and the diffstat. + +All S-O-B:s and Cc:s in the patch will be added to the CC list. + +To set it up, just install External Editor and tell it to use appp.sh as the +editor. + +Zenity is a required dependency. diff --git a/contrib/thunderbird-patch-inline/appp.sh b/contrib/thunderbird-patch-inline/appp.sh new file mode 100755 index 0000000000..5eb4a51643 --- /dev/null +++ b/contrib/thunderbird-patch-inline/appp.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Copyright 2008 Lukas Sandström <luksan@gmail.com> +# +# AppendPatch - A script to be used together with ExternalEditor +# for Mozilla Thunderbird to properly include patches inline in e-mails. + +# ExternalEditor can be downloaded at http://globs.org/articles.php?lng=en&pg=2 + +CONFFILE=~/.appprc + +SEP="-=-=-=-=-=-=-=-=-=# Don't remove this line #=-=-=-=-=-=-=-=-=-" +if [ -e "$CONFFILE" ] ; then + LAST_DIR=`grep -m 1 "^LAST_DIR=" "${CONFFILE}"|sed -e 's/^LAST_DIR=//'` + cd "${LAST_DIR}" +else + cd > /dev/null +fi + +PATCH=$(zenity --file-selection) + +if [ "$?" != "0" ] ; then + #zenity --error --text "No patchfile given." + exit 1 +fi + +cd - > /dev/null + +SUBJECT=`sed -n -e '/^Subject: /p' "${PATCH}"` +HEADERS=`sed -e '/^'"${SEP}"'$/,$d' $1` +BODY=`sed -e "1,/${SEP}/d" $1` +CMT_MSG=`sed -e '1,/^$/d' -e '/^---$/,$d' "${PATCH}"` +DIFF=`sed -e '1,/^---$/d' "${PATCH}"` + +CCS=`echo -e "$CMT_MSG\n$HEADERS" | sed -n -e 's/^Cc: \(.*\)$/\1,/gp' \ + -e 's/^Signed-off-by: \(.*\)/\1,/gp'` + +echo "$SUBJECT" > $1 +echo "Cc: $CCS" >> $1 +echo "$HEADERS" | sed -e '/^Subject: /d' -e '/^Cc: /d' >> $1 +echo "$SEP" >> $1 + +echo "$CMT_MSG" >> $1 +echo "---" >> $1 +if [ "x${BODY}x" != "xx" ] ; then + echo >> $1 + echo "$BODY" >> $1 + echo >> $1 +fi +echo "$DIFF" >> $1 + +LAST_DIR=`dirname "${PATCH}"` + +grep -v "^LAST_DIR=" "${CONFFILE}" > "${CONFFILE}_" +echo "LAST_DIR=${LAST_DIR}" >> "${CONFFILE}_" +mv "${CONFFILE}_" "${CONFFILE}" diff --git a/contrib/vim/README b/contrib/vim/README index 9e7881fea9..fca1e17251 100644 --- a/contrib/vim/README +++ b/contrib/vim/README @@ -1,8 +1,32 @@ -To syntax highlight git's commit messages, you need to: - 1. Copy syntax/gitcommit.vim to vim's syntax directory: - $ mkdir -p $HOME/.vim/syntax - $ cp syntax/gitcommit.vim $HOME/.vim/syntax - 2. Auto-detect the editing of git commit files: - $ cat >>$HOME/.vimrc <<'EOF' - autocmd BufNewFile,BufRead COMMIT_EDITMSG set filetype=gitcommit - EOF +Syntax highlighting for git commit messages, config files, etc. is +included with the vim distribution as of vim 7.2, and should work +automatically. + +If you have an older version of vim, you can get the latest syntax +files from the vim project: + + http://ftp.vim.org/pub/vim/runtime/syntax/git.vim + http://ftp.vim.org/pub/vim/runtime/syntax/gitcommit.vim + http://ftp.vim.org/pub/vim/runtime/syntax/gitconfig.vim + http://ftp.vim.org/pub/vim/runtime/syntax/gitrebase.vim + http://ftp.vim.org/pub/vim/runtime/syntax/gitsendemail.vim + +These files are also available via FTP at the same location. + +To install: + + 1. Copy these files to vim's syntax directory $HOME/.vim/syntax + 2. To auto-detect the editing of various git-related filetypes: + $ cat >>$HOME/.vim/filetype.vim <<'EOF' + autocmd BufNewFile,BufRead *.git/COMMIT_EDITMSG setf gitcommit + autocmd BufNewFile,BufRead *.git/config,.gitconfig setf gitconfig + autocmd BufNewFile,BufRead git-rebase-todo setf gitrebase + autocmd BufNewFile,BufRead .msg.[0-9]* + \ if getline(1) =~ '^From.*# This line is ignored.$' | + \ setf gitsendemail | + \ endif + autocmd BufNewFile,BufRead *.git/** + \ if getline(1) =~ '^\x\{40\}\>\|^ref: ' | + \ setf git | + \ endif + EOF diff --git a/contrib/vim/syntax/gitcommit.vim b/contrib/vim/syntax/gitcommit.vim deleted file mode 100644 index 332121b40e..0000000000 --- a/contrib/vim/syntax/gitcommit.vim +++ /dev/null @@ -1,18 +0,0 @@ -syn region gitLine start=/^#/ end=/$/ -syn region gitCommit start=/^# Changes to be committed:$/ end=/^#$/ contains=gitHead,gitCommitFile -syn region gitHead contained start=/^# (.*)/ end=/^#$/ -syn region gitChanged start=/^# Changed but not updated:/ end=/^#$/ contains=gitHead,gitChangedFile -syn region gitUntracked start=/^# Untracked files:/ end=/^#$/ contains=gitHead,gitUntrackedFile - -syn match gitCommitFile contained /^#\t.*/hs=s+2 -syn match gitChangedFile contained /^#\t.*/hs=s+2 -syn match gitUntrackedFile contained /^#\t.*/hs=s+2 - -hi def link gitLine Comment -hi def link gitCommit Comment -hi def link gitChanged Comment -hi def link gitHead Comment -hi def link gitUntracked Comment -hi def link gitCommitFile Type -hi def link gitChangedFile Constant -hi def link gitUntrackedFile Constant diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir index c6e154a84f..75e8b25817 100755 --- a/contrib/workdir/git-new-workdir +++ b/contrib/workdir/git-new-workdir @@ -22,7 +22,7 @@ branch=$3 # want to make sure that what is pointed to has a .git directory ... git_dir=$(cd "$orig_git" 2>/dev/null && git rev-parse --git-dir 2>/dev/null) || - die "\"$orig_git\" is not a git repository!" + die "Not a git repository: \"$orig_git\"" case "$git_dir" in .git) @@ -42,22 +42,28 @@ then fi # don't link to a workdir -if test -L "$git_dir/config" +if test -h "$git_dir/config" then die "\"$orig_git\" is a working directory only, please specify" \ "a complete repository." fi -# make sure the the links use full paths +# don't recreate a workdir over an existing repository +if test -e "$new_workdir" +then + die "destination directory '$new_workdir' already exists." +fi + +# make sure the links use full paths git_dir=$(cd "$git_dir"; pwd) # create the workdir mkdir -p "$new_workdir/.git" || die "unable to create \"$new_workdir\"!" -# create the links to the original repo. explictly exclude index, HEAD and +# create the links to the original repo. explicitly exclude index, HEAD and # logs/HEAD from the list since they are purely related to the current working # directory, and should not be shared. -for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache +for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache svn do case $x in */*) |