commit 3c09517e5dd154a63592ac2d78c582b9629dad90 Author: Saeed Noursalehi Date: Thu Feb 2 22:33:24 2017 -0800 Current snapshot of GVFS diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..82830bb7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +############################################################################### +# Do not normalize any line endings. +############################################################################### +* -text \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e9630e4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,218 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile +*.VC.opendb +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +*.dll +*.cab +*.cer diff --git a/AuthoringTests.md b/AuthoringTests.md new file mode 100644 index 00000000..50941771 --- /dev/null +++ b/AuthoringTests.md @@ -0,0 +1,59 @@ +# Authoring Tests + +## Functional Tests + +### 1. Running the functional tests + +Our functional tests are in the GVFS.FunctionalTests project. They are built on NUnit 3, which is available as a set of NuGet packages. + +To run the functional tests: + +1. Open GVFS.sln in Visual Studio +2. Build all, which will download the NUnit framework and runner +3. You have three options for how to run the tests, all of which are equivalent. + a. Run the GVFS.FunctionalTests project. Even better, set it as the default project and hit F5. + b. Use the command line runner. After building, execute ```Scripts\RunFunctionalTests.bat``` + c. If you want to use Visual Studio's Test Explorer, you need to install the NUnit 3 Test Adapter in VS | Tools | Extensions and Updates. + +Option 1 is probably the most convenient for developers. Option 2 will be used on the build machines. + +The functional tests take a set of parameters that indicate what paths and URLs to work with. If you want to customize those settings, they +can be found in GVFS.FunctionalTests\App.config. + +### 2. Running Full Suite of Tests vs. Smoke Tests + +By default, the GVFS functional tests run a subset of tests as a quick smoke test for developers. To run all tests, pass in the `--full-suite` flag + +### 3. Running specific tests + +Specific tests can be run by specifying `--test=` as the command line arguments to the functional +test project. + +### 4. How to write a functional test + +Each piece of functionality that we add to GVFS should have corresponding functional tests that clone a repo, mount GVFS, and use existing tools and file system +APIs to interact with the virtual repo. + +Since these are functional tests that can potentially modify the state of files on disk, you need to be careful to make sure each test can run in a clean +environment. There are three base classes that you can derive from when writing your tests. It's also important to put your new class into the same namespace +as the base class, because NUnit treats namespaces like test suites, and we have logic that keys off of that for deciding when to create enlistments. + +1. TestsWithLongRunningEnlistment + Before any test in this namespace is executed, we create a single enlistment and mount GVFS. We then run all tests in this namespace that derive + from this base class. Only put tests in here that are purely readonly and will leave the repo in a good state for future tests. + +2. TestsWithEnlistmentPerFixture + For any test fixture (a fixture is the same as a class in NUnit) that derives from this class, we create an enlistment and mount GVFS before running + any of the tests in the fixture, and then we unmount and delete the enlistment after all tests are done (but before any other fixture runs). If you need + to write a sequence of tests that manipulate the same repo, this is the right base class. + +3. TestsWithEnlistmentPerTestCase + Derive from this class if you need a brand new enlistment per test case. This is the most reliable, but also most expensive option. + +### 5. Updating the remote test branch + +By default, GVFS.FunctionalTests clones master, checks out the branch "FunctionalTests/YYYYMMDD" (with the day the FunctionalTests branch was created), +and then removes all remote tracking information. This is done to guarantee that remote changes to tip cannot break functional tests. If you need to update +the functional tests to use a new FunctionalTests branch, you'll need to create a new "FunctionalTests/YYYYMMDD" branch and update the 'Commitish' setting in App.Config, +and the project properties (Settings.Designer.cs, and Settings.settings) to have this new branch name. +Once you have verified your scenarios locally you can push the new FuncationTests branch and then your changes. \ No newline at end of file diff --git a/GVFS.sln b/GVFS.sln new file mode 100644 index 00000000..0bed3c64 --- /dev/null +++ b/GVFS.sln @@ -0,0 +1,146 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DCE11095-DA5F-4878-B58D-2702765560F5}" + ProjectSection(SolutionItems) = preProject + .gitattributes = .gitattributes + .gitignore = .gitignore + AuthoringTests.md = AuthoringTests.md + License.md = License.md + nuget.config = nuget.config + Protocol.md = Protocol.md + Readme.md = Readme.md + Settings.StyleCop = Settings.StyleCop + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GVFS", "GVFS", "{2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.GVFlt", "GVFS\GVFS.GVFlt\GVFS.GVFlt.csproj", "{1118B427-7063-422F-83B9-5023C8EC5A7A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.GvFltWrapper", "GVFS\GVFS.GvFltWrapper\GVFS.GvFltWrapper.vcxproj", "{FB0831AE-9997-401B-B31F-3A065FDBEB20}" + ProjectSection(ProjectDependencies) = postProject + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.Common", "GVFS\GVFS.Common\GVFS.Common.csproj", "{374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS", "GVFS\GVFS\GVFS.csproj", "{32220664-594C-4425-B9A0-88E0BE2F3D2A}" + ProjectSection(ProjectDependencies) = postProject + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + {BDA91EE5-C684-4FC5-A90A-B7D677421917} = {BDA91EE5-C684-4FC5-A90A-B7D677421917} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastFetch", "GVFS\FastFetch\FastFetch.csproj", "{07F2A520-2AB7-46DD-97C0-75D8E988D55B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.Tests", "GVFS\GVFS.Tests\GVFS.Tests.csproj", "{72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GVFS Tests", "GVFS Tests", "{C41F10F9-1163-4CFA-A465-EA728F75E9FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.UnitTests", "GVFS\GVFS.UnitTests\GVFS.UnitTests.csproj", "{8E0D0989-21F6-4DD8-946C-39F992523CC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.FunctionalTests", "GVFS\GVFS.FunctionalTests\GVFS.FunctionalTests.csproj", "{0F0A008E-AB12-40EC-A671-37A541B08C7F}" + ProjectSection(ProjectDependencies) = postProject + {07F2A520-2AB7-46DD-97C0-75D8E988D55B} = {07F2A520-2AB7-46DD-97C0-75D8E988D55B} + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5} = {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5} + {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {32220664-594C-4425-B9A0-88E0BE2F3D2A} + {BDA91EE5-C684-4FC5-A90A-B7D677421917} = {BDA91EE5-C684-4FC5-A90A-B7D677421917} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.NativeTests", "GVFS\GVFS.NativeTests\GVFS.NativeTests.vcxproj", "{3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.Hooks", "GVFS\GVFS.Hooks\GVFS.Hooks.csproj", "{BDA91EE5-C684-4FC5-A90A-B7D677421917}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GVFS.Mount", "GVFS\GVFS.Mount\GVFS.Mount.csproj", "{17498502-AEFF-4E70-90CC-1D0B56A8ADF5}" + ProjectSection(ProjectDependencies) = postProject + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GVFS.ReadObjectHook", "GVFS\GVFS.ReadObjectHook\GVFS.ReadObjectHook.vcxproj", "{5A6656D5-81C7-472C-9DC8-32D071CB2258}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{28674A4B-1223-4633-A460-C8CC39B09318}" + ProjectSection(SolutionItems) = preProject + Scripts\CreateCommonAssemblyVersion.bat = Scripts\CreateCommonAssemblyVersion.bat + Scripts\CreateCommonCliAssemblyVersion.bat = Scripts\CreateCommonCliAssemblyVersion.bat + Scripts\CreateCommonVersionHeader.bat = Scripts\CreateCommonVersionHeader.bat + Scripts\RunFunctionalTests.bat = Scripts\RunFunctionalTests.bat + Scripts\RunUnitTests.bat = Scripts\RunUnitTests.bat + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1118B427-7063-422F-83B9-5023C8EC5A7A}.Debug|x64.ActiveCfg = Debug|x64 + {1118B427-7063-422F-83B9-5023C8EC5A7A}.Debug|x64.Build.0 = Debug|x64 + {1118B427-7063-422F-83B9-5023C8EC5A7A}.Release|x64.ActiveCfg = Release|x64 + {1118B427-7063-422F-83B9-5023C8EC5A7A}.Release|x64.Build.0 = Release|x64 + {FB0831AE-9997-401B-B31F-3A065FDBEB20}.Debug|x64.ActiveCfg = Debug|x64 + {FB0831AE-9997-401B-B31F-3A065FDBEB20}.Debug|x64.Build.0 = Debug|x64 + {FB0831AE-9997-401B-B31F-3A065FDBEB20}.Release|x64.ActiveCfg = Release|x64 + {FB0831AE-9997-401B-B31F-3A065FDBEB20}.Release|x64.Build.0 = Release|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug|x64.ActiveCfg = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug|x64.Build.0 = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release|x64.ActiveCfg = Release|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release|x64.Build.0 = Release|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug|x64.ActiveCfg = Debug|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug|x64.Build.0 = Debug|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release|x64.ActiveCfg = Release|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release|x64.Build.0 = Release|x64 + {07F2A520-2AB7-46DD-97C0-75D8E988D55B}.Debug|x64.ActiveCfg = Debug|x64 + {07F2A520-2AB7-46DD-97C0-75D8E988D55B}.Debug|x64.Build.0 = Debug|x64 + {07F2A520-2AB7-46DD-97C0-75D8E988D55B}.Release|x64.ActiveCfg = Release|x64 + {07F2A520-2AB7-46DD-97C0-75D8E988D55B}.Release|x64.Build.0 = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug|x64.ActiveCfg = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug|x64.Build.0 = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release|x64.ActiveCfg = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release|x64.Build.0 = Release|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug|x64.ActiveCfg = Debug|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug|x64.Build.0 = Debug|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release|x64.ActiveCfg = Release|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release|x64.Build.0 = Release|x64 + {0F0A008E-AB12-40EC-A671-37A541B08C7F}.Debug|x64.ActiveCfg = Debug|x64 + {0F0A008E-AB12-40EC-A671-37A541B08C7F}.Debug|x64.Build.0 = Debug|x64 + {0F0A008E-AB12-40EC-A671-37A541B08C7F}.Release|x64.ActiveCfg = Release|x64 + {0F0A008E-AB12-40EC-A671-37A541B08C7F}.Release|x64.Build.0 = Release|x64 + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Debug|x64.ActiveCfg = Debug|x64 + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Debug|x64.Build.0 = Debug|x64 + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Release|x64.ActiveCfg = Release|x64 + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5}.Release|x64.Build.0 = Release|x64 + {BDA91EE5-C684-4FC5-A90A-B7D677421917}.Debug|x64.ActiveCfg = Debug|x64 + {BDA91EE5-C684-4FC5-A90A-B7D677421917}.Debug|x64.Build.0 = Debug|x64 + {BDA91EE5-C684-4FC5-A90A-B7D677421917}.Release|x64.ActiveCfg = Release|x64 + {BDA91EE5-C684-4FC5-A90A-B7D677421917}.Release|x64.Build.0 = Release|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug|x64.ActiveCfg = Debug|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug|x64.Build.0 = Debug|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release|x64.ActiveCfg = Release|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release|x64.Build.0 = Release|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug|x64.ActiveCfg = Debug|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug|x64.Build.0 = Debug|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release|x64.ActiveCfg = Release|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1118B427-7063-422F-83B9-5023C8EC5A7A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {FB0831AE-9997-401B-B31F-3A065FDBEB20} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {07F2A520-2AB7-46DD-97C0-75D8E988D55B} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {8E0D0989-21F6-4DD8-946C-39F992523CC6} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {0F0A008E-AB12-40EC-A671-37A541B08C7F} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {BDA91EE5-C684-4FC5-A90A-B7D677421917} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {28674A4B-1223-4633-A460-C8CC39B09318} = {DCE11095-DA5F-4878-B58D-2702765560F5} + EndGlobalSection +EndGlobal diff --git a/GVFS/FastFetch/App.config b/GVFS/FastFetch/App.config new file mode 100644 index 00000000..d740e886 --- /dev/null +++ b/GVFS/FastFetch/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GVFS/FastFetch/FastFetch.csproj b/GVFS/FastFetch/FastFetch.csproj new file mode 100644 index 00000000..b5940d42 --- /dev/null +++ b/GVFS/FastFetch/FastFetch.csproj @@ -0,0 +1,113 @@ + + + + + Debug + AnyCPU + {07F2A520-2AB7-46DD-97C0-75D8E988D55B} + Exe + Properties + FastFetch + FastFetch + v4.5.2 + 512 + true + + + + + true + ..\..\..\BuildOutput\FastFetch\bin\x64\Debug\ + ..\..\..\BuildOutput\FastFetch\obj\x64\Debug\ + DEBUG;TRACE + true + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + ..\..\..\BuildOutput\FastFetch\bin\x64\Release\ + ..\..\..\BuildOutput\FastFetch\obj\x64\Release\ + TRACE + true + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + + ..\..\..\packages\CommandLineParser.2.0.275-beta\lib\net45\CommandLine.dll + True + + + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + GVFS.Common + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/FastFetch/FastFetchVerb.cs b/GVFS/FastFetch/FastFetchVerb.cs new file mode 100644 index 00000000..b5c7eb1b --- /dev/null +++ b/GVFS/FastFetch/FastFetchVerb.cs @@ -0,0 +1,219 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace FastFetch +{ + [Verb("fastfetch", HelpText = "Fast-fetch a branch")] + public class FastFetchVerb + { + private const string DefaultBranch = "master"; + + [Option( + 'c', + "commit", + Required = false, + HelpText = "Commit to fetch")] + public string Commit { get; set; } + + [Option( + 'b', + "branch", + Required = false, + HelpText = "Branch to fetch")] + public string Branch { get; set; } + + [Option( + 's', + "silent", + Required = false, + Default = false, + HelpText = "Disables console logging")] + public bool Silent { get; set; } + + [Option( + "cache-server-url", + Required = false, + Default = "", + HelpText = "Defines the url of the cache server")] + public string CacheServerUrl { get; set; } + + [Option( + "chunk-size", + Required = false, + Default = 4000, + HelpText = "Sets the number of objects to be downloaded in a single pack")] + public int ChunkSize { get; set; } + + [Option( + "search-thread-count", + Required = false, + Default = 2, + HelpText = "Sets the number of threads to use for finding missing blobs. (0 for number of logical cores)")] + public int SearchThreadCount { get; set; } + + [Option( + "download-thread-count", + Required = false, + Default = 0, + HelpText = "Sets the number of threads to use for downloading. (0 for number of logical cores)")] + public int DownloadThreadCount { get; set; } + + [Option( + "index-thread-count", + Required = false, + Default = 0, + HelpText = "Sets the number of threads to use for indexing. (0 for number of logical cores)")] + public int IndexThreadCount { get; set; } + + [Option( + "checkout-thread-count", + Required = false, + Default = 0, + HelpText = "Sets the number of threads to use for indexing. (0 for number of logical cores)")] + public int CheckoutThreadCount { get; set; } + + [Option( + 'r', + "max-retries", + Required = false, + Default = 10, + HelpText = "Sets the maximum number of retries for downloading a pack")] + + public int MaxRetries { get; set; } + + [Option( + "git-path", + Default = "", + Required = false, + HelpText = "Sets the path and filename for git.exe if it isn't expected to be on %PATH%.")] + public string GitBinPath { get; set; } + + [Option( + "folders", + Required = false, + Default = "", + HelpText = "A semicolon-delimited list of paths to fetch")] + public string PathWhitelist { get; set; } + + [Option( + "folders-list", + Required = false, + Default = "", + HelpText = "A file containing line-delimited list of paths to fetch")] + public string PathWhitelistFile { get; set; } + + public void Execute() + { + // CmdParser doesn't strip quotes, and Path.Combine will throw + this.GitBinPath = this.GitBinPath.Replace("\"", string.Empty); + if (!GitProcess.GitExists(this.GitBinPath)) + { + Console.WriteLine( + "Could not find git.exe {0}", + !string.IsNullOrWhiteSpace(this.GitBinPath) ? "at " + this.GitBinPath : "on %PATH%"); + return; + } + + if (this.Commit != null && this.Branch != null) + { + Console.WriteLine("Cannot specify both a commit sha and a branch name to checkout."); + return; + } + + this.CacheServerUrl = Enlistment.StripObjectsEndpointSuffix(this.CacheServerUrl); + + this.SearchThreadCount = this.SearchThreadCount > 0 ? this.SearchThreadCount : Environment.ProcessorCount; + this.DownloadThreadCount = this.DownloadThreadCount > 0 ? this.DownloadThreadCount : Environment.ProcessorCount; + this.IndexThreadCount = this.IndexThreadCount > 0 ? this.IndexThreadCount : Environment.ProcessorCount; + this.CheckoutThreadCount = this.CheckoutThreadCount > 0 ? this.CheckoutThreadCount : Environment.ProcessorCount; + + this.GitBinPath = !string.IsNullOrWhiteSpace(this.GitBinPath) ? this.GitBinPath : GitProcess.GetInstalledGitBinPath(); + + Enlistment enlistment = (Enlistment)GVFSEnlistment.CreateFromCurrentDirectory(this.CacheServerUrl, this.GitBinPath) + ?? GitEnlistment.CreateFromCurrentDirectory(this.CacheServerUrl, this.GitBinPath); + + if (enlistment == null) + { + Console.WriteLine("Must be run within a .git repo or GVFS enlistment"); + return; + } + + string commitish = this.Commit ?? this.Branch ?? DefaultBranch; + + EventLevel maxVerbosity = this.Silent ? EventLevel.LogAlways : EventLevel.Informational; + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft.Git.FastFetch", "FastFetch")) + { + tracer.AddConsoleEventListener(maxVerbosity, Keywords.Any); + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + enlistment.CacheServerUrl, + new EventMetadata + { + { "TargetCommitish", commitish }, + }); + + FetchHelper fetchHelper = this.GetFetchHelper(tracer, enlistment); + + fetchHelper.MaxRetries = this.MaxRetries; + + if (!FetchHelper.TryLoadPathWhitelist(this.PathWhitelist, this.PathWhitelistFile, tracer, fetchHelper.PathWhitelist)) + { + Environment.ExitCode = 1; + return; + } + + try + { + bool isBranch = this.Commit == null; + fetchHelper.FastFetch(commitish, isBranch); + if (fetchHelper.HasFailures) + { + Environment.ExitCode = 1; + } + } + catch (AggregateException e) + { + Environment.ExitCode = 1; + foreach (Exception ex in e.Flatten().InnerExceptions) + { + tracer.RelatedError(ex.ToString()); + } + } + catch (Exception e) + { + Environment.ExitCode = 1; + tracer.RelatedError(e.ToString()); + } + + EventMetadata stopMetadata = new EventMetadata(); + stopMetadata.Add("Success", Environment.ExitCode == 0); + tracer.Stop(stopMetadata); + } + + if (Debugger.IsAttached) + { + Console.ReadKey(); + } + } + + private FetchHelper GetFetchHelper(ITracer tracer, Enlistment enlistment) + { + return new FetchHelper( + tracer, + enlistment, + this.ChunkSize, + this.SearchThreadCount, + this.DownloadThreadCount, + this.IndexThreadCount); + } + } +} diff --git a/GVFS/FastFetch/FetchHelper.cs b/GVFS/FastFetch/FetchHelper.cs new file mode 100644 index 00000000..286dbeb2 --- /dev/null +++ b/GVFS/FastFetch/FetchHelper.cs @@ -0,0 +1,216 @@ +using FastFetch.Git; +using FastFetch.Jobs; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace FastFetch +{ + public class FetchHelper + { + protected readonly Enlistment Enlistment; + protected readonly HttpGitObjects HttpGitObjects; + protected readonly GitObjects GitObjects; + protected readonly ITracer Tracer; + + protected readonly int ChunkSize; + protected readonly int SearchThreadCount; + protected readonly int DownloadThreadCount; + protected readonly int IndexThreadCount; + + protected readonly bool SkipConfigUpdate; + + private const string AreaPath = nameof(FetchHelper); + + // Shallow clones don't require their parent commits + private const int CommitDepth = 1; + + public FetchHelper( + ITracer tracer, + Enlistment enlistment, + int chunkSize, + int searchThreadCount, + int downloadThreadCount, + int indexThreadCount) + { + this.SearchThreadCount = searchThreadCount; + this.DownloadThreadCount = downloadThreadCount; + this.IndexThreadCount = indexThreadCount; + this.ChunkSize = chunkSize; + this.Tracer = tracer; + this.Enlistment = enlistment; + this.HttpGitObjects = new HttpGitObjects(tracer, enlistment, downloadThreadCount); + this.GitObjects = new GitObjects(tracer, enlistment, this.HttpGitObjects); + this.PathWhitelist = new List(); + + // We never want to update config settings for a GVFSEnlistment + this.SkipConfigUpdate = enlistment is GVFSEnlistment; + } + + public int MaxRetries + { + get { return this.HttpGitObjects.MaxRetries; } + set { this.HttpGitObjects.MaxRetries = value; } + } + + public bool HasFailures { get; protected set; } + + public List PathWhitelist { get; private set; } + + public static bool TryLoadPathWhitelist(string pathWhitelistInput, string pathWhitelistFile, ITracer tracer, List pathWhitelistOutput) + { + Func cleanPath = path => path.Trim(' ', '\r', '\n', '"').Replace('\\', '/').TrimStart('/'); + + pathWhitelistOutput.AddRange(pathWhitelistInput.Split(';').Select(cleanPath)); + + if (!string.IsNullOrWhiteSpace(pathWhitelistFile)) + { + if (File.Exists(pathWhitelistFile)) + { + pathWhitelistOutput.AddRange(File.ReadAllLines(pathWhitelistFile).Select(cleanPath)); + } + else + { + tracer.RelatedError("Could not find '{0}' for folder filtering.", pathWhitelistFile); + Console.WriteLine("Could not find '{0}' for folder filtering.", pathWhitelistFile); + return false; + } + } + + pathWhitelistOutput.RemoveAll(string.IsNullOrWhiteSpace); + return true; + } + + /// A specific branch to filter for, or null for all branches returned from info/refs + public virtual void FastFetch(string branchOrCommit, bool isBranch) + { + if (string.IsNullOrWhiteSpace(branchOrCommit)) + { + throw new FetchException("Must specify branch or commit to fetch"); + } + + GitRefs refs = null; + string commitToFetch; + if (isBranch) + { + refs = this.HttpGitObjects.QueryInfoRefs(branchOrCommit); + if (refs == null) + { + throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl); + } + else if (refs.Count == 0) + { + throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl); + } + + commitToFetch = refs.GetTipCommitIds().Single(); + } + else + { + commitToFetch = branchOrCommit; + } + + this.DownloadMissingCommit(commitToFetch, this.GitObjects); + + // Dummy output queue since we don't need to checkout available blobs + BlockingCollection availableBlobs = new BlockingCollection(); + + // Configure pipeline + // LsTreeHelper output => FindMissingBlobs => BatchDownload => IndexPack + LsTreeHelper blobEnumerator = new LsTreeHelper(this.PathWhitelist, this.Tracer, this.Enlistment); + FindMissingBlobsJob blobFinder = new FindMissingBlobsJob(this.SearchThreadCount, blobEnumerator.BlobIdOutput, availableBlobs, this.Tracer, this.Enlistment); + BatchObjectDownloadJob downloader = new BatchObjectDownloadJob(this.DownloadThreadCount, this.ChunkSize, blobFinder.DownloadQueue, availableBlobs, this.Tracer, this.Enlistment, this.HttpGitObjects, this.GitObjects); + IndexPackJob packIndexer = new IndexPackJob(this.IndexThreadCount, downloader.AvailablePacks, availableBlobs, this.Tracer, this.GitObjects); + + blobFinder.Start(); + downloader.Start(); + this.HasFailures |= !blobEnumerator.EnqueueAllBlobs(commitToFetch); + + // If indexing happens during searching, searching progressively gets slower, so wait on searching before indexing. + blobFinder.WaitForCompletion(); + this.HasFailures |= blobFinder.HasFailures; + + // Index regardless of failures, it'll shorten the next fetch. + packIndexer.Start(); + + downloader.WaitForCompletion(); + this.HasFailures |= downloader.HasFailures; + + packIndexer.WaitForCompletion(); + this.HasFailures |= packIndexer.HasFailures; + + if (!this.SkipConfigUpdate) + { + this.UpdateRefs(branchOrCommit, isBranch, refs); + + if (isBranch) + { + this.HasFailures |= !RefSpecHelpers.UpdateRefSpec(this.Tracer, this.Enlistment, branchOrCommit, refs); + } + } + } + + /// + /// * Updates any remote branch (N/A for fetch of detached commit) + /// * Updates shallow file + /// + protected virtual void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs refs) + { + UpdateRefsHelper refHelper = new UpdateRefsHelper(this.Enlistment); + string commitSha = null; + if (isBranch) + { + KeyValuePair remoteRef = refs.GetBranchRefPairs().Single(); + string remoteBranch = remoteRef.Key; + commitSha = remoteRef.Value; + + this.HasFailures |= !refHelper.UpdateRef(this.Tracer, remoteBranch, commitSha); + } + else + { + commitSha = branchOrCommit; + } + + // Update shallow file to ensure this is a valid shallow repo + File.AppendAllText(Path.Combine(this.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Shallow), commitSha + "\n"); + } + + protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects) + { + EventMetadata startMetadata = new EventMetadata(); + startMetadata.Add("CommitSha", commitSha); + startMetadata.Add("CommitDepth", CommitDepth); + + using (ITracer activity = this.Tracer.StartActivity("DownloadTrees", EventLevel.Informational, startMetadata)) + { + using (GitCatFileBatchCheckProcess catFileProcess = new GitCatFileBatchCheckProcess(this.Enlistment)) + { + if (!catFileProcess.ObjectExists(commitSha)) + { + if (!gitObjects.TryDownloadAndSaveCommits(new[] { commitSha }, commitDepth: CommitDepth)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ObjectsEndpointUrl", this.Enlistment.ObjectsEndpointUrl); + activity.RelatedError(metadata); + throw new FetchException("Could not download commits from {0}", this.Enlistment.ObjectsEndpointUrl); + } + } + } + } + } + + public class FetchException : Exception + { + public FetchException(string format, params object[] args) + : base(string.Format(format, args)) + { + } + } + } +} \ No newline at end of file diff --git a/GVFS/FastFetch/Git/DiffHelper.cs b/GVFS/FastFetch/Git/DiffHelper.cs new file mode 100644 index 00000000..7c079d72 --- /dev/null +++ b/GVFS/FastFetch/Git/DiffHelper.cs @@ -0,0 +1,265 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace GVFS.Common.Git +{ + public class DiffHelper + { + private const string AreaPath = nameof(DiffHelper); + + private ITracer tracer; + private List pathWhitelist; + private List deletedPaths = new List(); + private HashSet filesAdded = new HashSet(StringComparer.OrdinalIgnoreCase); + + private Enlistment enlistment; + private string targetCommitSha; + + private int additionalDirDeletes = 0; + private int additionalFileDeletes = 0; + + public DiffHelper(ITracer tracer, Enlistment enlistment, string targetCommitSha, IEnumerable pathWhitelist) + { + this.tracer = tracer; + this.pathWhitelist = new List(pathWhitelist); + this.enlistment = enlistment; + this.targetCommitSha = targetCommitSha; + + this.DirectoryOperations = new ConcurrentQueue(); + this.FileDeleteOperations = new ConcurrentQueue(); + this.FileAddOperations = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + this.RequiredBlobs = new BlockingCollection(); + } + + public bool HasFailures { get; private set; } + + public ConcurrentQueue DirectoryOperations { get; } + + public ConcurrentQueue FileDeleteOperations { get; } + + /// + /// Mapping from available sha to filenames where blob should be written + /// + public ConcurrentDictionary> FileAddOperations { get; } + + /// + /// Blobs required to perform a checkout of the destination + /// + public BlockingCollection RequiredBlobs { get; } + + public int TotalDirectoryOperations + { + get { return this.DirectoryOperations.Count + this.additionalDirDeletes; } + } + + public int TotalFileDeletes + { + get { return this.FileDeleteOperations.Count + this.additionalFileDeletes; } + } + + public void PerformDiff() + { + using (GitCatFileBatchProcess catFile = new GitCatFileBatchProcess(this.enlistment)) + { + GitProcess git = new GitProcess(this.enlistment); + string repoRoot = git.GetRepoRoot(); + + string targetTreeSha = catFile.GetTreeSha(this.targetCommitSha); + string headTreeSha = catFile.GetTreeSha("HEAD"); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("TargetTreeSha", targetTreeSha); + metadata.Add("HeadTreeSha", headTreeSha); + using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational, metadata)) + { + metadata = new EventMetadata(); + if (headTreeSha == null) + { + // Nothing is checked out (fresh git init), so we must search the entire tree. + git.LsTree(targetTreeSha, this.EnqueueOperationsFromLsTreeLine, recursive: true, showAllTrees: true); + metadata.Add("Operation", "LsTree"); + } + else + { + // Diff head and target, determine what needs to be done. + git.DiffTree(headTreeSha, targetTreeSha, line => this.EnqueueOperationsFromDiffTreeLine(this.tracer, repoRoot, line)); + metadata.Add("Operation", "DiffTree"); + } + + this.RequiredBlobs.CompleteAdding(); + + metadata.Add("Success", !this.HasFailures); + metadata.Add("DirectoryOperationsCount", this.TotalDirectoryOperations); + metadata.Add("FileDeleteOperationsCount", this.TotalFileDeletes); + metadata.Add("RequiredBlobsCount", this.RequiredBlobs.Count); + activity.Stop(metadata); + } + } + } + + public void ParseDiffFile(string filename, string repoRoot) + { + using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational)) + { + using (StreamReader file = new StreamReader(File.OpenRead(filename))) + { + while (!file.EndOfStream) + { + this.EnqueueOperationsFromDiffTreeLine(activity, repoRoot, file.ReadLine()); + } + } + } + } + + private void EnqueueOperationsFromLsTreeLine(string line) + { + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(line, this.enlistment.EnlistmentRoot); + if (result == null) + { + this.tracer.RelatedError("Unrecognized ls-tree line: {0}", line); + } + + if (!this.ResultIsInWhitelist(result)) + { + return; + } + + if (result.TargetIsDirectory) + { + this.DirectoryOperations.Enqueue(result); + } + else + { + this.EnqueueFileAddOperation(result); + } + } + + private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string repoRoot, string line) + { + if (!line.StartsWith(":")) + { + // Diff-tree starts with metadata we can ignore. + // Real diff lines always start with a colon + return; + } + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line, repoRoot); + if (!this.ResultIsInWhitelist(result)) + { + return; + } + + if (result.Operation == DiffTreeResult.Operations.Unknown || + result.Operation == DiffTreeResult.Operations.Unmerged) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Path", result.TargetFilename); + metadata.Add("ErrorMessage", "Unexpected diff operation: " + result.Operation); + activity.RelatedError(metadata); + this.HasFailures = true; + return; + } + + if (result.Operation == DiffTreeResult.Operations.Delete) + { + // Don't enqueue deletes that will be handled by recursively deleting their parent. + // Git traverses diffs in pre-order, so we are guaranteed to ignore child deletes here. + // Append trailing slash terminator to avoid matches with directory prefixes (Eg. \GVFS and \GVFS.Common) + string pathWithSlash = result.TargetFilename + "\\"; + if (this.deletedPaths.Any(path => pathWithSlash.StartsWith(path, StringComparison.OrdinalIgnoreCase))) + { + if (result.SourceIsDirectory || result.TargetIsDirectory) + { + Interlocked.Increment(ref this.additionalDirDeletes); + } + else + { + Interlocked.Increment(ref this.additionalFileDeletes); + } + + return; + } + + this.deletedPaths.Add(pathWithSlash); + } + + // Separate and enqueue all directory operations first. + if (result.SourceIsDirectory || result.TargetIsDirectory) + { + // Handle when a directory becomes a file. + // Files becoming directories is handled by HandleAllDirectoryOperations + if (result.Operation == DiffTreeResult.Operations.RenameEdit && + !result.TargetIsDirectory) + { + this.EnqueueFileAddOperation(result); + } + + this.DirectoryOperations.Enqueue(result); + } + else + { + switch (result.Operation) + { + case DiffTreeResult.Operations.Delete: + this.FileDeleteOperations.Enqueue(result.TargetFilename); + break; + case DiffTreeResult.Operations.RenameEdit: + this.FileDeleteOperations.Enqueue(result.SourceFilename); + this.EnqueueFileAddOperation(result); + break; + case DiffTreeResult.Operations.Modify: + case DiffTreeResult.Operations.CopyEdit: + case DiffTreeResult.Operations.Add: + this.EnqueueFileAddOperation(result); + break; + default: + activity.RelatedError("Unexpected diff operation from line: {0}", line); + break; + } + } + } + + private bool ResultIsInWhitelist(DiffTreeResult blobAdd) + { + return blobAdd.TargetFilename == null || + !this.pathWhitelist.Any() || + this.pathWhitelist.Any(path => blobAdd.TargetFilename.StartsWith(path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// This is not used in a multithreaded method, it doesn't need to be thread-safe + /// + private void EnqueueFileAddOperation(DiffTreeResult operation) + { + // Each filepath should be case-insensitive unique. If there are duplicates, only the last parsed one should remain. + if (!this.filesAdded.Add(operation.TargetFilename)) + { + foreach (KeyValuePair> kvp in this.FileAddOperations) + { + if (kvp.Value.Remove(operation.TargetFilename)) + { + break; + } + } + } + + HashSet operations = new HashSet(StringComparer.OrdinalIgnoreCase) { operation.TargetFilename }; + this.FileAddOperations.AddOrUpdate( + operation.TargetSha, + operations, + (key, oldValue) => + { + oldValue.Add(operation.TargetFilename); + return oldValue; + }); + + this.RequiredBlobs.Add(operation.TargetSha); + } + } +} diff --git a/GVFS/FastFetch/Git/GitPackIndex.cs b/GVFS/FastFetch/Git/GitPackIndex.cs new file mode 100644 index 00000000..835bf26c --- /dev/null +++ b/GVFS/FastFetch/Git/GitPackIndex.cs @@ -0,0 +1,47 @@ +using GVFS.Common.Physical.Git; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace FastFetch.Git +{ + public class GitPackIndex + { + private const uint PackIndexSignature = 0xff744f63; + private const int Sha1ByteLength = 20; + + public static IEnumerable GetShas(string filePath) + { + using (FileStream stream = File.OpenRead(filePath)) + using (BigEndianReader binReader = new BigEndianReader(stream)) + { + VerifyHeader(binReader); + + // Fanout table has 256 4-byte buckets corresponding to the number of objects prefixed by the bucket number + // Number is cumulative, so the total is always the last bucket value. + stream.Position += 255 * sizeof(uint); + uint totalObjects = binReader.ReadUInt32(); + for (int i = 0; i < totalObjects; ++i) + { + yield return BitConverter.ToString(binReader.ReadBytes(Sha1ByteLength)).Replace("-", string.Empty); + } + } + } + + private static void VerifyHeader(BinaryReader binReader) + { + uint signature = binReader.ReadUInt32(); + if (signature != PackIndexSignature) + { + throw new InvalidDataException("Bad pack header"); + } + + uint version = binReader.ReadUInt32(); + if (version != 2) + { + throw new InvalidDataException("Unsupported pack index version"); + } + } + } +} \ No newline at end of file diff --git a/GVFS/FastFetch/Git/LsTreeHelper.cs b/GVFS/FastFetch/Git/LsTreeHelper.cs new file mode 100644 index 00000000..5dbd52d8 --- /dev/null +++ b/GVFS/FastFetch/Git/LsTreeHelper.cs @@ -0,0 +1,71 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace FastFetch.Jobs +{ + public class LsTreeHelper + { + private const string AreaPath = nameof(LsTreeHelper); + + private List pathWhitelist; + private ITracer tracer; + private Enlistment enlistment; + + public LsTreeHelper( + IEnumerable pathWhitelist, + ITracer tracer, + Enlistment enlistment) + { + this.pathWhitelist = new List(pathWhitelist); + this.tracer = tracer; + this.enlistment = enlistment; + this.BlobIdOutput = new BlockingCollection(); + } + + public BlockingCollection BlobIdOutput { get; set; } + + public bool EnqueueAllBlobs(string rootTreeSha) + { + GitProcess git = new GitProcess(this.enlistment); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("TreeSha", rootTreeSha); + using (ITracer activity = this.tracer.StartActivity(AreaPath, EventLevel.Informational, metadata)) + { + GitProcess.Result result = git.LsTree(rootTreeSha, this.AddIfLineIsBlob, recursive: true); + if (result.HasErrors) + { + metadata.Add("ErrorMessage", result.Errors); + activity.RelatedError(metadata); + return false; + } + } + + this.BlobIdOutput.CompleteAdding(); + + return true; + } + + private void AddIfLineIsBlob(string blobLine) + { + int blobIdIndex = blobLine.IndexOf(GitCatFileProcess.BlobMarker); + if (blobIdIndex > -1) + { + string blobSha = blobLine.Substring(blobIdIndex + GitCatFileProcess.TreeMarker.Length, GVFSConstants.ShaStringLength); + string blobName = blobLine.Substring(blobLine.LastIndexOf('\t')).Trim(); + + if (!this.pathWhitelist.Any() || + this.pathWhitelist.Any(whitePath => blobName.StartsWith(whitePath, StringComparison.OrdinalIgnoreCase))) + { + this.BlobIdOutput.Add(blobSha); + } + } + } + } +} diff --git a/GVFS/FastFetch/Git/RefSpecHelpers.cs b/GVFS/FastFetch/Git/RefSpecHelpers.cs new file mode 100644 index 00000000..18d8e560 --- /dev/null +++ b/GVFS/FastFetch/Git/RefSpecHelpers.cs @@ -0,0 +1,41 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Linq; + +namespace FastFetch.Git +{ + public static class RefSpecHelpers + { + public const string RefsHeadsGitPath = "refs/heads/"; + + public static bool UpdateRefSpec(ITracer tracer, Enlistment enlistment, string branchOrCommit, GitRefs refs) + { + using (ITracer activity = tracer.StartActivity("UpdateRefSpec", EventLevel.Informational)) + { + const string OriginRefMapSettingName = "remote.origin.fetch"; + + // We must update the refspec to get proper "git pull" functionality. + string localBranch = branchOrCommit.StartsWith(RefsHeadsGitPath) ? branchOrCommit : (RefsHeadsGitPath + branchOrCommit); + string remoteBranch = refs.GetBranchRefPairs().Single().Key; + string refSpec = "+" + localBranch + ":" + remoteBranch; + + GitProcess git = new GitProcess(enlistment); + + // Replace all ref-specs this + // * ensures the default refspec (remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*) is removed which avoids some "git fetch/pull" failures + // * gives added "git fetch" performance since git will only fetch the branch provided in the refspec. + GitProcess.Result setResult = git.SetInLocalConfig(OriginRefMapSettingName, refSpec, replaceAll: true); + if (setResult.HasErrors) + { + activity.RelatedError("Could not update ref spec to {0}: {1}", refSpec, setResult.Errors); + return false; + } + } + + return true; + } + } +} diff --git a/GVFS/FastFetch/Git/UpdateRefsHelper.cs b/GVFS/FastFetch/Git/UpdateRefsHelper.cs new file mode 100644 index 00000000..8bb9d1a4 --- /dev/null +++ b/GVFS/FastFetch/Git/UpdateRefsHelper.cs @@ -0,0 +1,55 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; + +namespace FastFetch.Jobs +{ + public class UpdateRefsHelper + { + private const string AreaPath = nameof(UpdateRefsHelper); + + private Enlistment enlistment; + + public UpdateRefsHelper(Enlistment enlistment) + { + this.enlistment = enlistment; + } + + /// True on success, false otherwise + public bool UpdateRef(ITracer tracer, string refName, string targetCommitish) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("RefName", refName); + metadata.Add("TargetCommitish", targetCommitish); + using (ITracer activity = tracer.StartActivity(AreaPath, EventLevel.Informational, metadata)) + { + GitProcess gitProcess = new GitProcess(this.enlistment); + GitProcess.Result result = null; + if (this.IsSymbolicRef(targetCommitish)) + { + // Using update-ref with a branch name will leave a SHA in the ref file which detaches HEAD, so use symbolic-ref instead. + result = gitProcess.UpdateBranchSymbolicRef(refName, targetCommitish); + } + else + { + result = gitProcess.UpdateBranchSha(refName, targetCommitish); + } + + if (result.HasErrors) + { + activity.RelatedError(result.Errors); + return false; + } + + return true; + } + } + + private bool IsSymbolicRef(string targetCommitish) + { + return targetCommitish.StartsWith("refs/", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/GVFS/FastFetch/GitEnlistment.cs b/GVFS/FastFetch/GitEnlistment.cs new file mode 100644 index 00000000..c867075b --- /dev/null +++ b/GVFS/FastFetch/GitEnlistment.cs @@ -0,0 +1,33 @@ +using GVFS.Common; +using System; +using System.IO; +using System.Linq; + +namespace FastFetch +{ + public class GitEnlistment : Enlistment + { + private GitEnlistment(string repoRoot, string cacheBaseUrl, string gitBinPath) + : base(repoRoot, repoRoot, cacheBaseUrl, gitBinPath, gvfsHooksRoot: null) + { + } + + public static GitEnlistment CreateFromCurrentDirectory(string objectsEndpoint, string gitBinPath) + { + DirectoryInfo dirInfo = new DirectoryInfo(Environment.CurrentDirectory); + while (dirInfo != null && dirInfo.Exists) + { + DirectoryInfo[] dotGitDirs = dirInfo.GetDirectories(GVFSConstants.DotGit.Root); + + if (dotGitDirs.Count() == 1) + { + return new GitEnlistment(dirInfo.FullName, objectsEndpoint, gitBinPath); + } + + dirInfo = dirInfo.Parent; + } + + return null; + } + } +} diff --git a/GVFS/FastFetch/Jobs/BatchObjectDownloadJob.cs b/GVFS/FastFetch/Jobs/BatchObjectDownloadJob.cs new file mode 100644 index 00000000..0635cc05 --- /dev/null +++ b/GVFS/FastFetch/Jobs/BatchObjectDownloadJob.cs @@ -0,0 +1,253 @@ +using FastFetch.Jobs.Data; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace FastFetch.Jobs +{ + /// + /// Takes in blocks of object shas, downloads object shas as a pack or loose object, outputs pack locations (if applicable). + /// + public class BatchObjectDownloadJob : Job + { + private const string AreaPath = "BatchObjectDownloadJob"; + private const string DownloadAreaPath = "Download"; + + private static readonly TimeSpan HeartBeatPeriod = TimeSpan.FromSeconds(20); + + private readonly BlockingAggregator inputQueue; + + private int activeDownloadCount; + + private ITracer tracer; + private Enlistment enlistment; + private HttpGitObjects httpGitObjects; + private GitObjects gitObjects; + private Timer heartbeat; + + private long bytesDownloaded = 0; + + public BatchObjectDownloadJob( + int maxParallel, + int chunkSize, + BlockingCollection inputQueue, + BlockingCollection availableBlobs, + ITracer tracer, + Enlistment enlistment, + HttpGitObjects httpGitObjects, + GitObjects gitObjects) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational); + + this.inputQueue = new BlockingAggregator(inputQueue, chunkSize, objectIds => new BlobDownloadRequest(objectIds)); + + this.enlistment = enlistment; + this.httpGitObjects = httpGitObjects; + + this.gitObjects = gitObjects; + + this.AvailablePacks = new BlockingCollection(); + this.AvailableObjects = availableBlobs; + } + + public BlockingCollection AvailablePacks { get; } + + public BlockingCollection AvailableObjects { get; } + + protected override void DoBeforeWork() + { + this.heartbeat = new Timer(this.EmitHeartbeat, null, TimeSpan.Zero, HeartBeatPeriod); + base.DoBeforeWork(); + } + + protected override void DoWork() + { + BlobDownloadRequest request; + while (this.inputQueue.TryTake(out request)) + { + Interlocked.Increment(ref this.activeDownloadCount); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("PackId", request.PackId); + metadata.Add("ActiveDownloads", this.activeDownloadCount); + metadata.Add("NumberOfObjects", request.ObjectIds.Count); + + using (ITracer activity = this.tracer.StartActivity(DownloadAreaPath, EventLevel.Informational, metadata)) + { + try + { + RetryWrapper.InvocationResult result; + + if (request.ObjectIds.Count == 1) + { + result = this.httpGitObjects.TryDownloadLooseObject( + request.ObjectIds[0], + onSuccess: (tryCount, response) => this.WriteObjectOrPackAsync(request, tryCount, response), + onFailure: RetryWrapper.StandardErrorHandler(activity, DownloadAreaPath)); + } + else + { + HashSet successfulDownloads = new HashSet(StringComparer.OrdinalIgnoreCase); + result = this.httpGitObjects.TryDownloadObjects( + () => request.ObjectIds.Except(successfulDownloads), + commitDepth: 1, + onSuccess: (tryCount, response) => this.WriteObjectOrPackAsync(request, tryCount, response, successfulDownloads), + onFailure: RetryWrapper.StandardErrorHandler(activity, DownloadAreaPath), + preferBatchedLooseObjects: true); + } + + if (!result.Succeeded) + { + this.HasFailures = true; + } + + metadata.Add("Success", result.Succeeded); + metadata.Add("AttemptNumber", result.Attempts); + metadata["ActiveDownloads"] = this.activeDownloadCount - 1; + activity.Stop(metadata); + } + finally + { + Interlocked.Decrement(ref this.activeDownloadCount); + } + } + } + } + + protected override void DoAfterWork() + { + this.heartbeat.Dispose(); + this.heartbeat = null; + + this.AvailablePacks.CompleteAdding(); + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestCount", BlobDownloadRequest.TotalRequests); + metadata.Add("BytesDownloaded", this.bytesDownloaded); + this.tracer.Stop(metadata); + } + + private RetryWrapper.CallbackResult WriteObjectOrPackAsync( + BlobDownloadRequest request, + int tryCount, + HttpGitObjects.GitEndPointResponseData response, + HashSet successfulDownloads = null) + { + string fileName = null; + switch (response.ContentType) + { + case HttpGitObjects.ContentType.LooseObject: + string sha = request.ObjectIds.First(); + fileName = this.gitObjects.WriteLooseObject( + this.enlistment.WorkingDirectoryRoot, + response.Stream, + sha); + this.AvailableObjects.Add(sha); + break; + case HttpGitObjects.ContentType.PackFile: + fileName = this.gitObjects.WriteTempPackFile(response); + this.AvailablePacks.Add(new IndexPackRequest(fileName, request)); + break; + case HttpGitObjects.ContentType.BatchedLooseObjects: + // To reduce allocations, reuse the same buffer when writing objects in this batch + byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; + + OnLooseObject onLooseObject = (objectStream, sha1) => + { + this.gitObjects.WriteLooseObject( + this.enlistment.WorkingDirectoryRoot, + objectStream, + sha1, + bufToCopyWith); + this.AvailableObjects.Add(sha1); + + if (successfulDownloads != null) + { + successfulDownloads.Add(sha1); + } + + // This isn't strictly correct because we don't add object header bytes, + // just the actual compressed content length, but we expect the amount of + // header data to be negligible compared to the objects themselves. + Interlocked.Add(ref this.bytesDownloaded, objectStream.Length); + }; + + new BatchedLooseObjectDeserializer(response.Stream, onLooseObject).ProcessObjects(); + break; + } + + if (fileName != null) + { + // NOTE: If we are writing a file as part of this method, the only case + // where it's not expected to exist is when running unit tests + FileInfo info = new FileInfo(fileName); + if (info.Exists) + { + Interlocked.Add(ref this.bytesDownloaded, info.Length); + } + else + { + return new RetryWrapper.CallbackResult( + new HttpGitObjects.GitObjectTaskResult(false)); + } + } + + return new RetryWrapper.CallbackResult( + new HttpGitObjects.GitObjectTaskResult(true)); + } + + private void EmitHeartbeat(object state) + { + EventMetadata metadata = new EventMetadata(); + metadata["ActiveDownloads"] = this.activeDownloadCount; + this.tracer.RelatedEvent(EventLevel.Verbose, "DownloadHeartbeat", metadata); + } + + private class BlockingAggregator + { + private BlockingCollection inputQueue; + private int chunkSize; + private Func, OutputType> factory; + + public BlockingAggregator(BlockingCollection input, int chunkSize, Func, OutputType> factory) + { + this.inputQueue = input; + this.chunkSize = chunkSize; + this.factory = factory; + } + + public bool TryTake(out OutputType output) + { + List intermediary = new List(); + for (int i = 0; i < this.chunkSize; ++i) + { + InputType data; + if (this.inputQueue.TryTake(out data, millisecondsTimeout: -1)) + { + intermediary.Add(data); + } + else + { + break; + } + } + + if (intermediary.Any()) + { + output = this.factory(intermediary); + return true; + } + + output = default(OutputType); + return false; + } + } + } +} \ No newline at end of file diff --git a/GVFS/FastFetch/Jobs/Data/BlobDownloadRequest.cs b/GVFS/FastFetch/Jobs/Data/BlobDownloadRequest.cs new file mode 100644 index 00000000..e50080e9 --- /dev/null +++ b/GVFS/FastFetch/Jobs/Data/BlobDownloadRequest.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace FastFetch.Jobs.Data +{ + public class BlobDownloadRequest + { + private static int requestCounter = 0; + + public BlobDownloadRequest(IReadOnlyList objectIds) + { + this.ObjectIds = objectIds; + this.PackId = Interlocked.Increment(ref requestCounter); + } + + public static int TotalRequests + { + get + { + return requestCounter; + } + } + + public IReadOnlyList ObjectIds { get; } + + public int PackId { get; } + } +} diff --git a/GVFS/FastFetch/Jobs/Data/IndexPackRequest.cs b/GVFS/FastFetch/Jobs/Data/IndexPackRequest.cs new file mode 100644 index 00000000..8399fa2e --- /dev/null +++ b/GVFS/FastFetch/Jobs/Data/IndexPackRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FastFetch.Jobs.Data +{ + public class IndexPackRequest + { + public IndexPackRequest(string tempPackFile, BlobDownloadRequest downloadRequest) + { + this.TempPackFile = tempPackFile; + this.DownloadRequest = downloadRequest; + } + + public BlobDownloadRequest DownloadRequest { get; } + + public string TempPackFile { get; } + } +} \ No newline at end of file diff --git a/GVFS/FastFetch/Jobs/Data/TreeSearchRequest.cs b/GVFS/FastFetch/Jobs/Data/TreeSearchRequest.cs new file mode 100644 index 00000000..45cf1eea --- /dev/null +++ b/GVFS/FastFetch/Jobs/Data/TreeSearchRequest.cs @@ -0,0 +1,18 @@ +namespace FastFetch.Jobs.Data +{ + public class SearchTreeRequest + { + public SearchTreeRequest(string treeSha, string rootPath, bool shouldRecurse) + { + this.TreeSha = treeSha; + this.RootPath = rootPath; + this.ShouldRecurse = shouldRecurse; + } + + public bool ShouldRecurse { get; } + + public string TreeSha { get; } + + public string RootPath { get; } + } +} diff --git a/GVFS/FastFetch/Jobs/FindMissingBlobsJob.cs b/GVFS/FastFetch/Jobs/FindMissingBlobsJob.cs new file mode 100644 index 00000000..f79d7765 --- /dev/null +++ b/GVFS/FastFetch/Jobs/FindMissingBlobsJob.cs @@ -0,0 +1,89 @@ +using FastFetch.Jobs.Data; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace FastFetch.Jobs +{ + /// + /// Takes in search requests, searches each tree as requested, outputs blocks of missing blob shas. + /// + public class FindMissingBlobsJob : Job + { + private const string AreaPath = nameof(FindMissingBlobsJob); + private const string TreeSearchAreaPath = "TreeSearch"; + + private ITracer tracer; + private Enlistment enlistment; + private int missingBlobCount; + private int availableBlobCount; + + private BlockingCollection inputQueue; + + private ConcurrentHashSet alreadyFoundBlobIds; + private ProcessPool catFilePool; + + public FindMissingBlobsJob( + int maxParallel, + BlockingCollection inputQueue, + BlockingCollection availableBlobs, + ITracer tracer, + Enlistment enlistment) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational); + this.inputQueue = inputQueue; + this.enlistment = enlistment; + this.alreadyFoundBlobIds = new ConcurrentHashSet(); + + this.DownloadQueue = new BlockingCollection(); + this.AvailableBlobs = availableBlobs; + + this.catFilePool = new ProcessPool( + tracer, + () => new GitCatFileBatchCheckProcess(this.enlistment), + maxParallel); + } + + public BlockingCollection DownloadQueue { get; } + public BlockingCollection AvailableBlobs { get; } + + protected override void DoWork() + { + string blobId; + while (this.inputQueue.TryTake(out blobId, Timeout.Infinite)) + { + this.catFilePool.Invoke(catFileProcess => + { + if (!catFileProcess.ObjectExists(blobId)) + { + Interlocked.Increment(ref this.missingBlobCount); + this.DownloadQueue.Add(blobId); + } + else + { + Interlocked.Increment(ref this.availableBlobCount); + this.AvailableBlobs.Add(blobId); + } + }); + } + } + + protected override void DoAfterWork() + { + this.DownloadQueue.CompleteAdding(); + this.catFilePool.Dispose(); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("TotalMissingObjects", this.missingBlobCount); + metadata.Add("AvailableObjects", this.availableBlobCount); + this.tracer.Stop(metadata); + } + } +} diff --git a/GVFS/FastFetch/Jobs/IndexPackJob.cs b/GVFS/FastFetch/Jobs/IndexPackJob.cs new file mode 100644 index 00000000..1e00e7bc --- /dev/null +++ b/GVFS/FastFetch/Jobs/IndexPackJob.cs @@ -0,0 +1,79 @@ +using FastFetch.Jobs.Data; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System.Collections.Concurrent; +using System.Threading; + +namespace FastFetch.Jobs +{ + public class IndexPackJob : Job + { + private const string AreaPath = "IndexPackJob"; + private const string IndexPackAreaPath = "IndexPack"; + + private readonly BlockingCollection inputQueue; + + private ITracer tracer; + private GitObjects gitObjects; + + private long shasIndexed = 0; + + public IndexPackJob( + int maxParallel, + BlockingCollection inputQueue, + BlockingCollection availableBlobs, + ITracer tracer, + GitObjects gitObjects) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational); + this.inputQueue = inputQueue; + this.gitObjects = gitObjects; + this.AvailableBlobs = availableBlobs; + } + + public BlockingCollection AvailableBlobs { get; } + + protected override void DoWork() + { + IndexPackRequest request; + while (this.inputQueue.TryTake(out request, millisecondsTimeout: -1)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("PackId", request.DownloadRequest.PackId); + using (ITracer activity = this.tracer.StartActivity(IndexPackAreaPath, EventLevel.Informational, metadata)) + { + GitProcess.Result result = this.gitObjects.IndexTempPackFile(request.TempPackFile); + if (result.HasErrors) + { + EventMetadata errorMetadata = new EventMetadata(); + errorMetadata.Add("PackId", request.DownloadRequest.PackId); + errorMetadata.Add("ErrorMessage", result.Errors); + activity.RelatedError(errorMetadata); + this.HasFailures = true; + } + + if (!this.HasFailures) + { + foreach (string blobId in request.DownloadRequest.ObjectIds) + { + this.AvailableBlobs.Add(blobId); + Interlocked.Increment(ref this.shasIndexed); + } + } + + metadata.Add("Success", !this.HasFailures); + activity.Stop(metadata); + } + } + } + + protected override void DoAfterWork() + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ShasIndexed", this.shasIndexed); + this.tracer.Stop(metadata); + } + } +} diff --git a/GVFS/FastFetch/Jobs/Job.cs b/GVFS/FastFetch/Jobs/Job.cs new file mode 100644 index 00000000..7ab4c93c --- /dev/null +++ b/GVFS/FastFetch/Jobs/Job.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; + +namespace FastFetch.Jobs +{ + public abstract class Job + { + private int maxParallel; + private Thread[] workers; + + public Job(int maxParallel) + { + this.maxParallel = maxParallel; + } + + public bool HasFailures { get; protected set; } + + public void Start() + { + if (this.workers != null) + { + throw new InvalidOperationException("Cannot call start twice"); + } + + this.DoBeforeWork(); + + this.workers = new Thread[this.maxParallel]; + for (int i = 0; i < this.workers.Length; ++i) + { + this.workers[i] = new Thread(this.DoWork); + this.workers[i].Start(); + } + } + + public void WaitForCompletion() + { + if (this.workers == null) + { + throw new InvalidOperationException("Cannot wait for completion before start is called"); + } + + foreach (Thread t in this.workers) + { + t.Join(); + } + + this.DoAfterWork(); + this.workers = null; + } + + protected virtual void DoBeforeWork() + { + } + + protected abstract void DoWork(); + + protected abstract void DoAfterWork(); + } +} diff --git a/GVFS/FastFetch/Program.cs b/GVFS/FastFetch/Program.cs new file mode 100644 index 00000000..9af2ec91 --- /dev/null +++ b/GVFS/FastFetch/Program.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace FastFetch +{ + public class Program + { + public static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(fastFetch => fastFetch.Execute()); + } + } +} diff --git a/GVFS/FastFetch/Properties/AssemblyInfo.cs b/GVFS/FastFetch/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..196cf554 --- /dev/null +++ b/GVFS/FastFetch/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("FastFetch")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("FastFetch")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("07f2a520-2ab7-46dd-97c0-75d8e988d55b")] diff --git a/GVFS/FastFetch/packages.config b/GVFS/FastFetch/packages.config new file mode 100644 index 00000000..2619dfed --- /dev/null +++ b/GVFS/FastFetch/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Common/AntiVirusExclusions.cs b/GVFS/GVFS.Common/AntiVirusExclusions.cs new file mode 100644 index 00000000..2ce8470d --- /dev/null +++ b/GVFS/GVFS.Common/AntiVirusExclusions.cs @@ -0,0 +1,53 @@ +using System; + +namespace GVFS.Common +{ + public static class AntiVirusExclusions + { + public static void AddAntiVirusExclusion(string path) + { + try + { + CallPowershellCommand("Add-MpPreference -ExclusionPath \"" + path + "\""); + } + catch (Exception e) + { + Console.WriteLine("Unable to add exclusion: " + e.ToString()); + } + } + + public static bool TryGetIsPathExcluded(string path, out bool isExcluded) + { + isExcluded = false; + + try + { + ProcessResult getMpPrefrencesResult = CallPowershellCommand("Get-MpPreference | Select -ExpandProperty ExclusionPath"); + if (getMpPrefrencesResult.ExitCode == 0) + { + foreach (string excludedPath in getMpPrefrencesResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (excludedPath.Trim().Equals(path, StringComparison.OrdinalIgnoreCase)) + { + isExcluded = true; + break; + } + } + } + } + catch (Exception e) + { + Console.WriteLine("Unable to get exclusions:" + e.ToString()); + + return false; + } + + return true; + } + + private static ProcessResult CallPowershellCommand(string command) + { + return ProcessHelper.Run("powershell", "-NoProfile -Command \"& { " + command + " }\""); + } + } +} diff --git a/GVFS/GVFS.Common/BatchedLooseObjects/BatchedLooseObjectDeserializer.cs b/GVFS/GVFS.Common/BatchedLooseObjects/BatchedLooseObjectDeserializer.cs new file mode 100644 index 00000000..17943336 --- /dev/null +++ b/GVFS/GVFS.Common/BatchedLooseObjects/BatchedLooseObjectDeserializer.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.Common +{ + /// + /// Invoked when the full content of a single loose object is available. + /// + public delegate void OnLooseObject(Stream objectStream, string sha1); + + /// + /// Deserializer for concatenated loose objects. + /// + public class BatchedLooseObjectDeserializer + { + private const int NumObjectIdBytes = 20; + private const int NumObjectHeaderBytes = NumObjectIdBytes + sizeof(long); + private static readonly byte[] ExpectedHeader + = new byte[] + { + (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', // Magic + 1 // Version + }; + + private readonly Stream source; + private readonly OnLooseObject onLooseObject; + + public BatchedLooseObjectDeserializer(Stream source, OnLooseObject onLooseObject) + { + this.source = source; + this.onLooseObject = onLooseObject; + } + + /// + /// Read all the objects from the source stream and call for each. + /// + /// The total number of objects read + public int ProcessObjects() + { + this.ValidateHeader(); + + // Start reading objects + int numObjectsRead = 0; + byte[] curObjectHeader = new byte[NumObjectHeaderBytes]; + + while (true) + { + bool keepReading = this.ShouldContinueReading(curObjectHeader); + if (!keepReading) + { + break; + } + + // Get the length + long curLength = BitConverter.ToInt64(curObjectHeader, NumObjectIdBytes); + + // Handle the loose object + using (Stream rawObjectData = new RestrictedStream(this.source, 0, curLength, leaveOpen: true)) + { + string objectId = SHA1Util.HexStringFromBytes(curObjectHeader, NumObjectIdBytes); + + if (objectId.Equals(GVFSConstants.AllZeroSha)) + { + throw new RetryableException("Received all-zero SHA before end of stream"); + } + + this.onLooseObject(rawObjectData, objectId); + numObjectsRead++; + } + } + + return numObjectsRead; + } + + /// + /// Parse the current object header to check if we've reached the end. + /// + /// true if the end of the stream has been reached, false if not + private bool ShouldContinueReading(byte[] curObjectHeader) + { + int totalBytes = StreamUtil.TryReadGreedy( + this.source, + curObjectHeader, + 0, + curObjectHeader.Length); + + if (totalBytes == NumObjectHeaderBytes) + { + // Successful header read + return true; + } + else if (totalBytes == NumObjectIdBytes) + { + // We may have finished reading all the objects + for (int i = 0; i < NumObjectIdBytes; i++) + { + if (curObjectHeader[i] != 0) + { + throw new RetryableException( + string.Format( + "Reached end of stream before we got the expected zero-object ID Buffer: {0}", + SHA1Util.HexStringFromBytes(curObjectHeader))); + } + } + + return false; + } + else + { + throw new RetryableException( + string.Format( + "Reached end of stream before expected {0} or {1} bytes. Got {2}. Buffer: {3}", + NumObjectHeaderBytes, + NumObjectIdBytes, + totalBytes, + SHA1Util.HexStringFromBytes(curObjectHeader))); + } + } + + private void ValidateHeader() + { + byte[] headerBuf = new byte[ExpectedHeader.Length]; + StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); + if (!headerBuf.SequenceEqual(ExpectedHeader)) + { + throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); + } + } + } +} diff --git a/GVFS/GVFS.Common/BatchedLooseObjects/RestrictedStream.cs b/GVFS/GVFS.Common/BatchedLooseObjects/RestrictedStream.cs new file mode 100644 index 00000000..212eb144 --- /dev/null +++ b/GVFS/GVFS.Common/BatchedLooseObjects/RestrictedStream.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; + +namespace GVFS.Common +{ + /// + /// Stream wrapper for a length-limited subview of another stream. + /// + internal class RestrictedStream : Stream + { + private readonly Stream stream; + private readonly long length; + private readonly bool leaveOpen; + + private long position; + private bool closed; + + public RestrictedStream(Stream stream, long offset, long length, bool leaveOpen = false) + { + this.stream = stream; + this.length = length; + this.leaveOpen = leaveOpen; + + if (offset != 0) + { + if (!this.stream.CanSeek) + { + throw new InvalidOperationException(); + } + + this.stream.Seek(offset, SeekOrigin.Current); + } + } + + public override bool CanRead + { + get + { + return true; + } + } + + public override bool CanSeek + { + get + { + return this.stream.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override long Length + { + get + { + return this.length; + } + } + + public override long Position + { + get + { + return this.position; + } + + set + { + this.Seek(value, SeekOrigin.Begin); + } + } + + public override void Close() + { + if (!this.closed) + { + this.closed = true; + + if (!this.leaveOpen) + { + this.stream.Close(); + } + } + + base.Close(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesToRead = (int)(Math.Min(this.position + count, this.length) - this.position); + + // Some streams like HttpContent.ReadOnlyStream throw InvalidOperationException + // when reading 0 bytes from huge streams. If that changes we can remove this check. + if (bytesToRead == 0) + { + return 0; + } + + int toReturn = this.stream.Read(buffer, offset, bytesToRead); + + this.position += toReturn; + + return toReturn; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (!this.stream.CanSeek) + { + throw new InvalidOperationException(); + } + + long newPosition; + + switch (origin) + { + case SeekOrigin.Begin: + newPosition = offset; + break; + + case SeekOrigin.Current: + newPosition = this.position + offset; + break; + + case SeekOrigin.End: + newPosition = this.length + offset; + break; + + default: + throw new InvalidOperationException(); + } + + newPosition = Math.Max(Math.Min(this.length, newPosition), 0); + + this.stream.Seek(newPosition - this.position, SeekOrigin.Current); + + this.position = newPosition; + + return newPosition; + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/GVFS/GVFS.Common/CallbackResult.cs b/GVFS/GVFS.Common/CallbackResult.cs new file mode 100644 index 00000000..80ace25d --- /dev/null +++ b/GVFS/GVFS.Common/CallbackResult.cs @@ -0,0 +1,9 @@ +namespace GVFS.Common +{ + public enum CallbackResult + { + Success, + RetryableError, + FatalError + } +} diff --git a/GVFS/GVFS.Common/ConcurrentHashSet.cs b/GVFS/GVFS.Common/ConcurrentHashSet.cs new file mode 100644 index 00000000..fe252db1 --- /dev/null +++ b/GVFS/GVFS.Common/ConcurrentHashSet.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace GVFS.Common +{ + public class ConcurrentHashSet : IEnumerable + { + private ConcurrentDictionary dictionary; + + public ConcurrentHashSet() + { + this.dictionary = new ConcurrentDictionary(); + } + + public ConcurrentHashSet(IEqualityComparer comparer) + { + this.dictionary = new ConcurrentDictionary(comparer); + } + + public int Count + { + get { return this.dictionary.Count; } + } + + public bool Add(T entry) + { + return this.dictionary.TryAdd(entry, true); + } + + public bool Contains(T item) + { + return this.dictionary.ContainsKey(item); + } + + public void Clear() + { + this.dictionary.Clear(); + } + + public IEnumerator GetEnumerator() + { + return this.dictionary.Keys.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public bool TryRemove(T key) + { + bool value; + return this.dictionary.TryRemove(key, out value); + } + } +} diff --git a/GVFS/GVFS.Common/Enlistment.cs b/GVFS/GVFS.Common/Enlistment.cs new file mode 100644 index 00000000..4a98a36e --- /dev/null +++ b/GVFS/GVFS.Common/Enlistment.cs @@ -0,0 +1,135 @@ +using GVFS.Common.Git; +using System; +using System.IO; + +namespace GVFS.Common +{ + public abstract class Enlistment + { + private const string ObjectsEndpointSuffix = "/gvfs/objects"; + private const string PrefetchEndpointSuffix = "/gvfs/prefetch"; + + private const string DeprecatedObjectsEndpointGitConfigName = "gvfs.objects-endpoint"; + + private const string GVFSGitConfigPrefix = "gvfs."; + private const string CacheEndpointGitConfigSuffix = ".cache-server-url"; + + // New enlistment + protected Enlistment(string enlistmentRoot, string workingDirectoryRoot, string repoUrl, string cacheServerUrl, string gitBinPath, string gvfsHooksRoot) + { + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + throw new ArgumentException("Path to git.exe must be set"); + } + + this.EnlistmentRoot = enlistmentRoot; + this.WorkingDirectoryRoot = workingDirectoryRoot; + this.GitBinPath = gitBinPath; + this.GVFSHooksRoot = gvfsHooksRoot; + this.RepoUrl = repoUrl; + + this.SetComputedPaths(); + this.SetComputedURLs(cacheServerUrl); + } + + // Existing, configured enlistment + protected Enlistment(string enlistmentRoot, string workingDirectoryRoot, string cacheServerUrl, string gitBinPath, string gvfsHooksRoot) + { + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + throw new ArgumentException("Path to git.exe must be set"); + } + + this.EnlistmentRoot = enlistmentRoot; + this.WorkingDirectoryRoot = workingDirectoryRoot; + this.GitBinPath = gitBinPath; + this.GVFSHooksRoot = gvfsHooksRoot; + + this.SetComputedPaths(); + + GitProcess.Result originResult = new GitProcess(this).GetOriginUrl(); + if (originResult.HasErrors) + { + throw new InvalidRepoException("Could not get origin url. git error: " + originResult.Errors); + } + + this.RepoUrl = originResult.Output; + this.SetComputedURLs(cacheServerUrl); + } + + public string EnlistmentRoot { get; } + public string WorkingDirectoryRoot { get; } + public string DotGitRoot { get; private set; } + public string GitPackRoot { get; private set; } + public string RepoUrl { get; } + public string CacheServerUrl { get; private set; } + + public string ObjectsEndpointUrl { get; private set; } + + public string PrefetchEndpointUrl { get; private set; } + + public string GitBinPath { get; } + public string GVFSHooksRoot { get; } + + public static string StripObjectsEndpointSuffix(string input) + { + if (!string.IsNullOrWhiteSpace(input) && input.EndsWith(ObjectsEndpointSuffix)) + { + input = input.Substring(0, input.Length - ObjectsEndpointSuffix.Length); + } + + return input; + } + + protected static string GetCacheConfigSettingName(string repoUrl) + { + string sectionUrl = repoUrl.ToLowerInvariant() + .Replace("https://", string.Empty) + .Replace("http://", string.Empty) + .Replace('/', '.'); + + return GVFSGitConfigPrefix + sectionUrl + CacheEndpointGitConfigSuffix; + } + + protected string GetCacheServerUrlFromConfig(string repoUrl) + { + GitProcess git = new GitProcess(this); + string cacheConfigName = GetCacheConfigSettingName(repoUrl); + + string cacheServerUrl = git.GetFromConfig(cacheConfigName); + if (string.IsNullOrWhiteSpace(cacheServerUrl)) + { + // Try getting from the deprecated setting for compatibility reasons + cacheServerUrl = StripObjectsEndpointSuffix(git.GetFromConfig(DeprecatedObjectsEndpointGitConfigName)); + + // Upgrade for future runs, but not at clone time. + if (!string.IsNullOrWhiteSpace(cacheServerUrl) && Directory.Exists(this.WorkingDirectoryRoot)) + { + git.SetInLocalConfig(cacheConfigName, cacheServerUrl); + git.DeleteFromLocalConfig(DeprecatedObjectsEndpointGitConfigName); + } + } + + // Default to uncached url + if (string.IsNullOrWhiteSpace(cacheServerUrl)) + { + return repoUrl; + } + + return cacheServerUrl; + } + + private void SetComputedPaths() + { + this.DotGitRoot = Path.Combine(this.WorkingDirectoryRoot, GVFSConstants.DotGit.Root); + this.GitPackRoot = Path.Combine(this.WorkingDirectoryRoot, GVFSConstants.DotGit.Objects.Pack.Root); + } + + private void SetComputedURLs(string cacheServerUrl) + { + this.CacheServerUrl = !string.IsNullOrWhiteSpace(cacheServerUrl) ? cacheServerUrl : this.GetCacheServerUrlFromConfig(this.RepoUrl); + this.ObjectsEndpointUrl = this.CacheServerUrl + ObjectsEndpointSuffix; + this.PrefetchEndpointUrl = this.CacheServerUrl + PrefetchEndpointSuffix; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/FileBasedLock.cs b/GVFS/GVFS.Common/FileBasedLock.cs new file mode 100644 index 00000000..188d96d6 --- /dev/null +++ b/GVFS/GVFS.Common/FileBasedLock.cs @@ -0,0 +1,355 @@ +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.ComponentModel; +using System.IO; +using System.Text; + +namespace GVFS.Common +{ + public class FileBasedLock : IDisposable + { + private const int DefaultStreamWriterBufferSize = 1024; // Copied from: http://referencesource.microsoft.com/#mscorlib/system/io/streamwriter.cs,5516ce201dc06b5f + private const long InvalidFileLength = -1; + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); // Default encoding used by StreamWriter + + private readonly object deleteOnCloseStreamLock = new object(); + private readonly PhysicalFileSystem fileSystem; + private readonly string lockPath; + private ITracer tracer; + private FileStream deleteOnCloseStream; + + public FileBasedLock(PhysicalFileSystem fileSystem, ITracer tracer, string lockPath, string signature, ExistingLockCleanup existingLockCleanup) + { + this.fileSystem = fileSystem; + this.tracer = tracer; + this.lockPath = lockPath; + this.Signature = signature; + + if (existingLockCleanup != ExistingLockCleanup.LeaveExisting) + { + this.CleanupStaleLock(existingLockCleanup); + } + } + + public enum ExistingLockCleanup + { + LeaveExisting, + DeleteExisting, + DeleteExistingAndLogSignature + } + + public string Signature { get; private set; } + + public bool TryAcquireLockAndDeleteOnClose() + { + try + { + lock (this.deleteOnCloseStreamLock) + { + if (this.IsOpen()) + { + return true; + } + + this.deleteOnCloseStream = (FileStream)this.fileSystem.OpenFileStream( + this.lockPath, + FileMode.CreateNew, + (FileAccess)(NativeMethods.FileAccess.FILE_GENERIC_READ | NativeMethods.FileAccess.FILE_GENERIC_WRITE | NativeMethods.FileAccess.DELETE), + NativeMethods.FileAttributes.FILE_FLAG_DELETE_ON_CLOSE, + FileShare.Read); + + // Pass in true for leaveOpen to ensure that lockStream stays open + using (StreamWriter writer = new StreamWriter( + this.deleteOnCloseStream, + UTF8NoBOM, + DefaultStreamWriterBufferSize, + leaveOpen: true)) + { + this.WriteSignatureAndMessage(writer, message: null); + } + + return true; + } + } + catch (NativeMethods.Win32FileExistsException) + { + this.DisposeStream(); + return false; + } + catch (IOException e) + { + EventMetadata metadata = this.CreateLockMetadata("IOException caught while trying to acquire lock", e); + this.tracer.RelatedEvent(EventLevel.Warning, "TryAcquireLockAndDeleteOnClose", metadata); + + this.DisposeStream(); + return false; + } + catch (Win32Exception e) + { + EventMetadata metadata = this.CreateLockMetadata("Win32Exception caught while trying to acquire lock", e); + this.tracer.RelatedEvent(EventLevel.Warning, "TryAcquireLockAndDeleteOnClose", metadata); + + this.DisposeStream(); + return false; + } + catch (Exception e) + { + EventMetadata metadata = this.CreateLockMetadata("Unhandled exception caught while trying to acquire lock", e); + this.tracer.RelatedError("TryAcquireLockAndDeleteOnClose", metadata); + + this.DisposeStream(); + throw; + } + } + + public bool TryReleaseLock() + { + if (this.DisposeStream()) + { + return true; + } + + LockData lockData = this.GetLockDataFromDisk(); + if (lockData == null || lockData.Signature != this.Signature) + { + if (lockData == null) + { + throw new LockFileDoesNotExistException(this.lockPath); + } + + throw new LockSignatureDoesNotMatchException(this.lockPath, this.Signature, lockData.Signature); + } + + try + { + this.fileSystem.DeleteFile(this.lockPath); + } + catch (IOException e) + { + EventMetadata metadata = this.CreateLockMetadata("IOException caught while trying to release lock", e); + this.tracer.RelatedEvent(EventLevel.Warning, "TryReleaseLock", metadata); + + return false; + } + + return true; + } + + public bool IsOpen() + { + return this.deleteOnCloseStream != null; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + this.DisposeStream(); + } + + private LockData GetLockDataFromDisk() + { + if (this.LockFileExists()) + { + string existingSignature; + string existingMessage; + this.ReadLockFile(out existingSignature, out existingMessage); + return new LockData(existingSignature, existingMessage); + } + + return null; + } + + private void ReadLockFile(out string existingSignature, out string lockerMessage) + { + using (Stream fs = this.fileSystem.OpenFileStream(this.lockPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (StreamReader reader = new StreamReader(fs, UTF8NoBOM)) + { + existingSignature = reader.ReadLine(); + lockerMessage = reader.ReadLine(); + } + + existingSignature = existingSignature ?? string.Empty; + lockerMessage = lockerMessage ?? string.Empty; + } + + private bool LockFileExists() + { + return this.fileSystem.FileExists(this.lockPath); + } + + private void CleanupStaleLock(ExistingLockCleanup existingLockCleanup) + { + if (!this.LockFileExists()) + { + return; + } + + if (existingLockCleanup == ExistingLockCleanup.LeaveExisting) + { + throw new ArgumentException("CleanupStaleLock should not be called with LeaveExisting"); + } + + EventMetadata metadata = this.CreateLockMetadata(); + metadata.Add("existingLockCleanup", existingLockCleanup.ToString()); + + long length = InvalidFileLength; + try + { + FileProperties existingLockProperties = this.fileSystem.GetFileProperties(this.lockPath); + length = existingLockProperties.Length; + } + catch (Exception e) + { + metadata.Add("Exception", "Exception while getting lock file length: " + e.ToString()); + this.tracer.RelatedEvent(EventLevel.Warning, "CleanupEmptyLock", metadata); + } + + if (length == 0) + { + metadata.Add("Message", "Deleting empty lock file: " + this.lockPath); + this.tracer.RelatedEvent(EventLevel.Warning, "CleanupEmptyLock", metadata); + } + else + { + metadata.Add("Length", length == InvalidFileLength ? "Invalid" : length.ToString()); + + switch (existingLockCleanup) + { + case ExistingLockCleanup.DeleteExisting: + metadata.Add("Message", "Deleting stale lock file: " + this.lockPath); + this.tracer.RelatedEvent(EventLevel.Informational, "CleanupExistingLock", metadata); + break; + + case ExistingLockCleanup.DeleteExistingAndLogSignature: + string existingSignature; + try + { + string dummyLockerMessage; + this.ReadLockFile(out existingSignature, out dummyLockerMessage); + } + catch (Win32Exception e) + { + if (e.ErrorCode == NativeMethods.ERROR_FILE_NOT_FOUND) + { + // File was deleted before we could read its contents + return; + } + + throw; + } + + if (existingSignature == this.Signature) + { + metadata.Add("Message", "Deleting stale lock file: " + this.lockPath); + this.tracer.RelatedEvent(EventLevel.Informational, "CleanupExistingLock", metadata); + } + else + { + metadata.Add("ExistingLockSignature", existingSignature); + metadata.Add("Message", "Deleting stale lock file: " + this.lockPath + " with mismatched signature"); + this.tracer.RelatedEvent(EventLevel.Warning, "CleanupSignatureMismatchLock", metadata); + } + + break; + + default: + throw new InvalidOperationException("Invalid ExistingLockCleanup"); + } + } + + this.fileSystem.DeleteFile(this.lockPath); + } + + private void WriteSignatureAndMessage(StreamWriter writer, string message) + { + writer.WriteLine(this.Signature); + if (message != null) + { + writer.Write(message); + } + } + + private EventMetadata CreateLockMetadata(string message = null, Exception exception = null, bool errorMessage = false) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "FileBasedLock"); + metadata.Add("LockPath", this.lockPath); + metadata.Add("Signature", this.Signature); + + if (message != null) + { + metadata.Add(errorMessage ? "ErrorMessage" : "Message", message); + } + + if (exception != null) + { + metadata.Add("Exception", exception.ToString()); + } + + return metadata; + } + + private bool DisposeStream() + { + lock (this.deleteOnCloseStreamLock) + { + if (this.deleteOnCloseStream != null) + { + this.deleteOnCloseStream.Dispose(); + this.deleteOnCloseStream = null; + return true; + } + } + + return false; + } + + public class LockException : Exception + { + public LockException(string messageFormat, params string[] args) + : base(string.Format(messageFormat, args)) + { + } + } + + public class LockFileDoesNotExistException : LockException + { + public LockFileDoesNotExistException(string lockPath) + : base("Lock file {0} does not exist", lockPath) + { + } + } + + public class LockSignatureDoesNotMatchException : LockException + { + public LockSignatureDoesNotMatchException(string lockPath, string expectedSignature, string actualSignature) + : base( + "Lock file {0} does not contain expected signature '{1}' (existing signature: '{2}')", + lockPath, + expectedSignature, + actualSignature) + { + } + } + + public class LockData + { + public LockData(string signature, string message) + { + this.Signature = signature; + this.Message = message; + } + + public string Signature { get; } + + public string Message { get; } + } + } +} diff --git a/GVFS/GVFS.Common/GVFS.Common.csproj b/GVFS/GVFS.Common/GVFS.Common.csproj new file mode 100644 index 00000000..16f2c62c --- /dev/null +++ b/GVFS/GVFS.Common/GVFS.Common.csproj @@ -0,0 +1,179 @@ + + + + + Debug + AnyCPU + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} + Library + Properties + GVFS.Common + GVFS.Common + v4.5.2 + 512 + + + + + true + ..\..\..\BuildOutput\GVFS.Common\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.Common\obj\x64\Debug\ + DEBUG;TRACE + true + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + ..\..\..\BuildOutput\GVFS.Common\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.Common\obj\x64\Release\ + TRACE + true + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + 0.2.173.2 + + + + ..\..\..\packages\Microsoft.Database.Collections.Generic.1.9.4\lib\net40\Esent.Collections.dll + True + + + ..\..\..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll + True + + + ..\..\..\packages\Microsoft.Database.Isam.1.9.4\lib\net40\Esent.Isam.dll + True + + + False + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + False + ..\..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + + + + $(SolutionDir)\Scripts\CreateCommonAssemblyVersion.bat $(GVFSVersion) $(SolutionDir)\.. + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Common/GVFSConstants.cs b/GVFS/GVFS.Common/GVFSConstants.cs new file mode 100644 index 00000000..968f57f9 --- /dev/null +++ b/GVFS/GVFS.Common/GVFSConstants.cs @@ -0,0 +1,143 @@ +using GVFS.Common.Git; +using System.IO; + +namespace GVFS.Common +{ + public static class GVFSConstants + { + public const int ShaStringLength = 40; + + public const string RootFolderPath = "\\"; + public const char PathSeparator = '\\'; + public const string PathSeparatorString = "\\"; + public const char GitPathSeparator = '/'; + public const string GitPathSeparatorString = "/"; + + public const string AppName = "GitVirtualFileSystem"; + public const string AppGuid = "9a3cf8bb-ef4b-42df-ac4b-f5f50d114909"; + + public const string DotGVFSPath = ".gvfs"; + public const string GVFSLogFolderName = "logs"; + public const string VolumeLabel = "Git Virtual File System"; + + public const string GVFSConfigEndpointSuffix = "/gvfs/config"; + public const string InfoRefsEndpointSuffix = "/info/refs?service=git-upload-pack"; + + public const string VirtualizeObjectsGitConfigName = "core.virtualizeobjects"; + + public const string CatFileObjectTypeCommit = "commit"; + + public const string PrefetchPackPrefix = "prefetch"; + + public const string HeadCommitName = "HEAD"; + public const string GVFSHeadCommitName = "GVFS_HEAD"; + + public const string AllZeroSha = "0000000000000000000000000000000000000000"; + + public const string GVFSEtwProviderName = "Microsoft.Git.GVFS"; + + public const string WorkingDirectoryRootName = "src"; + + public const string GVFSHooksExecutableName = "GVFS.Hooks.exe"; + public const string GVFSReadObjectHookExecutableName = "GVFS.ReadObjectHook.exe"; + public const int InvalidProcessId = -1; + + public const string GitIsNotInstalledError = "Could not find git.exe. Ensure that Git is installed."; + + public static readonly GitVersion MinimumGitVersion = new GitVersion(2, 11, 0, "gvfs", 1, 3); + + public static class MediaTypes + { + public const string PrefetchPackFilesAndIndexesMediaType = "application/x-gvfs-timestamped-packfiles-indexes"; + public const string LooseObjectMediaType = "application/x-git-loose-object"; + public const string CustomLooseObjectsMediaType = "application/x-gvfs-loose-objects"; + public const string PackFileMediaType = "application/x-git-packfile"; + } + + public static class DatabaseNames + { + public const string BackgroundGitUpdates = "BackgroundGitUpdates"; + public const string BlobSizes = "BlobSizes"; + public const string DoNotProject = "DoNotProject"; + public const string RepoMetadata = "RepoMetadata"; + } + + public static class SpecialGitFiles + { + public const string GitAttributes = ".gitattributes"; + public const string GitIgnore = ".gitignore"; + } + + public static class DotGit + { + public const string Root = ".git"; + public const string HeadName = "HEAD"; + public const string IndexName = "index"; + public const string PackedRefsName = "packed-refs"; + public const string LockExtension = ".lock"; + + public static readonly string Config = Path.Combine(DotGit.Root, "config"); + public static readonly string Head = Path.Combine(DotGit.Root, HeadName); + public static readonly string Index = Path.Combine(DotGit.Root, IndexName); + public static readonly string PackedRefs = Path.Combine(DotGit.Root, PackedRefsName); + public static readonly string Shallow = Path.Combine(DotGit.Root, "shallow"); + + public static class Logs + { + public const string Name = "logs"; + public static readonly string HeadName = "HEAD"; + + public static readonly string Root = Path.Combine(DotGit.Root, Name); + public static readonly string Head = Path.Combine(Logs.Root, Logs.HeadName); + } + + public static class Hooks + { + public static readonly string ReadObjectName = "read-object"; + + public static readonly string Root = Path.Combine(DotGit.Root, "hooks"); + public static readonly string PreCommandPath = Path.Combine(Hooks.Root, "pre-command"); + public static readonly string ReadObjectPath = Path.Combine(Hooks.Root, ReadObjectName); + } + + public static class Info + { + public const string ExcludeName = "exclude"; + + public static readonly string Root = Path.Combine(DotGit.Root, "info"); + public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, "sparse-checkout"); + public static readonly string ExcludePath = Path.Combine(Info.Root, ExcludeName); + } + + public static class Refs + { + public const string Name = "refs"; + + public static readonly string Root = Path.Combine(DotGit.Root, Refs.Name); + + public static class Heads + { + public const string Name = "heads"; + public static readonly string Root = Path.Combine(DotGit.Refs.Root, Heads.Name); + } + } + + public static class Objects + { + public const string Name = "objects"; + public static readonly string Root = Path.Combine(DotGit.Root, Objects.Name); + + public static class Pack + { + public const string Name = "pack"; + public static readonly string Root = Path.Combine(Objects.Root, Pack.Name); + } + + public static class Info + { + public static readonly string Root = Path.Combine(Objects.Root, "info"); + } + } + } + } +} diff --git a/GVFS/GVFS.Common/GVFSContext.cs b/GVFS/GVFS.Common/GVFSContext.cs new file mode 100644 index 00000000..adec9e6f --- /dev/null +++ b/GVFS/GVFS.Common/GVFSContext.cs @@ -0,0 +1,45 @@ +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using System; + +namespace GVFS.Common +{ + public class GVFSContext : IDisposable + { + private bool disposedValue = false; + + public GVFSContext(ITracer tracer, PhysicalFileSystem fileSystem, GitRepo repository, GVFSEnlistment enlistment) + { + this.Tracer = tracer; + this.FileSystem = fileSystem; + this.Enlistment = enlistment; + this.Repository = repository; + } + + public ITracer Tracer { get; private set; } + public PhysicalFileSystem FileSystem { get; private set; } + public GitRepo Repository { get; private set; } + public GVFSEnlistment Enlistment { get; private set; } + + public void Dispose() + { + this.Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.Repository.Dispose(); + this.Tracer.Dispose(); + this.Tracer = null; + } + + this.disposedValue = true; + } + } + } +} diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs new file mode 100644 index 00000000..52133347 --- /dev/null +++ b/GVFS/GVFS.Common/GVFSEnlistment.cs @@ -0,0 +1,251 @@ +using GVFS.Common.Git; +using System; +using System.IO; +using System.Linq; +using System.Threading; + +namespace GVFS.Common +{ + public class GVFSEnlistment : Enlistment + { + // New enlistment + public GVFSEnlistment(string enlistmentRoot, string repoUrl, string cacheServerUrl, string gitBinPath, string gvfsHooksRoot) + : base( + enlistmentRoot, + Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName), + repoUrl, + cacheServerUrl, + gitBinPath, + gvfsHooksRoot) + { + this.SetComputedPaths(); + + // Mutex name cannot include '\' (other than the '\' after Global) + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682411(v=vs.85).aspx + this.EnlistmentMutex = new Mutex(false, "Global\\" + this.NamedPipeName.Replace('\\', ':')); + } + + // Existing, configured enlistment + public GVFSEnlistment(string enlistmentRoot, string cacheServerUrl, string gitBinPath, string gvfsHooksRoot) + : base( + enlistmentRoot, + Path.Combine(enlistmentRoot, GVFSConstants.WorkingDirectoryRootName), + cacheServerUrl, + gitBinPath, + gvfsHooksRoot) + { + this.SetComputedPaths(); + + // Mutex name cannot include '\' (other than the '\' after Global) + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682411(v=vs.85).aspx + this.EnlistmentMutex = new Mutex(false, GetMutexName(enlistmentRoot)); + } + + public Mutex EnlistmentMutex { get; } + + public string NamedPipeName { get; private set; } + + public string DotGVFSRoot { get; private set; } + + public string GVFSLogsRoot { get; private set; } + + public string GVFSHeadFile { get; private set; } + + public static GVFSEnlistment CreateFromCurrentDirectory(string cacheServerUrl, string gitBinRoot) + { + return CreateFromDirectory(Environment.CurrentDirectory, cacheServerUrl, gitBinRoot, null); + } + + public static string GetNamedPipeName(string enlistmentRoot) + { + return string.Format("GVFS_{0}", enlistmentRoot).ToUpper().Replace(':', '_'); + } + + public static string GetMutexName(string enlistmentRoot) + { + string pipeName = GetNamedPipeName(enlistmentRoot); + return "Global\\" + pipeName.Replace('\\', ':'); + } + + public static string GetEnlistmentRoot(string directory) + { + directory = directory.TrimEnd(GVFSConstants.PathSeparator); + DirectoryInfo dirInfo; + + try + { + dirInfo = new DirectoryInfo(directory); + } + catch (Exception) + { + return null; + } + + while (dirInfo != null && dirInfo.Exists) + { + DirectoryInfo[] dotGvfsDirs = dirInfo.GetDirectories(GVFSConstants.DotGVFSPath); + + if (dotGvfsDirs.Count() == 1) + { + return dirInfo.FullName; + } + + dirInfo = dirInfo.Parent; + } + + return null; + } + + public static GVFSEnlistment CreateFromDirectory(string directory, string cacheServerUrl, string gitBinRoot, string gvfsHooksRoot) + { + string enlistmentRoot = GetEnlistmentRoot(directory); + if (enlistmentRoot != null) + { + return new GVFSEnlistment(enlistmentRoot, cacheServerUrl, gitBinRoot, gvfsHooksRoot); + } + + return null; + } + + public static string ToFullPath(string originalValue, string toUseIfOriginalNullOrWhitespace) + { + if (string.IsNullOrWhiteSpace(originalValue)) + { + return toUseIfOriginalNullOrWhitespace; + } + + try + { + return Path.GetFullPath(originalValue); + } + catch (Exception) + { + return null; + } + } + + public static string GetNewGVFSLogFileName(string gvfsLogsRoot, string verb = "") + { + if (!Directory.Exists(gvfsLogsRoot)) + { + Directory.CreateDirectory(gvfsLogsRoot); + } + + string namePrefix = "gvfs_" + (string.IsNullOrEmpty(verb) ? string.Empty : (verb + "_")) + DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string fileName = Path.Combine( + gvfsLogsRoot, + namePrefix + ".log"); + + if (File.Exists(fileName)) + { + fileName = Path.Combine( + gvfsLogsRoot, + namePrefix + "_" + Guid.NewGuid().ToString("N") + ".log"); + } + + return fileName; + } + + public bool TrySetCacheServerUrlConfig() + { + GitProcess git = new Git.GitProcess(this); + string settingName = Enlistment.GetCacheConfigSettingName(this.RepoUrl); + return !git.SetInLocalConfig(settingName, this.CacheServerUrl).HasErrors; + } + + public bool TryCreateEnlistmentFolders() + { + try + { + Directory.CreateDirectory(this.EnlistmentRoot); + Directory.CreateDirectory(this.WorkingDirectoryRoot); + this.CreateHiddenDirectory(this.DotGVFSRoot); + } + catch (IOException) + { + return false; + } + + return true; + } + + public string GetMostRecentGVFSLogFileName() + { + DirectoryInfo logDirectory = new DirectoryInfo(this.GVFSLogsRoot); + if (!logDirectory.Exists) + { + return null; + } + + FileInfo[] files = logDirectory.GetFiles(); + if (files.Length == 0) + { + return null; + } + + return + files + .OrderByDescending(fileInfo => fileInfo.CreationTime) + .First() + .FullName; + } + + public bool TryParseGVFSHeadFile(out bool fileExists, out string error, out string commitId) + { + fileExists = false; + error = string.Empty; + commitId = string.Empty; + + string gvfsHeadFile = this.GVFSHeadFile; + if (File.Exists(gvfsHeadFile)) + { + fileExists = true; + try + { + string gvfsHeadContents = File.ReadAllText(gvfsHeadFile); + GitProcess.Result objectTypeResult = new GitProcess(this).CatFileGetType(gvfsHeadContents); + if (objectTypeResult.HasErrors) + { + error = string.Format("Error while determining the type of the commit stored in {0}: {1}", GVFSConstants.GVFSHeadCommitName, objectTypeResult.Errors); + return false; + } + else if (objectTypeResult.Output.StartsWith(GVFSConstants.CatFileObjectTypeCommit)) + { + commitId = gvfsHeadContents; + return true; + } + + error = string.Format("Contents of {0}: \"{1}\" is not a commit SHA", GVFSConstants.GVFSHeadCommitName, gvfsHeadContents); + return false; + } + catch (Exception e) + { + error = string.Format("Exception while parsing {0}: {1}", gvfsHeadFile, e.ToString()); + return false; + } + } + + error = string.Format("File \"{0}\" not found", gvfsHeadFile); + return false; + } + + private void SetComputedPaths() + { + this.NamedPipeName = GetNamedPipeName(this.EnlistmentRoot); + this.DotGVFSRoot = Path.Combine(this.EnlistmentRoot, GVFSConstants.DotGVFSPath); + this.GVFSLogsRoot = Path.Combine(this.DotGVFSRoot, GVFSConstants.GVFSLogFolderName); + this.GVFSHeadFile = Path.Combine(this.DotGVFSRoot, GVFSConstants.GVFSHeadCommitName); + } + + /// + /// Creates a hidden directory @ the given path. + /// If directory already exists, hides it. + /// + /// Path to desired hidden directory + private void CreateHiddenDirectory(string path) + { + DirectoryInfo dir = Directory.CreateDirectory(path); + dir.Attributes = FileAttributes.Hidden; + } + } +} diff --git a/GVFS/GVFS.Common/GVFSLock.cs b/GVFS/GVFS.Common/GVFSLock.cs new file mode 100644 index 00000000..2f5ab622 --- /dev/null +++ b/GVFS/GVFS.Common/GVFSLock.cs @@ -0,0 +1,216 @@ +using System; +using System.Diagnostics; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; + +namespace GVFS.Common +{ + public class GVFSLock + { + private readonly object acquisitionLock = new object(); + + private readonly ITracer tracer; + private NamedPipeMessages.AcquireLock.Data lockHolder; + + private bool isHeldInternally; + + public GVFSLock(ITracer tracer) + { + this.tracer = tracer; + } + + /// + /// Allows external callers (non-GVFS) to acquire the lock. + /// + /// The data for the external acquisition request. + /// + /// The current holder of the lock if the acquisition fails, or + /// the input request if it succeeds. + /// + /// True if the lock was acquired, false otherwise. + public bool TryAcquireLock( + NamedPipeMessages.AcquireLock.Data requester, + out NamedPipeMessages.AcquireLock.Data holder) + { + EventMetadata metadata = new EventMetadata(); + EventLevel eventLevel = EventLevel.Verbose; + metadata.Add("LockRequest", requester.ToString()); + try + { + lock (this.acquisitionLock) + { + if (this.isHeldInternally) + { + holder = null; + metadata.Add("CurrentLockHolder", "GVFS"); + metadata.Add("Result", "Denied"); + + return false; + } + + if (this.IsExternalProcessAlive() && + this.lockHolder.PID != requester.PID) + { + holder = this.lockHolder; + + metadata.Add("CurrentLockHolder", this.lockHolder.ToString()); + metadata.Add("Result", "Denied"); + return false; + } + + metadata.Add("Result", "Accepted"); + eventLevel = EventLevel.Informational; + + Process process; + if (ProcessHelper.TryGetProcess(requester.PID, out process) && + string.Equals(requester.OriginalCommand, ProcessHelper.GetCommandLine(process))) + { + this.lockHolder = requester; + holder = requester; + + return true; + } + else + { + // Process is no longer running so let it + // succeed since the process non-existence + // signals the lock release. + holder = null; + return true; + } + } + } + finally + { + this.tracer.RelatedEvent(eventLevel, "TryAcquireLockExternal", metadata); + } + } + + /// + /// Allow GVFS to acquire the lock. + /// + /// True if GVFS was able to acquire the lock or if it already held it. False othwerwise. + public bool TryAcquireLock() + { + EventMetadata metadata = new EventMetadata(); + EventLevel eventLevel = EventLevel.Verbose; + try + { + lock (this.acquisitionLock) + { + if (this.IsExternalProcessAlive()) + { + metadata.Add("CurrentLockHolder", this.lockHolder.ToString()); + metadata.Add("Full Command", this.lockHolder.OriginalCommand); + metadata.Add("Result", "Denied"); + return false; + } + + this.ClearHolder(); + this.isHeldInternally = true; + metadata.Add("Result", "Accepted"); + eventLevel = EventLevel.Informational; + return true; + } + } + finally + { + this.tracer.RelatedEvent(eventLevel, "TryAcquireLockInternal", metadata); + } + } + + /// + /// Allow GVFS to release the lock if it holds it. + /// + /// + /// This should only be invoked by GVFS and not external callers. + /// Release by external callers is implicit on process termination. + /// + public void ReleaseLock() + { + this.tracer.RelatedEvent(EventLevel.Verbose, "ReleaseLock", new EventMetadata()); + lock (this.acquisitionLock) + { + this.isHeldInternally = false; + } + } + + /// + /// Returns true if the lock is currently held by an external + /// caller that represents a git call using one of the specified git verbs. + /// + public bool IsLockedByGitVerb(params string[] verbs) + { + string command = this.GetLockedGitCommand(); + if (!string.IsNullOrEmpty(command)) + { + return GitHelper.IsVerb(command, verbs); + } + + return false; + } + + public string GetLockedGitCommand() + { + if (this.IsExternalProcessAlive()) + { + return this.lockHolder.ParsedCommand; + } + + return null; + } + + public string GetStatus() + { + if (this.isHeldInternally) + { + return "Held by GVFS."; + } + + string lockedCommand = this.GetLockedGitCommand(); + if (!string.IsNullOrEmpty(lockedCommand)) + { + return string.Format("Held by {0} (PID:{1})", lockedCommand, this.lockHolder.PID); + } + + return "Free"; + } + + private void ClearHolder() + { + this.lockHolder = null; + } + + private bool IsExternalProcessAlive() + { + lock (this.acquisitionLock) + { + if (this.isHeldInternally) + { + if (this.lockHolder != null) + { + throw new InvalidOperationException("Inconsistent GVFSLock state with external holder " + this.lockHolder.ToString()); + } + + return false; + } + + if (this.lockHolder == null) + { + return false; + } + + Process process; + if (ProcessHelper.TryGetProcess(this.lockHolder.PID, out process) && + string.Equals(this.lockHolder.OriginalCommand, ProcessHelper.GetCommandLine(process))) + { + return true; + } + + this.ClearHolder(); + return false; + } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Git/CatFileTimeoutException.cs b/GVFS/GVFS.Common/Git/CatFileTimeoutException.cs new file mode 100644 index 00000000..31c023d5 --- /dev/null +++ b/GVFS/GVFS.Common/Git/CatFileTimeoutException.cs @@ -0,0 +1,8 @@ +using System; + +namespace GVFS.Common.Git +{ + public class CatFileTimeoutException : TimeoutException + { + } +} diff --git a/GVFS/GVFS.Common/Git/DiffTreeResult.cs b/GVFS/GVFS.Common/Git/DiffTreeResult.cs new file mode 100644 index 00000000..6d8f93d9 --- /dev/null +++ b/GVFS/GVFS.Common/Git/DiffTreeResult.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Common.Git +{ + public class DiffTreeResult + { + private static readonly HashSet ValidTreeModes = new HashSet() { "040000" }; + + public enum Operations + { + Unknown, + CopyEdit, + RenameEdit, + Modify, + Delete, + Add, + Unmerged + } + + public Operations Operation { get; set; } + public bool SourceIsDirectory { get; set; } + public bool TargetIsDirectory { get; set; } + public string SourceFilename { get; set; } + public string TargetFilename { get; set; } + public string SourceSha { get; set; } + public string TargetSha { get; set; } + + public static DiffTreeResult ParseFromDiffTreeLine(string line, string repoRoot) + { + line = line.Substring(1); + + // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. + string[] parts = line.Split('\t'); + parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); + + DiffTreeResult result = new DiffTreeResult(); + result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); + result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); + result.SourceSha = parts[2]; + result.TargetSha = parts[3]; + result.Operation = DiffTreeResult.ParseOperation(parts[4]); + result.TargetFilename = ConvertPathToAbsoluteUtf8Path(repoRoot, parts.Last()); + result.SourceFilename = parts.Length == 7 ? ConvertPathToAbsoluteUtf8Path(repoRoot, parts[5]) : null; + return result; + } + + public static DiffTreeResult ParseFromLsTreeLine(string line, string repoRoot) + { + // Everything from ls-tree is an add. + int treeIndex = line.IndexOf(GitCatFileProcess.TreeMarker); + if (treeIndex >= 0) + { + DiffTreeResult treeAdd = new DiffTreeResult(); + treeAdd.TargetIsDirectory = true; + treeAdd.TargetFilename = ConvertPathToAbsoluteUtf8Path(repoRoot, line.Substring(line.LastIndexOf("\t") + 1)); + treeAdd.Operation = DiffTreeResult.Operations.Add; + + return treeAdd; + } + else + { + int blobIndex = line.IndexOf(GitCatFileProcess.BlobMarker); + if (blobIndex >= 0) + { + DiffTreeResult blobAdd = new DiffTreeResult(); + blobAdd.TargetSha = line.Substring(blobIndex + GitCatFileProcess.BlobMarker.Length, GVFSConstants.ShaStringLength); + blobAdd.TargetFilename = ConvertPathToAbsoluteUtf8Path(repoRoot, line.Substring(line.LastIndexOf("\t") + 1)); + blobAdd.Operation = DiffTreeResult.Operations.Add; + + return blobAdd; + } + else + { + return null; + } + } + } + + private static Operations ParseOperation(string gitOperationString) + { + switch (gitOperationString) + { + case "U": return Operations.Unmerged; + case "M": return Operations.Modify; + case "A": return Operations.Add; + case "D": return Operations.Delete; + case "X": return Operations.Unknown; + default: + if (gitOperationString.StartsWith("C")) + { + return Operations.CopyEdit; + } + else if (gitOperationString.StartsWith("R")) + { + return Operations.RenameEdit; + } + + throw new InvalidDataException("Unrecognized diff-tree operation: " + gitOperationString); + } + } + + private static string ConvertPathToAbsoluteUtf8Path(string repoRoot, string relativePath) + { + return Path.Combine(repoRoot, GitPathConverter.ConvertPathOctetsToUtf8(relativePath.Trim('"')).Replace('/', '\\')); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Git/GVFSConfigResponse.cs b/GVFS/GVFS.Common/Git/GVFSConfigResponse.cs new file mode 100644 index 00000000..70c2dbd0 --- /dev/null +++ b/GVFS/GVFS.Common/Git/GVFSConfigResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace GVFS.Common.Git +{ + public class GVFSConfigResponse + { + public IEnumerable AllowedGvfsClientVersions { get; set; } + + public class VersionRange + { + public Version Min { get; set; } + public Version Max { get; set; } + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitCatFileBatchCheckProcess.cs b/GVFS/GVFS.Common/Git/GitCatFileBatchCheckProcess.cs new file mode 100644 index 00000000..f41fad0c --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitCatFileBatchCheckProcess.cs @@ -0,0 +1,29 @@ +using System.IO; + +namespace GVFS.Common.Git +{ + public class GitCatFileBatchCheckProcess : GitCatFileProcess + { + public GitCatFileBatchCheckProcess(Enlistment enlistment) : base(enlistment, "--batch-check") + { + } + + public GitCatFileBatchCheckProcess(StreamReader stdOut, StreamWriter stdIn) : base(stdOut, stdIn) + { + } + + public bool TryGetObjectSize(string objectSha, out long size) + { + this.StdIn.Write(objectSha + "\n"); + string header; + return this.TryParseSizeFromStdOut(out header, out size); + } + + public bool ObjectExists(string objectSha) + { + this.StdIn.Write(objectSha + "\n"); + string header = this.StdOut.ReadLine(); + return header != null && !header.EndsWith("missing"); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Git/GitCatFileBatchProcess.cs b/GVFS/GVFS.Common/Git/GitCatFileBatchProcess.cs new file mode 100644 index 00000000..a62bd549 --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitCatFileBatchProcess.cs @@ -0,0 +1,310 @@ +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Common.Git +{ + public class GitCatFileBatchProcess : GitCatFileProcess + { + private const int BufferSize = 64 * 1024; + private static readonly HashSet ValidBlobModes = new HashSet() { "100644", "100755", "120000" }; + + public GitCatFileBatchProcess(Enlistment enlistment) : base(enlistment, "--batch") + { + } + + public GitCatFileBatchProcess(StreamReader stdOut, StreamWriter stdIn) : base(stdOut, stdIn) + { + } + + public IEnumerable GetTreeEntries(string commitId, string path) + { + IEnumerable foundShas; + if (this.TryGetShasForPath(commitId, path, isFolder: true, shas: out foundShas)) + { + HashSet alreadyAdded = new HashSet(StringComparer.OrdinalIgnoreCase); + List results = new List(); + foreach (string sha in foundShas) + { + foreach (GitTreeEntry entry in this.GetTreeEntries(sha)) + { + if (alreadyAdded.Add(entry.Name)) + { + results.Add(entry); + } + } + } + + return results; + } + + return new GitTreeEntry[0]; + } + + public IEnumerable GetTreeEntries(string sha) + { + string header; + char[] rawTreeChars; + this.StdIn.Write(sha + "\n"); + if (!this.TryReadCatFileBatchOutput(out header, out rawTreeChars) + || !header.Contains(GitCatFileProcess.TreeMarker)) + { + return new GitTreeEntry[0]; + } + + return this.ParseTree(new string(rawTreeChars)); + } + + public string GetTreeSha(string commitish) + { + string header; + char[] rawTreeChars; + this.StdIn.Write(commitish + "\n"); + if (!this.TryReadCatFileBatchOutput(out header, out rawTreeChars) + || !header.Contains(GitCatFileProcess.CommitMarker)) + { + return null; + } + + const string TreeLinePrefix = "tree "; + string commitDetails = new string(rawTreeChars); + string[] detailLines = commitDetails.Split('\n'); + if (detailLines.Length < 1 || !detailLines[0].StartsWith(TreeLinePrefix)) + { + throw new InvalidDataException("'tree' expected on first line of 'git cat-file'. Actual: " + (detailLines.Length == 0 ? "empty" : detailLines[0])); + } + + return detailLines[0].Substring(TreeLinePrefix.Length); + } + + public string GetCommitId(string commitish) + { + string header; + char[] rawTreeChars; + this.StdIn.Write(commitish + "\n"); + if (!this.TryReadCatFileBatchOutput(out header, out rawTreeChars)) + { + return null; + } + + int commitMarkerIndex = header.IndexOf(GitCatFileProcess.CommitMarker); + if (commitMarkerIndex < 0) + { + return null; + } + + return header.Substring(0, commitMarkerIndex); + } + + public bool TryGetFileSha(string commitId, string virtualPath, out string sha) + { + sha = null; + + IEnumerable foundShas; + if (this.TryGetShasForPath(commitId, virtualPath, isFolder: false, shas: out foundShas)) + { + if (foundShas.Count() > 1) + { + return false; + } + + sha = foundShas.Single(); + return true; + } + + return false; + } + + public bool TryCopyBlobContentStream(string blobSha, Action writeAction) + { + string header; + long blobSize; + this.StdIn.Write(blobSha + "\n"); + header = this.StdOut.ReadLineAsync().Timeout(GitCatFileProcess.ProcessReadTimeoutMs); + if (!this.TryParseSizeFromCatFileHeader(header, out blobSize)) + { + return false; + } + + if (!header.Contains(GitCatFileProcess.BlobMarker)) + { + // Even if not a blob, be sure to read the remaining bytes (+ 1 for \n) to leave the process in a good state + this.StdOut.CopyBlockTo(StreamWriter.Null, blobSize + 1); + return false; + } + + writeAction(this.StdOut, blobSize); + this.StdOut.CopyBlockTo(StreamWriter.Null, 1); + return true; + } + + public async Task CopyBlobContentStreamAsync(string blobSha, Stream destination) + { + string header; + long blobSize; + await this.StdIn.WriteAsync(blobSha + "\n"); + + header = await this.StdOut.ReadLineAsync(); + if (!this.TryParseSizeFromCatFileHeader(header, out blobSize)) + { + throw new InvalidDataException("Invalid cat-file response."); + } + + if (!header.Contains(GitCatFileProcess.BlobMarker)) + { + // Even if not a blob, be sure to read the remaining bytes (+ 1 for \n) to leave the process in a good state + await this.StdOut.CopyBlockToAsync(StreamWriter.Null, blobSize + 1); + } + + using (StreamWriter writer = new StreamWriter(destination, this.StdOut.CurrentEncoding, BufferSize, true)) + { + await this.StdOut.CopyBlockToAsync(writer, blobSize); + } + + await this.StdOut.CopyBlockToAsync(StreamWriter.Null, 1); + } + + private bool TryReadCatFileBatchOutput(out string header, out char[] str) + { + long remainingSize; + + header = this.StdOut.ReadLineAsync().Timeout(GitCatFileProcess.ProcessReadTimeoutMs); + if (!this.TryParseSizeFromCatFileHeader(header, out remainingSize)) + { + str = null; + return false; + } + + str = new char[remainingSize + 1]; // Grab trailing \n + this.StdOut.ReadBlockAsync(str, 0, str.Length).Timeout(GitCatFileProcess.ProcessReadTimeoutMs); + + return true; + } + + private bool TryParseSizeFromCatFileHeader(string header, out long remainingSize) + { + if (header == null || header.EndsWith("missing")) + { + remainingSize = 0; + return false; + } + + int spaceIdx = header.LastIndexOf(' '); + if (spaceIdx < 0) + { + throw new InvalidDataException("git cat-file has invalid header " + header); + } + + string sizeString = header.Substring(spaceIdx); + if (!long.TryParse(sizeString, out remainingSize) || remainingSize < 0) + { + remainingSize = 0; + return false; + } + + return true; + } + + private IEnumerable ParseTree(string rawTreeData) + { + int i = 0; + int len = rawTreeData.Length - 1; // Ingore the trailing \n + while (i < len) + { + int endOfObjMode = rawTreeData.IndexOf(' ', i); + if (endOfObjMode < 0) + { + throw new InvalidDataException("git cat-file content has invalid mode"); + } + + string objectMode = rawTreeData.Substring(i, endOfObjMode - i); + bool isBlob = ValidBlobModes.Contains(objectMode); + i = endOfObjMode + 1; // +1 to skip space + + int endOfObjName = rawTreeData.IndexOf('\0', i); + if (endOfObjName < 0) + { + throw new InvalidDataException("git cat-file content has invalid name"); + } + + string fileName = Encoding.UTF8.GetString(this.StdOut.CurrentEncoding.GetBytes(rawTreeData.Substring(i, endOfObjName - i))); + i = endOfObjName + 1; // +1 to skip null + + byte[] shaBytes = this.StdOut.CurrentEncoding.GetBytes(rawTreeData.Substring(i, 20)); + string sha = BitConverter.ToString(shaBytes).Replace("-", string.Empty); + if (sha.Length != GVFSConstants.ShaStringLength) + { + throw new InvalidDataException("git cat-file content has invalid sha: " + sha); + } + + i += 20; + + yield return new GitTreeEntry(fileName, sha, !isBlob, isBlob); + } + } + + /// + /// We are trying to get the sha of a single path. However, if that is the path of a folder, it can + /// potentially correspond to multiple git trees, and therefore we have to return multiple shas. + /// + /// This is due to the fact that git and Windows disagree on case sensitivity. If you add the folders + /// foo and Foo, git will store those as two different trees, but Windows will only ever create a single + /// folder that contains the union of the files inside both trees. In order to enumerate Foo correctly, + /// we have to treat both trees as if they are the same. + /// + /// This has one major problem, but Git for Windows has the same issue even with no GVFS in the picture. + /// If you have the files foo\A.txt and Foo\A.txt, after you checkout, git writes both of those files, + /// but whichever one gets written second overwrites the one that was written first, and git status + /// will always report one of them as deleted. In GVFS, we do a case-insensitive union of foo and Foo, + /// so we will end up with the same end result. + /// + private bool TryGetShasForPath(string commitId, string virtualPath, bool isFolder, out IEnumerable shas) + { + shas = Enumerable.Empty(); + + string rootTreeSha = this.GetTreeSha(commitId); + if (rootTreeSha == null) + { + return false; + } + + List currentLevelShas = new List(); + currentLevelShas.Add(rootTreeSha); + + string[] pathParts = virtualPath.Split(new char[] { GVFSConstants.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < pathParts.Length; ++i) + { + List nextLevelShas = new List(); + bool isTree = isFolder || i < pathParts.Length - 1; + + foreach (string treeSha in currentLevelShas) + { + IEnumerable childrenMatchingName = + this.GetTreeEntries(treeSha) + .Where(entry => + entry.IsTree == isTree && + string.Equals(pathParts[i], entry.Name, StringComparison.OrdinalIgnoreCase)); + foreach (GitTreeEntry childEntry in childrenMatchingName) + { + nextLevelShas.Add(childEntry.Sha); + } + } + + if (nextLevelShas.Count == 0) + { + return false; + } + + currentLevelShas = nextLevelShas; + } + + shas = currentLevelShas; + return true; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Git/GitCatFileProcess.cs b/GVFS/GVFS.Common/Git/GitCatFileProcess.cs new file mode 100644 index 00000000..5bf3a586 --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitCatFileProcess.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace GVFS.Common.Git +{ + public abstract class GitCatFileProcess : IDisposable + { + public const string TreeMarker = " tree "; + public const string BlobMarker = " blob "; + public const string CommitMarker = " commit "; + + protected const int ProcessReadTimeoutMs = 30000; + protected const int ProcessShutdownTimeoutMs = 2000; + + protected readonly StreamReader StdOut; + protected readonly StreamWriter StdIn; + + private Process catFileProcess; + private WindowsProcessJob job; + + public GitCatFileProcess(Enlistment enlistment, string catFileArgs) + { + // Errors only happen if we use incorrect 'cat-file --batch' parameters. Functional tests will catch these cases. + + // This git.exe should not need/use the working directory of the repo. + // Run git.exe in Environment.SystemDirectory to ensure the git.exe process + // does not touch the working directory + // TODO: 851558 Capture and log standard error output + this.catFileProcess = new GitProcess(enlistment).GetGitProcess( + "cat-file " + catFileArgs, + workingDirectory: Environment.SystemDirectory, + dotGitDirectory: enlistment.DotGitRoot, + useReadObjectHook: false, + redirectStandardError: false); + this.catFileProcess.Start(); + + // We have to use a job to ensure that we can kill the process correctly. The git.exe process that we launch + // immediately creates a child git.exe process, and if we just kill the process we created, the other one gets orphaned. + // By adding our process to a job and closing the job, we guarantee that both processes will exit. + this.job = new WindowsProcessJob(this.catFileProcess); + + this.StdIn = this.catFileProcess.StandardInput; + this.StdOut = this.catFileProcess.StandardOutput; + } + + public GitCatFileProcess(StreamReader stdOut, StreamWriter stdIn) + { + this.StdIn = stdIn; + this.StdOut = stdOut; + } + + public bool IsRunning() + { + return !this.catFileProcess.HasExited; + } + + public void Dispose() + { + this.Kill(); + } + + public void Kill() + { + if (this.job != null) + { + this.job.Dispose(); + this.job = null; + } + + if (this.catFileProcess != null) + { + this.catFileProcess.Dispose(); + this.catFileProcess = null; + } + } + + protected bool TryParseSizeFromStdOut(out string header, out long size) + { + // Git always output at least one \n terminated output, so we cannot hang here + header = this.StdOut.ReadLineAsync().Timeout(ProcessReadTimeoutMs); + + if (header == null || header.EndsWith("missing")) + { + size = 0; + return false; + } + + string sizeString = header.Substring(header.LastIndexOf(' ')); + if (!long.TryParse(sizeString, out size) || size < 0) + { + throw new InvalidDataException("git cat-file header has invalid size: " + sizeString); + } + + return true; + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitObjects.cs b/GVFS/GVFS.Common/Git/GitObjects.cs new file mode 100644 index 00000000..fdef86fc --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitObjects.cs @@ -0,0 +1,527 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace GVFS.Common.Git +{ + public class GitObjects + { + protected readonly ITracer Tracer; + protected readonly Enlistment Enlistment; + protected readonly HttpGitObjects GitObjectRequestor; + + private const string AreaPath = "GitObjects"; + + public GitObjects(ITracer tracer, Enlistment enlistment, HttpGitObjects httpGitObjects) + { + this.Tracer = tracer; + this.Enlistment = enlistment; + this.GitObjectRequestor = httpGitObjects; + } + + public enum DownloadAndSaveObjectResult + { + Success, + ObjectNotOnServer, + Error + } + + public virtual bool TryDownloadAndSaveCommits(IEnumerable commitShas, int commitDepth) + { + return this.TryDownloadAndSaveObjects(commitShas, commitDepth, preferLooseObjects: false); + } + + public bool TryDownloadAndSaveBlobs(IEnumerable blobShas) + { + return this.TryDownloadAndSaveObjects(blobShas, commitDepth: 1, preferLooseObjects: true); + } + + public void DownloadPrefetchPacks(long latestTimestamp) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("latestTimestamp", latestTimestamp); + + using (ITracer activity = this.Tracer.StartActivity(nameof(this.DownloadPrefetchPacks), EventLevel.Informational, metadata)) + { + RetryWrapper.InvocationResult result = this.GitObjectRequestor.TrySendProtocolRequest( + onSuccess: (tryCount, response) => this.DeserializePrefetchPacks(response, ref latestTimestamp), + onFailure: RetryWrapper.StandardErrorHandler(activity, nameof(this.DownloadPrefetchPacks)), + method: HttpMethod.Get, + endPointGenerator: () => new Uri( + string.Format( + "{0}?lastPackTimestamp={1}", + this.Enlistment.PrefetchEndpointUrl, + latestTimestamp)), + requestBodyGenerator: () => null, + acceptType: new MediaTypeWithQualityHeaderValue(GVFSConstants.MediaTypes.PrefetchPackFilesAndIndexesMediaType)); + + if (!result.Succeeded) + { + if (result.Result != null && result.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) + { + EventMetadata warning = new EventMetadata(); + warning.Add("ErrorMessage", "The server does not support /gvfs/prefetch."); + warning.Add(nameof(this.Enlistment.PrefetchEndpointUrl), this.Enlistment.PrefetchEndpointUrl); + activity.RelatedEvent(EventLevel.Warning, "CommandNotSupported", warning); + } + else + { + EventMetadata error = new EventMetadata(); + error.Add("latestTimestamp", latestTimestamp); + error.Add("Exception", result.Error); + error.Add("ErrorMessage", "DownloadPrefetchPacks failed."); + error.Add(nameof(this.Enlistment.PrefetchEndpointUrl), this.Enlistment.PrefetchEndpointUrl); + activity.RelatedError(error); + } + } + } + } + + public virtual string WriteLooseObject(string repoRoot, Stream responseStream, string sha, byte[] bufToCopyWith = null) + { + LooseObjectToWrite toWrite = GetLooseObjectDestination(repoRoot, sha); + + using (Stream fileStream = OpenTempLooseObjectStream(toWrite.TempFile, async: false)) + { + if (bufToCopyWith != null) + { + StreamUtil.CopyToWithBuffer(responseStream, fileStream, bufToCopyWith); + } + else + { + responseStream.CopyTo(fileStream); + } + } + + this.FinalizeTempFile(sha, toWrite); + + return toWrite.ActualFile; + } + + public virtual string WriteTempPackFile(HttpGitObjects.GitEndPointResponseData response) + { + string fileName = Path.GetRandomFileName(); + string fullPath = Path.Combine(this.Enlistment.GitPackRoot, fileName); + + this.TryWriteNamedPackOrIdx( + tracer: null, + source: response.Stream, + targetFullPath: fullPath, + throwOnError: true); + return fullPath; + } + + public virtual bool TryWriteNamedPackOrIdx( + ITracer tracer, + Stream source, + string targetFullPath, + bool throwOnError = false) + { + // It is important to write temp files then rename so that git + // does not mistake a half-written file for an invalid one. + string tempPath = targetFullPath + "temp"; + + try + { + using (Stream fileStream = File.OpenWrite(tempPath)) + { + source.CopyTo(fileStream); + } + + this.ValidateTempFile(tempPath, targetFullPath); + File.Move(tempPath, targetFullPath); + } + catch (Exception ex) + { + this.CleanupTempFile(this.Tracer, tempPath); + + if (tracer != null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", ex.ToString()); + metadata.Add("ErrorMessage", "Exception caught while writing pack or index"); + metadata.Add("TargetFullPath", targetFullPath); + tracer.RelatedError(metadata); + } + + if (throwOnError) + { + throw; + } + else + { + return false; + } + } + + return true; + } + + public virtual GitProcess.Result IndexTempPackFile(string tempPackPath) + { + string packfilePath = GetRandomPackName(this.Enlistment.GitPackRoot); + return this.IndexTempPackFile(tempPackPath, packfilePath); + } + + public virtual GitProcess.Result IndexTempPackFile(string tempPackPath, string packfilePath) + { + try + { + File.Move(tempPackPath, packfilePath); + + GitProcess.Result result = new GitProcess(this.Enlistment).IndexPack(packfilePath); + if (result.HasErrors) + { + File.Delete(packfilePath); + } + + return result; + } + catch (Exception e) + { + if (File.Exists(packfilePath)) + { + File.Delete(packfilePath); + } + + if (File.Exists(tempPackPath)) + { + File.Delete(tempPackPath); + } + + return new GitProcess.Result(string.Empty, e.Message, GitProcess.Result.GenericFailureCode); + } + } + + public virtual string[] ReadPackFileNames(string prefixFilter = "") + { + return Directory.GetFiles(this.Enlistment.GitPackRoot, prefixFilter + "*.pack"); + } + + protected virtual DownloadAndSaveObjectResult TryDownloadAndSaveObject(string objectSha) + { + if (objectSha == GVFSConstants.AllZeroSha) + { + return DownloadAndSaveObjectResult.Error; + } + + RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadLooseObject( + objectSha, + onSuccess: (tryCount, response) => + { + this.WriteLooseObject(this.Enlistment.WorkingDirectoryRoot, response.Stream, objectSha); + return new RetryWrapper.CallbackResult(new HttpGitObjects.GitObjectTaskResult(true)); + }, + onFailure: this.HandleDownloadAndSaveObjectError); + + if (output.Succeeded && output.Result.Success) + { + return DownloadAndSaveObjectResult.Success; + } + + if (output.Result != null && output.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) + { + return DownloadAndSaveObjectResult.ObjectNotOnServer; + } + + return DownloadAndSaveObjectResult.Error; + } + + private static string GetRandomPackName(string packRoot) + { + string packName = "pack-" + Guid.NewGuid().ToString("N") + ".pack"; + return Path.Combine(packRoot, packName); + } + + private static LooseObjectToWrite GetLooseObjectDestination(string repoRoot, string sha) + { + string firstTwoDigits = sha.Substring(0, 2); + string remainingDigits = sha.Substring(2); + string twoLetterFolderName = Path.Combine(repoRoot, GVFSConstants.DotGit.Objects.Root, firstTwoDigits); + Directory.CreateDirectory(twoLetterFolderName); + + return new LooseObjectToWrite( + tempFile: Path.Combine(twoLetterFolderName, Path.GetRandomFileName()), + actualFile: Path.Combine(twoLetterFolderName, remainingDigits)); + } + + private static FileStream OpenTempLooseObjectStream(string path, bool async) + { + FileOptions options = FileOptions.SequentialScan; + if (async) + { + options |= FileOptions.Asynchronous; + } + + return new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.None, + bufferSize: 4096, // .NET Default + options: options); + } + + private bool TryDownloadAndSaveObjects(IEnumerable objectIds, int commitDepth, bool preferLooseObjects) + { + RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadObjects( + objectIds, + commitDepth, + onSuccess: (tryCount, response) => this.TrySavePackOrLooseObject(objectIds, preferLooseObjects, response), + onFailure: (eArgs) => + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Operation", "DownloadAndSaveObjects"); + metadata.Add("WillRetry", eArgs.WillRetry); + metadata.Add("ErrorMessage", eArgs.Error.ToString()); + this.Tracer.RelatedError(metadata, Keywords.Network); + }, + preferBatchedLooseObjects: preferLooseObjects); + + return output.Succeeded && output.Result.Success; + } + + private void HandleDownloadAndSaveObjectError(RetryWrapper.ErrorEventArgs errorArgs) + { + // Silence logging 404's for object downloads. They are far more likely to be git checking for the + // previous existence of a new object than a truly missing object. + HttpGitObjects.HttpGitObjectsException ex = errorArgs.Error as HttpGitObjects.HttpGitObjectsException; + if (ex != null && ex.StatusCode == HttpStatusCode.NotFound) + { + return; + } + + RetryWrapper.StandardErrorHandler(this.Tracer, nameof(this.TryDownloadAndSaveObject))(errorArgs); + } + + /// + /// Uses a to read the packs from the stream. + /// + private RetryWrapper.CallbackResult DeserializePrefetchPacks( + HttpGitObjects.GitEndPointResponseData response, ref long latestTimestamp) + { + using (ITracer activity = this.Tracer.StartActivity(nameof(this.DeserializePrefetchPacks), EventLevel.Informational)) + { + PrefetchPacksDeserializer deserializer = new PrefetchPacksDeserializer(response.Stream); + + foreach (PrefetchPacksDeserializer.PackAndIndex pack in deserializer.EnumeratePacks()) + { + string packName = string.Format("{0}-{1}-{2}.pack", GVFSConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); + string packFullPath = Path.Combine(this.Enlistment.GitPackRoot, packName); + string idxName = string.Format("{0}-{1}-{2}.idx", GVFSConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); + string idxFullPath = Path.Combine(this.Enlistment.GitPackRoot, idxName); + + EventMetadata data = new EventMetadata(); + data["timestamp"] = pack.Timestamp.ToString(); + data["uniqueId"] = pack.UniqueId; + activity.RelatedEvent(EventLevel.Informational, "Receiving Pack/Index", data); + + // Write the pack + // If it fails, TryWriteNamedPackOrIdx cleans up the packfile and we retry the prefetch + if (!this.TryWriteNamedPackOrIdx(activity, pack.PackStream, packFullPath)) + { + return new RetryWrapper.CallbackResult(null, true); + } + + // We will try to build an index if the server does not send one + if (pack.IndexStream == null) + { + if (!this.TryBuildIndex(activity, pack, packFullPath)) + { + return new RetryWrapper.CallbackResult(null, true); + } + } + else if (!this.TryWriteNamedPackOrIdx(activity, pack.IndexStream, idxFullPath)) + { + // Try to build the index manually, then retry the prefetch + if (this.TryBuildIndex(activity, pack, packFullPath)) + { + // If we were able to recreate the failed index + // we can start the prefetch at the next timestamp + latestTimestamp = pack.Timestamp; + } + + // The download stream will not be in a good state if the index download fails. + // So we have to restart the prefetch + return new RetryWrapper.CallbackResult(null, true); + } + + latestTimestamp = pack.Timestamp; + } + + return new RetryWrapper.CallbackResult( + new HttpGitObjects.GitObjectTaskResult(true)); + } + } + + private bool TryBuildIndex( + ITracer activity, + PrefetchPacksDeserializer.PackAndIndex pack, + string packFullPath) + { + GitProcess.Result result = this.IndexTempPackFile(packFullPath, Path.ChangeExtension(packFullPath, ".pack")); + + if (result.HasErrors) + { + // IndexTempPackFile will delete the bad temp pack for us. + EventMetadata errorMetadata = new EventMetadata(); + errorMetadata.Add("Operation", "TryBuildIndex"); + errorMetadata.Add("pack", packFullPath); + errorMetadata.Add("ErrorMessage", result.Errors); + activity.RelatedError(errorMetadata); + } + + return !result.HasErrors; + } + + private void CleanupTempFile(ITracer activity, string packRoot, string file) + { + if (file == null) + { + return; + } + + string fullPath = Path.Combine(packRoot, file); + this.CleanupTempFile(activity, fullPath); + } + + private void CleanupTempFile(ITracer activity, string fullPath) + { + try + { + if (File.Exists(fullPath)) + { + File.Delete(fullPath); + } + } + catch (IOException failedDelete) + { + EventMetadata info = new EventMetadata(); + info.Add("ErrorMessage", "Exception cleaning up temp file"); + info.Add("file", fullPath); + info.Add("Exception", failedDelete.ToString()); + activity.RelatedEvent(EventLevel.Warning, "Warning", info); + } + } + + private void FinalizeTempFile(string sha, LooseObjectToWrite toWrite) + { + try + { + // Checking for existence reduces warning outputs when a streamed download tries. + if (!File.Exists(toWrite.ActualFile)) + { + this.ValidateTempFile(toWrite.TempFile, sha); + + File.Move(toWrite.TempFile, toWrite.ActualFile); + } + } + catch (IOException) + { + // IOExceptions happen when someone else is writing to our object. + // That implies they are doing what we're doing, which should be a success + } + finally + { + this.CleanupTempFile(this.Tracer, toWrite.TempFile); + } + } + + private void ValidateTempFile(string filePath, string intendedPurpose) + { + FileInfo info = new FileInfo(filePath); + if (info.Length == 0) + { + throw new RetryableException("Temp file for '" + intendedPurpose + "' was written with 0 bytes"); + } + else + { + using (Stream fs = info.OpenRead()) + { + byte[] buffer = new byte[10]; + int bytesRead = fs.Read(buffer, 0, buffer.Length); + if (buffer.Take(bytesRead).All(b => b == 0)) + { + throw new RetryableException("Temp file for '" + intendedPurpose + "' was written with " + buffer.Length + " null bytes"); + } + } + } + } + + private RetryWrapper.CallbackResult TrySavePackOrLooseObject(IEnumerable objectShas, bool unpackObjects, HttpGitObjects.GitEndPointResponseData responseData) + { + if (responseData.ContentType == HttpGitObjects.ContentType.LooseObject) + { + List objectShaList = objectShas.Distinct().ToList(); + if (objectShaList.Count != 1) + { + return new RetryWrapper.CallbackResult(new InvalidOperationException("Received loose object when multiple objects were requested."), shouldRetry: false); + } + + this.WriteLooseObject(this.Enlistment.WorkingDirectoryRoot, responseData.Stream, objectShaList[0]); + } + else if (responseData.ContentType == HttpGitObjects.ContentType.BatchedLooseObjects) + { + BatchedLooseObjectDeserializer deserializer = new BatchedLooseObjectDeserializer( + responseData.Stream, + (stream, sha) => this.WriteLooseObject(this.Enlistment.WorkingDirectoryRoot, stream, sha)); + deserializer.ProcessObjects(); + } + else + { + GitProcess.Result result = this.TryAddPackFile(responseData.Stream, unpackObjects); + if (result.HasErrors) + { + return new RetryWrapper.CallbackResult(new InvalidOperationException("Could not add pack file: " + result.Errors), shouldRetry: false); + } + } + + return new RetryWrapper.CallbackResult(new HttpGitObjects.GitObjectTaskResult(true)); + } + + private GitProcess.Result TryAddPackFile(Stream contents, bool unpackObjects) + { + Debug.Assert(contents != null, "contents should not be null"); + + GitProcess.Result result; + + if (unpackObjects) + { + result = new GitProcess(this.Enlistment).UnpackObjects(contents); + } + else + { + string packfilePath = GetRandomPackName(this.Enlistment.GitPackRoot); + using (FileStream fileStream = File.OpenWrite(packfilePath)) + { + contents.CopyTo(fileStream); + } + + this.ValidateTempFile(packfilePath, packfilePath); + + result = new GitProcess(this.Enlistment).IndexPack(packfilePath); + } + + return result; + } + + private struct LooseObjectToWrite + { + public readonly string TempFile; + public readonly string ActualFile; + + public LooseObjectToWrite(string tempFile, string actualFile) + { + this.TempFile = tempFile; + this.ActualFile = actualFile; + } + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitPathConverter.cs b/GVFS/GVFS.Common/Git/GitPathConverter.cs new file mode 100644 index 00000000..f57f2b8e --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitPathConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GVFS.Common.Git +{ + public static class GitPathConverter + { + private const int CharsInOctet = 3; + private const char OctetIndicator = '\\'; + + public static string ConvertPathOctetsToUtf8(string filePath) + { + if (filePath == null) + { + return null; + } + + int octetIndicatorIndex = filePath.IndexOf(OctetIndicator); + if (octetIndicatorIndex == -1) + { + return filePath; + } + + StringBuilder converted = new StringBuilder(); + List octets = new List(); + int index = 0; + while (octetIndicatorIndex != -1) + { + converted.Append(filePath.Substring(index, octetIndicatorIndex - index)); + while (octetIndicatorIndex < filePath.Length && filePath[octetIndicatorIndex] == OctetIndicator) + { + string octet = filePath.Substring(octetIndicatorIndex + 1, CharsInOctet); + octets.Add(Convert.ToByte(octet, 8)); + octetIndicatorIndex += CharsInOctet + 1; + } + + AddOctetsAsUtf8(converted, octets); + index = octetIndicatorIndex; + octetIndicatorIndex = filePath.IndexOf(OctetIndicator, octetIndicatorIndex); + } + + AddOctetsAsUtf8(converted, octets); + converted.Append(filePath.Substring(index)); + + return converted.ToString(); + } + + private static void AddOctetsAsUtf8(StringBuilder converted, List octets) + { + if (octets.Count > 0) + { + converted.Append(Encoding.UTF8.GetChars(octets.ToArray())); + octets.Clear(); + } + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs new file mode 100644 index 00000000..f26a87c1 --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -0,0 +1,517 @@ +using GVFS.Common.Physical; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Win32; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.Common.Git +{ + public class GitProcess + { + private const string GitProcessName = "git.exe"; + private const string GitBinRelativePath = "cmd\\git.exe"; + private const string GitInstallationRegistryKey = "SOFTWARE\\GitForWindows"; + private const string GitInstallationRegistryInstallPathValue = "InstallPath"; + + private object executionLock = new object(); + + private Enlistment enlistment; + + public GitProcess(Enlistment enlistment) + { + if (enlistment == null) + { + throw new ArgumentNullException(nameof(enlistment)); + } + + if (string.IsNullOrWhiteSpace(enlistment.GitBinPath)) + { + throw new ArgumentException(nameof(enlistment.GitBinPath)); + } + + this.enlistment = enlistment; + } + + public static bool GitExists(string gitBinPath) + { + if (!string.IsNullOrWhiteSpace(gitBinPath)) + { + return File.Exists(gitBinPath); + } + + return ProcessHelper.WhereDirectory(GitProcessName) != null; + } + + public static string GetInstalledGitBinPath() + { + string gitBinPath = RegistryUtils.GetStringFromRegistry(RegistryHive.LocalMachine, GitInstallationRegistryKey, GitInstallationRegistryInstallPathValue); + if (!string.IsNullOrWhiteSpace(gitBinPath)) + { + gitBinPath = Path.Combine(gitBinPath, GitBinRelativePath); + if (File.Exists(gitBinPath)) + { + return gitBinPath; + } + } + + return null; + } + + public static Result Init(Enlistment enlistment) + { + return new GitProcess(enlistment).InvokeGitOutsideEnlistment("init " + enlistment.WorkingDirectoryRoot); + } + + public static bool TryGetCredentials( + ITracer tracer, + Enlistment enlistment, + out string username, + out string password) + { + username = null; + password = null; + + using (ITracer activity = tracer.StartActivity("TryGetCredentials", EventLevel.Informational)) + { + Result gitCredentialOutput = new GitProcess(enlistment).InvokeGitOutsideEnlistment( + "credential fill", + stdin => stdin.Write("url=" + enlistment.RepoUrl + "\n\n"), + parseStdOutLine: null); + + if (gitCredentialOutput.HasErrors) + { + EventMetadata errorData = new EventMetadata(); + errorData.Add("ErrorMessage", "Git could not get credentials: " + gitCredentialOutput.Errors); + tracer.RelatedError(errorData, Keywords.Network); + return false; + } + + username = ParseValue(gitCredentialOutput.Output, "username="); + password = ParseValue(gitCredentialOutput.Output, "password="); + + bool success = username != null && password != null; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Success", success); + if (!success) + { + metadata.Add("Output", gitCredentialOutput.Output); + } + + activity.Stop(metadata); + return success; + } + } + + public static void RevokeCredential(Enlistment enlistment) + { + new GitProcess(enlistment).InvokeGitOutsideEnlistment( + "credential reject", + stdin => stdin.Write("url=" + enlistment.RepoUrl + "\n\n"), + null); + } + + public static Result Version(Enlistment enlistment) + { + return new GitProcess(enlistment).InvokeGitOutsideEnlistment("--version"); + } + + public bool IsValidRepo() + { + Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel"); + return !result.HasErrors; + } + + public Result RevParse(string gitRef) + { + return this.InvokeGitAgainstDotGitFolder("rev-parse " + gitRef); + } + + public string GetRepoRoot() + { + Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel"); + if (result.HasErrors) + { + throw new InvalidRepoException(result.Errors); + } + + return result.Output.TrimEnd('\r', '\n').Replace("/", "\\"); + } + + public void DeleteFromLocalConfig(string settingName) + { + this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName); + } + + public Result SetInLocalConfig(string settingName, string value, bool replaceAll = false) + { + return this.InvokeGitAgainstDotGitFolder(string.Format( + "config --local {0} {1} {2}", + replaceAll ? "--replace-all " : string.Empty, + settingName, + value)); + } + + public Result GetAllLocalConfig() + { + return this.InvokeGitAgainstDotGitFolder("config --list --local"); + } + + public string GetFromConfig(string settingName) + { + // This method is called at clone time, so the physical repo may not exist yet. + Result result = Directory.Exists(this.enlistment.WorkingDirectoryRoot) ? + this.InvokeGitAgainstDotGitFolder("config " + settingName) : + this.InvokeGitOutsideEnlistment("config " + settingName); + + // Git returns non-zero for non-existent settings and errors. + if (!result.HasErrors) + { + return result.Output.TrimEnd('\n'); + } + else if (result.Errors.Any()) + { + throw new InvalidRepoException("Error while reading '" + settingName + "' from config: " + result.Errors); + } + + return null; + } + + public Result GetOriginUrl() + { + Result result = this.InvokeGitAgainstDotGitFolder("remote -v"); + if (result.HasErrors) + { + return result; + } + + string[] lines = result.Output.Split('\r', '\n'); + string originFetchLine = lines.Where( + l => l.StartsWith("origin", StringComparison.OrdinalIgnoreCase) + && l.EndsWith("(fetch)")).FirstOrDefault(); + if (originFetchLine == null) + { + throw new InvalidRepoException("remote 'origin' is not configured for this repo"); + } + + string[] parts = originFetchLine.Split('\t', ' '); + return new Result(parts[1], string.Empty, 0); + } + + public void DiffTree(string sourceTreeish, string targetTreeish, Action onResult) + { + this.InvokeGitAgainstDotGitFolder("diff-tree -r -t " + sourceTreeish + " " + targetTreeish, null, onResult); + } + + public Result DiffWithNameOnlyAndFilterForAddedAndReanamedFiles(string commitish1, string commitish2) + { + return this.InvokeGitInWorkingDirectoryRoot("diff --name-only --diff-filter=AR " + commitish1 + " " + commitish2, useReadObjectHook: true); + } + + public Result CreateBranchWithUpstream(string branchToCreate, string upstreamBranch) + { + return this.InvokeGitAgainstDotGitFolder("branch --set-upstream " + branchToCreate + " " + upstreamBranch); + } + + public Result ForceCheckout(string target) + { + return this.InvokeGitInWorkingDirectoryRoot("checkout -f " + target, useReadObjectHook: false); + } + + public Result UpdateIndexVersion4() + { + return this.InvokeGitAgainstDotGitFolder("update-index --index-version 4"); + } + + public Result UnpackObjects(Stream packFileStream) + { + return this.InvokeGitAgainstDotGitFolder( + "unpack-objects", + stdin => + { + packFileStream.CopyTo(stdin.BaseStream); + stdin.Write('\n'); + }, + null); + } + + public Result IndexPack(string packfilePath) + { + return this.InvokeGitAgainstDotGitFolder("index-pack \"" + packfilePath + "\""); + } + + public Result RemoteAdd(string remoteName, string url) + { + return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url); + } + + public Result CatFilePretty(string objectId) + { + return this.InvokeGitAgainstDotGitFolder("cat-file -p " + objectId); + } + + public Result CatFileGetType(string objectId) + { + return this.InvokeGitAgainstDotGitFolder("cat-file -t " + objectId); + } + + public Result CatFileBatchCheckAll(Action parseStdOutLine) + { + return this.InvokeGitAgainstDotGitFolder("cat-file --batch-check --batch-all-objects", null, parseStdOutLine); + } + + public Result LsTree(string treeish, Action parseStdOutLine, bool recursive, bool showAllTrees = false) + { + return this.InvokeGitAgainstDotGitFolder( + "ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + treeish, + null, + parseStdOutLine); + } + + public Result SetUpstream(string branchName, string upstream) + { + return this.InvokeGitAgainstDotGitFolder("branch --set-upstream-to=" + upstream + " " + branchName); + } + + public Result UpdateBranchSymbolicRef(string refToUpdate, string targetRef) + { + return this.InvokeGitAgainstDotGitFolder("symbolic-ref " + refToUpdate + " " + targetRef); + } + + public Result UpdateBranchSha(string refToUpdate, string targetSha) + { + // If oldCommitResult doesn't fail, then the branch exists and update-ref will want the old sha + Result oldCommitResult = this.RevParse(refToUpdate); + string oldSha = string.Empty; + if (!oldCommitResult.HasErrors) + { + oldSha = oldCommitResult.Output.TrimEnd('\n'); + } + + return this.InvokeGitAgainstDotGitFolder("update-ref --no-deref " + refToUpdate + " " + targetSha + " " + oldSha); + } + + public Result ReadTree(string treeIsh) + { + return this.InvokeGitAgainstDotGitFolder("read-tree " + treeIsh); + } + + public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError) + { + ProcessStartInfo processInfo = new ProcessStartInfo(this.enlistment.GitBinPath); + processInfo.WorkingDirectory = workingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardInput = true; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = redirectStandardError; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + + processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; + processInfo.EnvironmentVariables["PATH"] = + string.Join( + ";", + this.enlistment.GitBinPath, + this.enlistment.GVFSHooksRoot ?? string.Empty); + + if (!useReadObjectHook) + { + command = "-c " + GVFSConstants.VirtualizeObjectsGitConfigName + "=false " + command; + } + + if (!string.IsNullOrEmpty(dotGitDirectory)) + { + command = "--git-dir=\"" + dotGitDirectory + "\" " + command; + } + + processInfo.Arguments = command; + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + return executingProcess; + } + + private static string ParseValue(string contents, string prefix) + { + int startIndex = contents.IndexOf(prefix) + prefix.Length; + if (startIndex >= 0 && startIndex < contents.Length) + { + int endIndex = contents.IndexOf('\n', startIndex); + if (endIndex >= 0 && endIndex < contents.Length) + { + return + contents + .Substring(startIndex, endIndex - startIndex) + .Trim('\r'); + } + } + + return null; + } + + /// + /// Invokes git.exe without a working directory set. + /// + /// + /// For commands where git doesn't need to be (or can't be) run from inside an enlistment. + /// eg. 'git init' or 'git credential' + /// + private Result InvokeGitOutsideEnlistment(string command) + { + return this.InvokeGitOutsideEnlistment(command, null, null); + } + + private Result InvokeGitOutsideEnlistment( + string command, + Action writeStdIn, + Action parseStdOutLine, + int timeout = -1) + { + return this.InvokeGitImpl( + command, + workingDirectory: Environment.SystemDirectory, + dotGitDirectory: null, + useReadObjectHook: false, + writeStdIn: writeStdIn, + parseStdOutLine: parseStdOutLine, + timeoutMs: timeout); + } + + /// + /// Invokes git.exe from an enlistment's repository root + /// + private Result InvokeGitInWorkingDirectoryRoot(string command, bool useReadObjectHook) + { + return this.InvokeGitImpl( + command, + workingDirectory: this.enlistment.WorkingDirectoryRoot, + dotGitDirectory: null, + useReadObjectHook: useReadObjectHook, + writeStdIn: null, + parseStdOutLine: null, + timeoutMs: -1); + } + + /// + /// Invokes git.exe against an enlistment's .git folder. + /// This method should be used only with git-commands that ignore the working directory + /// + private Result InvokeGitAgainstDotGitFolder(string command) + { + return this.InvokeGitAgainstDotGitFolder(command, null, null); + } + + private Result InvokeGitAgainstDotGitFolder( + string command, + Action writeStdIn, + Action parseStdOutLine) + { + // This git command should not need/use the working directory of the repo. + // Run git.exe in Environment.SystemDirectory to ensure the git.exe process + // does not touch the working directory + return this.InvokeGitImpl( + command, + workingDirectory: Environment.SystemDirectory, + dotGitDirectory: this.enlistment.DotGitRoot, + useReadObjectHook: false, + writeStdIn: writeStdIn, + parseStdOutLine: parseStdOutLine, + timeoutMs: -1); + } + + private Result InvokeGitImpl( + string command, + string workingDirectory, + string dotGitDirectory, + bool useReadObjectHook, + Action writeStdIn, + Action parseStdOutLine, + int timeoutMs) + { + try + { + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + using (Process executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, redirectStandardError: true)) + { + StringBuilder output = new StringBuilder(); + StringBuilder errors = new StringBuilder(); + + executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors.Append(args.Data + "\n"); + } + }; + executingProcess.OutputDataReceived += (sender, args) => + { + if (args.Data != null) + { + if (parseStdOutLine != null) + { + parseStdOutLine(args.Data); + } + else + { + output.Append(args.Data + "\n"); + } + } + }; + + lock (this.executionLock) + { + executingProcess.Start(); + + if (writeStdIn != null) + { + writeStdIn(executingProcess.StandardInput); + } + + executingProcess.BeginOutputReadLine(); + executingProcess.BeginErrorReadLine(); + + if (!executingProcess.WaitForExit(timeoutMs)) + { + executingProcess.Kill(); + return new Result(output.ToString(), "Operation timed out: " + errors.ToString(), Result.GenericFailureCode); + } + } + + return new Result(output.ToString(), errors.ToString(), executingProcess.ExitCode); + } + } + catch (Win32Exception e) + { + return new Result(string.Empty, e.Message, Result.GenericFailureCode); + } + } + + public class Result + { + public const int SuccessCode = 0; + public const int GenericFailureCode = 1; + + public Result(string output, string errors, int returnCode) + { + this.Output = output; + this.Errors = errors; + this.ReturnCode = returnCode; + } + + public string Output { get; } + public string Errors { get; } + public int ReturnCode { get; } + + public bool HasErrors + { + get { return this.ReturnCode != SuccessCode; } + } + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitRefs.cs b/GVFS/GVFS.Common/Git/GitRefs.cs new file mode 100644 index 00000000..37595a9c --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitRefs.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace GVFS.Common.Git +{ + public class GitRefs + { + private const string Head = "HEAD\0"; + private const string Master = "master"; + private const string HeadRefPrefix = "refs/heads/"; + private const string TagsRefPrefix = "refs/tags/"; + private const string OriginRemoteRefPrefix = "refs/remotes/origin/"; + + private Dictionary commitsPerRef; + + private string remoteHeadCommitId = null; + + public GitRefs(IEnumerable infoRefsResponse, string branch) + { + // First 4 characters of a given line are the length of the line and not part of the commit id so + // skip them (https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols) + this.commitsPerRef = + infoRefsResponse + .Where(line => + line.Contains(" " + HeadRefPrefix) || + (line.Contains(" " + TagsRefPrefix) && !line.Contains("^"))) + .Where(line => + branch == null || + line.EndsWith(HeadRefPrefix + branch)) + .Select(line => line.Split(' ')) + .ToDictionary( + line => line[1].Replace(HeadRefPrefix, OriginRemoteRefPrefix), + line => line[0].Substring(4)); + + string lineWithHeadCommit = infoRefsResponse.FirstOrDefault(line => line.Contains(Head)); + + if (lineWithHeadCommit != null) + { + string[] tokens = lineWithHeadCommit.Split(' '); + + if (tokens.Length >= 2 && tokens[1].StartsWith(Head)) + { + // First 8 characters are not part of the commit id so skip them + this.remoteHeadCommitId = tokens[0].Substring(8); + } + } + } + + public int Count + { + get { return this.commitsPerRef.Count; } + } + + public IEnumerable GetTipCommitIds() + { + return this.commitsPerRef.Values; + } + + public string GetDefaultBranch() + { + IEnumerable> headRefMatches = this.commitsPerRef.Where(reference => + reference.Value == this.remoteHeadCommitId + && reference.Key.StartsWith(OriginRemoteRefPrefix)); + + if (headRefMatches.Count() == 0 || headRefMatches.Count(reference => reference.Key == (OriginRemoteRefPrefix + Master)) > 0) + { + // Default to master if no HEAD or if the commit ID or the dafult branch matches master (this is + // the same behavior as git.exe) + return Master; + } + + // If the HEAD commit ID does not match master grab the first branch that matches + string defaultBranch = headRefMatches.First().Key; + + if (defaultBranch.Length < OriginRemoteRefPrefix.Length) + { + return Master; + } + + return defaultBranch.Substring(OriginRemoteRefPrefix.Length); + } + + /// + /// Checks if the specified branch exists (case sensitive) + /// + public bool HasBranch(string branch) + { + string branchRef = OriginRemoteRefPrefix + branch; + return this.commitsPerRef.ContainsKey(branchRef); + } + + public IEnumerable> GetBranchRefPairs() + { + return this.commitsPerRef.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)); + } + + public string ToPackedRefs() + { + StringBuilder sb = new StringBuilder(); + const string LF = "\n"; + + sb.Append("# pack-refs with: peeled fully-peeled" + LF); + foreach (string refName in this.commitsPerRef.Keys.OrderBy(key => key)) + { + sb.Append(this.commitsPerRef[refName] + " " + refName + LF); + } + + return sb.ToString(); + } + } +} diff --git a/GVFS/GVFS.Common/Git/GitTreeEntry.cs b/GVFS/GVFS.Common/Git/GitTreeEntry.cs new file mode 100644 index 00000000..208196ba --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitTreeEntry.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Common.Git +{ + public class GitTreeEntry + { + public GitTreeEntry(string name, string sha, bool isTree, bool isBlob) + { + this.Name = name; + this.Sha = sha; + this.IsTree = isTree; + this.IsBlob = isBlob; + } + + public string Name { get; private set; } + public string Sha { get; private set; } + public bool IsTree { get; private set; } + public bool IsBlob { get; private set; } + + public long Size { get; set; } + } +} diff --git a/GVFS/GVFS.Common/Git/GitVersion.cs b/GVFS/GVFS.Common/Git/GitVersion.cs new file mode 100644 index 00000000..002b1564 --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitVersion.cs @@ -0,0 +1,130 @@ +namespace GVFS.Common.Git +{ + public class GitVersion + { + public GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) + { + this.Major = major; + this.Minor = minor; + this.Build = build; + this.Platform = platform; + this.Revision = revision; + this.MinorRevision = minorRevision; + } + + public int Major { get; private set; } + public int Minor { get; private set; } + public string Platform { get; private set; } + public int Build { get; private set; } + public int Revision { get; private set; } + public int MinorRevision { get; private set; } + + public static bool TryParse(string input, out GitVersion version) + { + version = null; + int major, minor, build, revision, minorRevision; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + string[] parsedComponents = input.Split(new char[] { '.' }); + int parsedComponentsLength = parsedComponents.Length; + if (parsedComponentsLength < 5) + { + return false; + } + + if (!TryParseComponent(parsedComponents[0], out major)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[1], out minor)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[2], out build)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[4], out revision)) + { + return false; + } + + if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + { + minorRevision = 0; + } + + string platform = parsedComponents[3]; + + version = new GitVersion(major, minor, build, platform, revision, minorRevision); + return true; + } + + public bool IsLessThan(GitVersion other) + { + return this.CompareVersionNumbers(other) < 0; + } + + public override string ToString() + { + return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); + } + + private static bool TryParseComponent(string component, out int parsedComponent) + { + if (!int.TryParse(component, out parsedComponent)) + { + return false; + } + + if (parsedComponent < 0) + { + return false; + } + + return true; + } + + private int CompareVersionNumbers(GitVersion other) + { + if (other == null) + { + return -1; + } + + if (this.Major != other.Major) + { + return this.Major.CompareTo(other.Major); + } + + if (this.Minor != other.Minor) + { + return this.Minor.CompareTo(other.Minor); + } + + if (this.Build != other.Build) + { + return this.Build.CompareTo(other.Build); + } + + if (this.Revision != other.Revision) + { + return this.Revision.CompareTo(other.Revision); + } + + if (this.MinorRevision != other.MinorRevision) + { + return this.MinorRevision.CompareTo(other.MinorRevision); + } + + return 0; + } + } +} diff --git a/GVFS/GVFS.Common/Git/HttpGitObjects.cs b/GVFS/GVFS.Common/Git/HttpGitObjects.cs new file mode 100644 index 00000000..d8f3f254 --- /dev/null +++ b/GVFS/GVFS.Common/Git/HttpGitObjects.cs @@ -0,0 +1,642 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Common.Git +{ + public class HttpGitObjects + { + private const string AreaPath = "HttpGitObjects"; + private const int HttpTimeoutMinutes = 10; + private const int DefaultMaxRetries = 5; + private const int AuthorizationBackoffMinutes = 1; + + private static readonly MediaTypeWithQualityHeaderValue CustomLooseObjectsHeader + = new MediaTypeWithQualityHeaderValue(GVFSConstants.MediaTypes.CustomLooseObjectsMediaType); + + private static HttpClient client; + + private readonly ProductInfoHeaderValue userAgentHeader; + + private Enlistment enlistment; + private ITracer tracer; + + private DateTime authRetryBackoff = DateTime.MinValue; + private bool credentialHasBeenRevoked = false; + + private object gitAuthorizationLock = new object(); + private string gitAuthorization; + + static HttpGitObjects() + { + client = new HttpClient(); + client.Timeout = TimeSpan.FromMinutes(HttpTimeoutMinutes); + } + + public HttpGitObjects(ITracer tracer, Enlistment enlistment, int maxConnections) + { + this.tracer = tracer; + this.enlistment = enlistment; + this.MaxRetries = DefaultMaxRetries; + ServicePointManager.DefaultConnectionLimit = maxConnections; + + this.userAgentHeader = new ProductInfoHeaderValue(ProcessHelper.GetEntryClassName(), ProcessHelper.GetCurrentProcessVersion()); + } + + public enum ContentType + { + None, + LooseObject, + BatchedLooseObjects, + PackFile + } + + public int MaxRetries { get; set; } + + public bool TryRefreshCredentials() + { + return this.TryGetCredentials(out this.gitAuthorization); + } + + public virtual List QueryForFileSizes(IEnumerable objectIds) + { + string objectIdsJson = ToJsonList(objectIds); + Uri gvfsEndpoint = new Uri(this.enlistment.RepoUrl + "/gvfs/sizes"); + + EventMetadata metadata = new EventMetadata(); + int objectIdCount = objectIds.Count(); + if (objectIdCount > 10) + { + metadata.Add("ObjectIdCount", objectIdCount); + } + else + { + metadata.Add("ObjectIdJson", objectIdsJson); + } + + this.tracer.RelatedEvent(EventLevel.Informational, "QueryFileSizes", metadata, Keywords.Network); + + RetryWrapper> retrier = new RetryWrapper>(this.MaxRetries); + retrier.OnFailure += RetryWrapper>.StandardErrorHandler(this.tracer, "QueryFileSizes"); + + RetryWrapper>.InvocationResult requestTask = retrier.InvokeAsync( + async tryCount => + { + GitEndPointResponseData response = this.SendRequest(gvfsEndpoint, HttpMethod.Post, objectIdsJson); + if (response.HasErrors) + { + return new RetryWrapper>.CallbackResult(response.Error, response.ShouldRetry); + } + + using (Stream objectSizesStream = response.Stream) + using (StreamReader reader = new StreamReader(objectSizesStream)) + { + string objectSizesString = await reader.ReadToEndAsync(); + List objectSizes = JsonConvert.DeserializeObject>(objectSizesString); + return new RetryWrapper>.CallbackResult(objectSizes); + } + }).Result; + + return requestTask.Result ?? new List(0); + } + + public GVFSConfigResponse QueryGVFSConfig() + { + Uri gvfsConfigEndpoint; + string gvfsConfigEndpointString = this.enlistment.RepoUrl + GVFSConstants.GVFSConfigEndpointSuffix; + try + { + gvfsConfigEndpoint = new Uri(gvfsConfigEndpointString); + } + catch (UriFormatException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Method", nameof(this.QueryGVFSConfig)); + metadata.Add("ErrorMessage", e); + metadata.Add("Url", gvfsConfigEndpointString); + this.tracer.RelatedError(metadata, Keywords.Network); + + return null; + } + + RetryWrapper retrier = new RetryWrapper(this.MaxRetries); + retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.tracer, "QueryGvfsConfig"); + + RetryWrapper.InvocationResult output = retrier.Invoke( + tryCount => + { + GitEndPointResponseData response = this.SendRequest(gvfsConfigEndpoint, HttpMethod.Get, null); + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); + } + + using (Stream responseStream = response.Stream) + using (StreamReader reader = new StreamReader(responseStream)) + { + try + { + return new RetryWrapper.CallbackResult( + JsonConvert.DeserializeObject(reader.ReadToEnd())); + } + catch (JsonReaderException e) + { + return new RetryWrapper.CallbackResult(e, false); + } + } + }); + + return output.Result; + } + + public virtual GitRefs QueryInfoRefs(string branch) + { + Uri infoRefsEndpoint; + try + { + infoRefsEndpoint = new Uri(this.enlistment.RepoUrl + GVFSConstants.InfoRefsEndpointSuffix); + } + catch (UriFormatException) + { + return null; + } + + RetryWrapper retrier = new RetryWrapper(this.MaxRetries); + retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.tracer, "QueryInfoRefs"); + + RetryWrapper.InvocationResult output = retrier.Invoke( + tryCount => + { + GitEndPointResponseData response = this.SendRequest(infoRefsEndpoint, HttpMethod.Get, null); + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); + } + + using (Stream responseStream = response.Stream) + using (StreamReader reader = new StreamReader(responseStream)) + { + List infoRefsResponse = new List(); + while (!reader.EndOfStream) + { + infoRefsResponse.Add(reader.ReadLine()); + } + + return new RetryWrapper.CallbackResult(new GitRefs(infoRefsResponse, branch)); + } + }); + + return output.Result; + } + + /// + /// Get the s to download and store in the pack directory for bootstrapping + /// + public IList TryGetBootstrapPackSources(Uri bootstrapSource, string branchName) + { + IList packUris = null; + + RetryWrapper.InvocationResult output = this.TrySendProtocolRequest( + onSuccess: (tryCount, response) => + { + using (StreamReader sr = new StreamReader(response.Stream)) + { + packUris = JsonConvert.DeserializeObject(sr.ReadToEnd()).PackUris; + } + + return new RetryWrapper.CallbackResult(new GitObjectTaskResult(true)); + }, + onFailure: RetryWrapper.StandardErrorHandler(this.tracer, nameof(this.TryGetBootstrapPackSources)), + method: HttpMethod.Post, + endPoint: bootstrapSource, + requestBody: JsonConvert.SerializeObject(new { branchName = branchName })); + + return packUris; + } + + public virtual RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ObjectId", objectId); + + this.tracer.RelatedEvent(EventLevel.Informational, "DownloadLooseObject", metadata, Keywords.Network); + + return this.TrySendProtocolRequest( + onSuccess, + onFailure, + HttpMethod.Get, + new Uri(this.enlistment.ObjectsEndpointUrl + "/" + objectId)); + } + + public virtual RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + // We pass the query generator in as a function because we don't want the consumer to know about JSON or network retry logic, + // but we still want the consumer to be able to change the query on each retry if we fail during their onSuccess handler. + return this.TrySendProtocolRequest( + onSuccess, + onFailure, + HttpMethod.Post, + new Uri(this.enlistment.ObjectsEndpointUrl), + () => this.ObjectIdsJsonGenerator(objectIdGenerator, commitDepth), + preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); + } + + public virtual RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + string objectIdsJson = CreateObjectIdJson(objectIds, commitDepth); + int objectCount = objectIds.Count(); + EventMetadata metadata = new EventMetadata(); + if (objectCount < 10) + { + metadata.Add("ObjectIds", string.Join(", ", objectIds)); + } + else + { + metadata.Add("ObjectIdCount", objectCount); + } + + this.tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); + + return this.TrySendProtocolRequest( + onSuccess, + onFailure, + HttpMethod.Post, + new Uri(this.enlistment.ObjectsEndpointUrl), + objectIdsJson, + preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Uri endPoint, + string requestBody = null, + MediaTypeWithQualityHeaderValue acceptType = null) + { + return this.TrySendProtocolRequest( + onSuccess, + onFailure, + method, + endPoint, + () => requestBody, + acceptType); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Uri endPoint, + Func requestBodyGenerator, + MediaTypeWithQualityHeaderValue acceptType = null) + { + return this.TrySendProtocolRequest( + onSuccess, + onFailure, + method, + () => endPoint, + requestBodyGenerator, + acceptType); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Func endPointGenerator, + Func requestBodyGenerator, + MediaTypeWithQualityHeaderValue acceptType = null) + { + RetryWrapper retrier = new RetryWrapper(this.MaxRetries); + if (onFailure != null) + { + retrier.OnFailure += onFailure; + } + + return retrier.Invoke( + tryCount => + { + GitEndPointResponseData response = this.SendRequest( + endPointGenerator(), + method, + requestBodyGenerator(), + acceptType); + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry, new GitObjectTaskResult(response.StatusCode)); + } + + using (Stream responseStream = response.Stream) + { + return onSuccess(tryCount, response); + } + }); + } + + private static string ToJsonList(IEnumerable strings) + { + return "[\"" + string.Join("\",\"", strings) + "\"]"; + } + + private static string CreateObjectIdJson(IEnumerable strings, int commitDepth) + { + return "{\"commitDepth\": " + commitDepth + ", \"objectIds\":" + ToJsonList(strings) + "}"; + } + + private static bool ShouldRetry(HttpStatusCode statusCode) + { + // Retry timeouts and 5xx errors + int statusInt = (int)statusCode; + if (statusCode == HttpStatusCode.RequestTimeout || + (statusInt >= 500 && statusInt < 600)) + { + return true; + } + + return false; + } + + private bool TryGetCredentials(out string authString) + { + authString = this.gitAuthorization; + if (authString == null) + { + lock (this.gitAuthorizationLock) + { + if (this.gitAuthorization == null) + { + string gitUsername; + string gitPassword; + bool backingOff = DateTime.Now < this.authRetryBackoff; + if (this.credentialHasBeenRevoked) + { + // Update backoff after an immediate first retry. + this.authRetryBackoff = DateTime.Now.AddMinutes(AuthorizationBackoffMinutes); + } + + if (backingOff || + !GitProcess.TryGetCredentials(this.tracer, this.enlistment, out gitUsername, out gitPassword)) + { + authString = null; + return false; + } + + this.gitAuthorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(gitUsername + ":" + gitPassword)); + } + + authString = this.gitAuthorization; + } + } + + return true; + } + + private GitEndPointResponseData SendRequest( + Uri requestUri, + HttpMethod httpMethod, + string requestContent, + MediaTypeWithQualityHeaderValue acceptType = null) + { + string authString; + if (!this.TryGetCredentials(out authString)) + { + string message = + this.authRetryBackoff == DateTime.MinValue + ? "Authorization failed." + : "Authorization failed. No retries will be made until: " + this.authRetryBackoff; + + return new GitEndPointResponseData( + HttpStatusCode.Unauthorized, + new HttpGitObjectsException(HttpStatusCode.Unauthorized, message), + shouldRetry: false); + } + + HttpRequestMessage request = new HttpRequestMessage(httpMethod, requestUri); + request.Headers.UserAgent.Add(this.userAgentHeader); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString); + + if (acceptType != null) + { + request.Headers.Accept.Add(acceptType); + } + + if (requestContent != null) + { + request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + } + + try + { + HttpResponseMessage response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result; + if (response.StatusCode == HttpStatusCode.OK) + { + string contentType = string.Empty; + IEnumerable values; + if (response.Content.Headers.TryGetValues("Content-Type", out values)) + { + contentType = values.First(); + } + + this.credentialHasBeenRevoked = false; + Stream responseStream = response.Content.ReadAsStreamAsync().Result; + return new GitEndPointResponseData(response.StatusCode, contentType, responseStream); + } + else + { + string errorMessage = response.Content.ReadAsStringAsync().Result; + int statusInt = (int)response.StatusCode; + + if (string.IsNullOrWhiteSpace(errorMessage)) + { + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + lock (this.gitAuthorizationLock) + { + // Wipe the username and password so we can try recovering if applicable. + this.gitAuthorization = null; + if (!this.credentialHasBeenRevoked) + { + GitProcess.RevokeCredential(this.enlistment); + this.credentialHasBeenRevoked = true; + return new GitEndPointResponseData( + response.StatusCode, + new HttpGitObjectsException(response.StatusCode, "Server returned error code 401 (Unauthorized). Your PAT may be expired."), + shouldRetry: true); + } + else + { + this.authRetryBackoff = DateTime.MaxValue; + return new GitEndPointResponseData( + response.StatusCode, + new HttpGitObjectsException(response.StatusCode, "Server returned error code 401 (Unauthorized) after successfully renewing your PAT. You may not have access to this repo"), + shouldRetry: false); + } + } + } + else + { + errorMessage = string.Format("Server returned error code {0} ({1})", statusInt, response.StatusCode); + } + } + + return new GitEndPointResponseData(response.StatusCode, new HttpGitObjectsException(response.StatusCode, errorMessage), ShouldRetry(response.StatusCode)); + } + } + catch (TaskCanceledException) + { + string errorMessage = string.Format("Request to {0} timed out", requestUri); + return new GitEndPointResponseData(HttpStatusCode.RequestTimeout, new HttpGitObjectsException(HttpStatusCode.RequestTimeout, errorMessage), shouldRetry: true); + } + catch (WebException ex) + { + return new GitEndPointResponseData(HttpStatusCode.InternalServerError, ex, shouldRetry: true); + } + } + + private string ObjectIdsJsonGenerator(Func> objectIdGenerator, int commitDepth) + { + IEnumerable objectIds = objectIdGenerator(); + string objectIdsJson = CreateObjectIdJson(objectIds, commitDepth); + int objectCount = objectIds.Count(); + EventMetadata metadata = new EventMetadata(); + if (objectCount < 10) + { + metadata.Add("ObjectIds", string.Join(", ", objectIds)); + } + else + { + metadata.Add("ObjectIdCount", objectCount); + } + + this.tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); + return objectIdsJson; + } + + public class GitObjectSize + { + public readonly string Id; + public readonly long Size; + + [JsonConstructor] + public GitObjectSize(string id, long size) + { + this.Id = id; + this.Size = size; + } + } + + public class GitObjectTaskResult + { + public GitObjectTaskResult(bool success) + { + this.Success = success; + } + + public GitObjectTaskResult(HttpStatusCode statusCode) + : this(statusCode == HttpStatusCode.OK) + { + this.HttpStatusCodeResult = statusCode; + } + + public bool Success { get; } + public HttpStatusCode HttpStatusCodeResult { get; } + } + + public class GitEndPointResponseData + { + /// + /// Constructor used when GitEndPointResponseData contains an error response + /// + public GitEndPointResponseData(HttpStatusCode statusCode, Exception error, bool shouldRetry) + { + this.StatusCode = statusCode; + this.Error = error; + this.ShouldRetry = shouldRetry; + } + + /// + /// Constructor used when GitEndPointResponseData contains a successful response + /// + public GitEndPointResponseData(HttpStatusCode statusCode, string contentType, Stream responseStream) + : this(statusCode, null, false) + { + this.Stream = responseStream; + this.ContentType = MapContentType(contentType); + } + + /// + /// Stream returned by a successful response. If the response is an error, Stream will be null + /// + public Stream Stream { get; } + + public Exception Error { get; } + + public bool ShouldRetry { get; } + + public HttpStatusCode StatusCode { get; } + + public bool HasErrors + { + get { return this.StatusCode != HttpStatusCode.OK; } + } + + public ContentType ContentType { get; } + + /// + /// Convert from a string-based Content-Type to + /// + private static ContentType MapContentType(string contentType) + { + switch (contentType) + { + case GVFSConstants.MediaTypes.LooseObjectMediaType: + return ContentType.LooseObject; + case GVFSConstants.MediaTypes.CustomLooseObjectsMediaType: + return ContentType.BatchedLooseObjects; + case GVFSConstants.MediaTypes.PackFileMediaType: + return ContentType.PackFile; + default: + return ContentType.None; + } + } + } + + public class HttpGitObjectsException : Exception + { + public HttpGitObjectsException(HttpStatusCode statusCode, string ex) : base(ex) + { + this.StatusCode = statusCode; + } + + public HttpStatusCode StatusCode { get; } + } + + private class BootstrapResponse + { + public IList PackUris { get; set; } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/GitHelper.cs b/GVFS/GVFS.Common/GitHelper.cs new file mode 100644 index 00000000..1483067b --- /dev/null +++ b/GVFS/GVFS.Common/GitHelper.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; + +namespace GVFS.Common +{ + public static class GitHelper + { + /// + /// Determines whether the given command line represents any of the git verbs passed in. + /// + /// The git command line. + /// A list of verbs (eg. "status" not "git status"). + /// True if the command line represents any of the verbs, false otherwise. + public static bool IsVerb(string commandLine, params string[] verbs) + { + if (verbs == null || verbs.Length < 1) + { + throw new ArgumentException("At least one verb must be provided.", nameof(verbs)); + } + + return + verbs.Any(v => + { + string verbCommand = "git " + v; + return + commandLine == verbCommand || + commandLine.StartsWith(verbCommand + " "); + }); + } + + /// + /// Returns true if the string is length 40 and all valid hex characters + /// + public static bool IsValidFullSHA(string sha) + { + return sha.Length == 40 && !sha.Any(c => !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')); + } + } +} diff --git a/GVFS/GVFS.Common/HeartbeatThread.cs b/GVFS/GVFS.Common/HeartbeatThread.cs new file mode 100644 index 00000000..7099b039 --- /dev/null +++ b/GVFS/GVFS.Common/HeartbeatThread.cs @@ -0,0 +1,40 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Threading; + +namespace GVFS.Common +{ + public class HeartbeatThread + { + private static readonly TimeSpan HeartBeatWaitTime = TimeSpan.FromMinutes(15); + + private readonly ITracer tracer; + + private Timer thread; + private DateTime startTime; + + public HeartbeatThread(ITracer tracer) + { + this.tracer = tracer; + } + + public void Start() + { + this.startTime = DateTime.Now; + this.thread = new Timer( + this.EmitHeartbeat, + state: null, + dueTime: HeartBeatWaitTime, + period: HeartBeatWaitTime); + } + + private void EmitHeartbeat(object unusedState) + { + EventMetadata metadata = new Tracing.EventMetadata(); + metadata.Add("MinutesUptime", (long)(DateTime.Now - this.startTime).TotalMinutes); + metadata.Add("MinutesSinceLast", (int)HeartBeatWaitTime.TotalMinutes); + this.tracer.RelatedEvent(EventLevel.Verbose, "Heartbeat", metadata); + } + } +} diff --git a/GVFS/GVFS.Common/IBackgroundOperation.cs b/GVFS/GVFS.Common/IBackgroundOperation.cs new file mode 100644 index 00000000..e6e59aac --- /dev/null +++ b/GVFS/GVFS.Common/IBackgroundOperation.cs @@ -0,0 +1,9 @@ +using System; + +namespace GVFS.Common +{ + public interface IBackgroundOperation + { + Guid Id { get; set; } + } +} diff --git a/GVFS/GVFS.Common/InvalidRepoException.cs b/GVFS/GVFS.Common/InvalidRepoException.cs new file mode 100644 index 00000000..8b12736a --- /dev/null +++ b/GVFS/GVFS.Common/InvalidRepoException.cs @@ -0,0 +1,12 @@ +using System; + +namespace GVFS.Common +{ + public class InvalidRepoException : Exception + { + public InvalidRepoException(string message) + : base(message) + { + } + } +} diff --git a/GVFS/GVFS.Common/MountParameters.cs b/GVFS/GVFS.Common/MountParameters.cs new file mode 100644 index 00000000..8db475b2 --- /dev/null +++ b/GVFS/GVFS.Common/MountParameters.cs @@ -0,0 +1,12 @@ +namespace GVFS.Common +{ + public static class MountParameters + { + public const string Verbosity = "verbosity"; + public const string Keywords = "keywords"; + public const string DebugWindow = "debug-window"; + + public const string DefaultVerbosity = "Informational"; + public const string DefaultKeywords = "Any"; + } +} diff --git a/GVFS/GVFS.Common/NamedPipes/BrokenPipeException.cs b/GVFS/GVFS.Common/NamedPipes/BrokenPipeException.cs new file mode 100644 index 00000000..49864538 --- /dev/null +++ b/GVFS/GVFS.Common/NamedPipes/BrokenPipeException.cs @@ -0,0 +1,13 @@ +using System; +using System.IO; + +namespace GVFS.Common.NamedPipes +{ + public class BrokenPipeException : Exception + { + public BrokenPipeException(string message, IOException innerException) + : base(message, innerException) + { + } + } +} diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeClient.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeClient.cs new file mode 100644 index 00000000..212e712c --- /dev/null +++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeClient.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.IO.Pipes; + +namespace GVFS.Common.NamedPipes +{ + public class NamedPipeClient : IDisposable + { + private string pipeName; + private NamedPipeClientStream clientStream; + private StreamReader reader; + private StreamWriter writer; + + public NamedPipeClient(string pipeName) + { + this.pipeName = pipeName; + } + + public bool Connect(int timeoutMilliseconds = 3000) + { + if (this.clientStream != null) + { + throw new InvalidOperationException(); + } + + try + { + this.clientStream = new NamedPipeClientStream(this.pipeName); + this.clientStream.Connect(timeoutMilliseconds); + } + catch (TimeoutException) + { + return false; + } + catch (IOException) + { + return false; + } + + this.reader = new StreamReader(this.clientStream); + this.writer = new StreamWriter(this.clientStream); + + return true; + } + + public void SendRequest(NamedPipeMessages.Message message) + { + this.SendRequest(message.ToString()); + } + + public void SendRequest(string message) + { + this.ValidateConnection(); + + try + { + this.writer.WriteLine(message); + this.writer.Flush(); + } + catch (IOException e) + { + throw new BrokenPipeException("Unable to send: " + message, e); + } + } + + public string ReadRawResponse() + { + try + { + string response = this.reader.ReadLine(); + if (response == null) + { + throw new BrokenPipeException("Unable to read from pipe", null); + } + + return response; + } + catch (IOException e) + { + throw new BrokenPipeException("Unable to read from pipe", e); + } + } + + public NamedPipeMessages.Message ReadResponse() + { + return NamedPipeMessages.Message.FromString(this.ReadRawResponse()); + } + + public void Dispose() + { + this.ValidateConnection(); + + this.clientStream.Dispose(); + this.clientStream = null; + this.reader = null; + this.writer = null; + } + + private void ValidateConnection() + { + if (this.clientStream == null) + { + throw new InvalidOperationException("There is no connection"); + } + } + } +} diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs new file mode 100644 index 00000000..72db23a7 --- /dev/null +++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs @@ -0,0 +1,242 @@ +using Newtonsoft.Json; +using System; + +namespace GVFS.Common.NamedPipes +{ + public static class NamedPipeMessages + { + public const string UnknownRequest = "UnknownRequest"; + public const string UnknownGVFSState = "UnknownGVFSState"; + + private const char MessageSeparator = '|'; + + public static class GetStatus + { + public const string Request = "GetStatus"; + public const string Mounting = "Mounting"; + public const string Ready = "Ready"; + public const string Unmounting = "Unmounting"; + public const string MountFailed = "MountFailed"; + + public class Response + { + public string MountStatus { get; set; } + public string EnlistmentRoot { get; set; } + public string RepoUrl { get; set; } + public string ObjectsUrl { get; set; } + public int BackgroundOperationCount { get; set; } + public string LockStatus { get; set; } + public int DiskLayoutVersion { get; set; } + + public static Response FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + } + } + + public static class Unmount + { + public const string Request = "Unmount"; + public const string NotMounted = "NotMounted"; + public const string Acknowledged = "Ack"; + public const string Completed = "Complete"; + public const string AlreadyUnmounting = "AlreadyUnmounting"; + public const string MountFailed = "MountFailed"; + } + + public static class DownloadObject + { + public const string DownloadRequest = "DLO"; + public const string SuccessResult = "S"; + public const string DownloadFailed = "F"; + public const string InvalidSHAResult = "InvalidSHA"; + public const string MountNotReadyResult = "MountNotReady"; + + public class Request + { + public Request(Message message) + { + this.RequestSha = message.Body; + } + + public string RequestSha { get; } + + public Message CreateMessage() + { + return new Message(DownloadRequest, this.RequestSha); + } + } + + public class Response + { + public Response(string result) + { + this.Result = result; + } + + public string Result { get; } + + public Message CreateMessage() + { + return new Message(this.Result, null); + } + } + } + + public static class AcquireLock + { + public const string AcquireRequest = "AcquireLock"; + public const string DenyGVFSResult = "LockDeniedGVFS"; + public const string DenyGitResult = "LockDeniedGit"; + public const string AcceptResult = "LockAcquired"; + public const string MountNotReadyResult = "MountNotReady"; + + public class Request + { + public Request(int pid, string parsedCommand, string originalCommand) + { + this.RequestData = new Data(pid, parsedCommand, originalCommand); + } + + public Request(Message message) + { + this.RequestData = message.DeserializeBody(); + } + + public Data RequestData { get; } + + public Message CreateMessage() + { + return new Message(AcquireRequest, this.RequestData); + } + } + + public class Response + { + public Response(string result, Data responseData = null) + { + this.Result = result; + this.ResponseData = responseData; + } + + public Response(Message message) + { + this.Result = message.Header; + this.ResponseData = message.DeserializeBody(); + } + + public string Result { get; } + + public Data ResponseData { get; } + + public Message CreateMessage() + { + return new Message(this.Result, this.ResponseData); + } + } + + public class Data + { + public Data(int pid, string parsedCommand, string originalCommand) + { + this.PID = pid; + this.ParsedCommand = parsedCommand; + this.OriginalCommand = originalCommand; + } + + public int PID { get; set; } + + /// + /// The command line requesting the lock, built internally for parsing purposes. + /// e.g. "git status", "git rebase" + /// + public string ParsedCommand { get; set; } + + /// + /// The command line for the process requesting the lock, as kept by the OS. + /// e.g. "c:\bin\git\git.exe git-rebase origin/master" + /// + public string OriginalCommand { get; set; } + + public override string ToString() + { + return this.ParsedCommand + " (" + this.PID + ")"; + } + } + } + + public class Message + { + public Message(string header, object body) + : this(header, JsonConvert.SerializeObject(body)) + { + } + + private Message(string header, string body) + { + this.Header = header; + this.Body = body; + } + + public string Header { get; } + + public string Body { get; } + + public static Message FromString(string message) + { + string header = null; + string body = null; + if (!string.IsNullOrEmpty(message)) + { + string[] parts = message.Split(new[] { NamedPipeMessages.MessageSeparator }, count: 2); + header = parts[0]; + if (parts.Length > 1) + { + body = parts[1]; + } + } + + return new Message(header, body); + } + + public TBody DeserializeBody() + { + if (string.IsNullOrEmpty(this.Body)) + { + return default(TBody); + } + + try + { + return JsonConvert.DeserializeObject(this.Body); + } + catch (JsonException jsonException) + { + throw new ArgumentException("Unrecognized body contents.", nameof(this.Body), jsonException); + } + } + + public override string ToString() + { + string result = string.Empty; + if (!string.IsNullOrEmpty(this.Header)) + { + result = this.Header; + } + + if (this.Body != null) + { + result = result + NamedPipeMessages.MessageSeparator + this.Body; + } + + return result; + } + } + } +} diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeServer.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeServer.cs new file mode 100644 index 00000000..34ba5c5b --- /dev/null +++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeServer.cs @@ -0,0 +1,117 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Threading; + +namespace GVFS.Common.NamedPipes +{ + public class NamedPipeServer + { + private string pipeName; + private Action handleConnection; + + public NamedPipeServer(string pipeName, Action handleConnection) + { + this.pipeName = pipeName; + this.handleConnection = handleConnection; + } + + public void Start() + { + this.CreateNewListenerThread(); + } + + private void CreateNewListenerThread() + { + new Thread(this.ListenForNewConnection).Start(); + } + + private void ListenForNewConnection() + { + PipeSecurity security = new PipeSecurity(); + security.AddAccessRule(new PipeAccessRule("Users", PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule("CREATOR OWNER", PipeAccessRights.FullControl, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule("SYSTEM", PipeAccessRights.FullControl, AccessControlType.Allow)); + + NamedPipeServerStream serverStream = new NamedPipeServerStream( + this.pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.WriteThrough, + 0, // default inBufferSize + 0, // default outBufferSize + security, + HandleInheritability.None); + + serverStream.WaitForConnection(); + + this.CreateNewListenerThread(); + + using (Connection connection = new Connection(serverStream)) + { + this.handleConnection(connection); + } + } + + public class Connection : IDisposable + { + private NamedPipeServerStream serverStream; + private StreamReader reader; + private StreamWriter writer; + + public Connection(NamedPipeServerStream serverStream) + { + this.serverStream = serverStream; + this.reader = new StreamReader(this.serverStream); + this.writer = new StreamWriter(this.serverStream); + } + + public bool IsConnected + { + get { return this.serverStream.IsConnected; } + } + + public string ReadRequest() + { + try + { + return this.reader.ReadLine(); + } + catch (IOException) + { + return null; + } + } + + public bool TrySendResponse(string message) + { + try + { + this.writer.WriteLine(message); + this.writer.Flush(); + + return true; + } + catch (IOException) + { + return false; + } + } + + public bool TrySendResponse(NamedPipeMessages.Message message) + { + return this.TrySendResponse(message.ToString()); + } + + public void Dispose() + { + this.serverStream.Dispose(); + this.serverStream = null; + this.reader = null; + this.writer = null; + } + } + } +} diff --git a/GVFS/GVFS.Common/NativeMethods.cs b/GVFS/GVFS.Common/NativeMethods.cs new file mode 100644 index 00000000..11586498 --- /dev/null +++ b/GVFS/GVFS.Common/NativeMethods.cs @@ -0,0 +1,158 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace GVFS.Common +{ + public static class NativeMethods + { + public const int ERROR_FILE_NOT_FOUND = 2; + public const int ERROR_FILE_EXISTS = 80; + + public enum FileAttributes : uint + { + FILE_ATTRIBUTE_READONLY = 1, + FILE_ATTRIBUTE_HIDDEN = 2, + FILE_ATTRIBUTE_SYSTEM = 4, + FILE_ATTRIBUTE_DIRECTORY = 16, + FILE_ATTRIBUTE_ARCHIVE = 32, + FILE_ATTRIBUTE_DEVICE = 64, + FILE_ATTRIBUTE_NORMAL = 128, + FILE_ATTRIBUTE_TEMPORARY = 256, + FILE_ATTRIBUTE_SPARSEFILE = 512, + FILE_ATTRIBUTE_REPARSEPOINT = 1024, + FILE_ATTRIBUTE_COMPRESSED = 2048, + FILE_ATTRIBUTE_OFFLINE = 4096, + FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192, + FILE_ATTRIBUTE_ENCRYPTED = 16384, + FILE_FLAG_FIRST_PIPE_INSTANCE = 524288, + FILE_FLAG_OPEN_NO_RECALL = 1048576, + FILE_FLAG_OPEN_REPARSE_POINT = 2097152, + FILE_FLAG_POSIX_SEMANTICS = 16777216, + FILE_FLAG_BACKUP_SEMANTICS = 33554432, + FILE_FLAG_DELETE_ON_CLOSE = 67108864, + FILE_FLAG_SEQUENTIAL_SCAN = 134217728, + FILE_FLAG_RANDOM_ACCESS = 268435456, + FILE_FLAG_NO_BUFFERING = 536870912, + FILE_FLAG_OVERLAPPED = 1073741824, + FILE_FLAG_WRITE_THROUGH = 2147483648 + } + + public enum FileAccess : uint + { + FILE_READ_DATA = 1, + FILE_LIST_DIRECTORY = 1, + FILE_WRITE_DATA = 2, + FILE_ADD_FILE = 2, + FILE_APPEND_DATA = 4, + FILE_ADD_SUBDIRECTORY = 4, + FILE_CREATE_PIPE_INSTANCE = 4, + FILE_READ_EA = 8, + FILE_WRITE_EA = 16, + FILE_EXECUTE = 32, + FILE_TRAVERSE = 32, + FILE_DELETE_CHILD = 64, + FILE_READ_ATTRIBUTES = 128, + FILE_WRITE_ATTRIBUTES = 256, + SPECIFIC_RIGHTS_ALL = 65535, + DELETE = 65536, + READ_CONTROL = 131072, + STANDARD_RIGHTS_READ = 131072, + STANDARD_RIGHTS_WRITE = 131072, + STANDARD_RIGHTS_EXECUTE = 131072, + WRITE_DAC = 262144, + WRITE_OWNER = 524288, + STANDARD_RIGHTS_REQUIRED = 983040, + SYNCHRONIZE = 1048576, + FILE_GENERIC_READ = 1179785, + FILE_GENERIC_EXECUTE = 1179808, + FILE_GENERIC_WRITE = 1179926, + STANDARD_RIGHTS_ALL = 2031616, + FILE_ALL_ACCESS = 2032127, + ACCESS_SYSTEM_SECURITY = 16777216, + MAXIMUM_ALLOWED = 33554432, + GENERIC_ALL = 268435456, + GENERIC_EXECUTE = 536870912, + GENERIC_WRITE = 1073741824, + GENERIC_READ = 2147483648 + } + + public static SafeFileHandle OpenFile( + string filePath, + FileMode fileMode, + FileAccess fileAccess, + FileShare fileShare, + FileAttributes fileAttributes) + { + SafeFileHandle output = CreateFile(filePath, fileAccess, fileShare, IntPtr.Zero, fileMode, fileAttributes | FileAttributes.FILE_FLAG_OVERLAPPED, IntPtr.Zero); + if (output.IsInvalid) + { + ThrowWin32Exception(Marshal.GetLastWin32Error()); + } + + return output; + } + + /// + /// Lock specified directory, so it can't be deleted or renamed by any other process + /// The trick is to open a handle on the directory (FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + /// and keep it. If it is a junction the second option is required, and if it is a standard directory it is ignored. + /// Caller must call Close() or Dispose() on the returned safe handle to release the lock + /// + /// Path to existing directory junction + /// SafeFileHandle + public static SafeFileHandle LockDirectory(string path) + { + SafeFileHandle result = CreateFile( + path, + FileAccess.GENERIC_READ, + FileShare.Read, + IntPtr.Zero, + FileMode.Open, + FileAttributes.FILE_FLAG_BACKUP_SEMANTICS | FileAttributes.FILE_FLAG_OPEN_REPARSE_POINT, + IntPtr.Zero); + if (result.IsInvalid) + { + ThrowWin32Exception(Marshal.GetLastWin32Error()); + } + + return result; + } + + public static void ThrowWin32Exception(int error, params int[] ignoreErrors) + { + if (ignoreErrors.Any(ignored => ignored == error)) + { + return; + } + + if (error == ERROR_FILE_EXISTS) + { + throw new Win32FileExistsException(); + } + + throw new Win32Exception(error); + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern SafeFileHandle CreateFile( + [In] string lpFileName, + [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, + FileShare dwShareMode, + [In] IntPtr lpSecurityAttributes, + [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, + [MarshalAs(UnmanagedType.U4)]FileAttributes dwFlagsAndAttributes, + [In] IntPtr hTemplateFile); + + public class Win32FileExistsException : Win32Exception + { + public Win32FileExistsException() + : base(NativeMethods.ERROR_FILE_EXISTS) + { + } + } + } +} diff --git a/GVFS/GVFS.Common/Physical/FileSystem/DirectoryItemInfo.cs b/GVFS/GVFS.Common/Physical/FileSystem/DirectoryItemInfo.cs new file mode 100644 index 00000000..d46db072 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/FileSystem/DirectoryItemInfo.cs @@ -0,0 +1,10 @@ +namespace GVFS.Common.Physical.FileSystem +{ + public class DirectoryItemInfo + { + public string Name { get; set; } + public string FullName { get; set; } + public long Length { get; set; } + public bool IsDirectory { get; set; } + } +} diff --git a/GVFS/GVFS.Common/Physical/FileSystem/FileProperties.cs b/GVFS/GVFS.Common/Physical/FileSystem/FileProperties.cs new file mode 100644 index 00000000..3ffd7ff9 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/FileSystem/FileProperties.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; + +namespace GVFS.Common.Physical.FileSystem +{ + public class FileProperties + { + public static readonly FileProperties DefaultFile = new FileProperties(FileAttributes.Normal, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); + public static readonly FileProperties DefaultDirectory = new FileProperties(FileAttributes.Directory, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); + + public FileProperties(FileAttributes attributes, DateTime creationTimeUTC, DateTime lastAccessTimeUTC, DateTime lastWriteTimeUTC, long length) + { + this.FileAttributes = attributes; + this.CreationTimeUTC = creationTimeUTC; + this.LastAccessTimeUTC = lastAccessTimeUTC; + this.LastWriteTimeUTC = lastWriteTimeUTC; + this.Length = length; + } + + public FileAttributes FileAttributes { get; private set; } + public DateTime CreationTimeUTC { get; private set; } + public DateTime LastAccessTimeUTC { get; private set; } + public DateTime LastWriteTimeUTC { get; private set; } + public long Length { get; private set; } + } +} diff --git a/GVFS/GVFS.Common/Physical/FileSystem/PhysicalFileSystem.cs b/GVFS/GVFS.Common/Physical/FileSystem/PhysicalFileSystem.cs new file mode 100644 index 00000000..06d5f254 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/FileSystem/PhysicalFileSystem.cs @@ -0,0 +1,169 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace GVFS.Common.Physical.FileSystem +{ + public class PhysicalFileSystem + { + public const int DefaultStreamBufferSize = 8192; + + // https://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.internalbuffersize(v=vs.110).aspx: + // Max FileSystemWatcher.InternalBufferSize is 64 KB + private const int WatcherBufferSize = 64 * 1024; + + public static void RecursiveDelete(string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + + foreach (FileInfo file in directory.GetFiles()) + { + file.Attributes = FileAttributes.Normal; + file.Delete(); + } + + foreach (DirectoryInfo subDirectory in directory.GetDirectories()) + { + RecursiveDelete(subDirectory.FullName); + } + + directory.Delete(); + } + + public virtual bool FileExists(string path) + { + return File.Exists(path); + } + + public virtual void DeleteFile(string path) + { + File.Delete(path); + } + + public virtual string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public virtual IEnumerable ReadLines(string path) + { + return File.ReadLines(path); + } + + public virtual void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); + } + + public virtual Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode) + { + return this.OpenFileStream(path, fileMode, fileAccess, NativeMethods.FileAttributes.FILE_ATTRIBUTE_NORMAL, shareMode); + } + + public virtual Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, NativeMethods.FileAttributes attributes, FileShare shareMode) + { + FileAccess access = fileAccess & FileAccess.ReadWrite; + return new FileStream((SafeFileHandle)this.OpenFile(path, fileMode, fileAccess, (FileAttributes)attributes, shareMode), access, DefaultStreamBufferSize, true); + } + + public virtual SafeHandle OpenFile(string path, FileMode fileMode, FileAccess fileAccess, FileAttributes attributes, FileShare shareMode) + { + return NativeMethods.OpenFile(path, fileMode, (NativeMethods.FileAccess)fileAccess, shareMode, (NativeMethods.FileAttributes)attributes); + } + + public virtual void DeleteDirectory(string path, bool recursive = false) + { + RecursiveDelete(path); + } + + /// + /// Lock specified directory, so it can't be deleted or renamed by any other process + /// + /// Path to existing directory junction + public virtual SafeFileHandle LockDirectory(string path) + { + return NativeMethods.LockDirectory(path); + } + + public virtual IEnumerable ItemsInDirectory(string path) + { + DirectoryInfo ntfsDirectory = new DirectoryInfo(path); + foreach (FileSystemInfo ntfsItem in ntfsDirectory.GetFileSystemInfos()) + { + DirectoryItemInfo itemInfo = new DirectoryItemInfo() + { + FullName = ntfsItem.FullName, + Name = ntfsItem.Name, + IsDirectory = (ntfsItem.Attributes & FileAttributes.Directory) != 0 + }; + + if (!itemInfo.IsDirectory) + { + itemInfo.Length = ((FileInfo)ntfsItem).Length; + } + + yield return itemInfo; + } + } + + public virtual FileProperties GetFileProperties(string path) + { + FileInfo entry = new FileInfo(path); + if (entry.Exists) + { + return new FileProperties( + entry.Attributes, + entry.CreationTimeUtc, + entry.LastAccessTimeUtc, + entry.LastWriteTimeUtc, + entry.Length); + } + else + { + return FileProperties.DefaultFile; + } + } + + public virtual IDisposable MonitorChanges( + string directory, + NotifyFilters notifyFilter, + Action onCreate, + Action onRename, + Action onDelete) + { + FileSystemWatcher watcher = new FileSystemWatcher(directory); + watcher.IncludeSubdirectories = true; + watcher.NotifyFilter = notifyFilter; + watcher.InternalBufferSize = WatcherBufferSize; + watcher.EnableRaisingEvents = true; + if (onCreate != null) + { + watcher.Created += (sender, args) => onCreate(args); + } + + if (onRename != null) + { + watcher.Renamed += (sender, args) => + { + // Skip the event if args.Name is null. + // Name can be null if the FileSystemWatcher's buffer has an entry for OLD_NAME that is not followed by an + // entry for NEW_NAME. This scenario results in two rename events being fired, the first with a null Name and the + // second with a null OldName. + if (args.Name != null) + { + onRename(args); + } + }; + } + + if (onDelete != null) + { + watcher.Deleted += (sender, args) => onDelete(args); + } + + return watcher; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Physical/FileSystem/StreamReaderExtensions.cs b/GVFS/GVFS.Common/Physical/FileSystem/StreamReaderExtensions.cs new file mode 100644 index 00000000..26946621 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/FileSystem/StreamReaderExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace GVFS.Common.Physical.FileSystem +{ + public static class StreamReaderExtensions + { + private const int ReadWriteTimeoutMs = 10000; + private const int BufferSize = 64 * 1024; + + public static void CopyBlockTo(this StreamReader input, StreamWriter writer, long numBytes) + where TTimeoutException : TimeoutException, new() + { + char[] buffer = new char[BufferSize]; + int read; + while (numBytes > 0) + { + int bytesToRead = Math.Min(buffer.Length, (int)numBytes); + read = input.ReadBlockAsync(buffer, 0, bytesToRead).Timeout(ReadWriteTimeoutMs); + if (read <= 0) + { + break; + } + + writer.WriteAsync(buffer, 0, read).Timeout(ReadWriteTimeoutMs); + numBytes -= read; + } + } + + public static async Task CopyBlockToAsync(this StreamReader input, StreamWriter writer, long numBytes) + { + char[] buffer = new char[BufferSize]; + int read; + while (numBytes > 0) + { + int bytesToRead = Math.Min(buffer.Length, (int)Math.Min(numBytes, int.MaxValue)); + read = await input.ReadBlockAsync(buffer, 0, bytesToRead); + if (read <= 0) + { + break; + } + + await writer.WriteAsync(buffer, 0, read); + numBytes -= read; + } + } + } +} diff --git a/GVFS/GVFS.Common/Physical/Git/BigEndianReader.cs b/GVFS/GVFS.Common/Physical/Git/BigEndianReader.cs new file mode 100644 index 00000000..e247d30e --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/BigEndianReader.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Text; + +namespace GVFS.Common.Physical.Git +{ + public class BigEndianReader : BinaryReader + { + public BigEndianReader(Stream input) + : base(input, Encoding.Default, leaveOpen: true) + { + } + + public override short ReadInt16() + { + return EndianHelper.Swap(base.ReadInt16()); + } + + public override int ReadInt32() + { + return EndianHelper.Swap(base.ReadInt32()); + } + + public override long ReadInt64() + { + return EndianHelper.Swap(base.ReadInt64()); + } + + public override ushort ReadUInt16() + { + return EndianHelper.Swap(base.ReadUInt16()); + } + + public override uint ReadUInt32() + { + return EndianHelper.Swap(base.ReadUInt32()); + } + + public override ulong ReadUInt64() + { + return EndianHelper.Swap(base.ReadUInt64()); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Physical/Git/CopyBlobContentTimeoutException.cs b/GVFS/GVFS.Common/Physical/Git/CopyBlobContentTimeoutException.cs new file mode 100644 index 00000000..f5912558 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/CopyBlobContentTimeoutException.cs @@ -0,0 +1,8 @@ +using System; + +namespace GVFS.Common.Physical.Git +{ + public class CopyBlobContentTimeoutException : TimeoutException + { + } +} diff --git a/GVFS/GVFS.Common/Physical/Git/EndianHelper.cs b/GVFS/GVFS.Common/Physical/Git/EndianHelper.cs new file mode 100644 index 00000000..64a79405 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/EndianHelper.cs @@ -0,0 +1,47 @@ +namespace GVFS.Common.Physical.Git +{ + public static class EndianHelper + { + public static short Swap(short source) + { + return (short)Swap((ushort)source); + } + + public static int Swap(int source) + { + return (int)Swap((uint)source); + } + + public static long Swap(long source) + { + return (long)((ulong)source); + } + + public static ushort Swap(ushort source) + { + return (ushort)(((source & 0x000000FF) << 8) | + ((source & 0x0000FF00) >> 8)); + } + + public static uint Swap(uint source) + { + return ((source & 0x000000FF) << 24) + | ((source & 0x0000FF00) << 8) + | ((source & 0x00FF0000) >> 8) + | ((source & 0xFF000000) >> 24); + } + + public static ulong Swap(ulong source) + { + return + ((source & 0x00000000000000FF) << 56) + | ((source & 0x000000000000FF00) << 40) + | ((source & 0x0000000000FF0000) << 24) + | ((source & 0x00000000FF000000) << 8) + | ((source & 0x000000FF00000000) >> 8) + | ((source & 0x0000FF0000000000) >> 24) + | ((source & 0x00FF000000000000) >> 40) + | ((source & 0xFF00000000000000) >> 56); + } + } +} diff --git a/GVFS/GVFS.Common/Physical/Git/GVFSGitObjects.cs b/GVFS/GVFS.Common/Physical/Git/GVFSGitObjects.cs new file mode 100644 index 00000000..d967ed4c --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/GVFSGitObjects.cs @@ -0,0 +1,117 @@ +using GVFS.Common.Git; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace GVFS.Common.Physical.Git +{ + public class GVFSGitObjects : GitObjects + { + private static readonly TimeSpan NegativeCacheTTL = TimeSpan.FromSeconds(30); + + private string objectsPath; + private ConcurrentDictionary objectNegativeCache; + + public GVFSGitObjects(GVFSContext context, HttpGitObjects httpGitObjects) + : base(context.Tracer, context.Enlistment, httpGitObjects) + { + this.Context = context; + this.objectsPath = Path.Combine(context.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Objects.Root); + + this.objectNegativeCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + public virtual string HeadTreeSha + { + get { return this.Context.Repository.GetHeadTreeSha(); } + } + + protected GVFSContext Context { get; private set; } + + public virtual SafeHandle OpenGitObject(string firstTwoShaDigits, string remainingShaDigits) + { + return + this.OpenLooseObject(this.objectsPath, firstTwoShaDigits, remainingShaDigits) + ?? this.DownloadObject(firstTwoShaDigits, remainingShaDigits); + } + + public bool TryCopyBlobContentStream(string sha, Action writeAction) + { + if (!this.Context.Repository.TryCopyBlobContentStream(sha, writeAction)) + { + if (!this.TryDownloadAndSaveObject(sha.Substring(0, 2), sha.Substring(2))) + { + return false; + } + + return this.Context.Repository.TryCopyBlobContentStream(sha, writeAction); + } + + return true; + } + + public bool TryDownloadAndSaveObject(string firstTwoShaDigits, string remainingShaDigits) + { + DateTime negativeCacheRequestTime; + string objectId = firstTwoShaDigits + remainingShaDigits; + + if (this.objectNegativeCache.TryGetValue(objectId, out negativeCacheRequestTime)) + { + if (negativeCacheRequestTime > DateTime.Now.Subtract(NegativeCacheTTL)) + { + return false; + } + + this.objectNegativeCache.TryRemove(objectId, out negativeCacheRequestTime); + } + + DownloadAndSaveObjectResult result = this.TryDownloadAndSaveObject(objectId); + + switch (result) + { + case DownloadAndSaveObjectResult.Success: + return true; + case DownloadAndSaveObjectResult.ObjectNotOnServer: + this.objectNegativeCache.AddOrUpdate(objectId, DateTime.Now, (unused1, unused2) => DateTime.Now); + return false; + case DownloadAndSaveObjectResult.Error: + return false; + default: + throw new InvalidOperationException("Unknown DownloadAndSaveObjectResult value"); + } + } + + public bool TryGetBlobSizeLocally(string sha, out long length) + { + return this.Context.Repository.TryGetBlobLength(sha, out length); + } + + public List GetFileSizes(IEnumerable objectIds) + { + return this.GitObjectRequestor.QueryForFileSizes(objectIds); + } + + private SafeHandle OpenLooseObject(string objectsRoot, string firstTwoShaDigits, string remainingShaDigits) + { + string looseObjectPath = Path.Combine( + objectsRoot, + firstTwoShaDigits, + remainingShaDigits); + + if (this.Context.FileSystem.FileExists(looseObjectPath)) + { + return this.Context.FileSystem.OpenFile(looseObjectPath, FileMode.Open, (FileAccess)NativeMethods.FileAccess.FILE_GENERIC_READ, FileAttributes.Normal, FileShare.Read); + } + + return null; + } + + private SafeHandle DownloadObject(string firstTwoShaDigits, string remainingShaDigits) + { + this.TryDownloadAndSaveObject(firstTwoShaDigits, remainingShaDigits); + return this.OpenLooseObject(this.objectsPath, firstTwoShaDigits, remainingShaDigits); + } + } +} diff --git a/GVFS/GVFS.Common/Physical/Git/GitIndex.cs b/GVFS/GVFS.Common/Physical/Git/GitIndex.cs new file mode 100644 index 00000000..d36c5710 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/GitIndex.cs @@ -0,0 +1,378 @@ +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace GVFS.Common.Physical.Git +{ + public class GitIndex : IDisposable + { + private const ushort ExtendedBit = 0x4000; + private const ushort SkipWorktreeBit = 0x4000; + private const int BaseEntryLength = 62; + private const int MaxPathBufferSize = 4096; + + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private Dictionary pathOffsets; + private bool pathOffsetsIsInvalid; + private string indexPath; + private string lockPath; + private ITracer tracer; + private Enlistment enlistment; + private Stream indexFileStream; + private FileBasedLock gitIndexLock; + + public GitIndex(ITracer tracer, Enlistment enlistment, string virtualIndexPath, string virtualIndexLockPath) + { + this.indexPath = virtualIndexPath; + this.lockPath = virtualIndexLockPath; + this.pathOffsetsIsInvalid = true; + this.tracer = tracer; + this.enlistment = enlistment; + } + + public void Initialize() + { + this.gitIndexLock = new FileBasedLock( + new PhysicalFileSystem(), + this.tracer, + this.lockPath, + "GVFS", + FileBasedLock.ExistingLockCleanup.DeleteExisting); + } + + public CallbackResult Open() + { + if (!File.Exists(this.indexPath)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("ErrorMessage", "Can't open the index because it doesn't exist"); + this.tracer.RelatedError(metadata); + + return CallbackResult.FatalError; + } + + if (!this.gitIndexLock.TryAcquireLockAndDeleteOnClose()) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + this.tracer.RelatedEvent(EventLevel.Verbose, "OpenCantAcquireIndexLock", metadata); + + return CallbackResult.RetryableError; + } + + CallbackResult result = CallbackResult.FatalError; + try + { + // TODO 667979: check if the index is missing and generate a new one if needed + + this.indexFileStream = new FileStream(this.indexPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read); + result = CallbackResult.Success; + } + catch (IOException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "IOException in Open (RetryableError)"); + this.tracer.RelatedError(metadata); + + result = CallbackResult.RetryableError; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Exception in Open (FatalError)"); + this.tracer.RelatedError(metadata); + + result = CallbackResult.FatalError; + } + finally + { + if (result != CallbackResult.Success) + { + if (!this.gitIndexLock.TryReleaseLock()) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("ErrorMessage", "Unable to release index.lock in Open (FatalError)"); + this.tracer.RelatedError(metadata); + + result = CallbackResult.FatalError; + } + } + } + + return result; + } + + public CallbackResult Close() + { + if (this.indexFileStream != null) + { + this.indexFileStream.Dispose(); + this.indexFileStream = null; + } + + try + { + if (!this.gitIndexLock.IsOpen() || + this.gitIndexLock.TryReleaseLock()) + { + return CallbackResult.Success; + } + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Fatal Exception in Close"); + this.tracer.RelatedError(metadata); + } + + return CallbackResult.FatalError; + } + + public virtual CallbackResult ClearSkipWorktreeAndUpdateEntry(string filePath, DateTime createTimeUtc, DateTime lastWriteTimeUtc, uint fileSize) + { + try + { + if (this.pathOffsetsIsInvalid) + { + this.pathOffsetsIsInvalid = false; + this.ParseIndex(); + + if (this.pathOffsetsIsInvalid) + { + return CallbackResult.RetryableError; + } + } + + string gitStyleFilePath = filePath.TrimStart(GVFSConstants.PathSeparator).Replace(GVFSConstants.PathSeparator, GVFSConstants.GitPathSeparator); + long offset; + if (this.pathOffsets.TryGetValue(gitStyleFilePath, out offset)) + { + if (createTimeUtc == DateTime.MinValue || + lastWriteTimeUtc == DateTime.MinValue || + fileSize == 0) + { + try + { + FileInfo fileInfo = new FileInfo(Path.Combine(this.enlistment.WorkingDirectoryRoot, filePath)); + if (fileInfo.Exists) + { + createTimeUtc = fileInfo.CreationTimeUtc; + lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; + fileSize = (uint)fileInfo.Length; + } + } + catch (IOException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("filePath", filePath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "IOException caught while trying to get FileInfo for index entry"); + this.tracer.RelatedError(metadata); + } + } + + uint ctimeSeconds = this.ToUnixEpochSeconds(createTimeUtc); + uint ctimeNanosecondFraction = this.ToUnixNanosecondFraction(createTimeUtc); + uint mtimeSeconds = this.ToUnixEpochSeconds(lastWriteTimeUtc); + uint mtimeNanosecondFraction = this.ToUnixNanosecondFraction(lastWriteTimeUtc); + + this.indexFileStream.Seek(offset, SeekOrigin.Begin); + + this.indexFileStream.Write(BitConverter.GetBytes(EndianHelper.Swap(ctimeSeconds)), 0, 4); // ctime seconds + this.indexFileStream.Write(BitConverter.GetBytes(EndianHelper.Swap(ctimeNanosecondFraction)), 0, 4); // ctime nanosecond fractions + this.indexFileStream.Write(BitConverter.GetBytes(EndianHelper.Swap(mtimeSeconds)), 0, 4); // mtime seconds + this.indexFileStream.Write(BitConverter.GetBytes(EndianHelper.Swap(mtimeNanosecondFraction)), 0, 4); // mtime nanosecond fractions + this.indexFileStream.Seek(20, SeekOrigin.Current); // dev + ino + mode + uid + gid + this.indexFileStream.Write(BitConverter.GetBytes(EndianHelper.Swap(fileSize)), 0, 4); // size + this.indexFileStream.Seek(22, SeekOrigin.Current); // sha + flags + this.indexFileStream.Write(new byte[2] { 0, 0 }, 0, 2); // extended flags + this.indexFileStream.Flush(); + + this.pathOffsets.Remove(gitStyleFilePath); + } + } + catch (IOException e) + { + this.pathOffsetsIsInvalid = true; + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "IOException in ClearSkipWorktreeBitWhileHoldingIndexLock (RetryableError)"); + this.tracer.RelatedError(metadata); + + return CallbackResult.RetryableError; + } + catch (Exception e) + { + this.pathOffsetsIsInvalid = true; + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GitIndex"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Exception in ClearSkipWorktreeBitWhileHoldingIndexLock (FatalError)"); + this.tracer.RelatedError(metadata); + + return CallbackResult.FatalError; + } + + return CallbackResult.Success; + } + + public void Invalidate() + { + if (!this.gitIndexLock.IsOpen()) + { + this.pathOffsetsIsInvalid = true; + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (this.gitIndexLock != null) + { + this.gitIndexLock.Dispose(); + this.gitIndexLock = null; + } + } + } + + private uint ToUnixNanosecondFraction(DateTime datetime) + { + if (datetime > UnixEpoch) + { + TimeSpan timediff = datetime - UnixEpoch; + double nanoseconds = (timediff.TotalSeconds - Math.Truncate(timediff.TotalSeconds)) * 1000000000; + return Convert.ToUInt32(nanoseconds); + } + else + { + return 0; + } + } + + private uint ToUnixEpochSeconds(DateTime datetime) + { + if (datetime > UnixEpoch) + { + return Convert.ToUInt32(Math.Truncate((datetime - UnixEpoch).TotalSeconds)); + } + else + { + return 0; + } + } + + private void ParseIndex() + { + this.pathOffsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.indexFileStream.Position = 0; + using (BigEndianReader reader = new BigEndianReader(this.indexFileStream)) + { + reader.ReadBytes(4); + uint version = reader.ReadUInt32(); + uint entryCount = reader.ReadUInt32(); + + int previousPathLength = 0; + byte[] pathBuffer = new byte[MaxPathBufferSize]; + for (int i = 0; i < entryCount; i++) + { + // If the path offsets gets set as invalid we can bail + // since the index will have to be reparsed + if (this.pathOffsetsIsInvalid) + { + return; + } + + long entryOffset = this.indexFileStream.Position; + int entryLength = BaseEntryLength; + reader.ReadBytes(60); + + ushort flags = reader.ReadUInt16(); + bool isExtended = (flags & ExtendedBit) == ExtendedBit; + int pathLength = (ushort)((flags << 20) >> 20); + entryLength += pathLength; + + bool skipWorktree = false; + if (isExtended) + { + ushort extendedFlags = reader.ReadUInt16(); + skipWorktree = (extendedFlags & SkipWorktreeBit) == SkipWorktreeBit; + entryLength += 2; + } + + if (version == 4) + { + int replaceLength = this.ReadReplaceLength(reader); + byte ch; + int index = previousPathLength - replaceLength; + while ((ch = reader.ReadByte()) != '\0') + { + if (index >= pathBuffer.Length) + { + throw new InvalidOperationException("Git index path entry too large."); + } + + pathBuffer[index] = ch; + ++index; + } + + previousPathLength = index; + if (skipWorktree) + { + this.pathOffsets[Encoding.UTF8.GetString(pathBuffer, 0, index)] = entryOffset; + } + } + else + { + byte[] path = reader.ReadBytes(pathLength); + int nullbytes = 8 - (entryLength % 8); + reader.ReadBytes(nullbytes); + + if (skipWorktree) + { + this.pathOffsets[Encoding.UTF8.GetString(path)] = entryOffset; + } + } + } + } + } + + private int ReadReplaceLength(BinaryReader reader) + { + int headerByte = reader.ReadByte(); + int offset = headerByte & 0x7f; + + // Terminate the loop when the high bit is no longer set. + for (int i = 0; (headerByte & 0x80) != 0; i++) + { + headerByte = reader.ReadByte(); + + offset += 1; + offset = (offset << 7) + (headerByte & 0x7f); + } + + return offset; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Physical/Git/GitRepo.cs b/GVFS/GVFS.Common/Physical/Git/GitRepo.cs new file mode 100644 index 00000000..6a982fc3 --- /dev/null +++ b/GVFS/GVFS.Common/Physical/Git/GitRepo.cs @@ -0,0 +1,143 @@ +using GVFS.Common.Git; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.Common.Physical.Git +{ + public class GitRepo : IDisposable + { + private string workingDirectoryPath; + private ITracer tracer; + + private PhysicalFileSystem fileSystem; + + private GitIndex index; + private ProcessPool catFileProcessPool; + private ProcessPool batchCheckProcessPool; + + public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem, GitIndex index) + { + this.tracer = tracer; + this.workingDirectoryPath = enlistment.WorkingDirectoryRoot; + this.fileSystem = fileSystem; + this.index = index; + + this.GVFSLock = new GVFSLock(tracer); + + this.batchCheckProcessPool = new ProcessPool( + tracer, + () => new GitCatFileBatchCheckProcess(enlistment), + Environment.ProcessorCount); + this.catFileProcessPool = new ProcessPool( + tracer, + () => new GitCatFileBatchProcess(enlistment), + Environment.ProcessorCount); + } + + public GitIndex Index + { + get { return this.index; } + } + + public GVFSLock GVFSLock + { + get; + private set; + } + + public void Initialize() + { + this.Index.Initialize(); + } + + public virtual string GetHeadTreeSha() + { + return this.catFileProcessPool.Invoke( + catFile => catFile.GetTreeSha(GVFSConstants.HeadCommitName)); + } + + public virtual string GetHeadCommitId() + { + return this.catFileProcessPool.Invoke( + catFile => catFile.GetCommitId(GVFSConstants.HeadCommitName)); + } + + public virtual bool TryCopyBlobContentStream(string blobSha, Action writeAction) + { + return this.catFileProcessPool.Invoke( + catFile => catFile.TryCopyBlobContentStream(blobSha, writeAction)); + } + + public virtual bool TryGetBlobLength(string blobSha, out long size) + { + long? output = this.batchCheckProcessPool.Invoke( + catFileBatch => + { + long value; + if (catFileBatch.TryGetObjectSize(blobSha, out value)) + { + return value; + } + + return null; + }); + + if (output.HasValue) + { + size = output.Value; + return true; + } + + size = 0; + return false; + } + + public virtual bool TryGetFileSha(string commitId, string virtualPath, out string sha) + { + sha = this.catFileProcessPool.Invoke( + catFile => + { + string innerSha; + if (catFile.TryGetFileSha(commitId, virtualPath, out innerSha)) + { + return innerSha; + } + + return null; + }); + + return !string.IsNullOrWhiteSpace(sha); + } + + public virtual IEnumerable GetTreeEntries(string commitId, string path) + { + return this.catFileProcessPool.Invoke(catFile => catFile.GetTreeEntries(commitId, path)); + } + + public virtual IEnumerable GetTreeEntries(string sha) + { + return this.catFileProcessPool.Invoke(catFile => catFile.GetTreeEntries(sha)); + } + + public void Dispose() + { + if (this.catFileProcessPool != null) + { + this.catFileProcessPool.Dispose(); + } + + if (this.batchCheckProcessPool != null) + { + this.batchCheckProcessPool.Dispose(); + } + + if (this.index != null) + { + this.index.Dispose(); + } + } + } +} diff --git a/GVFS/GVFS.Common/Physical/RegistryUtils.cs b/GVFS/GVFS.Common/Physical/RegistryUtils.cs new file mode 100644 index 00000000..9830b7ea --- /dev/null +++ b/GVFS/GVFS.Common/Physical/RegistryUtils.cs @@ -0,0 +1,33 @@ +using Microsoft.Win32; + +namespace GVFS.Common.Physical +{ + public class RegistryUtils + { + public static string GetStringFromRegistry(RegistryHive registryHive, string key, string valueName) + { + string value = GetStringFromRegistry(registryHive, key, valueName, RegistryView.Registry64); + if (value == null) + { + value = GetStringFromRegistry(registryHive, key, valueName, RegistryView.Registry32); + } + + return value; + } + + private static string GetStringFromRegistry(RegistryHive registryHive, string key, string valueName, RegistryView view) + { + RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, view); + var localKeySub = localKey.OpenSubKey(key); + + object value = localKeySub == null ? null : localKeySub.GetValue(valueName); + + if (value == null) + { + return null; + } + + return (string)value; + } + } +} diff --git a/GVFS/GVFS.Common/Physical/RepoMetadata.cs b/GVFS/GVFS.Common/Physical/RepoMetadata.cs new file mode 100644 index 00000000..c9fcd71f --- /dev/null +++ b/GVFS/GVFS.Common/Physical/RepoMetadata.cs @@ -0,0 +1,139 @@ +using GVFS.Common.Tracing; +using Microsoft.Isam.Esent.Collections.Generic; +using System; +using System.IO; + +namespace GVFS.Common.Physical +{ + public class RepoMetadata : IDisposable + { + private PersistentDictionary repoMetadata; + + public RepoMetadata(string dotGVFSPath) + { + this.repoMetadata = new PersistentDictionary( + Path.Combine(dotGVFSPath, GVFSConstants.DatabaseNames.RepoMetadata)); + } + + public static int GetCurrentDiskLayoutVersion() + { + return DiskLayoutVersion.CurrentDiskLayoutVerion; + } + + public static bool CheckDiskLayoutVersion(string dotGVFSPath, out string error) + { + if (!Directory.Exists(Path.Combine(dotGVFSPath, GVFSConstants.DatabaseNames.RepoMetadata))) + { + error = DiskLayoutVersion.MissingVersionError; + return false; + } + + try + { + using (RepoMetadata repoMetadata = new RepoMetadata(dotGVFSPath)) + { + return repoMetadata.CheckDiskLayoutVersion(out error); + } + } + catch (Exception e) + { + error = "Failed to check disk layout version of enlistment, Exception: " + e.ToString(); + return false; + } + } + + public void SaveCurrentDiskLayoutVersion() + { + DiskLayoutVersion.SaveCurrentDiskLayoutVersion(this.repoMetadata); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (this.repoMetadata != null) + { + this.repoMetadata.Dispose(); + this.repoMetadata = null; + } + } + + private bool CheckDiskLayoutVersion(out string error) + { + return DiskLayoutVersion.CheckDiskLayoutVersion(this.repoMetadata, out error); + } + + private static class DiskLayoutVersion + { + // The current disk layout version. This number should be bumped whenever a disk format change is made + // that would impact and older GVFS's ability to mount the repo + public const int CurrentDiskLayoutVerion = 3; + + public const string MissingVersionError = "Enlistment disk layout version not found, check if a breaking change has been made to GVFS since cloning this enlistment."; + + private const string DiskLayoutVersionKey = "DiskLayoutVersion"; + + // MaxDiskLayoutVersion ensures that olders versions of GVFS will not try to mount newer enlistments (if the + // disk layout of the newer GVFS is incompatible). + // GVFS will only mount if the disk layout version of the repo is <= MaxDiskLayoutVersion + private const int MaxDiskLayoutVersion = CurrentDiskLayoutVerion; + + // MinDiskLayoutVersion ensures that GVFS will not attempt to mount an older repo if there has been a breaking format + // change since that enlistment was cloned. + // - GVFS will only mount if the disk layout version of the repo is >= MinDiskLayoutVersion + // - Bump this version number only when a breaking change is being made (i.e. upgrade is not supported) + private const int MinDiskLayoutVersion = 3; + + public static void SaveCurrentDiskLayoutVersion(PersistentDictionary repoMetadata) + { + repoMetadata[DiskLayoutVersionKey] = CurrentDiskLayoutVerion.ToString(); + repoMetadata.Flush(); + } + + public static bool CheckDiskLayoutVersion(PersistentDictionary repoMetadata, out string error) + { + error = string.Empty; + string value; + if (repoMetadata.TryGetValue(DiskLayoutVersionKey, out value)) + { + int persistedVersionNumber; + if (!int.TryParse(value, out persistedVersionNumber)) + { + error = "Failed to parse persisted disk layout version number"; + return false; + } + + if (persistedVersionNumber < MinDiskLayoutVersion) + { + error = string.Format( + "Breaking change to GVFS disk layout has been made since cloning. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1} \r\nMinimum supported version: {2}", + persistedVersionNumber, + CurrentDiskLayoutVerion, + MinDiskLayoutVersion); + + return false; + } + else if (persistedVersionNumber > MaxDiskLayoutVersion) + { + error = string.Format( + "Changes to GVFS disk layout do not allow mounting after downgrade. Try mounting again using a more recent version of GVFS. \r\nEnlistment disk layout version: {0} \r\nGVFS disk layout version: {1}", + persistedVersionNumber, + CurrentDiskLayoutVerion); + return false; + } + } + else + { + error = MissingVersionError; + return false; + } + + return true; + } + } + } +} diff --git a/GVFS/GVFS.Common/PrefetchPacks/PrefetchPacksDeserializer.cs b/GVFS/GVFS.Common/PrefetchPacks/PrefetchPacksDeserializer.cs new file mode 100644 index 00000000..ee26af1b --- /dev/null +++ b/GVFS/GVFS.Common/PrefetchPacks/PrefetchPacksDeserializer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.Common +{ + /// + /// Deserializer for packs and indexes for prefetch packs. + /// + public class PrefetchPacksDeserializer + { + private const int NumPackHeaderBytes = 3 * sizeof(long); + + private static readonly byte[] PrefetchPackExpectedHeader + = new byte[] + { + (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', // Magic + 1 // Version + }; + + private readonly Stream source; + + public PrefetchPacksDeserializer( + Stream source) + { + this.source = source; + } + + /// + /// Read all the packs and indexes from the source stream and return a for each pack + /// and index. Caller must consume pack stream fully before the index stream. + /// + public IEnumerable EnumeratePacks() + { + this.ValidateHeader(); + + // Start reading objects + byte[] buffer = new byte[NumPackHeaderBytes]; + + int packCount = this.ReadPackCount(buffer); + + for (int i = 0; i < packCount; i++) + { + long timestamp; + long packLength; + long indexLength; + this.ReadPackHeader(buffer, out timestamp, out packLength, out indexLength); + + using (Stream packData = new RestrictedStream(this.source, 0, packLength, leaveOpen: true)) + using (Stream indexData = indexLength > 0 ? new RestrictedStream(this.source, 0, indexLength, leaveOpen: true) : null) + { + yield return new PackAndIndex(packData, indexData, timestamp); + } + } + } + + /// + /// Read the ushort pack count + /// + private ushort ReadPackCount(byte[] buffer) + { + StreamUtil.TryReadGreedy(this.source, buffer, 0, 2); + return BitConverter.ToUInt16(buffer, 0); + } + + /// + /// Parse the current pack header + /// + private void ReadPackHeader( + byte[] buffer, + out long timestamp, + out long packLength, + out long indexLength) + { + int totalBytes = StreamUtil.TryReadGreedy( + this.source, + buffer, + 0, + NumPackHeaderBytes); + + if (totalBytes == NumPackHeaderBytes) + { + timestamp = BitConverter.ToInt64(buffer, 0); + packLength = BitConverter.ToInt64(buffer, 8); + indexLength = BitConverter.ToInt64(buffer, 16); + } + else + { + throw new RetryableException( + string.Format( + "Reached end of stream before expected {0} bytes. Got {1}. Buffer: {2}", + NumPackHeaderBytes, + totalBytes, + SHA1Util.HexStringFromBytes(buffer))); + } + } + + private void ValidateHeader() + { + byte[] headerBuf = new byte[PrefetchPackExpectedHeader.Length]; + StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); + if (!headerBuf.SequenceEqual(PrefetchPackExpectedHeader)) + { + throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); + } + } + + public class PackAndIndex + { + public PackAndIndex(Stream packStream, Stream idxStream, long timestamp) + { + this.PackStream = packStream; + this.IndexStream = idxStream; + this.Timestamp = timestamp; + this.UniqueId = Guid.NewGuid().ToString("N"); + } + + public Stream PackStream { get; } + public Stream IndexStream { get; } + public long Timestamp { get; } + public string UniqueId { get; } + } + } +} diff --git a/GVFS/GVFS.Common/ProcessHelper.cs b/GVFS/GVFS.Common/ProcessHelper.cs new file mode 100644 index 00000000..551e637c --- /dev/null +++ b/GVFS/GVFS.Common/ProcessHelper.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace GVFS.Common +{ + public static class ProcessHelper + { + /// + /// Get the process Id for the highest process with the given name in the current process hierarchy. + /// + /// The name of the parent process to consider (e.g. git.exe) + /// The process Id or -1 if not found. + public static int GetParentProcessId(string parentName) + { + Dictionary processesSnapshot = Process.GetProcesses().ToDictionary(p => p.Id); + + int highestParentId = GVFSConstants.InvalidProcessId; + Process currentProcess = Process.GetCurrentProcess(); + while (true) + { + ProcessBasicInformation processBasicInfo; + int size; + int result = + NtQueryInformationProcess( + currentProcess.Handle, + 0, // Denotes ProcessBasicInformation + out processBasicInfo, + Marshal.SizeOf(typeof(ProcessBasicInformation)), + out size); + + int potentialParentId = processBasicInfo.InheritedFromUniqueProcessId.ToInt32(); + if (result != 0 || potentialParentId == 0) + { + return GetProcessIdIfHasName(highestParentId, parentName); + } + + Process processFound; + if (processesSnapshot.TryGetValue(potentialParentId, out processFound)) + { + if (processFound.MainModule.ModuleName.Equals(parentName, StringComparison.OrdinalIgnoreCase)) + { + highestParentId = potentialParentId; + } + else if (highestParentId > 0) + { + return GetProcessIdIfHasName(highestParentId, parentName); + } + } + else + { + if (highestParentId > 0) + { + return GetProcessIdIfHasName(highestParentId, parentName); + } + + return GVFSConstants.InvalidProcessId; + } + + currentProcess = Process.GetProcessById(potentialParentId); + } + } + + public static bool TryGetProcess(int processId, out Process process) + { + try + { + process = Process.GetProcessById(processId); + return true; + } + catch (ArgumentException) + { + process = null; + return false; + } + } + + public static ProcessResult Run(string programName, string args, bool redirectOutput = true) + { + ProcessStartInfo processInfo = new ProcessStartInfo(programName); + processInfo.UseShellExecute = false; + processInfo.RedirectStandardInput = true; + processInfo.RedirectStandardOutput = redirectOutput; + processInfo.RedirectStandardError = redirectOutput; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.Arguments = args; + + return Run(processInfo); + } + + public static void StartBackgroundProcess(string programName, string args, bool createWindow) + { + ProcessStartInfo processInfo = new ProcessStartInfo(programName, args); + + if (createWindow) + { + processInfo.WindowStyle = ProcessWindowStyle.Minimized; + } + else + { + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + } + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + + executingProcess.Start(); + } + + public static string GetCurrentProcessLocation() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + return Path.GetDirectoryName(assembly.Location); + } + + public static string GetEntryClassName() + { + Assembly assembly = Assembly.GetEntryAssembly(); + if (assembly == null) + { + // The PR build tests doesn't produce an entry assembly because it is run from unmanaged code, + // so we'll fall back on using this assembly. This should never ever happen for a normal exe invocation. + assembly = Assembly.GetExecutingAssembly(); + } + + return assembly.GetName().Name; + } + + public static string GetCurrentProcessVersion() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + return fileVersionInfo.ProductVersion; + } + + public static bool IsAdminElevated() + { + using (WindowsIdentity id = WindowsIdentity.GetCurrent()) + { + return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public static string WhereDirectory(string processName) + { + ProcessResult result = ProcessHelper.Run("where", processName); + if (result.ExitCode != 0) + { + return null; + } + + string firstPath = + string.IsNullOrWhiteSpace(result.Output) + ? null + : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (firstPath == null) + { + return null; + } + + try + { + return Path.GetDirectoryName(firstPath); + } + catch (IOException) + { + return null; + } + } + + public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null) + { + using (Process executingProcess = new Process()) + { + string output = string.Empty; + string errors = string.Empty; + + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + executingProcess.StartInfo = processInfo; + executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors = errors + args.Data + errorMsgDelimeter; + } + }; + + if (executionLock != null) + { + lock (executionLock) + { + output = StartProcess(executingProcess); + } + } + else + { + output = StartProcess(executingProcess); + } + + return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); + } + } + + public static string GetCommandLine(Process process) + { + using (ManagementObjectSearcher wmiSearch = + new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id)) + { + foreach (ManagementBaseObject commandLineObject in wmiSearch.Get()) + { + return process.StartInfo.FileName + " " + commandLineObject["CommandLine"]; + } + } + + return string.Empty; + } + + private static int GetProcessIdIfHasName(int processId, string expectedName) + { + if (ProcessIdHasName(processId, expectedName)) + { + return processId; + } + else + { + return GVFSConstants.InvalidProcessId; + } + } + + private static bool ProcessIdHasName(int processId, string expectedName) + { + Process process; + if (TryGetProcess(processId, out process)) + { + return process.MainModule.ModuleName.Equals(expectedName, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private static string StartProcess(Process executingProcess) + { + executingProcess.Start(); + + if (executingProcess.StartInfo.RedirectStandardError) + { + executingProcess.BeginErrorReadLine(); + } + + string output = string.Empty; + if (executingProcess.StartInfo.RedirectStandardOutput) + { + output = executingProcess.StandardOutput.ReadToEnd(); + } + + executingProcess.WaitForExit(); + + return output; + } + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess( + IntPtr processHandle, + int processInformationClass, + out ProcessBasicInformation processInformation, + int processInformationLength, + out int returnLength); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessBasicInformation + { + public IntPtr ExitStatus; + public IntPtr PebBaseAddress; + public IntPtr AffinityMask; + public IntPtr BasePriority; + public UIntPtr UniqueProcessId; + public IntPtr InheritedFromUniqueProcessId; + } + } +} diff --git a/GVFS/GVFS.Common/ProcessPool.cs b/GVFS/GVFS.Common/ProcessPool.cs new file mode 100644 index 00000000..b0cb91c8 --- /dev/null +++ b/GVFS/GVFS.Common/ProcessPool.cs @@ -0,0 +1,158 @@ +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; + +namespace GVFS.Common +{ + public class ProcessPool : IDisposable where TProcess : GitCatFileProcess + { + private const int TryAddTimeoutMilliseconds = 10; + private const int TryTakeTimeoutMilliseconds = 10; + private const int IdleSecondsBeforeCleanup = 10; + + // To help the idle processes to get cleaned up close to when the process passes the IdleSecondsBeforeCleanup + // we set the timer to pop at half the IdleSecondsBeforeCleanup value + private const int CleanupTimerPeriodMilliseconds = IdleSecondsBeforeCleanup * 1000 / 2; + + private readonly Func createProcess; + private readonly BlockingCollection pool; + private readonly ITracer tracer; + private readonly Timer cleanupTimer; + + public ProcessPool(ITracer tracer, Func createProcess, int size) + { + Debug.Assert(size > 0, "ProcessPool: size must be greater than 0"); + + this.tracer = tracer; + this.createProcess = createProcess; + this.pool = new BlockingCollection(size); + this.cleanupTimer = new Timer(x => this.CleanUpPool(shutdownAllProcesses: false), null, 0, CleanupTimerPeriodMilliseconds); + } + + public void Dispose() + { + this.cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); + this.cleanupTimer.Dispose(); + this.pool.CompleteAdding(); + this.CleanUpPool(shutdownAllProcesses: true); + } + + public void Invoke(Action function) + { + this.Invoke(process => { function(process); return false; }); + } + + public TResult Invoke(Func function) + { + TProcess process = null; + bool returnToPool = true; + + try + { + process = this.GetRunningProcessFromPool(); + TResult result = function(process); + + // Retry once if the process crashed while we were running it + if (!process.IsRunning()) + { + process = this.GetRunningProcessFromPool(); + result = function(process); + } + + return result; + } + catch + { + returnToPool = false; + throw; + } + finally + { + if (returnToPool) + { + this.ReturnToPool(process); + } + else + { + process.Kill(); + } + } + } + + private TProcess GetRunningProcessFromPool() + { + RunningProcess poolProcess; + if (this.pool.TryTake(out poolProcess, TryTakeTimeoutMilliseconds)) + { + return poolProcess.Process; + } + else + { + return this.createProcess(); + } + } + + private void ReturnToPool(TProcess process) + { + if (process != null && process.IsRunning()) + { + if (this.pool.IsAddingCompleted || + !this.pool.TryAdd(new RunningProcess(process), TryAddTimeoutMilliseconds)) + { + // No more adding to the pool or trying to add to the pool failed + process.Kill(); + } + } + } + + private void CleanUpPool(bool shutdownAllProcesses) + { + int numberInPool = this.pool.Count; + for (int i = 0; i < numberInPool; i++) + { + RunningProcess poolProcess; + if (this.pool.TryTake(out poolProcess)) + { + if (shutdownAllProcesses || this.pool.IsAddingCompleted) + { + poolProcess.Dispose(); + } + else if (poolProcess.Process.IsRunning() && + poolProcess.LastUsed.AddSeconds(IdleSecondsBeforeCleanup) > DateTime.Now) + { + this.pool.TryAdd(poolProcess, TryAddTimeoutMilliseconds); + } + else + { + // Process is either not running or has been idle too long + poolProcess.Dispose(); + } + } + } + } + + private class RunningProcess : IDisposable + { + public RunningProcess(TProcess process) + { + this.Process = process; + this.LastUsed = DateTime.Now; + } + + public TProcess Process { get; private set; } + public DateTime LastUsed { get; } + + public void Dispose() + { + if (this.Process != null) + { + this.Process.Dispose(); + this.Process = null; + } + } + } + } +} diff --git a/GVFS/GVFS.Common/ProcessResult.cs b/GVFS/GVFS.Common/ProcessResult.cs new file mode 100644 index 00000000..9f90cb28 --- /dev/null +++ b/GVFS/GVFS.Common/ProcessResult.cs @@ -0,0 +1,16 @@ +namespace GVFS.Common +{ + public class ProcessResult + { + public ProcessResult(string output, string errors, int exitCode) + { + this.Output = output; + this.Errors = errors; + this.ExitCode = exitCode; + } + + public string Output { get; } + public string Errors { get; } + public int ExitCode { get; } + } +} diff --git a/GVFS/GVFS.Common/Properties/AssemblyInfo.cs b/GVFS/GVFS.Common/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..2491de95 --- /dev/null +++ b/GVFS/GVFS.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.Common")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9ea6ff63-6bb0-4440-9bfb-0ae79a8f9ba9")] diff --git a/GVFS/GVFS.Common/ReliableBackgroundOperations.cs b/GVFS/GVFS.Common/ReliableBackgroundOperations.cs new file mode 100644 index 00000000..aafcd2f1 --- /dev/null +++ b/GVFS/GVFS.Common/ReliableBackgroundOperations.cs @@ -0,0 +1,367 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Isam.Esent.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace GVFS.Common +{ + public class ReliableBackgroundOperations : IDisposable where TBackgroundOperation : IBackgroundOperation + { + private const int ActionRetryDelayMS = 50; + private const int MaxCallbackAttemptsOnShutdown = 5; + private const int LogUpdateTaskThreshold = 25000; + private static readonly string EtwArea = "ProcessBackgroundOperations"; + + private readonly ReaderWriterLockSlim acquisitionLock; + private PersistentDictionary persistence; + + private ConcurrentQueue backgroundOperations; + private AutoResetEvent wakeUpThread; + private Task backgroundThread; + private bool isStopping; + + private GVFSContext context; + + private Func preCallback; + private Func callback; + private Func postCallback; + + public ReliableBackgroundOperations( + GVFSContext context, + Func preCallback, + Func callback, + Func postCallback, + string databaseName) + { + this.acquisitionLock = new ReaderWriterLockSlim(); + this.persistence = new PersistentDictionary( + Path.Combine(context.Enlistment.DotGVFSRoot, databaseName)); + + this.backgroundOperations = new ConcurrentQueue(); + this.wakeUpThread = new AutoResetEvent(true); + + this.context = context; + this.preCallback = preCallback; + this.callback = callback; + this.postCallback = postCallback; + } + + private enum AcquireGitLockResult + { + LockAcquired, + ShuttingDown + } + + public int Count + { + get { return this.backgroundOperations.Count; } + } + + public void Start() + { + this.EnqueueSavedOperations(); + this.backgroundThread = Task.Run((Action)this.ProcessBackgroundOperations); + if (this.backgroundOperations.Count > 0) + { + this.wakeUpThread.Set(); + } + } + + public void Enqueue(TBackgroundOperation backgroundOperation) + { + this.persistence[backgroundOperation.Id] = backgroundOperation; + this.persistence.Flush(); + + if (!this.isStopping) + { + this.backgroundOperations.Enqueue(backgroundOperation); + this.wakeUpThread.Set(); + } + } + + public void Shutdown() + { + this.isStopping = true; + this.wakeUpThread.Set(); + this.backgroundThread.Wait(); + } + + public void ObtainAcquisitionLock() + { + this.acquisitionLock.EnterReadLock(); + } + + public void ReleaseAcquisitionLock() + { + if (this.acquisitionLock.IsReadLockHeld) + { + this.acquisitionLock.ExitReadLock(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (this.persistence != null) + { + this.persistence.Dispose(); + this.persistence = null; + } + + if (this.backgroundThread != null) + { + this.backgroundThread.Dispose(); + this.backgroundThread = null; + } + } + + private void EnqueueSavedOperations() + { + foreach (Guid operationId in this.persistence.Keys) + { + // We are setting the Id here because there may be old operations that + // were persisted without the Id begin set in the background operation object + TBackgroundOperation backgroundOperation = this.persistence[operationId]; + backgroundOperation.Id = operationId; + this.backgroundOperations.Enqueue(backgroundOperation); + } + } + + private AcquireGitLockResult WaitToAcquireGitLock() + { + while (!this.context.Repository.GVFSLock.TryAcquireLock()) + { + if (this.isStopping) + { + return AcquireGitLockResult.ShuttingDown; + } + + Thread.Sleep(ActionRetryDelayMS); + } + + return AcquireGitLockResult.LockAcquired; + } + + private void ReleaseGitLockIfNecessary() + { + try + { + // Only release GVFS lock if the queue is empty. If it's not empty then another thread + // added something to the queue, allow it to continue processing. + while (this.backgroundOperations.IsEmpty) + { + // An external caller (eg. GVFLT callback) will hold reader status while adding something to the queue. + // If unable to enter writer status, wait and try again if the queue is still empty. + if (this.acquisitionLock.TryEnterWriteLock(millisecondsTimeout: 10)) + { + this.context.Repository.GVFSLock.ReleaseLock(); + break; + } + + Thread.Sleep(millisecondsTimeout: 10); + } + } + catch (Exception e) + { + this.LogErrorAndExit("gitLock.TryReleaseLock threw Exception, shutting down", e); + } + finally + { + if (this.acquisitionLock.IsWriteLockHeld) + { + this.acquisitionLock.ExitWriteLock(); + } + } + } + + private void ProcessBackgroundOperations() + { + TBackgroundOperation backgroundOperation; + + while (true) + { + AcquireGitLockResult acquireLockResult = AcquireGitLockResult.ShuttingDown; + + try + { + this.wakeUpThread.WaitOne(); + + if (this.isStopping) + { + return; + } + + acquireLockResult = this.WaitToAcquireGitLock(); + switch (acquireLockResult) + { + case AcquireGitLockResult.LockAcquired: + break; + case AcquireGitLockResult.ShuttingDown: + return; + default: + this.LogErrorAndExit("Invalid AcquireGitLockResult result"); + return; + } + + this.RunCallbackUntilSuccess(this.preCallback, "PreCallback"); + + int tasksProcessed = 0; + while (this.backgroundOperations.TryPeek(out backgroundOperation)) + { + if (tasksProcessed % LogUpdateTaskThreshold == 0 && + (tasksProcessed >= LogUpdateTaskThreshold || this.backgroundOperations.Count >= LogUpdateTaskThreshold)) + { + this.LogTaskProcessingStatus(tasksProcessed); + } + + if (this.isStopping) + { + // If we are stopping, then GVFlt has already been shut down + // Some of the queued background tasks may require GVFlt, and so it is unsafe to + // proceed. GVFS will resume any queued tasks next time it is mounted + this.persistence.Flush(); + return; + } + + CallbackResult callbackResult = this.callback(backgroundOperation); + switch (callbackResult) + { + case CallbackResult.Success: + this.backgroundOperations.TryDequeue(out backgroundOperation); + this.persistence.Remove(backgroundOperation.Id); + ++tasksProcessed; + break; + + case CallbackResult.RetryableError: + if (!this.isStopping) + { + Thread.Sleep(ActionRetryDelayMS); + } + + break; + + case CallbackResult.FatalError: + this.LogErrorAndExit("Callback encountered fatal error, exiting process"); + break; + + default: + this.LogErrorAndExit("Invalid background operation result"); + break; + } + } + + this.persistence.Flush(); + + if (tasksProcessed >= LogUpdateTaskThreshold) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("BackgroundOperations", EtwArea); + metadata.Add("TasksProcessed", tasksProcessed); + metadata.Add("Message", "Processing background tasks complete"); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "TaskProcessingStatus", metadata); + } + + if (this.isStopping) + { + return; + } + } + catch (Exception e) + { + this.LogErrorAndExit("ProcessBackgroundOperations caught unhandled exception, exiting process", e); + } + finally + { + if (acquireLockResult == AcquireGitLockResult.LockAcquired) + { + this.RunCallbackUntilSuccess(this.postCallback, "PostCallback"); + this.ReleaseGitLockIfNecessary(); + } + } + } + } + + private void RunCallbackUntilSuccess(Func callback, string errorHeader) + { + while (true) + { + CallbackResult callbackResult = callback(); + switch (callbackResult) + { + case CallbackResult.Success: + return; + + case CallbackResult.RetryableError: + if (this.isStopping) + { + return; + } + + Thread.Sleep(ActionRetryDelayMS); + break; + + case CallbackResult.FatalError: + this.LogErrorAndExit(errorHeader + " encountered fatal error, exiting process"); + return; + + default: + this.LogErrorAndExit(errorHeader + " result could not be found"); + return; + } + } + } + + private void LogWarning(string message) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add("Message", message); + this.context.Tracer.RelatedEvent(EventLevel.Warning, "Warning", metadata); + } + + private void LogError(string message, Exception e = null) + { + this.LogError(message, e, exit: false); + } + + private void LogErrorAndExit(string message, Exception e = null) + { + this.LogError(message, e, exit: true); + } + + private void LogError(string message, Exception e, bool exit) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + metadata.Add("ErrorMessage", message); + this.context.Tracer.RelatedError(metadata); + if (exit) + { + Environment.Exit(1); + } + } + + private void LogTaskProcessingStatus(int tasksProcessed) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("BackgroundOperations", EtwArea); + metadata.Add("TasksProcessed", tasksProcessed); + metadata.Add("TasksRemaining", this.backgroundOperations.Count); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "TaskProcessingStatus", metadata); + } + } +} diff --git a/GVFS/GVFS.Common/RetryWrapper.cs b/GVFS/GVFS.Common/RetryWrapper.cs new file mode 100644 index 00000000..e67c4006 --- /dev/null +++ b/GVFS/GVFS.Common/RetryWrapper.cs @@ -0,0 +1,257 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace GVFS.Common +{ + public class RetryWrapper + { + private const float MaxBackoffInSeconds = 300; // 5 minutes + private const float DefaultExponentialBackoffBase = 2; + + private readonly int maxRetries; + private readonly float exponentialBackoffBase; + + private Random rng = new Random(); + + public RetryWrapper(int maxRetries, float exponentialBackoffBase = DefaultExponentialBackoffBase) + { + this.maxRetries = maxRetries; + this.exponentialBackoffBase = exponentialBackoffBase; + } + + public event Action OnFailure = delegate { }; + + public static Action StandardErrorHandler(ITracer tracer, string actionName) + { + return eArgs => + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("AttemptNumber", eArgs.TryCount); + metadata.Add("Operation", actionName); + metadata.Add("WillRetry", eArgs.WillRetry); + metadata.Add("ErrorMessage", eArgs.Error != null ? eArgs.Error.Message : null); + tracer.RelatedError(metadata, Keywords.Network); + + // Emit with stack at a higher verbosity. + metadata["ErrorMessage"] = eArgs.Error != null ? eArgs.Error.ToString() : null; + tracer.RelatedEvent(EventLevel.Verbose, JsonEtwTracer.NetworkErrorEventName, metadata, Keywords.Network); + }; + } + + public async Task InvokeAsync(Func> toInvoke) + { + // Use 1-based counting. This makes reporting look a lot nicer and saves a lot of +1s + for (int tryCount = 1; tryCount <= this.maxRetries; ++tryCount) + { + try + { + CallbackResult result = await toInvoke(tryCount); + if (result.HasErrors) + { + if (!this.ShouldRetry(tryCount, null, result)) + { + return new InvocationResult(tryCount, result.Error, result.Result); + } + } + else + { + return new InvocationResult(tryCount, true, result.Result); + } + } + catch (Exception e) + { + Exception exceptionToReport = + e is AggregateException + ? ((AggregateException)e).Flatten().InnerException + : e; + + if (!this.IsHandlableException(exceptionToReport)) + { + throw; + } + + if (!this.ShouldRetry(tryCount, exceptionToReport, null)) + { + return new InvocationResult(tryCount, exceptionToReport); + } + } + + // Don't wait for the first retry, since it might just be transient. + // Don't wait after the last try. tryCount is 1-based, so last attempt is tryCount == maxRetries + if (tryCount > 1 && tryCount < this.maxRetries) + { + // Exponential backoff + double backOffSeconds = Math.Min(Math.Pow(this.exponentialBackoffBase, tryCount), MaxBackoffInSeconds); + + // Timeout usually happens when the server is overloaded. If we give all machines the same timeout they will all make + // another request at approximately the same time causing the problem to happen again and again. To avoid that we + // introduce a random timeout. To avoid scaling it too high or too low, it is +- 10% of the average backoff + backOffSeconds *= .9 + (this.rng.NextDouble() * .2); + await Task.Delay(TimeSpan.FromSeconds(backOffSeconds)); + } + } + + // This shouldn't be hit because ShouldRetry will cause a more useful message first. + return new InvocationResult(this.maxRetries, new Exception("Unexpected failure after retrying")); + } + + public InvocationResult Invoke(Func toInvoke) + { + // Use 1-based counting. This makes reporting look a lot nicer and saves a lot of +1s + for (int tryCount = 1; tryCount <= this.maxRetries; ++tryCount) + { + try + { + CallbackResult result = toInvoke(tryCount); + if (result.HasErrors) + { + if (!this.ShouldRetry(tryCount, null, result)) + { + return new InvocationResult(tryCount, result.Error, result.Result); + } + } + else + { + return new InvocationResult(tryCount, true, result.Result); + } + } + catch (Exception e) + { + Exception exceptionToReport = + e is AggregateException + ? ((AggregateException)e).Flatten().InnerException + : e; + + if (!this.IsHandlableException(exceptionToReport)) + { + throw; + } + + if (!this.ShouldRetry(tryCount, exceptionToReport, null)) + { + return new InvocationResult(tryCount, exceptionToReport); + } + } + + // Don't wait for the first retry, since it might just be transient. + // Don't wait after the last try. tryCount is 1-based, so last attempt is tryCount == maxRetries + if (tryCount > 1 && tryCount < this.maxRetries) + { + // Exponential backoff + double backOffSeconds = Math.Min(Math.Pow(this.exponentialBackoffBase, tryCount), MaxBackoffInSeconds); + + // Timeout usually happens when the server is overloaded. If we give all machines the same timeout they will all make + // another request at approximately the same time causing the problem to happen again and again. To avoid that we + // introduce a random timeout. To avoid scaling it too high or too low, it is +- 10% of the average backoff + backOffSeconds *= .9 + (this.rng.NextDouble() * .2); + Thread.Sleep(TimeSpan.FromSeconds(backOffSeconds)); + } + } + + // This shouldn't be hit because ShouldRetry will cause a more useful message first. + return new InvocationResult(this.maxRetries, new Exception("Unexpected failure after retrying")); + } + + private bool IsHandlableException(Exception e) + { + return + e is HttpException || + e is HttpRequestException || + e is IOException || + e is RetryableException; + } + + private bool ShouldRetry(int tryCount, Exception e, CallbackResult result) + { + bool willRetry = tryCount < this.maxRetries && + (result == null || result.ShouldRetry); + + if (e != null) + { + this.OnFailure(new ErrorEventArgs(e, tryCount, willRetry)); + } + else + { + this.OnFailure(new ErrorEventArgs(result.Error, tryCount, willRetry)); + } + + return willRetry; + } + + public class ErrorEventArgs + { + public ErrorEventArgs(Exception error, int tryCount, bool willRetry) + { + this.Error = error; + this.TryCount = tryCount; + this.WillRetry = willRetry; + } + + public bool WillRetry { get; } + + public int TryCount { get; } + + public Exception Error { get; } + } + + public class InvocationResult + { + public InvocationResult(int tryCount, bool succeeded, T result) + { + this.Attempts = tryCount; + this.Succeeded = true; + this.Result = result; + } + + public InvocationResult(int tryCount, Exception error) + { + this.Attempts = tryCount; + this.Succeeded = false; + this.Error = error; + } + + public InvocationResult(int tryCount, Exception error, T result) + : this(tryCount, error) + { + this.Result = result; + } + + public T Result { get; } + public int Attempts { get; } + public bool Succeeded { get; } + public Exception Error { get; } + } + + public class CallbackResult + { + public CallbackResult(T result) + { + this.Result = result; + } + + public CallbackResult(Exception error, bool shouldRetry) + { + this.HasErrors = true; + this.Error = error; + this.ShouldRetry = shouldRetry; + } + + public CallbackResult(Exception error, bool shouldRetry, T result) + : this(error, shouldRetry) + { + this.Result = result; + } + + public bool HasErrors { get; } + public Exception Error { get; } + public bool ShouldRetry { get; } + public T Result { get; } + } + } +} diff --git a/GVFS/GVFS.Common/RetryableException.cs b/GVFS/GVFS.Common/RetryableException.cs new file mode 100644 index 00000000..499b3ebc --- /dev/null +++ b/GVFS/GVFS.Common/RetryableException.cs @@ -0,0 +1,11 @@ +using System; + +namespace GVFS.Common +{ + public class RetryableException : Exception + { + public RetryableException(string message) : base(message) + { + } + } +} diff --git a/GVFS/GVFS.Common/ReturnCode.cs b/GVFS/GVFS.Common/ReturnCode.cs new file mode 100644 index 00000000..4bd862e8 --- /dev/null +++ b/GVFS/GVFS.Common/ReturnCode.cs @@ -0,0 +1,9 @@ +namespace GVFS.Common +{ + public enum ReturnCode + { + Success = 0, + RebootRequired = 2, + GenericError = 3 + } +} diff --git a/GVFS/GVFS.Common/SHA1Util.cs b/GVFS/GVFS.Common/SHA1Util.cs new file mode 100644 index 00000000..f46f5db0 --- /dev/null +++ b/GVFS/GVFS.Common/SHA1Util.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace GVFS.Common +{ + public static class SHA1Util + { + public static string SHA1HashStringForUTF8String(string s) + { + return HexStringFromBytes(SHA1ForUTF8String(s)); + } + + public static byte[] SHA1ForUTF8String(string s) + { + byte[] bytes = Encoding.UTF8.GetBytes(s); + + using (SHA1 sha1 = SHA1.Create()) + { + return sha1.ComputeHash(bytes); + } + } + + /// + /// Returns a string representation of a byte array from the first + /// bytes of the buffer. + /// + public static string HexStringFromBytes(byte[] buf, int numBytes = -1) + { + unsafe + { + numBytes = numBytes == -1 ? buf.Length : numBytes; + + fixed (byte* unsafeBuf = buf) + { + int charIndex = 0; + byte* currentByte = unsafeBuf; + char[] chars = new char[numBytes * 2]; + for (int i = 0; i < numBytes; i++) + { + char first = (char)(((*currentByte >> 4) & 0x0F) + 0x30); + char second = (char)((*currentByte & 0x0F) + 0x30); + chars[charIndex++] = first >= 0x3A ? (char)(first + 0x27) : first; + chars[charIndex++] = second >= 0x3A ? (char)(second + 0x27) : second; + + currentByte++; + } + + return new string(chars); + } + } + } + } +} diff --git a/GVFS/GVFS.Common/StreamUtil.cs b/GVFS/GVFS.Common/StreamUtil.cs new file mode 100644 index 00000000..6dd7a31b --- /dev/null +++ b/GVFS/GVFS.Common/StreamUtil.cs @@ -0,0 +1,62 @@ +using System.IO; + +namespace GVFS.Common +{ + public class StreamUtil + { + /// + /// .NET default buffer size uses as of 8/30/16 + /// + public const int DefaultCopyBufferSize = 81920; + + /// + /// Copies all bytes from the source stream to the destination stream. This is an exact copy + /// of Stream.CopyTo(), but can uses the supplied buffer instead of allocating a new one. + /// + /// + /// As of .NET 4.6, each call to Stream.CopyTo() allocates a new 80K byte[] buffer, which + /// consumes many more resources than reusing one we already have if the scenario allows it. + /// + /// Source stream to copy from + /// Destination stream to copy to + /// + /// Shared buffer to use. If null, we allocate one with the same size .NET would otherwise use. + /// + public static void CopyToWithBuffer(Stream source, Stream destination, byte[] buffer = null) + { + buffer = buffer ?? new byte[DefaultCopyBufferSize]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, read); + } + } + + /// + /// Call until either bytes are read or + /// the end of is reached. + /// + /// Buffer to read bytes into. + /// Offset in to start reading into. + /// Maximum number of bytes to read. + /// + /// Number of bytes read, may be less than if end was reached. + /// + public static int TryReadGreedy(Stream stream, byte[] buf, int offset, int count) + { + int totalRead = 0; + while (totalRead < count) + { + int read = stream.Read(buf, offset + totalRead, count - totalRead); + if (read == 0) + { + break; + } + + totalRead += read; + } + + return totalRead; + } + } +} diff --git a/GVFS/GVFS.Common/TaskExtensions.cs b/GVFS/GVFS.Common/TaskExtensions.cs new file mode 100644 index 00000000..808d525b --- /dev/null +++ b/GVFS/GVFS.Common/TaskExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; + +namespace GVFS.Common +{ + public static class TaskExtensions + { + public static void Timeout(this Task self, int timeoutMs) + where TTimeoutException : TimeoutException, new() + { + if (!self.Wait(timeoutMs)) + { + throw new TTimeoutException(); + } + } + + public static T Timeout(this Task self, int timeoutMs) + where TTimeoutException : TimeoutException, new() + { + if (!self.Wait(timeoutMs)) + { + throw new TTimeoutException(); + } + + return self.Result; + } + + public static async Task TimeoutAsync(this Task self, int timeoutMs) + where TTimeoutException : TimeoutException, new() + { + Task timeout = Task.Delay(timeoutMs); + Task completedFirst = await Task.WhenAny(timeout, self); + if (timeout == completedFirst) + { + throw new TTimeoutException(); + } + } + + public static async Task TimeoutAsync(this Task self, int timeoutMs) + where TTimeoutException : TimeoutException, new() + { + Task timeout = Task.Delay(timeoutMs); + Task completedFirst = await Task.WhenAny(timeout, self); + if (timeout == completedFirst) + { + throw new TTimeoutException(); + } + + return await self; + } + } +} diff --git a/GVFS/GVFS.Common/Tracing/ConsoleEventListener.cs b/GVFS/GVFS.Common/Tracing/ConsoleEventListener.cs new file mode 100644 index 00000000..18a5deea --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/ConsoleEventListener.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Diagnostics.Tracing; + +namespace GVFS.Common.Tracing +{ + public class ConsoleEventListener : InProcEventListener + { + public ConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) + : base(maxVerbosity, keywordFilter) + { + } + + public override void RecordMessage(string message) + { + Console.WriteLine(message); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Tracing/EventMetadata.cs b/GVFS/GVFS.Common/Tracing/EventMetadata.cs new file mode 100644 index 00000000..ac9016c1 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/EventMetadata.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace GVFS.Common.Tracing +{ + // This is a convenience class to make code around event metadata look nicer. + // It's more obvious to see EventMetadata than Dictionary everywhere. + public class EventMetadata : Dictionary + { + } +} diff --git a/GVFS/GVFS.Common/Tracing/ITracer.cs b/GVFS/GVFS.Common/Tracing/ITracer.cs new file mode 100644 index 00000000..4339e783 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/ITracer.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Diagnostics.Tracing; + +namespace GVFS.Common.Tracing +{ + public interface ITracer : IDisposable + { + ITracer StartActivity(string activityName, EventLevel level); + + ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata); + + void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata); + + void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keywords); + + void RelatedError(EventMetadata metadata); + + void RelatedError(EventMetadata metadata, Keywords keywords); + + void RelatedError(string message); + + void RelatedError(string format, params object[] args); + + void Stop(EventMetadata metadata); + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Tracing/InProcEventListener.cs b/GVFS/GVFS.Common/Tracing/InProcEventListener.cs new file mode 100644 index 00000000..f00f68d5 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/InProcEventListener.cs @@ -0,0 +1,55 @@ +using Microsoft.Diagnostics.Tracing; +using System; +using System.Text; + +namespace GVFS.Common.Tracing +{ + public abstract class InProcEventListener : EventListener + { + private EventLevel maxVerbosity; + private EventKeywords keywordFilter; + + public InProcEventListener(EventLevel maxVerbosity, Keywords keywordFilter) + { + this.maxVerbosity = maxVerbosity; + this.keywordFilter = (EventKeywords)keywordFilter; + } + + public abstract void RecordMessage(string message); + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (!this.IsEnabled(eventData.Level, eventData.Keywords)) + { + return; + } + + StringBuilder eventLine = new StringBuilder(); + eventLine.AppendFormat("[{0}] {1}", DateTime.Now, eventData.EventName); + if (eventData.Opcode != 0) + { + eventLine.AppendFormat(" ({0})", eventData.Opcode); + } + + if (eventData.Payload != null) + { + eventLine.Append(":"); + + for (int i = 0; i < eventData.PayloadNames.Count; i++) + { + // Space prefix avoids a string.Join. + eventLine.AppendFormat(" {0}: {1}", eventData.PayloadNames[i], eventData.Payload[i]); + } + } + + this.RecordMessage(eventLine.ToString()); + } + + protected bool IsEnabled(EventLevel level, EventKeywords keyword) + { + return this.keywordFilter != (EventKeywords)Keywords.None && + this.maxVerbosity >= level && + this.keywordFilter.HasFlag(keyword); + } + } +} diff --git a/GVFS/GVFS.Common/Tracing/JsonEtwTracer.cs b/GVFS/GVFS.Common/Tracing/JsonEtwTracer.cs new file mode 100644 index 00000000..e0781de1 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/JsonEtwTracer.cs @@ -0,0 +1,266 @@ +using Microsoft.Diagnostics.Tracing; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace GVFS.Common.Tracing +{ + public class JsonEtwTracer : ITracer + { + public const string NetworkErrorEventName = "NetworkError"; + + private string activityName; + private Guid parentActivityId; + private Guid activityId; + private bool stopped = false; + private Stopwatch duration = Stopwatch.StartNew(); + + private EventLevel startStopLevel; + private List listeners; + + public JsonEtwTracer(string providerName, string activityName) + : this( + new EventSource(providerName, EventSourceSettings.EtwSelfDescribingEventFormat), + Guid.Empty, + activityName, + EventLevel.Informational) + { + this.listeners = new List(); + } + + private JsonEtwTracer( + EventSource eventSource, + Guid parentActivityId, + string activityName, + EventLevel startStopLevel) + { + this.EvtSource = eventSource; + this.parentActivityId = parentActivityId; + this.activityName = activityName; + this.startStopLevel = startStopLevel; + + this.activityId = Guid.NewGuid(); + } + + public EventSource EvtSource { get; } + + public static string GetNameFromEnlistmentPath(string enlistmentRootPath) + { + return "Microsoft-GVFS_" + enlistmentRootPath.ToUpper().Replace(':', '_'); + } + + public void AddConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) + { + this.AddEventListener( + new ConsoleEventListener(maxVerbosity, keywordFilter), + maxVerbosity); + } + + public void AddLogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter) + { + this.AddEventListener( + new LogFileEventListener(logFilePath, maxVerbosity, keywordFilter), + maxVerbosity); + } + + public void Dispose() + { + this.Stop(null); + + // If we have no parent, then we are the root tracer and should dispose our eventsource. + if (this.parentActivityId == Guid.Empty) + { + if (this.listeners != null) + { + foreach (InProcEventListener listener in this.listeners) + { + listener.Dispose(); + } + + this.listeners = null; + } + + this.EvtSource.Dispose(); + } + } + + public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata) + { + this.RelatedEvent(level, eventName, metadata, Keywords.None); + } + + public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keyword) + { + EventSourceOptions options = this.CreateDefaultOptions(level, keyword); + this.WriteEvent(eventName, metadata, ref options); + } + + public virtual void RelatedError(EventMetadata metadata) + { + this.RelatedError(metadata, Keywords.None); + } + + public virtual void RelatedError(EventMetadata metadata, Keywords keywords) + { + this.RelatedEvent(EventLevel.Error, GetCategorizedErrorEventName(keywords), metadata, keywords); + } + + public virtual void RelatedError(string message) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ErrorMessage", message); + this.RelatedError(metadata); + } + + public virtual void RelatedError(string format, params object[] args) + { + this.RelatedError(string.Format(format, args)); + } + + public void Stop(EventMetadata metadata) + { + if (this.stopped) + { + return; + } + + this.duration.Stop(); + this.stopped = true; + + EventSourceOptions options = this.CreateDefaultOptions(this.startStopLevel, Keywords.None); + options.Opcode = EventOpcode.Stop; + + metadata = metadata ?? new EventMetadata(); + metadata.Add("DurationMs", this.duration.ElapsedMilliseconds); + + this.WriteEvent(this.activityName, metadata, ref options); + } + + public ITracer StartActivity(string childActivityName, EventLevel startStopLevel) + { + return this.StartActivity(childActivityName, startStopLevel, null); + } + + public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, EventMetadata startMetadata) + { + JsonEtwTracer subTracer = new JsonEtwTracer(this.EvtSource, this.activityId, childActivityName, startStopLevel); + subTracer.WriteStartEvent(startMetadata); + + return subTracer; + } + + public void WriteStartEvent( + string enlistmentRoot, + string repoUrl, + string cacheServerUrl, + EventMetadata additionalMetadata = null) + { + EventMetadata metadata = new EventMetadata(); + + metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); + + if (enlistmentRoot != null) + { + metadata.Add("EnlistmentRoot", enlistmentRoot); + } + + if (repoUrl != null) + { + metadata.Add("Remote", Uri.EscapeUriString(repoUrl)); + } + + if (cacheServerUrl != null) + { + // Changing this key to CacheServerUrl will mess with our telemetry, so it stays for historical reasons + metadata.Add("ObjectsEndpoint", Uri.EscapeUriString(cacheServerUrl)); + } + + if (additionalMetadata != null) + { + foreach (string key in additionalMetadata.Keys) + { + metadata.Add(key, additionalMetadata[key]); + } + } + + this.WriteStartEvent(metadata); + } + + public void WriteStartEvent(EventMetadata metadata) + { + EventSourceOptions options = this.CreateDefaultOptions(this.startStopLevel, Keywords.None); + options.Opcode = EventOpcode.Start; + + this.WriteEvent(this.activityName, metadata, ref options); + } + + private static string GetCategorizedErrorEventName(Keywords keywords) + { + switch (keywords) + { + case Keywords.Network: return NetworkErrorEventName; + default: return "Error"; + } + } + + private void WriteEvent(string eventName, EventMetadata metadata, ref EventSourceOptions options) + { + if (metadata != null) + { + JsonPayload payload = new JsonPayload(metadata); + EventSource.SetCurrentThreadActivityId(this.activityId); + this.EvtSource.Write(eventName, ref options, ref this.activityId, ref this.parentActivityId, ref payload); + } + else + { + EmptyStruct payload = new EmptyStruct(); + EventSource.SetCurrentThreadActivityId(this.activityId); + this.EvtSource.Write(eventName, ref options, ref this.activityId, ref this.parentActivityId, ref payload); + } + } + + private EventSourceOptions CreateDefaultOptions(EventLevel level, Keywords keywords) + { + EventSourceOptions options = new EventSourceOptions(); + options.Keywords = (EventKeywords)keywords; + options.Level = level; + return options; + } + + private void AddEventListener(InProcEventListener listener, EventLevel maxVerbosity) + { + if (this.listeners == null) + { + throw new InvalidOperationException("You can only register a listener on the root tracer object"); + } + + if (maxVerbosity >= EventLevel.Verbose) + { + listener.RecordMessage(string.Format("ETW Provider name: {0} ({1})", this.EvtSource.Name, this.EvtSource.Guid)); + listener.RecordMessage("Activity Id: " + this.activityId); + } + + listener.EnableEvents(this.EvtSource, maxVerbosity); + this.listeners.Add(listener); + } + + // Needed to pass relatedId without metadata + [EventData] + private struct EmptyStruct + { + } + + [EventData] + private struct JsonPayload + { + public JsonPayload(object serializableObject) + { + this.Json = JsonConvert.SerializeObject(serializableObject); + } + + [EventField] + public string Json { get; } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Tracing/Keywords.cs b/GVFS/GVFS.Common/Tracing/Keywords.cs new file mode 100644 index 00000000..d26507c9 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/Keywords.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Common.Tracing +{ + public enum Keywords : long + { + None = 1 << 0, + Network = 1 << 1, + Any = ~0, + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/Tracing/LogFileEventListener.cs b/GVFS/GVFS.Common/Tracing/LogFileEventListener.cs new file mode 100644 index 00000000..c392b562 --- /dev/null +++ b/GVFS/GVFS.Common/Tracing/LogFileEventListener.cs @@ -0,0 +1,41 @@ +using Microsoft.Diagnostics.Tracing; +using System.IO; + +namespace GVFS.Common.Tracing +{ + public class LogFileEventListener : InProcEventListener + { + private FileStream logFile; + private TextWriter writer; + + public LogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter) + : base(maxVerbosity, keywordFilter) + { + this.logFile = File.Open(logFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read); + this.writer = StreamWriter.Synchronized(new StreamWriter(this.logFile)); + } + + public override void RecordMessage(string message) + { + this.writer.WriteLine(message); + this.writer.Flush(); + } + + public override void Dispose() + { + if (this.writer != null) + { + this.writer.Dispose(); + this.writer = null; + } + + if (this.logFile != null) + { + this.logFile.Dispose(); + this.logFile = null; + } + + base.Dispose(); + } + } +} diff --git a/GVFS/GVFS.Common/WindowsProcessJob.cs b/GVFS/GVFS.Common/WindowsProcessJob.cs new file mode 100644 index 00000000..b42d0a8e --- /dev/null +++ b/GVFS/GVFS.Common/WindowsProcessJob.cs @@ -0,0 +1,145 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace GVFS.Common +{ + public class WindowsProcessJob : IDisposable + { + private SafeJobHandle jobHandle; + private bool disposed; + + public WindowsProcessJob(Process process) + { + IntPtr newHandle = Native.CreateJobObject(IntPtr.Zero, null); + if (newHandle == IntPtr.Zero) + { + throw new InvalidOperationException("Unable to create a job. Error: " + Marshal.GetLastWin32Error()); + } + + this.jobHandle = new SafeJobHandle(newHandle); + + Native.JOBOBJECT_BASIC_LIMIT_INFORMATION info = new Native.JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + }; + + Native.JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new Native.JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + int length = Marshal.SizeOf(typeof(Native.JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + if (!Native.SetInformationJobObject(this.jobHandle, Native.JobObjectInfoType.ExtendedLimitInformation, ref extendedInfo, (uint)length)) + { + throw new InvalidOperationException("Unable to configure the job. Error: " + Marshal.GetLastWin32Error()); + } + + if (!Native.AssignProcessToJobObject(this.jobHandle, process.Handle)) + { + throw new InvalidOperationException("Unable to add process to the job. Error: " + Marshal.GetLastWin32Error()); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.disposed) + { + this.jobHandle.Dispose(); + this.jobHandle = null; + + this.disposed = true; + } + } + + private static class Native + { + public enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr CreateJobObject(IntPtr attributes, string name); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetInformationJobObject(SafeJobHandle jobHandle, JobObjectInfoType infoType, [In] ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobObjectInfo, uint jobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AssignProcessToJobObject(SafeJobHandle jobHandle, IntPtr processHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr handle); + + [StructLayout(LayoutKind.Sequential)] + public struct IO_COUNTERS + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public uint Length; + public IntPtr SecurityDescriptor; + public int InheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + } + + private sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeJobHandle(IntPtr handle) : base(true) + { + this.SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return Native.CloseHandle(this.handle); + } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/packages.config b/GVFS/GVFS.Common/packages.config new file mode 100644 index 00000000..a4c3a96d --- /dev/null +++ b/GVFS/GVFS.Common/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs b/GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs new file mode 100644 index 00000000..d64c0151 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs @@ -0,0 +1,7 @@ +namespace GVFS.FunctionalTests.Category +{ + public static class CategoryConstants + { + public const string FastFetch = "FastFetch"; + } +} diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs new file mode 100644 index 00000000..057b3786 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs @@ -0,0 +1,224 @@ +using GVFS.FunctionalTests.Properties; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.IO; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public class BashRunner : ShellRunner + { + private static string[] fileNotFoundMessages = new string[] + { + "cannot stat", + "cannot remove", + "No such file or directory" + }; + + private static string[] invalidMovePathMessages = new string[] + { + "cannot move", + "No such file or directory" + }; + + private static string[] moveDirectoryNotSupportedMessage = new string[] + { + "Function not implemented" + }; + + private static string[] permissionDeniedMessage = new string[] + { + "Permission denied" + }; + + private readonly string pathToBash; + + public BashRunner() + { + if (File.Exists(Settings.Default.PathToBash)) + { + this.pathToBash = Settings.Default.PathToBash; + } + else + { + this.pathToBash = "bash.exe"; + } + } + + protected override string FileName + { + get + { + return this.pathToBash; + } + } + + public override bool FileExists(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + string command = string.Format("-c \"[ -f {0} ] && echo {1} || echo {2}\"", bashPath, ShellRunner.SuccessOutput, ShellRunner.FailureOutput); + + string output = this.RunProcess(command).Trim(); + + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); + string targetBashPath = this.ConvertWinPathToBashPath(targetPath); + + return this.RunProcess(string.Format("-c \"mv {0} {1}\"", sourceBashPath, targetBashPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // BashRunner does nothing special when a failure is expected, so just confirm source file is still present + this.MoveFile(sourcePath, targetPath); + this.FileExists(sourcePath).ShouldEqual(true); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); + string targetBashPath = this.ConvertWinPathToBashPath(targetPath); + + return this.RunProcess(string.Format("-c \"mv -f {0} {1}\"", sourceBashPath, targetBashPath)); + } + + public override string DeleteFile(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + return this.RunProcess(string.Format("-c \"rm {0}\"", bashPath)); + } + + public override string ReadAllText(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + string output = this.RunProcess(string.Format("-c \"cat {0}\"", bashPath)); + + // Bash sometimes sticks a trailing "\n" at the end of the output that we need to remove + // Until we can figure out why we cannot use this runner with files that have trailing newlines + if (output.Length > 0 && + output.Substring(output.Length - 1).Equals("\n", StringComparison.InvariantCultureIgnoreCase) && + !(output.Length > 1 && + output.Substring(output.Length - 2).Equals("\r\n", StringComparison.InvariantCultureIgnoreCase))) + { + output = output.Remove(output.Length - 1, 1); + } + + return output; + } + + public override void AppendAllText(string path, string contents) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"echo -n \\\"{0}\\\" >> {1}\"", contents, bashPath)); + } + + public override void CreateEmptyFile(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"touch {0}\"", bashPath)); + } + + public override void WriteAllText(string path, string contents) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"echo \\\"{0}\\\" > {1}\"", contents, bashPath)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // BashRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + string output = this.RunProcess(string.Format("-c \"[ -d {0} ] && echo {1} || echo {2}\"", bashPath, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); + + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(invalidMovePathMessages); + } + + public override void CreateDirectory(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"mkdir {0}\"", bashPath)); + } + + public override string DeleteDirectory(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + return this.RunProcess(string.Format("-c \"rm -rf {0}\"", bashPath)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + this.DeleteFile(path).ShouldContain(permissionDeniedMessage); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + // Delete directory silently succeeds when deleting a non-existent path + this.DeleteDirectory(path); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + Assert.Fail("Unlike the other runners, bash.exe does not check folder handle before recusively deleting"); + } + + private string ConvertWinPathToBashPath(string winPath) + { + string bashPath = string.Concat("/", winPath); + bashPath = bashPath.Replace(":\\", "/"); + bashPath = bashPath.Replace('\\', '/'); + return bashPath; + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs new file mode 100644 index 00000000..02ce550d --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs @@ -0,0 +1,185 @@ +using GVFS.Tests.Should; +using System; +using System.IO; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public class CmdRunner : ShellRunner + { + private const string ProcessName = "CMD.exe"; + + private static string[] missingFileErrorMessages = new string[] + { + "The system cannot find the file specified.", + "The system cannot find the path specified.", + "Could Not Find" + }; + + private static string[] moveDirectoryFailureMessage = new string[] + { + "0 dir(s) moved" + }; + + private static string[] fileUsedByAnotherProcessMessage = new string[] + { + "The process cannot access the file because it is being used by another process" + }; + + protected override string FileName + { + get + { + return ProcessName; + } + } + + public override bool FileExists(string path) + { + if (this.DirectoryExists(path)) + { + return false; + } + + string output = this.RunProcess(string.Format("/C if exist {0} (echo {1}) else (echo {2})", path, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); + + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("/C move {0} {1}", sourcePath, targetPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // CmdRunner does nothing special when a failure is expected + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("/C move /Y {0} {1}", sourcePath, targetPath)); + } + + public override string DeleteFile(string path) + { + return this.RunProcess(string.Format("/C del {0}", path)); + } + + public override string ReadAllText(string path) + { + return this.RunProcess(string.Format("/C type {0}", path)); + } + + public override void CreateEmptyFile(string path) + { + this.RunProcess(string.Format("/C type NUL > {0}", path)); + } + + public override void AppendAllText(string path, string contents) + { + // Use echo|set /p with "" to avoid adding any trailing whitespace or newline + // to the contents + this.RunProcess(string.Format("/C echo|set /p =\"{0}\" >> {1}", contents, path)); + } + + public override void WriteAllText(string path, string contents) + { + // Use echo|set /p with "" to avoid adding any trailing whitespace or newline + // to the contents + this.RunProcess(string.Format("/C echo|set /p =\"{0}\" > {1}", contents, path)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // CmdRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + string parentDirectory = Path.GetDirectoryName(path); + string targetName = Path.GetFileName(path); + + string output = this.RunProcess(string.Format("/C dir /A:d /B {0}", parentDirectory)); + string[] directories = output.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string directory in directories) + { + if (directory.Equals(targetName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override void CreateDirectory(string path) + { + this.RunProcess(string.Format("/C mkdir {0}", path)); + } + + public override string DeleteDirectory(string path) + { + return this.RunProcess(string.Format("/C rmdir /q /s {0}", path)); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); + } + + public string RunCommand(string command) + { + return this.RunProcess(string.Format("/C {0}", command)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + // CMD does not report any error messages when access is denied, so just confirm the file still exists + this.DeleteFile(path); + this.FileExists(path).ShouldEqual(true); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs new file mode 100644 index 00000000..534911aa --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs @@ -0,0 +1,113 @@ +using System; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public abstract class FileSystemRunner + { + /// + /// String that identifies which list to use when running tests + /// + public const string TestRunners = "Runners"; + + private static FileSystemRunner defaultRunner = new CmdRunner(); + + /// + /// Runners to use when the debugger is not attached + /// + private static object[] allRunners = + { + new object[] { new SystemIORunner() }, + new object[] { new CmdRunner() }, + new object[] { new PowerShellRunner() }, + new object[] { new BashRunner() }, + }; + + /// + /// Runners to use when the debugger is attached + /// + private static object[] debugRunners = + { + new object[] { defaultRunner } + }; + + public static bool UseAllRunners { get; set; } + + public static object[] Runners + { + get { return UseAllRunners ? allRunners : debugRunners; } + } + + /// + /// Default runner to use (for tests that do not need to be run with multiple runners) + /// + public static FileSystemRunner DefaultRunner + { + get { return defaultRunner; } + } + + // File methods + public abstract bool FileExists(string path); + public abstract string MoveFile(string sourcePath, string targetPath); + + /// + /// Attempt to move the specified file to the specifed target path. By calling this method the caller is + /// indicating that they expect the move to fail. However, the caller is responsible for verifying that + /// the move failed. + /// + /// Path to existing file + /// Path to target file (target of the move) + public abstract void MoveFileShouldFail(string sourcePath, string targetPath); + public abstract void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath); + public abstract string ReplaceFile(string sourcePath, string targetPath); + public abstract void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath); + public abstract string DeleteFile(string path); + public abstract void DeleteFile_FileShouldNotBeFound(string path); + public abstract void DeleteFile_AccessShouldBeDenied(string path); + public abstract string ReadAllText(string path); + public abstract void ReadAllText_FileShouldNotBeFound(string path); + + public abstract void CreateEmptyFile(string path); + + /// + /// Write the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that + /// the write succeeded. + /// + /// Path to file + /// File contents + public abstract void WriteAllText(string path, string contents); + + /// + /// Append the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that + /// the write succeeded. + /// + /// Path to file + /// File contents + public abstract void AppendAllText(string path, string contents); + + /// + /// Attempt to write the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to fail. However, the caller is responsible for verifying that + /// the write failed. + /// + /// Expected type of exception to be thrown + /// Path to file + /// File contents + public abstract void WriteAllTextShouldFail(string path, string contents) where ExceptionType : Exception; + + // Directory methods + public abstract bool DirectoryExists(string path); + public abstract void MoveDirectory(string sourcePath, string targetPath); + public abstract void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath); + public abstract void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath); + public abstract void CreateDirectory(string path); + + /// + /// A recursive delete of a directory + /// + public abstract string DeleteDirectory(string path); + public abstract void DeleteDirectory_DirectoryShouldNotBeFound(string path); + public abstract void DeleteDirectory_ShouldBeBlockedByProcess(string path); + } +} diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/PowerShellRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/PowerShellRunner.cs new file mode 100644 index 00000000..1995c2ed --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/PowerShellRunner.cs @@ -0,0 +1,192 @@ +using GVFS.Tests.Should; +using System.IO; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public class PowerShellRunner : ShellRunner + { + private const string ProcessName = "powershell.exe"; + + private static string[] missingFileErrorMessages = new string[] + { + "Cannot find path" + }; + + private static string[] invalidPathErrorMessages = new string[] + { + "Could not find a part of the path" + }; + + private static string[] moveDirectoryNotSupportedMessage = new string[] + { + "The request is not supported." + }; + + private static string[] fileUsedByAnotherProcessMessage = new string[] + { + "The process cannot access the file because it is being used by another process" + }; + + private static string[] permissionDeniedMessage = new string[] + { + "PermissionDenied" + }; + + protected override string FileName + { + get + { + return ProcessName; + } + } + + public override bool FileExists(string path) + { + string parentDirectory = Path.GetDirectoryName(path); + string targetName = Path.GetFileName(path); + + // Use -force so that hidden items are returned as well + string command = string.Format("-Command \"&{{ Get-ChildItem -force {0} | where {{$_.Attributes -NotLike '*Directory*'}} | where {{$_.Name -eq '{1}' }} }}\"", parentDirectory, targetName); + string output = this.RunProcess(command).Trim(); + + if (output.Length == 0 || output.Contains("PathNotFound") || output.Contains("ItemNotFound")) + { + return false; + } + + return true; + } + + public override string MoveFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} }}\"", sourcePath, targetPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // PowerShellRunner does nothing special when a failure is expected + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force }}\"", sourcePath, targetPath)); + } + + public override string DeleteFile(string path) + { + return this.RunProcess(string.Format("-Command \"& {{ Remove-Item {0} }}\"", path)); + } + + public override string ReadAllText(string path) + { + string output = this.RunProcess(string.Format("-Command \"& {{ Get-Content -Raw {0} }}\"", path), errorMsgDelimeter: "\r\n"); + + // Get-Content insists on sticking a trailing "\r\n" at the end of the output that we need to remove + output.Length.ShouldBeAtLeast(2); + output.Substring(output.Length - 2).ShouldEqual("\r\n"); + output = output.Remove(output.Length - 2, 2); + + return output; + } + + public override void AppendAllText(string path, string contents) + { + this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -Append -NoNewline}}\"", path, contents)); + } + + public override void CreateEmptyFile(string path) + { + this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType file {0}}}\"", path)); + } + + public override void WriteAllText(string path, string contents) + { + this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -NoNewline}}\"", path, contents)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // PowerShellRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + string command = string.Format("-Command \"&{{ Test-Path {0} -PathType Container }}\"", path); + string output = this.RunProcess(command).Trim(); + + if (output.Contains("True")) + { + return true; + } + + return false; + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(invalidPathErrorMessages); + } + + public override void CreateDirectory(string path) + { + this.RunProcess(string.Format("-Command \"&{{ New-Item {0} -type directory}}\"", path)); + } + + public override string DeleteDirectory(string path) + { + return this.RunProcess(string.Format("-Command \"&{{ Remove-Item -Force -Recurse {0} }}\"", path)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + this.DeleteFile(path).ShouldContain(permissionDeniedMessage); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); + } + + protected override string RunProcess(string command, string errorMsgDelimeter = "") + { + return base.RunProcess("-NoProfile " + command, errorMsgDelimeter); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/ShellRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/ShellRunner.cs new file mode 100644 index 00000000..d6d24272 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/ShellRunner.cs @@ -0,0 +1,27 @@ +using GVFS.FunctionalTests.Tools; +using System.Diagnostics; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public abstract class ShellRunner : FileSystemRunner + { + protected const string SuccessOutput = "True"; + protected const string FailureOutput = "False"; + + protected abstract string FileName { get; } + + protected virtual string RunProcess(string arguments, string errorMsgDelimeter = "") + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.UseShellExecute = false; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.CreateNoWindow = true; + startInfo.FileName = this.FileName; + startInfo.Arguments = arguments; + + ProcessResult result = ProcessHelper.Run(startInfo, errorMsgDelimeter: errorMsgDelimeter); + return !string.IsNullOrEmpty(result.Output) ? result.Output : result.Errors; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/SystemIORunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/SystemIORunner.cs new file mode 100644 index 00000000..400ba6c1 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/SystemIORunner.cs @@ -0,0 +1,197 @@ +using NUnit.Framework; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace GVFS.FunctionalTests.FileSystemRunners +{ + public class SystemIORunner : FileSystemRunner + { + public static void RecursiveDelete(string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + + foreach (FileInfo file in directory.GetFiles()) + { + file.Attributes = FileAttributes.Normal; + + RetryOnException(() => file.Delete()); + } + + foreach (DirectoryInfo subDirectory in directory.GetDirectories()) + { + SystemIORunner.RecursiveDelete(subDirectory.FullName); + } + + RetryOnException(() => directory.Delete()); + } + + public override bool FileExists(string path) + { + return File.Exists(path); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + File.Move(sourcePath, targetPath); + return string.Empty; + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveFileShouldFail should not be run with the debugger attached"); + } + + this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + File.Replace(sourcePath, targetPath, null); + return string.Empty; + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ShouldFail(() => { this.ReplaceFile(sourcePath, targetPath); }); + } + + public override string DeleteFile(string path) + { + File.Delete(path); + return string.Empty; + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + // Delete file silently succeeds when file is non-existent + this.DeleteFile(path); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + this.ShouldFail(() => { this.DeleteFile(path); }); + } + + public override string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public override void CreateEmptyFile(string path) + { + using (FileStream fs = File.Create(path)) + { + } + } + + public override void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); + } + + public override void AppendAllText(string path, string contents) + { + File.AppendAllText(path, contents); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("WriteAllTextShouldFail should not be run with the debugger attached"); + } + + this.ShouldFail(() => { this.WriteAllText(path, contents); }); + } + + public override bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + Directory.Move(sourcePath, targetPath); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveDirectory_RequestShouldNotBeSupported should not be run with the debugger attached"); + } + + Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveDirectory_TargetShouldBeInvalid should not be run with the debugger attached"); + } + + Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); + } + + public override void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } + + public override string DeleteDirectory(string path) + { + SystemIORunner.RecursiveDelete(path); + return string.Empty; + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.ShouldFail(() => { this.DeleteDirectory(path); }); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + Assert.Fail("DeleteDirectory_ShouldBeBlockedByProcess not supported by SystemIORunner"); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ShouldFail(() => { this.ReadAllText(path); }); + } + + private static void RetryOnException(Action action) + { + for (int i = 0; i < 10; i++) + { + try + { + action(); + break; + } + catch (IOException) + { + Thread.Sleep(500); + } + catch (UnauthorizedAccessException) + { + Thread.Sleep(500); + } + } + } + + private void ShouldFail(Action action) where ExceptionType : Exception + { + Assert.Catch(() => action()); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj b/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj new file mode 100644 index 00000000..31bb0f63 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj @@ -0,0 +1,201 @@ + + + + Debug + AnyCPU + {0F0A008E-AB12-40EC-A671-37A541B08C7F} + Exe + Properties + GVFS.FunctionalTests + GVFS.FunctionalTests + v4.5.2 + 512 + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + ..\..\..\BuildOutput\GVFS.FunctionalTests\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.FunctionalTests\obj\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + ..\..\..\BuildOutput\GVFS.FunctionalTests\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.FunctionalTests\obj\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + GVFS.FunctionalTests.Program + + + Always + + + + False + ..\..\..\packages\Microsoft.Database.Collections.Generic.1.9.4\lib\net40\Esent.Collections.dll + True + + + False + ..\..\..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll + True + + + False + ..\..\..\packages\Microsoft.Database.Isam.1.9.4\lib\net40\Esent.Isam.dll + True + + + False + ..\..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll + True + + + False + ..\..\..\packages\NUnitLite.3.5.0\lib\net45\nunitlite.dll + True + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + Designer + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC} + GVFS.Tests + + + + + + + False + + + False + + + False + + + False + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + xcopy /Y $(SolutionDir)..\BuildOutput\GVFS\bin\$(Platform)\$(Configuration)\* $(TargetDir) +xcopy /Y $(SolutionDir)..\BuildOutput\GVFS.Mount\bin\$(Platform)\$(Configuration)\* $(TargetDir) +xcopy /Y $(SolutionDir)..\BuildOutput\FastFetch\bin\$(Platform)\$(Configuration)\* $(TargetDir) +xcopy /Y $(SolutionDir)..\BuildOutput\GVFS.Hooks\bin\$(Platform)\$(Configuration)\* $(TargetDir) + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Program.cs b/GVFS/GVFS.FunctionalTests/Program.cs new file mode 100644 index 00000000..93c5ec2c --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Program.cs @@ -0,0 +1,21 @@ +using GVFS.Tests; +using System; + +namespace GVFS.FunctionalTests +{ + public class Program + { + public static void Main(string[] args) + { + NUnitRunner runner = new NUnitRunner(args); + + if (runner.HasCustomArg("--full-suite")) + { + Console.WriteLine("Running the full suite of tests"); + FileSystemRunners.FileSystemRunner.UseAllRunners = true; + } + + Environment.ExitCode = runner.RunTests(Properties.Settings.Default.TestRepeatCount); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Properties/AssemblyInfo.cs b/GVFS/GVFS.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..53e95019 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.FunctionalTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.FunctionalTests")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0f0a008e-ab12-40ec-a671-37a541b08c7f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GVFS/GVFS.FunctionalTests/Properties/Settings.Designer.cs b/GVFS/GVFS.FunctionalTests/Properties/Settings.Designer.cs new file mode 100644 index 00000000..b2018b85 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Properties/Settings.Designer.cs @@ -0,0 +1,125 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GVFS.FunctionalTests.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("GVFS.exe")] + public string PathToGVFS { + get { + return ((string)(this["PathToGVFS"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Repos\\GVFSFunctionalTests\\enlistment")] + public string EnlistmentRoot { + get { + return ((string)(this["EnlistmentRoot"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("TODO")] + public string RepoToClone { + get { + return ((string)(this["RepoToClone"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("1")] + public int TestRepeatCount { + get { + return ((int)(this["TestRepeatCount"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string PathToNuget { + get { + return ((string)(this["PathToNuget"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Program Files\\Git\\bin\\bash.exe")] + public string PathToBash { + get { + return ((string)(this["PathToBash"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("FunctionalTests/20170130")] + public string Commitish { + get { + return ((string)(this["Commitish"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Repos\\GVFSFunctionalTests\\ControlRepo")] + public string ControlGitRepoRoot { + get { + return ((string)(this["ControlGitRepoRoot"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Repos\\GVFSFunctionalTests\\FastFetch\\Test")] + public string FastFetchRoot { + get { + return ((string)(this["FastFetchRoot"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Repos\\GVFSFunctionalTests\\FastFetch\\Control")] + public string FastFetchControl { + get { + return ((string)(this["FastFetchControl"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("C:\\Program Files\\Git\\cmd\\git.exe")] + public string PathToGit { + get { + return ((string)(this["PathToGit"])); + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Properties/Settings.settings b/GVFS/GVFS.FunctionalTests/Properties/Settings.settings new file mode 100644 index 00000000..2fe057cb --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Properties/Settings.settings @@ -0,0 +1,39 @@ + + + + + + GVFS.exe + + + C:\Repos\GVFSFunctionalTests\enlistment + + + TODO + + + 1 + + + + + + C:\Program Files\Git\bin\bash.exe + + + FunctionalTests/20170130 + + + C:\Repos\GVFSFunctionalTests\ControlRepo + + + C:\Repos\GVFSFunctionalTests\FastFetch\Test + + + C:\Repos\GVFSFunctionalTests\FastFetch\Control + + + C:\Program Files\Git\cmd\git.exe + + + \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Should/FileSystemShouldExtensions.cs b/GVFS/GVFS.FunctionalTests/Should/FileSystemShouldExtensions.cs new file mode 100644 index 00000000..3ceafeec --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Should/FileSystemShouldExtensions.cs @@ -0,0 +1,243 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.FunctionalTests.Should +{ + public static class FileSystemShouldExtensions + { + public static FileAdapter ShouldBeAFile(this string path, FileSystemRunner runner) + { + return new FileAdapter(path, runner); + } + + public static FileAdapter ShouldBeAFile(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) + { + return new FileAdapter(fileSystemInfo.FullName, runner); + } + + public static DirectoryAdapter ShouldBeADirectory(this string path, FileSystemRunner runner) + { + return new DirectoryAdapter(path, runner); + } + + public static DirectoryAdapter ShouldBeADirectory(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) + { + return new DirectoryAdapter(fileSystemInfo.FullName, runner); + } + + public static string ShouldNotExistOnDisk(this string path, FileSystemRunner runner) + { + runner.FileExists(path).ShouldEqual(false); + runner.DirectoryExists(path).ShouldEqual(false); + return path; + } + + public class FileAdapter + { + private const int MaxWaitMS = 2000; + private const int ThreadSleepMS = 100; + + private FileSystemRunner runner; + + public FileAdapter(string path, FileSystemRunner runner) + { + this.runner = runner; + this.runner.FileExists(path).ShouldEqual(true); + this.Path = path; + } + + public string Path + { + get; private set; + } + + public string WithContents() + { + return this.runner.ReadAllText(this.Path); + } + + public FileAdapter WithContents(string expectedContents) + { + this.runner.ReadAllText(this.Path).ShouldEqual(expectedContents); + return this; + } + + public FileAdapter WithCaseMatchingName(string expectedName) + { + FileInfo fileInfo = new FileInfo(this.Path); + string parentPath = System.IO.Path.GetDirectoryName(this.Path); + DirectoryInfo parentInfo = new DirectoryInfo(parentPath); + Assert.AreEqual(expectedName.Equals(parentInfo.GetFileSystemInfos(fileInfo.Name)[0].Name, StringComparison.Ordinal), true); + return this; + } + + public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) + { + FileInfo info = new FileInfo(this.Path); + info.CreationTime.ShouldEqual(creation); + info.LastAccessTime.ShouldEqual(lastAccess); + info.LastWriteTime.ShouldEqual(lastWrite); + + return info; + } + + public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes) + { + FileInfo info = this.WithInfo(creation, lastWrite, lastAccess); + info.Attributes.ShouldEqual(attributes); + return info; + } + + public FileInfo WithAttribute(FileAttributes attribute) + { + FileInfo info = new FileInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(true); + return info; + } + + public FileInfo WithoutAttribute(FileAttributes attribute) + { + FileInfo info = new FileInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(false); + return info; + } + } + + public class DirectoryAdapter + { + private FileSystemRunner runner; + + public DirectoryAdapter(string path, FileSystemRunner runner) + { + this.runner = runner; + this.runner.DirectoryExists(path).ShouldEqual(true); + this.Path = path; + } + + public string Path + { + get; private set; + } + + public void WithNoItems() + { + Directory.EnumerateFileSystemEntries(this.Path).ShouldBeEmpty(); + } + + public FileSystemInfo WithOneItem() + { + return this.WithItems(1).Single(); + } + + public IEnumerable WithItems(int expectedCount) + { + IEnumerable items = this.WithItems(); + items.Count().ShouldEqual(expectedCount); + return items; + } + + public IEnumerable WithItems() + { + DirectoryInfo directory = new DirectoryInfo(this.Path); + IEnumerable items = directory.GetFileSystemInfos(); + items.Any().ShouldEqual(true); + return items; + } + + public DirectoryAdapter WithDeepStructure(string otherPath) + { + otherPath.ShouldBeADirectory(this.runner); + CompareDirectories(otherPath, this.Path); + return this; + } + + public DirectoryAdapter WithCaseMatchingName(string expectedName) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + string parentPath = System.IO.Path.GetDirectoryName(this.Path); + DirectoryInfo parentInfo = new DirectoryInfo(parentPath); + Assert.AreEqual(expectedName.Equals(parentInfo.GetDirectories(info.Name)[0].Name, StringComparison.Ordinal), true); + return this; + } + + public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + info.CreationTime.ShouldEqual(creation); + info.LastAccessTime.ShouldEqual(lastAccess); + info.LastWriteTime.ShouldEqual(lastWrite); + + return info; + } + + public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes) + { + DirectoryInfo info = this.WithInfo(creation, lastWrite, lastAccess); + info.Attributes.ShouldEqual(attributes); + return info; + } + + public DirectoryInfo WithAttribute(FileAttributes attribute) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(true); + return info; + } + + private static void CompareDirectories(string expectedPath, string actualPath) + { + IEnumerable expectedEntries = Directory.EnumerateFileSystemEntries(expectedPath, "*", SearchOption.AllDirectories); + IEnumerable actualEntries = Directory.EnumerateFileSystemEntries(actualPath, "*", SearchOption.AllDirectories); + + string dotGitFolder = System.IO.Path.DirectorySeparatorChar + TestConstants.DotGit.Root + System.IO.Path.DirectorySeparatorChar; + IEnumerator expectedEnumerator = expectedEntries + .Where(x => !x.Contains(dotGitFolder)) + .Select(x => x.Replace(expectedPath, string.Empty)) + .OrderBy(x => x) + .GetEnumerator(); + IEnumerator actualEnumerator = actualEntries + .Where(x => !x.Contains(dotGitFolder)) + .Select(x => x.Replace(actualPath, string.Empty)) + .OrderBy(x => x) + .GetEnumerator(); + + bool expectedMoved = expectedEnumerator.MoveNext(); + bool actualMoved = actualEnumerator.MoveNext(); + + while (expectedMoved && actualMoved) + { + actualEnumerator.Current.ShouldEqual(expectedEnumerator.Current); + expectedMoved = expectedEnumerator.MoveNext(); + actualMoved = actualEnumerator.MoveNext(); + } + + StringBuilder errorEntries = new StringBuilder(); + if (expectedMoved) + { + do + { + errorEntries.AppendLine(string.Format("Missing entry {0}", expectedEnumerator.Current)); + } + while (expectedEnumerator.MoveNext()); + } + + while (actualEnumerator.MoveNext()) + { + errorEntries.AppendLine(string.Format("Extra entry {0}", actualEnumerator.Current)); + } + + if (errorEntries.Length > 0) + { + Assert.Fail(errorEntries.ToString()); + } + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs new file mode 100644 index 00000000..c8ef69e0 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs @@ -0,0 +1,34 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class DiagnoseTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem; + + public DiagnoseTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase] + public void DiagnoseProducesZipFile() + { + Directory.Exists(this.Enlistment.DiagnosticsRoot).ShouldEqual(false); + string output = this.Enlistment.Diagnose(); + + IEnumerable files = Directory.EnumerateFiles(this.Enlistment.DiagnosticsRoot); + files.ShouldBeNonEmpty(); + string zipFilePath = files.First(); + + zipFilePath.EndsWith(".zip").ShouldEqual(true); + output.Contains(zipFilePath).ShouldEqual(true); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitCommandsTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitCommandsTests.cs new file mode 100644 index 00000000..7db2767f --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitCommandsTests.cs @@ -0,0 +1,1047 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class GitCommandsTests : TestsWithEnlistmentPerFixture + { + private const string EncodingFileFolder = "FilenameEncoding"; + private const string EncodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt"; + private const string ContentWhenEditingFile = "// Adding a comment to the file"; + private const string EditFilePath = @"GVFS\GVFS.Common\GVFSContext.cs"; + private const string DeleteFilePath = @"GVFS\GVFS\Program.cs"; + private const string RenameFilePathFrom = @"GVFS\GVFS.Common\Physical\FileSystem\FileProperties.cs"; + private const string RenameFilePathTo = @"GVFS\GVFS.Common\Physical\FileSystem\FileProperties2.cs"; + private const string RenameFolderPathFrom = @"GVFS\GVFS.Common\Physical\FileSystem"; + private const string RenameFolderPathTo = @"GVFS\GVFS.Common\Physical\FileSyst3m"; + private const string UnknownTestName = "Unknown"; + private FileSystemRunner fileSystem; + + public GitCommandsTests() + { + this.fileSystem = new SystemIORunner(); + } + + public ControlGitRepo ControlGitRepo + { + get; private set; + } + + public override void CreateEnlistment() + { + base.CreateEnlistment(); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.abbrev 12"); + + this.ControlGitRepo = ControlGitRepo.Create(); + this.ControlGitRepo.Initialize(); + } + + public override void DeleteEnlistment() + { + base.DeleteEnlistment(); + this.ControlGitRepo.Delete(); + } + + [SetUp] + public void TestSetup() + { + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.fileSystem) + .WithDeepStructure(this.ControlGitRepo.RootPath); + this.ValidateGitCommand("status"); + } + + [TearDown] + public void TestTearDown() + { + this.CheckHeadCommitTree(); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.fileSystem) + .WithDeepStructure(this.ControlGitRepo.RootPath); + + this.RunGitCommandNoProgress("reset --hard HEAD"); + this.ValidateGitCommand("clean -d -f -x"); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.fileSystem) + .WithDeepStructure(this.ControlGitRepo.RootPath); + } + + [TestCase] + public void VerifyTestFilesExist() + { + // Sanity checks to ensure that the test files we expect to be in our test repo are present + Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename).ShouldBeAFile(this.fileSystem); + Path.Combine(this.Enlistment.RepoRoot, EditFilePath).ShouldBeAFile(this.fileSystem); + Path.Combine(this.Enlistment.RepoRoot, DeleteFilePath).ShouldBeAFile(this.fileSystem); + Path.Combine(this.Enlistment.RepoRoot, RenameFilePathFrom).ShouldBeAFile(this.fileSystem); + Path.Combine(this.Enlistment.RepoRoot, RenameFolderPathFrom).ShouldBeADirectory(this.fileSystem); + } + + [TestCase] + public void StatusTest() + { + this.ValidateGitCommand("status"); + } + + [TestCase] + public void StatusShortTest() + { + this.ValidateGitCommand("status -s"); + } + + [TestCase] + public void BranchTest() + { + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void NewBranchTest() + { + this.ValidateGitCommand("branch tests/functional/NewBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void DeleteBranchTest() + { + this.ValidateGitCommand("branch tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + this.ValidateGitCommand("branch -d tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void RenameCurrentBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/RenameBranchTest"); + this.ValidateGitCommand("branch -m tests/functional/RenameBranchTest2"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void UntrackedFileTest() + { + this.ValidateGitCommand("checkout -b tests/functional/UntrackedFileTest"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Add new file\""); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void UntrackedEmptyFileTest() + { + this.ValidateGitCommand("checkout -b tests/functional/UntrackedEmptyFileTest"); + this.CreateEmptyFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Add new file\""); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CheckoutNewBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchTest"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CreateFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchSwitchBranchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void DeleteFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackDeleteFolderTest() + { + // 663045 - Confirm that folder can be deleted after deleting file then changing + // branches + string deleteFolderPath = @"GVFS\GVFS\CommandLine"; + string deleteFilePath = deleteFolderPath + @"\CloneHelper.cs"; + + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFile(deleteFilePath)); + this.DeleteFolder(deleteFolderPath); + } + + [TestCase] + public void AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that folder can be deleted after adding a file then changing + // branches + string newFileParentFolderPath = @"GVFS\GVFS\CommandLine"; + string newFilePath = newFileParentFolderPath + @"\testfile.txt"; + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFilePath, newFileContents), + test: "AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileParentFolderPath); + this.FileShouldHaveContents(newFilePath, newFileContents); + } + + [TestCase] + public void AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that grandparent folder can be deleted after adding a (granchild) file + // then changing branches + string newFileGrandParentFolderPath = @"GVFS\GVFS\CommandLine"; + string newFilePath = newFileGrandParentFolderPath + @"\testfile.txt"; + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFilePath, newFileContents), + test: "AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileGrandParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileGrandParentFolderPath); + this.FileShouldHaveContents(newFilePath, newFileContents); + } + + [TestCase] + public void CaseOnlyRenameFileAndChangeBranches() + { + // 693190 - Confirm that file does not disappear after case-only rename and branch + // changes + string newBranchName = "tests/functional/CaseOnlyRenameFileAndChangeBranches"; + string oldFileName = "Readme.md"; + string newFileName = "README.md"; + + this.ValidateGitCommand("checkout -b " + newBranchName); + this.ValidateGitCommand("mv {0} {1}", oldFileName, newFileName); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for CaseOnlyRenameFileAndChangeBranches\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.FileShouldHaveCaseMatchingName(newFileName, oldFileName); + + this.ValidateGitCommand("checkout " + newBranchName); + this.FileShouldHaveCaseMatchingName(newFileName, newFileName); + } + + [TestCase] + public void DeleteFolderAndChangeBranchToFolderWithDifferentCase() + { + // 692765 - Recursive sparse-checkout entries for folders should be case insensitive when + // changing branches + + string folderName = "GVFlt_MultiThreadTest"; + + string sparseFile = Path.Combine(this.Enlistment.RepoRoot, @".git\info\sparse-checkout"); + sparseFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldNotContain(folderName); + + this.FolderShouldHaveCaseMatchingName(folderName, "GVFlt_MultiThreadTest"); + this.DeleteFolder(folderName); + + // b5fd7d23706a18cff3e2b8225588d479f7e51138 is the commit prior to deleting GVFLT_MultiThreadTest + // and re-adding it as as GVFlt_MultiThreadTest + this.ValidateGitCommand("checkout b5fd7d23706a18cff3e2b8225588d479f7e51138"); + this.FolderShouldHaveCaseMatchingName(folderName, "GVFLT_MultiThreadTest"); + + // TODO 696642: Because GVFS can leave around empty enumerated folders, switch back to + // ControlGitRepo.Commitish so that the control repo has the same folders as GVFS when [TearDown] + // Validation occurs. + // If we do not switch back to ControlGitRepo.Commitish, the GVFS repo will have folders left from + // the enumeration that occurs in [Setup] (and this will not match the control repo) + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void EditFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.EditFile); + } + + [TestCase] + public void RenameFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.RenameFile); + } + + [TestCase] + [Ignore("Disabled until moving partial folders is supported")] + public void MoveFolderCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.MoveFolder); + } + + [TestCase] + public void AddFileCommitThenDeleteAndCommit() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_after"); + string filePath = @"GVFS\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete file for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.fileSystem) + .WithDeepStructure(this.ControlGitRepo.RootPath); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_after"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = @"GVFS\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = @"GVFS\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = folderPath + @"\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = folderPath + @"\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetHard() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = folderPath + @"\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = folderPath + @"\testfile.txt"; + this.CreateFile(filePath, "Some new content for the file"); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --mixed HEAD"); + } + + [TestCase] + public void AddFoldersAndFilesAndRenameFolder() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFoldersAndFilesAndRenameFolder"); + + string topMostNewFolder = "AddFoldersAndFilesAndRenameFolder_Test"; + this.CreateFolder(topMostNewFolder); + this.CreateFile(topMostNewFolder + @"\top_level_test_file.txt", "test contents"); + + string testFolderLevel1 = topMostNewFolder + @"\TestFolderLevel1"; + this.CreateFolder(testFolderLevel1); + this.CreateFile(testFolderLevel1 + @"\level_1_test_file.txt", "test contents"); + + string testFolderLevel2 = testFolderLevel1 + @"\TestFolderLevel2"; + this.CreateFolder(testFolderLevel2); + this.CreateFile(testFolderLevel2 + @"\level_2_test_file.txt", "test contents"); + + string testFolderLevel3 = testFolderLevel2 + @"\TestFolderLevel3"; + this.CreateFolder(testFolderLevel3); + this.CreateFile(testFolderLevel3 + @"\level_3_test_file.txt", "test contents"); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel3, testFolderLevel2 + @"\TestFolderLevel3Renamed"); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel2, testFolderLevel1 + @"\TestFolderLevel2Renamed"); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel1, topMostNewFolder + @"\TestFolderLevel1Renamed"); + this.ValidateGitCommand("status"); + + this.MoveFolder(topMostNewFolder, "AddFoldersAndFilesAndRenameFolder_TestRenamed"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void AddFileAfterFolderRename() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileAfterFolderRename"); + + string folder = "AddFileAfterFolderRename_Test"; + string renamedFolder = "AddFileAfterFolderRename_TestRenamed"; + this.CreateFolder(folder); + this.MoveFolder(folder, renamedFolder); + this.CreateFile(renamedFolder + @"\test_file.txt", "test contents"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoft"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void ResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void ResetMixed2() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2"); + this.ValidateGitCommand("reset HEAD~1"); + } + + [TestCase] + public void ManuallyModifyHead() + { + this.ValidateGitCommand("status"); + this.ReplaceText(TestConstants.DotGit.Head, "f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + this.ValidateGitCommand("status"); + } + + [TestCase] + [Ignore("TODO 690810 - Invesigate why this test is failing")] + public void ResetSoftTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoftTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --soft 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --soft 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + [Ignore("TODO 690810 - Invesigate why this test is failing")] + public void ResetMixedTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --mixed 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --mixed 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + [Ignore("TODO 690810 - Invesigate why this test is failing")] + public void ResetMixed2Twice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2Twice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetHardAfterCreate() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreate"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEdit() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEdit"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDelete() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDelete"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterCreateAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreateAndAdd"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEditAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEditAndAdd"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDeleteAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDeleteAndAdd"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ChangeTwoBranchesAndMerge() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_1"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_2"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge second branch\""); + this.ValidateGitCommand("merge tests/functional/ChangeTwoBranchesAndMerge_1"); + } + + [TestCase] + public void ChangeBranchAndCherryPickIntoAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.ValidateGitCommand("tag DeleteForCherryPick"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_2"); + this.RunGitCommand("cherry-pick DeleteForCherryPick"); + } + + [TestCase] + public void ChangeBranchAndMergeRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.RunGitCommand("rebase --merge tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); + } + + [TestCase] + public void ChangeBranchAndRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("rebase tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); + } + + [TestCase] + public void StashChanges() + { + this.ValidateGitCommand("checkout -b tests/functional/StashChanges"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("stash"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/StashChanges_2"); + this.RunGitCommand("stash pop"); + } + + [TestCase] + public void OpenFileThenCheckout() + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.EditFilePath); + + // Open files with ReadWrite sharing because depending on the state of the index (and the mtimes), git might need to read the file + // as part of status (while we have the handle open). + using (FileStream virtualFS = File.Open(virtualFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter virtualWriter = new StreamWriter(virtualFS)) + using (FileStream controlFS = File.Open(controlFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter controlWriter = new StreamWriter(controlFS)) + { + this.ValidateGitCommand("checkout -b tests/functional/OpenFileThenCheckout"); + virtualWriter.WriteLine("// Adding a line for testing purposes"); + controlWriter.WriteLine("// Adding a line for testing purposes"); + this.ValidateGitCommand("status"); + } + + // NOTE: Due to optimizations in checkout -b, the modified files will not be included as part of the + // success message. Validate that the succcess messages match, and the call to validate "status" below + // will ensure that GVFS is still reporting the edited file as modified. + + string controlRepoRoot = this.ControlGitRepo.RootPath; + string gvfsRepoRoot = this.Enlistment.RepoRoot; + string command = "checkout -b tests/functional/OpenFileThenCheckout_2"; + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitProcess.InvokeProcess(gvfsRepoRoot, command); + actualResult.Errors.ShouldEqual(expectedResult.Errors); + actualResult.Errors.ShouldContain("Switched to a new branch"); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void EditFileNeedingUtf8Encoding() + { + this.ValidateGitCommand("checkout -b tests/functional/EditFileNeedingUtf8Encoding"); + this.ValidateGitCommand("status"); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, EncodingFileFolder, EncodingFilename); + + string contents = virtualFile.ShouldBeAFile(this.fileSystem).WithContents(); + string expectedContents = controlFile.ShouldBeAFile(this.fileSystem).WithContents(); + contents.ShouldEqual(expectedContents); + + // Check that the entry in the sparse-checkout matches + string sparseCheckoutFile = Path.Combine(this.Enlistment.RepoRoot, TestConstants.DotGit.Info.SparseCheckout); + sparseCheckoutFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldContain(EncodingFilename); + this.ValidateGitCommand("status"); + + this.AppendAllText(virtualFile, ContentWhenEditingFile); + this.AppendAllText(controlFile, ContentWhenEditingFile); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void UseAlias() + { + this.ValidateGitCommand("config --local alias.potato status"); + this.ValidateGitCommand("potato"); + } + + private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + } + + private void StageChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + } + + private void CommitChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", test); + } + + private void CommitChangesSwitchBranchSwitchBack(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + string branch = string.Format("tests/functional/{0}", test); + this.ValidateGitCommand("checkout -b {0}", branch); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", branch); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.fileSystem) + .WithDeepStructure(this.ControlGitRepo.RootPath); + + this.ValidateGitCommand("checkout {0}", branch); + } + + private void CheckHeadCommitTree() + { + this.ValidateGitCommand("ls-tree HEAD"); + } + + // Some commands compute a new commit sha, which is dependent on time and therefore + // won't match what is in the control repo. For those commands, we just ensure that + // the errors match what we expect, but we skip comparing the output + private void RunGitCommand(string command, params object[] args) + { + string controlRepoRoot = this.ControlGitRepo.RootPath; + string gvfsRepoRoot = this.Enlistment.RepoRoot; + command = string.Format(command, args); + + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command); + actualResult.Errors.ShouldEqual(expectedResult.Errors); + + if (command != "status") + { + this.ValidateGitCommand("status"); + } + } + + // Ensure that errors match when running git commands. However, if the actual errors are just + // "Checking out files:" lines, those lines will be ignored + // TODO 881663: Determine why reset --hard HEAD sometimes produces "Checking out files:" output + private void RunGitCommandNoProgress(string command, params object[] args) + { + string controlRepoRoot = this.ControlGitRepo.RootPath; + string gvfsRepoRoot = this.Enlistment.RepoRoot; + command = string.Format(command, args); + + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command); + + if (expectedResult.Errors.Length > 0) + { + actualResult.Errors.ShouldEqual(expectedResult.Errors); + } + else if (actualResult.Errors.Length > 0) + { + foreach (string errorLine in actualResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + errorLine.ShouldContain("Checking out files:"); + } + } + + if (command != "status") + { + this.ValidateGitCommand("status"); + } + } + + private void ValidateGitCommand(string command, params object[] args) + { + GitHelpers.ValidateGitCommand( + this.Enlistment, + this.ControlGitRepo, + command, + args); + } + + private void CreateFile() + { + this.CreateFile("tempFile.txt", "Some content here"); + } + + private void CreateEmptyFile() + { + string filePath = "emptyFile.txt"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.fileSystem.CreateEmptyFile(virtualFile); + this.fileSystem.CreateEmptyFile(controlFile); + } + + private void CreateFile(string filePath, string content) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.fileSystem.WriteAllText(virtualFile, content); + this.fileSystem.WriteAllText(controlFile, content); + } + + private void CreateFolder(string folderPath) + { + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.fileSystem.CreateDirectory(virtualFolder); + this.fileSystem.CreateDirectory(controlFolder); + } + + private void EditFile() + { + this.AppendAllText(GitCommandsTests.EditFilePath, ContentWhenEditingFile); + } + + private void AppendAllText(string filePath, string content) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.fileSystem.AppendAllText(virtualFile, content); + this.fileSystem.AppendAllText(controlFile, content); + } + + private void ReplaceText(string filePath, string newContent) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.fileSystem.WriteAllText(virtualFile, newContent); + this.fileSystem.WriteAllText(controlFile, newContent); + } + + private void DeleteFile() + { + this.DeleteFile(GitCommandsTests.DeleteFilePath); + } + + private void DeleteFile(string filePath) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.fileSystem.DeleteFile(virtualFile); + this.fileSystem.DeleteFile(controlFile); + virtualFile.ShouldNotExistOnDisk(this.fileSystem); + controlFile.ShouldNotExistOnDisk(this.fileSystem); + } + + private void DeleteFolder(string folderPath) + { + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.fileSystem.DeleteDirectory(virtualFolder); + this.fileSystem.DeleteDirectory(controlFolder); + virtualFolder.ShouldNotExistOnDisk(this.fileSystem); + controlFolder.ShouldNotExistOnDisk(this.fileSystem); + } + + private void RenameFile() + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathTo); + this.fileSystem.MoveFile(virtualFileFrom, virtualFileTo); + this.fileSystem.MoveFile(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.fileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.fileSystem); + } + + private void MoveFolder() + { + this.MoveFolder(GitCommandsTests.RenameFolderPathFrom, GitCommandsTests.RenameFolderPathTo); + } + + private void MoveFolder(string pathFrom, string pathTo) + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); + this.fileSystem.MoveDirectory(virtualFileFrom, virtualFileTo); + this.fileSystem.MoveDirectory(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.fileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.fileSystem); + } + + private void FolderShouldExist(string folderPath) + { + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.fileSystem); + controlFolder.ShouldBeADirectory(this.fileSystem); + } + + private void ShouldNotExistOnDisk(string path) + { + string virtualPath = Path.Combine(this.Enlistment.RepoRoot, path); + string controlPath = Path.Combine(this.ControlGitRepo.RootPath, path); + virtualPath.ShouldNotExistOnDisk(this.fileSystem); + controlPath.ShouldNotExistOnDisk(this.fileSystem); + } + + private void FileShouldHaveContents(string filePath, string contents) + { + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents(contents); + controlFilePath.ShouldBeAFile(this.fileSystem).WithContents(contents); + } + + private void FileShouldHaveCaseMatchingName(string filePath, string caseSensitiveName) + { + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + virtualFilePath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(caseSensitiveName); + controlFilePath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(caseSensitiveName); + } + + private void FolderShouldHaveCaseMatchingName(string folderPath, string caseSensitiveName) + { + string virtualFolderPath = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolderPath.ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(caseSensitiveName); + controlFolderPath.ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(caseSensitiveName); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs new file mode 100644 index 00000000..446eaf5a --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs @@ -0,0 +1,143 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(GitFilesTestsRunners), GitFilesTestsRunners.TestRunners)] + public class GitFilesTests : TestsWithEnlistmentPerFixture + { + private const string ExcludeFileContentsBeforeChange = +@"# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ +* +"; + private const string ExcludeFileContentsAfterChange = +@"# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ +* +!/* +"; + + private FileSystemRunner fileSystem; + + public GitFilesTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase, Order(1)] + public void CreateFileTest() + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, "tempFile.txt"); + string excludeFile = Path.Combine(this.Enlistment.RepoRoot, GitHelpers.ExcludeFilePath); + excludeFile.ShouldBeAFile(this.fileSystem).WithContents(ExcludeFileContentsBeforeChange.Replace("\r\n", "\n")); + this.fileSystem.WriteAllText(virtualFile, "Some content here"); + + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); + + virtualFile.ShouldBeAFile(this.fileSystem).WithContents("Some content here"); + excludeFile.ShouldBeAFile(this.fileSystem).WithContents(ExcludeFileContentsAfterChange.Replace("\r\n", "\n")); + } + + [TestCase, Order(2)] + public void ReadingFileUpdatesTimestampsAndSizeInIndex() + { + string gitFileToCheck = "GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, gitFileToCheck.Replace('/', '\\')); + ProcessResult initialResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files --debug -svmodc " + gitFileToCheck); + initialResult.ShouldNotBeNull(); + initialResult.Output.ShouldNotBeNull(); + initialResult.Output.StartsWith("S ").ShouldEqual(true); + initialResult.Output.ShouldContain("ctime: 0:0", "mtime: 0:0", "size: 0\t"); + + using (FileStream fileStreamToRead = File.OpenRead(virtualFile)) + { + fileStreamToRead.ReadByte(); + } + + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations did not complete."); + + ProcessResult afterUpdateResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files --debug -svmodc " + gitFileToCheck); + afterUpdateResult.ShouldNotBeNull(); + afterUpdateResult.Output.ShouldNotBeNull(); + afterUpdateResult.Output.StartsWith("H ").ShouldEqual(true); + afterUpdateResult.Output.ShouldNotContain("ctime: 0:0", "mtime: 0:0", "size: 0\t"); + afterUpdateResult.Output.ShouldContain("size: 161\t"); + } + + [TestCase, Order(3)] + public void CreatedFileWillGetSkipworktreeBitCleared() + { + string fileToTest = "GVFS\\GVFS.Common\\RetryWrapper.cs"; + string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, fileToTest); + string gitFileToTest = fileToTest.Replace('\\', '/'); + this.VerifyWorktreeBit(gitFileToTest, LsFilesStatus.SkipWorktree); + + ManualResetEventSlim resetEvent = GitHelpers.AcquireGVFSLock(this.Enlistment); + + this.fileSystem.WriteAllText(fileToCreate, "Anything can go here"); + this.fileSystem.FileExists(fileToCreate).ShouldEqual(true); + resetEvent.Set(); + + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations did not complete."); + + string sparseCheckoutFile = Path.Combine(this.Enlistment.RepoRoot, TestConstants.DotGit.Info.SparseCheckout); + sparseCheckoutFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldContain(gitFileToTest); + this.VerifyWorktreeBit(gitFileToTest, LsFilesStatus.Cached); + } + + private void VerifyWorktreeBit(string path, char expectedStatus) + { + ProcessResult lsfilesResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "ls-files -svomdc " + path); + lsfilesResult.ShouldNotBeNull(); + lsfilesResult.Output.ShouldNotBeNull(); + lsfilesResult.Output.Length.ShouldBeAtLeast(2); + lsfilesResult.Output[0].ShouldEqual(expectedStatus); + } + + private static class LsFilesStatus + { + public const char Cached = 'H'; + public const char SkipWorktree = 'S'; + } + + private class GitFilesTestsRunners + { + public const string TestRunners = "Runners"; + + public static object[] Runners + { + get + { + // Don't use the BashRunner for GitFilesTests as the BashRunner always strips off the last trailing newline (\n) + // and we expect there to be a trailing new line + List runners = new List(); + foreach (object[] runner in FileSystemRunner.Runners.ToList()) + { + if (!(runner.ToList().First() is BashRunner)) + { + runners.Add(new object[] { runner.ToList().First() }); + } + } + + return runners.ToArray(); + } + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs new file mode 100644 index 00000000..388547f8 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs @@ -0,0 +1,244 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using Microsoft.Isam.Esent.Collections.Generic; +using NUnit.Framework; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class MountTests : TestsWithEnlistmentPerFixture + { + private const int GVFSGenericError = 3; + private const uint GenericRead = 2147483648; + private const uint FileFlagBackupSemantics = 3355443; + private const string IndexLockPath = ".git\\index.lock"; + private const string RepoMetadataDatabaseName = "RepoMetadata"; + private const string DiskLayoutVersionKey = "DiskLayoutVersion"; + + private FileSystemRunner fileSystem; + + public MountTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] + public void SecondMountAttemptFails(string mountSubfolder) + { + this.MountShouldFail("already mounted", this.Enlistment.GetVirtualPathTo(mountSubfolder)); + } + + [TestCase] + public void MountFailsOutsideEnlistment() + { + this.MountShouldFail("is not a valid GVFS enlistment", Path.GetDirectoryName(this.Enlistment.EnlistmentRoot)); + } + + [TestCase] + public void MountCopiesMissingReadObjectHook() + { + this.Enlistment.UnmountGVFS(); + + string readObjectPath = this.Enlistment.GetVirtualPathTo(@".git\hooks\read-object.exe"); + readObjectPath.ShouldBeAFile(this.fileSystem); + this.fileSystem.DeleteFile(readObjectPath); + readObjectPath.ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.MountGVFS(); + readObjectPath.ShouldBeAFile(this.fileSystem); + } + + [TestCase] + public void MountCleansStaleIndexLock() + { + this.MountCleansIndexLock(lockFileContents: "GVFS"); + } + + [TestCase] + public void MountCleansEmptyIndexLock() + { + this.MountCleansIndexLock(lockFileContents: string.Empty); + } + + [TestCase] + public void MountCleansUnknownIndexLock() + { + this.MountCleansIndexLock(lockFileContents: "Bogus lock file contents"); + } + + [TestCase] + public void MountFailsWhenNoOnDiskVersion() + { + this.Enlistment.UnmountGVFS(); + + // Get the current disk layout version + string currentVersion = this.GetPersistedDiskLayoutVersion().ShouldNotBeNull(); + int currentVersionNum; + int.TryParse(currentVersion, out currentVersionNum).ShouldEqual(true); + + // Move the RepoMetadata database to a temp folder + string versionDatabasePath = Path.Combine(this.Enlistment.DotGVFSRoot, RepoMetadataDatabaseName); + versionDatabasePath.ShouldBeADirectory(this.fileSystem); + + string tempDatabasePath = versionDatabasePath + "_MountFailsWhenNoOnDiskVersion"; + tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveDirectory(versionDatabasePath, tempDatabasePath); + versionDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + + this.MountShouldFail("Enlistment disk layout version not found"); + + // Move the RepoMetadata database back + this.fileSystem.MoveDirectory(tempDatabasePath, versionDatabasePath); + tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + versionDatabasePath.ShouldBeADirectory(this.fileSystem); + + this.Enlistment.MountGVFS(); + } + + [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] + public void MountFailsAfterBreakingDowngrade(string mountSubfolder) + { + MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem); + this.Enlistment.UnmountGVFS(); + + string currentVersion = this.GetPersistedDiskLayoutVersion().ShouldNotBeNull(); + int currentVersionNum; + int.TryParse(currentVersion, out currentVersionNum).ShouldEqual(true); + this.SaveDiskLayoutVersion((currentVersionNum + 1).ToString()); + + this.MountShouldFail("do not allow mounting after downgrade", this.Enlistment.GetVirtualPathTo(mountSubfolder)); + + this.SaveDiskLayoutVersion(currentVersionNum.ToString()); + this.Enlistment.MountGVFS(); + } + + // Ported from GVFlt's BugRegressionTest + [TestCase] + public void GVFlt_CMDHangNoneActiveInstance() + { + this.Enlistment.UnmountGVFS(); + + IntPtr handle = CreateFile( + Path.Combine(this.Enlistment.RepoRoot, "aaa", "aaaa"), + GenericRead, + FileShare.Read, + IntPtr.Zero, + FileMode.Open, + FileFlagBackupSemantics, + IntPtr.Zero); + + int lastError = Marshal.GetLastWin32Error(); + + IntPtr invalid_handle = new IntPtr(-1); + handle.ShouldEqual(invalid_handle); + lastError.ShouldNotEqual(0); // 0 == ERROR_SUCCESS + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr CreateFile( + [In] string fileName, + uint desiredAccess, + FileShare shareMode, + [In] IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)]FileMode creationDisposition, + uint flagsAndAttributes, + [In] IntPtr templateFile); + + private void MountCleansIndexLock(string lockFileContents) + { + this.Enlistment.UnmountGVFS(); + + string indexLockVirtualPath = this.Enlistment.GetVirtualPathTo(IndexLockPath); + indexLockVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + + if (string.IsNullOrEmpty(lockFileContents)) + { + this.fileSystem.CreateEmptyFile(indexLockVirtualPath); + } + else + { + this.fileSystem.AppendAllText(indexLockVirtualPath, lockFileContents); + } + + this.Enlistment.MountGVFS(); + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); + indexLockVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + } + + private void SaveDiskLayoutVersion(string value) + { + using (PersistentDictionary dictionary = new PersistentDictionary( + Path.Combine(this.Enlistment.DotGVFSRoot, RepoMetadataDatabaseName))) + { + dictionary[DiskLayoutVersionKey] = value; + dictionary.Flush(); + } + } + + private string GetPersistedDiskLayoutVersion() + { + using (PersistentDictionary dictionary = new PersistentDictionary( + Path.Combine(this.Enlistment.DotGVFSRoot, RepoMetadataDatabaseName))) + { + string value; + if (dictionary.TryGetValue(DiskLayoutVersionKey, out value)) + { + return value; + } + + return null; + } + } + + private void MountShouldFail(string expectedErrorMessage, string mountWorkingDirectory = null) + { + string pathToGVFS = Path.Combine(TestContext.CurrentContext.TestDirectory, Properties.Settings.Default.PathToGVFS); + string enlistmentRoot = this.Enlistment.EnlistmentRoot; + + ProcessStartInfo processInfo = new ProcessStartInfo(pathToGVFS); + processInfo.Arguments = "mount"; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.WorkingDirectory = string.IsNullOrEmpty(mountWorkingDirectory) ? enlistmentRoot : mountWorkingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + result.ExitCode.ShouldEqual(GVFSGenericError); + result.Output.ShouldContain(expectedErrorMessage); + } + + private class MountSubfolders + { + public const string MountFolders = "Folders"; + private static object[] mountFolders = + { + new object[] { string.Empty }, + new object[] { "GVFS" }, + }; + + public static object[] Folders + { + get + { + return mountFolders; + } + } + + public static void EnsureSubfoldersOnDisk(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem) + { + // Enumerate the directory to ensure that the folder is on disk after GVFS is unmounted + foreach (object[] folder in Folders) + { + string folderPath = enlistment.GetVirtualPathTo((string)folder[0]); + folderPath.ShouldBeADirectory(fileSystem).WithItems(); + } + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs new file mode 100644 index 00000000..5c0d75a7 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs @@ -0,0 +1,150 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class MoveRenameFileTests : TestsWithEnlistmentPerFixture + { + private const string TestFileContents = +@"using NUnitLite; +using System; +using System.Threading; + +namespace GVFS.StressTests +{ + public class Program + { + public static void Main(string[] args) + { + string[] test_args = args; + + for (int i = 0; i < Properties.Settings.Default.TestRepeatCount; i++) + { + Console.WriteLine(""Starting pass {0}"", i + 1); + DateTime now = DateTime.Now; + new AutoRun().Execute(test_args); + Console.WriteLine(""Completed pass {0} in {1}"", i + 1, DateTime.Now - now); + Console.WriteLine(); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + + Console.WriteLine(""All tests completed. Press Enter to exit.""); + Console.ReadLine(); + } + } +}"; + + private FileSystemRunner fileSystem; + + public MoveRenameFileTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase] + public void ChangeUnhydratedFileName() + { + string oldFilename = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\Program.cs"; + string newFilename = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\renamed_Program.cs"; + + // Don't read oldFilename or check for its existence before calling MoveFile, because doing so + // can cause the file to hydrate + this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newFilename), this.Enlistment.GetVirtualPathTo(oldFilename)); + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase] + public void ChangeUnhydratedFileNameCase() + { + string oldName = "Readme.md"; + string newName = "readme.md"; + + string oldVirtualPath = this.Enlistment.GetVirtualPathTo(oldName); + string newVirtualPath = this.Enlistment.GetVirtualPathTo(newName); + + this.ChangeUnhydratedFileCase(oldName, oldVirtualPath, newName, newVirtualPath, knownFileContents: null); + } + + [TestCase] + public void ChangeNestedUnhydratedFileNameCase() + { + string oldName = "Program.cs"; + string newName = "program.cs"; + string folderName = "Test_EPF_MoveRenameFileTests\\ChangeNestedUnhydratedFileNameCase\\"; + + string oldVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, oldName)); + string newVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, newName)); + + this.ChangeUnhydratedFileCase(oldName, oldVirtualPath, newName, newVirtualPath, TestFileContents); + } + + [TestCase] + public void MoveUnhydratedFileToDotGitFolder() + { + string targetFolderName = ".git"; + this.Enlistment.GetVirtualPathTo(targetFolderName).ShouldBeADirectory(this.fileSystem); + + string testFileName = "Program.cs"; + string testFileFolder = "Test_EPF_MoveRenameFileTests\\MoveUnhydratedFileToDotGitFolder"; + string testFilePathSubPath = testFileFolder + "\\" + testFileName; + + string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(targetFolderName), testFileName); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFilePathSubPath), newTestFileVirtualPath); + this.Enlistment.GetVirtualPathTo(testFilePathSubPath).ShouldNotExistOnDisk(this.fileSystem); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + + this.fileSystem.DeleteFile(newTestFileVirtualPath); + newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase] + public void MoveVirtualNTFSFileToOverwriteUnhydratedFile() + { + string targetFilename = ".gitattributes"; + + string sourceFilename = "SourceFile.txt"; + string sourceFileContents = "The Source"; + + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(sourceFilename), sourceFileContents); + this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents); + + this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename)); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents); + + this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem); + } + + private void ChangeUnhydratedFileCase( + string oldName, + string oldVirtualPath, + string newName, + string newVirtualPath, + string knownFileContents) + { + this.fileSystem.MoveFile(oldVirtualPath, newVirtualPath); + string fileContents = newVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(newName).WithContents(); + fileContents.ShouldBeNonEmpty(); + if (knownFileContents != null) + { + fileContents.ShouldEqual(knownFileContents); + } + + this.fileSystem.MoveFile(newVirtualPath, oldVirtualPath); + oldVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(oldName).WithContents(fileContents); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs new file mode 100644 index 00000000..2a948158 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs @@ -0,0 +1,155 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class MoveRenameFileTests_2 : TestsWithEnlistmentPerFixture + { + private const string TestFileFolder = "Test_EPF_MoveRenameFileTests_2"; + + // Test_EPF_MoveRenameFileTests_2\RunUnitTests.bat + private const string RunUnitTestsContents = +@"@ECHO OFF +IF ""%1""=="""" (SET ""Configuration=Debug"") ELSE (SET ""Configuration=%1"") + +%~dp0\..\..\BuildOutput\GVFS.UnitTests\bin\x64\%Configuration%\GVFS.UnitTests.exe"; + + // Test_EPF_MoveRenameFileTests_2\RunFunctionalTests.bat + private const string RunFunctioanlTestsContents = +@"@ECHO OFF +IF ""%1""=="""" (SET ""Configuration=Debug"") ELSE (SET ""Configuration=%1"") + +%~dp0\..\..\BuildOutput\GVFS.FunctionalTests\bin\x64\%Configuration%\GVFS.FunctionalTests.exe %2"; + + private FileSystemRunner fileSystem; + + public MoveRenameFileTests_2(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + // This test needs the GVFS folder to not exist on physical disk yet, so run it first + [TestCase, Order(1)] + public void MoveUnhydratedFileToUnhydratedFolderAndWrite() + { + string testFileContents = RunUnitTestsContents; + string testFileName = "RunUnitTests.bat"; + + // Assume there will always be a GVFS folder when running tests + string testFolderName = "GVFS"; + + string oldTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName); + string newTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName + "\\" + testFileName); + + this.fileSystem.MoveFile(oldTestFileVirtualPath, newTestFileVirtualPath); + oldTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testFileContents); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(this.fileSystem); + + // Writing after the move should succeed + string newText = "New file text for test file"; + this.fileSystem.WriteAllText(newTestFileVirtualPath, newText); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(newText); + } + + [TestCase, Order(2)] + public void MoveUnhydratedFileToNewFolderAndWrite() + { + string testFolderName = "test_folder"; + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(this.fileSystem); + + string testFileName = "RunFunctionalTests.bat"; + string testFileContents = RunFunctioanlTestsContents; + + string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFolderName); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName), newTestFileVirtualPath); + this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName).ShouldNotExistOnDisk(this.fileSystem); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testFileContents); + + // Writing after the move should succeed + string newText = "New file text for test file"; + this.fileSystem.WriteAllText(newTestFileVirtualPath, newText); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem); + newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(newText); + + this.fileSystem.DeleteFile(newTestFileVirtualPath); + newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase, Order(3)] + public void MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite() + { + string targetFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunFunctionalTests.bat"; + string sourceFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunUnitTests.bat"; + string sourceFileContents = RunUnitTestsContents; + + // Overwriting one unhydrated file with another should create a file at the target + this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename)); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents); + + // Source file should be gone + this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem); + + // Writing after move should succeed + string newText = "New file text for target file"; + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), newText); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(newText); + } + + [TestCase, Order(4)] + public void CaseOnlyRenameFileInSubfolder() + { + string oldFilename = "CaseOnlyRenameFileInSubfolder.txt"; + string oldVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFileFolder, oldFilename)); + oldVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(oldFilename); + + string newFilename = "caseonlyrenamefileinsubfolder.txt"; + string newVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFileFolder, newFilename)); + + // Rename file, and confirm file name case was updated + this.fileSystem.MoveFile(oldVirtualPath, newVirtualPath); + newVirtualPath.ShouldBeAFile(this.fileSystem).WithCaseMatchingName(newFilename); + } + + [TestCase, Order(5)] + public void MoveUnhydratedFileToOverwriteFullFileAndWrite() + { + string targetFilename = "TargetFile.txt"; + string targetFileContents = "The Target"; + + string sourceFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteFullFileAndWrite\\MoveUnhydratedFileToOverwriteFullFileAndWrite.txt"; + string sourceFileContents = +@" + + + +"; + + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), targetFileContents); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(targetFileContents); + + // Overwriting a virtual NTFS file with an unprojected file should leave a file on disk at the + // target location + this.fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename)); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(sourceFileContents); + + // Source file should be gone + this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldNotExistOnDisk(this.fileSystem); + + // Writes should succeed after move + string newText = "New file text for Readme.md"; + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), newText); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(this.fileSystem).WithContents(newText); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFolderTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFolderTests.cs new file mode 100644 index 00000000..e6638f85 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFolderTests.cs @@ -0,0 +1,173 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class MoveRenameFolderTests : TestsWithEnlistmentPerFixture + { + public const string TestFileContents = +@"// dllmain.cpp : Defines the entry point for the DLL application. +#include ""stdafx.h"" + +BOOL APIENTRY DllMain( HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + UNREFERENCED_PARAMETER(hModule); + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +"; + private FileSystemRunner fileSystem; + + public MoveRenameFolderTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase] + public void RenameFolderShouldFail() + { + string testFileName = "RenameFolderShouldFail.cpp"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\RenameFolderShouldFail\\source"; + string newFolderName = "Test_EPF_MoveRenameFolderTests\\RenameFolderShouldFail\\sourcerenamed"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveDirectory_RequestShouldNotBeSupported(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(newFolderName)); + + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.Enlistment.GetVirtualPathTo(Path.Combine(newFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase] + [Ignore("Disabled until moving partial folders is supported")] + public void ChangeUnhydratedFolderName() + { + string testFileName = "ChangeUnhydratedFolderName.cpp"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\ChangeUnhydratedFolderName\\source"; + string newFolderName = "Test_EPF_MoveRenameFolderTests\\ChangeUnhydratedFolderName\\source_renamed"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(newFolderName)); + + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(newFolderName, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase] + [Ignore("Disabled until moving partial folders is supported")] + public void MoveUnhydratedFolderToNewFolder() + { + string testFileName = "MoveUnhydratedFolderToVirtualNTFSFolder.cpp"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\MoveUnhydratedFolderToVirtualNTFSFolder"; + + string newFolderName = "NewPerFixtureParent"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(newFolderName)); + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldBeADirectory(this.fileSystem); + + string movedFolderPath = Path.Combine(newFolderName, "EnlistmentPerFixture"); + this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(movedFolderPath)); + + this.Enlistment.GetVirtualPathTo(movedFolderPath).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(movedFolderPath, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase] + [Ignore("591744 - Cannot move folders from outside src\\.git to inside src\\.git")] + public void MoveUnhydratedFolderToFullFolderInDotGitFolder() + { + string testFileName = "MoveUnhydratedFolderToFullFolderInDotGitFolder.cpp"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\MoveUnhydratedFolderToFullFolderInDotGitFolder"; + + string newFolderName = ".git\\NewPerFixtureParent"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(newFolderName)); + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldBeADirectory(this.fileSystem); + + string movedFolderPath = Path.Combine(newFolderName, "Should"); + this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(movedFolderPath)); + + this.Enlistment.GetVirtualPathTo(movedFolderPath).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(movedFolderPath, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase] + [Ignore("Disabled until moving partial folders is supported")] + public void MoveAndRenameUnhydratedFolderToNewFolder() + { + string testFileName = "MoveAndRenameUnhydratedFolderToNewFolder.cpp"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\MoveAndRenameUnhydratedFolderToNewFolder"; + + string newFolderName = "NewPerTestCaseParent"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(newFolderName)); + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldBeADirectory(this.fileSystem); + + string movedFolderPath = Path.Combine(newFolderName, "MoveAndRenameUnhydratedFolderToNewFolder_renamed"); + this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(movedFolderPath)); + + this.Enlistment.GetVirtualPathTo(movedFolderPath).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldNotExistOnDisk(this.fileSystem); + + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(movedFolderPath, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase] + [Ignore("Disabled until moving partial folders is supported")] + public void MoveFolderWithUnhydratedAndFullContents() + { + string testFileName = "MoveFolderWithUnhydratedAndFullContents.cs"; + string oldFolderName = "Test_EPF_MoveRenameFolderTests\\MoveFolderWithUnhydratedAndFullContents"; + + string newFile = "TestFile.txt"; + string newFileContents = "Contents of TestFile.txt"; + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, newFile)), newFileContents); + + string newFolderName = "New_MoveFolderWithUnhydratedAndFullContents"; + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(newFolderName)); + this.Enlistment.GetVirtualPathTo(newFolderName).ShouldBeADirectory(this.fileSystem); + + string movedFolderPath = Path.Combine(newFolderName, "MoveFolderWithUnhydratedAndFullContents_renamed"); + this.fileSystem.MoveDirectory(this.Enlistment.GetVirtualPathTo(oldFolderName), this.Enlistment.GetVirtualPathTo(movedFolderPath)); + + this.Enlistment.GetVirtualPathTo(movedFolderPath).ShouldBeADirectory(this.fileSystem); + this.Enlistment.GetVirtualPathTo(oldFolderName).ShouldNotExistOnDisk(this.fileSystem); + + // Test file should have been moved + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, testFileName)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(movedFolderPath, testFileName)).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + + // New file should have been moved + this.Enlistment.GetVirtualPathTo(Path.Combine(oldFolderName, newFile)).ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.GetVirtualPathTo(Path.Combine(movedFolderPath, newFile)).ShouldBeAFile(this.fileSystem).WithContents(newFileContents); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs new file mode 100644 index 00000000..71f1deee --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs @@ -0,0 +1,35 @@ +using GVFS.FunctionalTests.Tools; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public abstract class TestsWithEnlistmentPerFixture + { + public TestsWithEnlistmentPerFixture() + { + } + + public GVFSFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [OneTimeSetUp] + public virtual void CreateEnlistment() + { + string pathToGvfs = Path.Combine(TestContext.CurrentContext.TestDirectory, Properties.Settings.Default.PathToGVFS); + this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(pathToGvfs); + } + + [OneTimeTearDown] + public virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.UnmountAndDeleteAll(); + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs new file mode 100644 index 00000000..d4e20d43 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorkingDirectoryTests.cs @@ -0,0 +1,463 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.InteropServices; +using System.Text; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class WorkingDirectoryTests : TestsWithEnlistmentPerFixture + { + private const int CurrentPlaceholderVersion = 1; + private const string TestFileContents = +@"// dllmain.cpp : Defines the entry point for the DLL application. +#include ""stdafx.h"" + +BOOL APIENTRY DllMain( HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + UNREFERENCED_PARAMETER(hModule); + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +"; + + private FileSystemRunner fileSystem; + + public WorkingDirectoryTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase, Order(1)] + public void ProjectedFileHasExpectedContents() + { + this.Enlistment.GetVirtualPathTo("Test_EPF_WorkingDirectoryTests\\ProjectedFileHasExpectedContents.cpp").ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase, Order(2)] + public void StreamAccessReadWriteMemoryMappedProjectedFile() + { + string filename = @"Test_EPF_WorkingDirectoryTests\StreamAccessReadWriteMemoryMappedProjectedFile.cs"; + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + string contents = fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(); + StringBuilder contentsBuilder = new StringBuilder(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + // Length of the Byte-order-mark that will be at the start of the memory mapped file. + // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx + int bomOffset = 3; + + // offset -> Number of bytes from the start of the file where the view starts + int offset = 64; + int size = contents.Length; + string newContent = "**NEWCONTENT**"; + + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size - offset + bomOffset)) + { + streamAccessor.CanRead.ShouldEqual(true); + streamAccessor.CanWrite.ShouldEqual(true); + + for (int i = offset; i < size - offset; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contents[i - bomOffset]); + } + + // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file) + streamAccessor.Seek(0, SeekOrigin.Begin); + byte[] newContentBuffer = Encoding.ASCII.GetBytes(newContent); + + streamAccessor.Write(newContentBuffer, 0, newContent.Length); + + for (int i = 0; i < newContent.Length; ++i) + { + contentsBuilder[offset + i - bomOffset] = newContent[i]; + } + + contents = contentsBuilder.ToString(); + } + + // Verify the file has the new contents inserted into it + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size + bomOffset)) + { + // Skip the BOM + for (int i = 0; i < bomOffset; ++i) + { + streamAccessor.ReadByte(); + } + + for (int i = 0; i < size; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contents[i]); + } + } + } + + // Confirm the new contents was written to disk + fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contents); + } + + [TestCase, Order(3)] + public void RandomAccessReadWriteMemoryMappedProjectedFile() + { + string filename = @"Test_EPF_WorkingDirectoryTests\RandomAccessReadWriteMemoryMappedProjectedFile.cs"; + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + + string contents = fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(); + StringBuilder contentsBuilder = new StringBuilder(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + // Length of the Byte-order-mark that will be at the start of the memory mapped file. + // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx + int bomOffset = 3; + + // offset -> Number of bytes from the start of the file where the view starts + int offset = 64; + int size = contents.Length; + string newContent = "**NEWCONTENT**"; + + using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset, size - offset + bomOffset)) + { + randomAccessor.CanRead.ShouldEqual(true); + randomAccessor.CanWrite.ShouldEqual(true); + + for (int i = 0; i < size - offset; ++i) + { + ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i + offset - bomOffset]); + } + + for (int i = 0; i < newContent.Length; ++i) + { + // Convert to byte before writing rather than writing as char, because char version will write a 16-bit + // unicode char + randomAccessor.Write(i, Convert.ToByte(newContent[i])); + ((char)randomAccessor.ReadByte(i)).ShouldEqual(newContent[i]); + } + + for (int i = 0; i < newContent.Length; ++i) + { + contentsBuilder[offset + i - bomOffset] = newContent[i]; + } + + contents = contentsBuilder.ToString(); + } + + // Verify the file has the new contents inserted into it + using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size + bomOffset)) + { + for (int i = 0; i < size; ++i) + { + ((char)randomAccessor.ReadByte(i + bomOffset)).ShouldEqual(contents[i]); + } + } + } + + // Confirm the new contents was written to disk + fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contents); + } + + [TestCase, Order(4)] + public void StreamAndRandomAccessReadWriteMemoryMappedProjectedFile() + { + string filename = @"Test_EPF_WorkingDirectoryTests\StreamAndRandomAccessReadWriteMemoryMappedProjectedFile.cs"; + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + + StringBuilder contentsBuilder = new StringBuilder(); + + // Length of the Byte-order-mark that will be at the start of the memory mapped file. + // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd374101(v=vs.85).aspx + int bomOffset = 3; + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + // The text length of StreamAndRandomAccessReadWriteMemoryMappedProjectedFile.cs was determined + // outside of this test so that the test would not hydrate the file before we access via MemoryMappedFile + int fileTextLength = 13762; + + int size = bomOffset + fileTextLength; + + int streamAccessWriteOffset = 64; + int randomAccessWriteOffset = 128; + + string newStreamAccessContent = "**NEW_STREAM_CONTENT**"; + string newRandomAccessConents = "&&NEW_RANDOM_CONTENT&&"; + + // Read (and modify) contents using stream accessor + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size)) + { + streamAccessor.CanRead.ShouldEqual(true); + streamAccessor.CanWrite.ShouldEqual(true); + + for (int i = 0; i < size; ++i) + { + contentsBuilder.Append((char)streamAccessor.ReadByte()); + } + + // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file) + streamAccessor.Seek(streamAccessWriteOffset, SeekOrigin.Begin); + byte[] newContentBuffer = Encoding.ASCII.GetBytes(newStreamAccessContent); + + streamAccessor.Write(newContentBuffer, 0, newStreamAccessContent.Length); + + for (int i = 0; i < newStreamAccessContent.Length; ++i) + { + contentsBuilder[streamAccessWriteOffset + i] = newStreamAccessContent[i]; + } + } + + // Read (and modify) contents using random accessor + using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size)) + { + randomAccessor.CanRead.ShouldEqual(true); + randomAccessor.CanWrite.ShouldEqual(true); + + // Confirm the random accessor reads the same content that was read (and written) by the stream + // accessor + for (int i = 0; i < size; ++i) + { + ((char)randomAccessor.ReadByte(i)).ShouldEqual(contentsBuilder[i]); + } + + // Write some new content + for (int i = 0; i < newRandomAccessConents.Length; ++i) + { + // Convert to byte before writing rather than writing as char, because char version will write a 16-bit + // unicode char + randomAccessor.Write(i + randomAccessWriteOffset, Convert.ToByte(newRandomAccessConents[i])); + ((char)randomAccessor.ReadByte(i + randomAccessWriteOffset)).ShouldEqual(newRandomAccessConents[i]); + } + + for (int i = 0; i < newRandomAccessConents.Length; ++i) + { + contentsBuilder[randomAccessWriteOffset + i] = newRandomAccessConents[i]; + } + } + + // Verify the file one more time with a stream accessor + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size)) + { + for (int i = 0; i < size; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contentsBuilder[i]); + } + } + } + + // Remove the BOM before comparing with the contents of the file on disk + contentsBuilder.Remove(0, bomOffset); + + // Confirm the new contents was written to the file + fileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(contentsBuilder.ToString()); + } + + [TestCase, Order(5)] + public void MoveProjectedFileToInvalidFolder() + { + string targetFolderName = "test_folder"; + string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName); + targetFolderVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + + string sourceFolderName = "Test_EPF_WorkingDirectoryTests"; + string testFileName = "MoveProjectedFileToInvalidFolder.config"; + string sourcePath = Path.Combine(sourceFolderName, testFileName); + string sourceVirtualPath = this.Enlistment.GetVirtualPathTo(sourcePath); + + string newTestFileVirtualPath = Path.Combine(targetFolderVirtualPath, testFileName); + + this.fileSystem.MoveFileShouldFail(sourceVirtualPath, newTestFileVirtualPath); + newTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + + sourceVirtualPath.ShouldBeAFile(this.fileSystem); + + targetFolderVirtualPath.ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase, Order(6)] + public void EnumerateAndReadDoesNotChangeEnumerationOrder() + { + string folderVirtualPath = this.Enlistment.GetVirtualPathTo("EnumerateAndReadTestFiles"); + NativeTests.EnumerateAndReadDoesNotChangeEnumerationOrder(folderVirtualPath).ShouldEqual(true); + folderVirtualPath.ShouldBeADirectory(this.fileSystem); + folderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems(); + } + + [TestCase, Order(7)] + public void HydratingFileUsesNameCaseFromRepo() + { + string fileName = "Readme.md"; + string parentFolderPath = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(fileName)); + parentFolderPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(fileName, StringComparison.Ordinal)); + + // Hydrate file with a request using different file name case + string wrongCaseFilePath = this.Enlistment.GetVirtualPathTo(fileName.ToUpper()); + string fileContents = wrongCaseFilePath.ShouldBeAFile(this.fileSystem).WithContents(); + + // File on disk should have original case projected from repo + parentFolderPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(fileName, StringComparison.Ordinal)); + } + + [TestCase, Order(8)] + public void HydratingNestedFileUsesNameCaseFromRepo() + { + string filePath = "GVFS\\FastFetch\\Properties\\AssemblyInfo.cs"; + string filePathAllCaps = filePath.ToUpper(); + string parentFolderVirtualPathAllCaps = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(filePathAllCaps)); + parentFolderVirtualPathAllCaps.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(Path.GetFileName(filePath), StringComparison.Ordinal)); + + // Hydrate file with a request using different file name case + string wrongCaseFilePath = this.Enlistment.GetVirtualPathTo(filePathAllCaps); + string fileContents = wrongCaseFilePath.ShouldBeAFile(this.fileSystem).WithContents(); + + // File on disk should have original case projected from repo + string parentFolderVirtualPath = this.Enlistment.GetVirtualPathTo(Path.GetDirectoryName(filePath)); + parentFolderVirtualPath.ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(Path.GetFileName(filePath), StringComparison.Ordinal)); + + // Confirm all folders up to root have the correct case + string parentFolderPath = Path.GetDirectoryName(filePath); + while (!string.IsNullOrWhiteSpace(parentFolderPath)) + { + string folderName = Path.GetFileName(parentFolderPath); + parentFolderPath = Path.GetDirectoryName(parentFolderPath); + this.Enlistment.GetVirtualPathTo(parentFolderPath).ShouldBeADirectory(this.fileSystem).WithItems().ShouldContainSingle(info => info.Name.Equals(folderName, StringComparison.Ordinal)); + } + } + + [TestCase, Order(9)] + public void WriteToHydratedFileAfterRemount() + { + string fileName = "Test_EPF_WorkingDirectoryTests\\WriteToHydratedFileAfterRemount.cpp"; + string virtualFilePath = this.Enlistment.GetVirtualPathTo(fileName); + string fileContents = virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents(); + + // Remount + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + string appendedText = "Text to append"; + this.fileSystem.AppendAllText(virtualFilePath, appendedText); + virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents + appendedText); + } + + [TestCase, Order(10)] + public void ReadDeepProjectedFile() + { + string testFilePath = "Test_EPF_WorkingDirectoryTests\\1\\2\\3\\4\\ReadDeepProjectedFile.cpp"; + this.Enlistment.GetVirtualPathTo(testFilePath).ShouldBeAFile(this.fileSystem).WithContents(TestFileContents); + } + + [TestCase, Order(11)] + public void FilePlaceHolderHasVersionInfo() + { + string sha = "BB1C8B9ADA90D6B8F6C88F12C6DDB07C186155BD"; + string virtualFilePath = this.Enlistment.GetVirtualPathTo("GVFlt_BugRegressionTest\\GVFlt_ModifyFileInScratchAndDir\\ModifyFileInScratchAndDir.txt"); + virtualFilePath.ShouldBeAFile(this.fileSystem).WithContents(); + + ProcessResult revParseHeadResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse HEAD"); + string commitID = revParseHeadResult.Output.Trim(); + + NativeTests.PlaceHolderHasVersionInfo(virtualFilePath, CurrentPlaceholderVersion, sha, commitID).ShouldEqual(true); + } + + [TestCase, Order(12), Ignore("Results in an access violation in the functional test on the build server")] + public void FolderPlaceHolderHasVersionInfo() + { + string virtualFilePath = this.Enlistment.GetVirtualPathTo("GVFlt_BugRegressionTest\\GVFlt_ModifyFileInScratchAndDir"); + + ProcessResult revParseHeadResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "rev-parse HEAD"); + string commitID = revParseHeadResult.Output.Trim(); + + NativeTests.PlaceHolderHasVersionInfo(virtualFilePath, CurrentPlaceholderVersion, string.Empty, commitID).ShouldEqual(true); + } + + [TestCase, Order(13)] + public void FolderContentsProjectedAfterFolderCreateAndCheckout() + { + string folderName = "GVFlt_MultiThreadTest"; + + // 575d597cf09b2cd1c0ddb4db21ce96979010bbcb did not have the folder GVFlt_MultiThreadTest + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout 575d597cf09b2cd1c0ddb4db21ce96979010bbcb"); + + string sparseFile = this.Enlistment.GetVirtualPathTo(@".git\info\sparse-checkout"); + sparseFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldNotContain(folderName); + + string virtualFolderPath = this.Enlistment.GetVirtualPathTo(folderName); + virtualFolderPath.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(virtualFolderPath); + + // b5fd7d23706a18cff3e2b8225588d479f7e51138 was the commit prior to deleting GVFLT_MultiThreadTest + // 692765: Note that test also validates case insensitivity as GVFlt_MultiThreadTest is named GVFLT_MultiThreadTest + // in this commit + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout b5fd7d23706a18cff3e2b8225588d479f7e51138"); + + this.Enlistment.GetVirtualPathTo(folderName + "\\OpenForReadsSameTime\\test").ShouldBeAFile(this.fileSystem).WithContents("123 \r\n"); + this.Enlistment.GetVirtualPathTo(folderName + "\\OpenForWritesSameTime\\test").ShouldBeAFile(this.fileSystem).WithContents("123 \r\n"); + } + + [TestCase, Order(14)] + public void FolderContentsCorrectAfterCreateNewFolderRenameAndCheckoutCommitWithSameFolder() + { + // f1bce402a7a980a8320f3f235cf8c8fdade4b17a is the commit prior to adding Test_EPF_MoveRenameFolderTests + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + + // Confirm that no other test has created this folder or put it in the sparse-checkout + string folderName = "Test_EPF_MoveRenameFolderTests"; + string folder = this.Enlistment.GetVirtualPathTo(folderName); + folder.ShouldNotExistOnDisk(this.fileSystem); + + string sparseFile = this.Enlistment.GetVirtualPathTo(@".git\info\sparse-checkout"); + sparseFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldNotContain(folderName); + + // Confirm sparse-checkout picks up renamed folder + string newFolder = this.Enlistment.GetVirtualPathTo("newFolder"); + this.fileSystem.CreateDirectory(newFolder); + this.fileSystem.MoveDirectory(newFolder, folder); + + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); + sparseFile.ShouldBeAFile(this.fileSystem).WithContents().ShouldContain(folderName); + + // Confirm that subfolders of Test_EPF_MoveRenameFolderTests are projected after switching back to this.ControlGitRepo.Commitish + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + Properties.Settings.Default.Commitish); + + folder.ShouldBeADirectory(this.fileSystem); + (folder + @"\ChangeUnhydratedFolderName\source\ChangeUnhydratedFolderName.cpp").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFolderTests.TestFileContents); + (folder + @"\MoveAndRenameUnhydratedFolderToNewFolder\MoveAndRenameUnhydratedFolderToNewFolder.cpp").ShouldBeAFile(this.fileSystem).WithContents(TestFileContents).WithContents(MoveRenameFolderTests.TestFileContents); + (folder + @"\MoveFolderWithUnhydratedAndFullContents\MoveFolderWithUnhydratedAndFullContents.cpp").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFolderTests.TestFileContents); + (folder + @"\MoveUnhydratedFolderToFullFolderInDotGitFolder\MoveUnhydratedFolderToFullFolderInDotGitFolder.cpp").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFolderTests.TestFileContents); + (folder + @"\MoveUnhydratedFolderToVirtualNTFSFolder\MoveUnhydratedFolderToVirtualNTFSFolder.cpp").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFolderTests.TestFileContents); + (folder + @"\RenameFolderShouldFail\source\RenameFolderShouldFail.cpp").ShouldBeAFile(this.fileSystem).WithContents(MoveRenameFolderTests.TestFileContents); + } + + private class NativeTests + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool EnumerateAndReadDoesNotChangeEnumerationOrder(string folderVirtualPath); + + [DllImport("GVFS.NativeTests.dll", CharSet = CharSet.Ansi)] + public static extern bool PlaceHolderHasVersionInfo( + string virtualPath, + int version, + [MarshalAs(UnmanagedType.LPWStr)]string sha, + [MarshalAs(UnmanagedType.LPWStr)]string commit); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs new file mode 100644 index 00000000..fa0e29bb --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs @@ -0,0 +1,70 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public class CaseOnlyFolderRenameTests : TestsWithEnlistmentPerTestCase + { + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + [Ignore("Disabled until moving partial folders is supported")] + public void CaseRenameFoldersAndRemountAndReanmeAgain(FileSystemRunner fileSystem) + { + // Projected folder without a physical folder + string parentFolderName = "GVFS"; + string oldGVFSSubFolderName = "GVFS"; + string oldGVFSSubFolderPath = Path.Combine(parentFolderName, oldGVFSSubFolderName); + string newGVFSSubFolderName = "gvfs"; + string newGVFSSubFolderPath = Path.Combine(parentFolderName, newGVFSSubFolderName); + + this.Enlistment.GetVirtualPathTo(oldGVFSSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(oldGVFSSubFolderName); + + // Use NativeMethods rather than the runner as it supports case-only rename + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(oldGVFSSubFolderPath), this.Enlistment.GetVirtualPathTo(newGVFSSubFolderPath)); + + this.Enlistment.GetVirtualPathTo(newGVFSSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newGVFSSubFolderName); + + // Projected folder with a physical folder + string oldTestsSubFolderName = "GVFS.FunctionalTests"; + string oldTestsSubFolderPath = Path.Combine(parentFolderName, oldTestsSubFolderName); + string newTestsSubFolderName = "gvfs.functionaltests"; + string newTestsSubFolderPath = Path.Combine(parentFolderName, newTestsSubFolderName); + + string fileToAdd = "NewFile.txt"; + string fileToAddContent = "This is new file text."; + string fileToAddPath = this.Enlistment.GetVirtualPathTo(Path.Combine(oldTestsSubFolderPath, fileToAdd)); + fileSystem.WriteAllText(fileToAddPath, fileToAddContent); + + this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(oldTestsSubFolderName); + + // Use NativeMethods rather than the runner as it supports case-only rename + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath)); + + this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newTestsSubFolderName); + + // Remount + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + this.Enlistment.GetVirtualPathTo(newGVFSSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newGVFSSubFolderName); + this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newTestsSubFolderName); + this.Enlistment.GetVirtualPathTo(Path.Combine(newTestsSubFolderPath, fileToAdd)).ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToAddContent); + + // Rename each folder again + string finalGVFSSubFolderName = "gvFS"; + string finalGVFSSubFolderPath = Path.Combine(parentFolderName, finalGVFSSubFolderName); + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(newGVFSSubFolderPath), this.Enlistment.GetVirtualPathTo(finalGVFSSubFolderPath)); + this.Enlistment.GetVirtualPathTo(finalGVFSSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(finalGVFSSubFolderName); + + string finalTestsSubFolderName = "gvfs.FunctionalTESTS"; + string finalTestsSubFolderPath = Path.Combine(parentFolderName, finalTestsSubFolderName); + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath)); + this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath).ShouldBeADirectory(fileSystem).WithCaseMatchingName(finalTestsSubFolderName); + this.Enlistment.GetVirtualPathTo(Path.Combine(finalTestsSubFolderPath, fileToAdd)).ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToAddContent); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedSparseExcludeTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedSparseExcludeTests.cs new file mode 100644 index 00000000..555878fc --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedSparseExcludeTests.cs @@ -0,0 +1,72 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Linq; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public class PersistedSparseExcludeTests : TestsWithEnlistmentPerTestCase + { + private const string FileToAdd = @"GVFS\TestAddFile.txt"; + private const string FileToUpdate = @"GVFS\GVFS\Program.cs"; + private const string FileToDelete = "Readme.md"; + private const string FolderToCreate = "PersistedSparseExcludeTests_NewFolder"; + private const string ExcludeFilePath = @".git\info\exclude"; + private const string SparseCheckoutFilePath = @".git\info\sparse-checkout"; + private static string[] expectedExcludeFileContents = new string[] + { + "!/*", + "!/GVFS", + "!/GVFS/*", + "*", + "!/PersistedSparseExcludeTests_NewFolder", + "!/PersistedSparseExcludeTests_NewFolder/*" + }; + private static string[] expectedSparseFileContents = new string[] + { + "/.gitattributes", + "/GVFS/GVFS/Program.cs", + "/GVFS/TestAddFile.txt", + "/Readme.md", + "/PersistedSparseExcludeTests_NewFolder/" + }; + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void ExcludeSparseFileSavedAfterRemount(FileSystemRunner fileSystem) + { + string fileToAdd = this.Enlistment.GetVirtualPathTo(FileToAdd); + fileSystem.WriteAllText(fileToAdd, "Contents for the new file"); + + string fileToUpdate = this.Enlistment.GetVirtualPathTo(FileToUpdate); + fileSystem.AppendAllText(fileToUpdate, "// Testing"); + + string fileToDelete = this.Enlistment.GetVirtualPathTo(FileToDelete); + fileSystem.DeleteFile(fileToDelete); + + string folderToCreate = this.Enlistment.GetVirtualPathTo(FolderToCreate); + fileSystem.CreateDirectory(folderToCreate); + + // Remount + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + string excludeFile = this.Enlistment.GetVirtualPathTo(ExcludeFilePath); + string excludeFileContents = excludeFile.ShouldBeAFile(fileSystem).WithContents(); + + excludeFileContents.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Where(x => !x.StartsWith("#")) // Exclude comments + .OrderBy(x => x) + .ShouldMatchInOrder(expectedExcludeFileContents.OrderBy(x => x)); + + string sparseFile = this.Enlistment.GetVirtualPathTo(SparseCheckoutFilePath); + sparseFile.ShouldBeAFile(fileSystem).WithContents() + .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Where(x => expectedSparseFileContents.Contains(x)) // Exclude extra entries for files hydrated during test + .OrderBy(x => x) + .ShouldMatchInOrder(expectedSparseFileContents.OrderBy(x => x)); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedWorkingDirectoryTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedWorkingDirectoryTests.cs new file mode 100644 index 00000000..a5bb59e7 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PersistedWorkingDirectoryTests.cs @@ -0,0 +1,189 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public class PersistedWorkingDirectoryTests : TestsWithEnlistmentPerTestCase + { + public void MountMatchesRemount() + { + List fileEntriesBefore = new List(Directory.EnumerateFileSystemEntries(this.Enlistment.RepoRoot)); + + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + List fileEntriesAfter = new List(Directory.EnumerateFileSystemEntries(this.Enlistment.RepoRoot)); + + fileEntriesBefore.Count.ShouldEqual(fileEntriesAfter.Count); + fileEntriesBefore.ShouldContain(fileEntriesAfter, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void PersistedDirectoryLazyLoad(FileSystemRunner fileSystem) + { + const string EnumerateDirectoryName = "GVFS\\GVFS"; + + string[] subFolders = new string[] + { + Path.Combine(EnumerateDirectoryName, "Properties"), + Path.Combine(EnumerateDirectoryName, "CommandLine") + }; + + string[] subFiles = new string[] + { + Path.Combine(EnumerateDirectoryName, "App.config"), + Path.Combine(EnumerateDirectoryName, "GitVirtualFileSystem.ico"), + Path.Combine(EnumerateDirectoryName, "GVFS.csproj"), + Path.Combine(EnumerateDirectoryName, "packages.config"), + Path.Combine(EnumerateDirectoryName, "Program.cs"), + Path.Combine(EnumerateDirectoryName, "Setup.iss") + }; + + string enumerateDirectoryPath = this.Enlistment.GetVirtualPathTo(EnumerateDirectoryName); + fileSystem.DirectoryExists(enumerateDirectoryPath).ShouldEqual(true); + + foreach (string folder in subFolders) + { + string directoryPath = this.Enlistment.GetVirtualPathTo(folder); + fileSystem.DirectoryExists(directoryPath).ShouldEqual(true); + } + + foreach (string file in subFiles) + { + string filePath = this.Enlistment.GetVirtualPathTo(file); + fileSystem.FileExists(filePath).ShouldEqual(true); + } + + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + foreach (string folder in subFolders) + { + string directoryPath = this.Enlistment.GetVirtualPathTo(folder); + fileSystem.DirectoryExists(directoryPath).ShouldEqual(true); + } + + foreach (string file in subFiles) + { + string filePath = this.Enlistment.GetVirtualPathTo(file); + fileSystem.FileExists(filePath).ShouldEqual(true); + } + } + + /// + /// This test is intentionally one monolithic test. Because we have to mount/remount to + /// test persistence, we want to save as much time in tests runs as possible by only + /// remounting once. + /// + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void PersistedDirectoryTests(FileSystemRunner fileSystem) + { + // Delete File Setup + string deleteFileName = ".gitattributes"; + string deleteFilepath = this.Enlistment.GetVirtualPathTo(deleteFileName); + fileSystem.DeleteFile(deleteFilepath); + + // Delete Folder Setup + string deleteFolderName = "GVFS\\GVFS"; + string deleteFolderPath = this.Enlistment.GetVirtualPathTo(deleteFolderName); + fileSystem.DeleteDirectory(deleteFolderPath); + + // Add File Setup + string fileToAdd = "NewFile.txt"; + string fileToAddContent = "This is new file text."; + string fileToAddPath = this.Enlistment.GetVirtualPathTo(fileToAdd); + fileSystem.WriteAllText(fileToAddPath, fileToAddContent); + + // Add Folder Setup + string directoryToAdd = "NewDirectory"; + string directoryToAddPath = this.Enlistment.GetVirtualPathTo(directoryToAdd); + fileSystem.CreateDirectory(directoryToAddPath); + + // Move File Setup + string fileToMove = this.Enlistment.GetVirtualPathTo("FileToMove.txt"); + string fileToMoveNewPath = this.Enlistment.GetVirtualPathTo("MovedFile.txt"); + string fileToMoveContent = "This is new file text."; + fileSystem.WriteAllText(fileToMove, fileToMoveContent); + fileSystem.MoveFile(fileToMove, fileToMoveNewPath); + + // Replace File Setup + string fileToReplace = this.Enlistment.GetVirtualPathTo("FileToReplace.txt"); + string fileToReplaceNewPath = this.Enlistment.GetVirtualPathTo("ReplacedFile.txt"); + string fileToReplaceContent = "This is new file text."; + string fileToReplaceOldContent = "This is very different file text."; + fileSystem.WriteAllText(fileToReplace, fileToReplaceContent); + fileSystem.WriteAllText(fileToReplaceNewPath, fileToReplaceOldContent); + fileSystem.ReplaceFile(fileToReplace, fileToReplaceNewPath); + + // MoveFolderPersistsOnRemount Setup + string directoryToMove = this.Enlistment.GetVirtualPathTo("MoveDirectory"); + string directoryMoveTarget = this.Enlistment.GetVirtualPathTo("MoveDirectoryTarget"); + string newDirectory = Path.Combine(directoryMoveTarget, "MoveDirectory_renamed"); + string childFile = Path.Combine(directoryToMove, "MoveFile.txt"); + string movedChildFile = Path.Combine(newDirectory, "MoveFile.txt"); + string moveFileContents = "This text file is getting moved"; + fileSystem.CreateDirectory(directoryToMove); + fileSystem.CreateDirectory(directoryMoveTarget); + fileSystem.WriteAllText(childFile, moveFileContents); + fileSystem.MoveDirectory(directoryToMove, newDirectory); + + // NestedLoadAndWriteAfterMount Setup + // Write a file to GVFS to ensure it has a physical folder + string childFileToAdd = "GVFS\\ChildFileToAdd.txt"; + string childFileToAddContent = "This is new child file in the GVFS folder."; + string childFileToAddPath = this.Enlistment.GetVirtualPathTo(childFileToAdd); + fileSystem.WriteAllText(childFileToAddPath, childFileToAddContent); + + // Remount + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + // Delete File Validation + deleteFilepath.ShouldNotExistOnDisk(fileSystem); + + // Delete Folder Validation + deleteFolderPath.ShouldNotExistOnDisk(fileSystem); + + // Add File Validation + fileToAddPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToAddContent); + + // Add Folder Validation + directoryToAddPath.ShouldBeADirectory(fileSystem); + + // Move File Validation + fileToMove.ShouldNotExistOnDisk(fileSystem); + fileToMoveNewPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToMoveContent); + + // Replace File Validation + fileToReplace.ShouldNotExistOnDisk(fileSystem); + fileToReplaceNewPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(fileToReplaceContent); + + // MoveFolderPersistsOnRemount Validation + directoryToMove.ShouldNotExistOnDisk(fileSystem); + + directoryMoveTarget.ShouldBeADirectory(fileSystem); + newDirectory.ShouldBeADirectory(fileSystem); + movedChildFile.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(moveFileContents); + + // NestedLoadAndWriteAfterMount Validation + childFileToAddPath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(childFileToAddContent); + string childFolder = "GVFS\\GVFS.FunctionalTests"; + string childFolderPath = this.Enlistment.GetVirtualPathTo(childFolder); + childFolderPath.ShouldBeADirectory(fileSystem); + string postMountChildFile = "PostMountChildFile.txt"; + string postMountChildFileContent = "This is new child file added after the mount"; + string postMountChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(childFolder, postMountChildFile)); + fileSystem.WriteAllText(postMountChildFilePath, postMountChildFileContent); // Verify we can create files in subfolders of GVFS + postMountChildFilePath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(postMountChildFileContent); + + // 663045 - Ensure that folder can be deleted after a new file is added and GVFS is remounted + fileSystem.DeleteDirectory(childFolderPath); + childFolderPath.ShouldNotExistOnDisk(fileSystem); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PrefetchVerbTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PrefetchVerbTests.cs new file mode 100644 index 00000000..ee9447e2 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/PrefetchVerbTests.cs @@ -0,0 +1,101 @@ +using GVFS.FunctionalTests.Category; +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + [Category(CategoryConstants.FastFetch)] + public class PrefetchVerbTests : TestsWithEnlistmentPerTestCase + { + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void PrefetchFetchesAtRootLevel(FileSystemRunner fileSystem) + { + // Root-level files and folders starting with R (currently just Readme.md) and gvflt\gvflt.nuspec + string output = this.Enlistment.PrefetchFolder("R;gvflt_fileeatest\\oneeaattributewillpass.txt"); + output.ShouldContain("\"TotalMissingObjects\":2"); + + // Note: It is expected to always have .gitattributes + HashSet expectedFiles = new HashSet(StringComparer.OrdinalIgnoreCase) + { + ".gitattributes", + "Readme.md", + "GVFlt_FileEATest/OneEAAttributeWillPass.txt" + }; + + this.AllFetchedFilePathsShouldPassCheck(expectedFiles.Contains); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void PrefetchIsAllowedToDoNothing(FileSystemRunner fileSystem) + { + string output = this.Enlistment.PrefetchFolder("NoFileHasThisName.IHope"); + output.ShouldContain("\"TotalMissingObjects\":0"); + + // It is expected to have .gitattributes files always. But that is all. + this.AllFetchedFilePathsShouldPassCheck(file => file.Equals(".gitattributes", StringComparison.OrdinalIgnoreCase)); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void PrefetchFetchesDirectoriesRecursively(FileSystemRunner fileSystem) + { + // Everything under the gvfs folder. Include some duplicates for variety. + string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file"); + File.WriteAllLines(tempFilePath, new[] { "gvfs/", "gvfs/gvfs", "gvfs/" }); + + string output = this.Enlistment.PrefetchFolderBasedOnFile(tempFilePath); + File.Delete(tempFilePath); + + output.ShouldContain("\"TotalMissingObjects\":283"); + + this.AllFetchedFilePathsShouldPassCheck(file => file.StartsWith("gvfs/", StringComparison.OrdinalIgnoreCase) + || file.Equals(".gitattributes", StringComparison.OrdinalIgnoreCase)); + } + + private void AllFetchedFilePathsShouldPassCheck(Func checkPath) + { + // Form a cache map of sha => path + string[] allObjects = GitProcess.Invoke(this.Enlistment.RepoRoot, "cat-file --batch-check --batch-all-objects").Split('\n'); + string[] gitlsLines = GitProcess.Invoke(this.Enlistment.RepoRoot, "ls-tree -r HEAD").Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + Dictionary> allPaths = new Dictionary>(); + foreach (string line in gitlsLines) + { + string sha = this.GetShaFromLsLine(line); + string path = this.GetPathFromLsLine(line); + if (!allPaths.ContainsKey(sha)) + { + allPaths.Add(sha, new List()); + } + + allPaths[sha].Add(path); + } + + foreach (string sha in allObjects.Where(line => line.Contains(" blob ")).Select(line => line.Substring(0, 40))) + { + allPaths.ContainsKey(sha).ShouldEqual(true, "Found a blob that wasn't in the tree: " + sha); + + // A single blob should map to multiple files, so if any pass for a single sha, we have to give a pass. + allPaths[sha].Any(path => checkPath(path)).ShouldEqual(true); + } + } + + private string GetShaFromLsLine(string line) + { + string output = line.Substring(line.LastIndexOf('\t') - 40, 40); + return output; + } + + private string GetPathFromLsLine(string line) + { + int idx = line.LastIndexOf('\t') + 1; + string output = line.Substring(idx, line.Length - idx); + return output; + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RebaseTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RebaseTests.cs new file mode 100644 index 00000000..4705e498 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RebaseTests.cs @@ -0,0 +1,68 @@ +using GVFS.FunctionalTests.Tools; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public class RebaseTests : TestsWithEnlistmentPerTestCase + { + public override void CreateEnlistment() + { + base.CreateEnlistment(); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.abbrev 12"); + } + + public override void DeleteEnlistment() + { + base.DeleteEnlistment(); + } + + [TestCase] + public void RebaseSmallNoConflicts() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; + + ControlGitRepo controlGitRepo = ControlGitRepo.Create(); + controlGitRepo.Initialize(); + controlGitRepo.Fetch(sourceCommit); + controlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand(controlGitRepo, "checkout {0}", sourceCommit); + this.ValidateGitCommand(controlGitRepo, "rebase {0}", targetCommit); + } + + [TestCase] + public void RebaseSmallOneFileConflict() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 99fc72275f950b0052c8548bbcf83a851f2b4467 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "99fc72275f950b0052c8548bbcf83a851f2b4467"; + + ControlGitRepo controlGitRepo = ControlGitRepo.Create(); + controlGitRepo.Initialize(); + controlGitRepo.Fetch(sourceCommit); + controlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand(controlGitRepo, "checkout {0}", sourceCommit); + this.ValidateGitCommand(controlGitRepo, "rebase {0}", targetCommit); + } + + private void ValidateGitCommand(ControlGitRepo controlGitRepo, string command, params object[] args) + { + GitHelpers.ValidateGitCommand( + this.Enlistment, + controlGitRepo, + command, + args); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs new file mode 100644 index 00000000..9981d2c2 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs @@ -0,0 +1,31 @@ +using GVFS.FunctionalTests.Tools; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public abstract class TestsWithEnlistmentPerTestCase + { + public GVFSFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [SetUp] + public virtual void CreateEnlistment() + { + string pathToGvfs = Path.Combine(TestContext.CurrentContext.TestDirectory, Properties.Settings.Default.PathToGVFS); + this.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(pathToGvfs); + } + + [TearDown] + public virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.UnmountAndDeleteAll(); + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs b/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs new file mode 100644 index 00000000..75e67ecf --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/FastFetchTests.cs @@ -0,0 +1,178 @@ +using GVFS.FunctionalTests.Category; +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Properties; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; + +namespace GVFS.FunctionalTests.Tests +{ + [TestFixture] + [Category(CategoryConstants.FastFetch)] + public class FastFetchTests + { + private readonly string fastFetchRepoRoot = Settings.Default.FastFetchRoot; + private readonly string fastFetchControlRoot = Settings.Default.FastFetchControl; + + [OneTimeSetUp] + public void InitControlRepo() + { + Directory.CreateDirectory(this.fastFetchControlRoot); + GitProcess.Invoke("C:\\", "clone -b " + Settings.Default.Commitish + " " + Settings.Default.RepoToClone + " " + this.fastFetchControlRoot); + } + + [SetUp] + public void InitRepo() + { + Directory.CreateDirectory(this.fastFetchRepoRoot); + GitProcess.Invoke(this.fastFetchRepoRoot, "init"); + GitProcess.Invoke(this.fastFetchRepoRoot, "remote add origin " + Settings.Default.RepoToClone); + } + + [TearDown] + public void TearDownTests() + { + SystemIORunner.RecursiveDelete(this.fastFetchRepoRoot); + } + + [OneTimeTearDown] + public void DeleteControlRepo() + { + SystemIORunner.RecursiveDelete(this.fastFetchControlRoot); + } + + [TestCase] + public void CanFetchIntoEmptyGitRepoAndCheckoutWithGit() + { + this.RunFastFetch("-b " + Settings.Default.Commitish); + + // Ensure origin/master has been created + this.GetRefTreeSha("remotes/origin/" + Settings.Default.Commitish).ShouldNotBeNull(); + + ProcessResult checkoutResult = GitProcess.InvokeProcess(this.fastFetchRepoRoot, "checkout " + Settings.Default.Commitish); + checkoutResult.Errors.ShouldEqual("Switched to a new branch '" + Settings.Default.Commitish + "'\r\n"); + checkoutResult.Output.ShouldEqual("Branch " + Settings.Default.Commitish + " set up to track remote branch " + Settings.Default.Commitish + " from origin.\n"); + + // When checking out with git, must manually update shallow. + ProcessResult updateRefResult = GitProcess.InvokeProcess(this.fastFetchRepoRoot, "update-ref shallow " + Settings.Default.Commitish); + updateRefResult.ExitCode.ShouldEqual(0); + updateRefResult.Errors.ShouldBeEmpty(); + updateRefResult.Output.ShouldBeEmpty(); + + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + + this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner) + .WithDeepStructure(this.fastFetchControlRoot); + } + + [TestCase] + public void CanFetchAndCheckoutBranchIntoEmptyGitRepo() + { + this.RunFastFetch("--checkout -b " + Settings.Default.Commitish); + + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + + this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner) + .WithDeepStructure(this.fastFetchControlRoot); + } + + [TestCase] + public void CanFetchAndCheckoutCommitIntoEmptyGitRepo() + { + // Get the commit sha for the branch the control repo is on + string commitSha = GitProcess.Invoke(this.fastFetchControlRoot, "log -1 --format=%H").Trim(); + + this.RunFastFetch("--checkout -c " + commitSha); + + string headFilePath = Path.Combine(this.fastFetchRepoRoot, TestConstants.DotGit.Head); + File.ReadAllText(headFilePath).Trim().ShouldEqual(commitSha); + + // Ensure no errors are thrown with git log + GitHelpers.CheckGitCommand(this.fastFetchRepoRoot, "log"); + + this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner) + .WithDeepStructure(this.fastFetchControlRoot); + } + + [TestCase] + public void CanFetchAndCheckoutBetweenTwoBranchesIntoEmptyGitRepo() + { + this.RunFastFetch("--checkout -b " + Settings.Default.Commitish); + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + + // Switch to master + this.RunFastFetch("--checkout -b master"); + this.CurrentBranchShouldEqual("master"); + + // And back + this.RunFastFetch("--checkout -b " + Settings.Default.Commitish); + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + + this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner) + .WithDeepStructure(this.fastFetchControlRoot); + } + + [TestCase] + public void CanDetectAlreadyUpToDate() + { + this.RunFastFetch("--checkout -b " + Settings.Default.Commitish); + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + + this.RunFastFetch(" -b " + Settings.Default.Commitish).ShouldContain("\"TotalMissingObjects\":0"); + this.RunFastFetch("--checkout -b " + Settings.Default.Commitish).ShouldContain("\"RequiredBlobsCount\":0"); + + this.CurrentBranchShouldEqual(Settings.Default.Commitish); + this.fastFetchRepoRoot.ShouldBeADirectory(FileSystemRunner.DefaultRunner) + .WithDeepStructure(this.fastFetchControlRoot); + } + + private void CurrentBranchShouldEqual(string commitish) + { + // Ensure remote branch has been created + this.GetRefTreeSha("remotes/origin/" + commitish).ShouldNotBeNull(); + + // And head has been updated to local branch, which are both updated + this.GetRefTreeSha("HEAD") + .ShouldNotBeNull() + .ShouldEqual(this.GetRefTreeSha(commitish)); + + // Ensure no errors are thrown with git log + GitHelpers.CheckGitCommand(this.fastFetchRepoRoot, "log"); + } + + private string GetRefTreeSha(string refName) + { + string headInfo = GitProcess.Invoke(this.fastFetchRepoRoot, "cat-file -p " + refName); + if (string.IsNullOrEmpty(headInfo) || headInfo.EndsWith("missing")) + { + return null; + } + + string[] headInfoLines = headInfo.Split('\n'); + headInfoLines[0].StartsWith("tree").ShouldEqual(true); + int firstSpace = headInfoLines[0].IndexOf(' '); + string headTreeSha = headInfoLines[0].Substring(firstSpace + 1); + headTreeSha.Length.ShouldEqual(40); + return headTreeSha; + } + + private string RunFastFetch(string args) + { + ProcessStartInfo processInfo = new ProcessStartInfo("fastfetch.exe"); + processInfo.Arguments = args; + processInfo.WorkingDirectory = this.fastFetchRepoRoot; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + result.Output.Contains("Error").ShouldEqual(false); + result.Errors.ShouldBeEmpty(); + result.ExitCode.ShouldEqual(0); + return result.Output; + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitMoveRenameTests.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitMoveRenameTests.cs new file mode 100644 index 00000000..4a6ce692 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitMoveRenameTests.cs @@ -0,0 +1,149 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class GitMoveRenameTests : TestsWithLongRunningEnlistment + { + private string testFileContents = "0123456789"; + private FileSystemRunner fileSystem; + public GitMoveRenameTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase, Order(1)] + public void GitStatus() + { + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "nothing to commit, working tree clean"); + } + + [TestCase, Order(2)] + public void GitStatusAfterNewFile() + { + string filename = "new.cs"; + + this.fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), this.testFileContents); + + this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + filename); + } + + [TestCase, Order(3)] + public void GitStatusAfterFileNameCaseChange() + { + string oldFilename = "new.cs"; + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(this.fileSystem); + + string newFilename = "New.cs"; + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + newFilename); + } + + [TestCase, Order(4)] + public void GitStatusAfterFileRename() + { + string oldFilename = "New.cs"; + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(this.fileSystem); + + string newFilename = "test.cs"; + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + newFilename); + } + + [TestCase, Order(5)] + public void GitStatusAndObjectAfterGitAdd() + { + string existingFilename = "test.cs"; + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "add " + existingFilename, + new string[] { }); + + // Status should be correct + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Changes to be committed:", + existingFilename); + + // Object file for the test file should have the correct contents + ProcessResult result = GitProcess.InvokeProcess( + this.Enlistment.RepoRoot, + "hash-object " + existingFilename); + + string objectHash = result.Output.Trim(); + result.Errors.ShouldBeEmpty(); + + this.Enlistment.GetObjectPathTo(objectHash).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "cat-file -p " + objectHash, + this.testFileContents); + } + + [TestCase, Order(6)] + public void GitStatusAfterUnstage() + { + string existingFilename = "test.cs"; + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "reset HEAD " + existingFilename, + new string[] { }); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + existingFilename); + } + + [TestCase, Order(7)] + public void GitStatusAfterFileDelete() + { + string existingFilename = "test.cs"; + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); + this.fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(existingFilename)); + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); + + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "nothing to commit, working tree clean"); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitObjectManipulationTests.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitObjectManipulationTests.cs new file mode 100644 index 00000000..7afe9dc1 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitObjectManipulationTests.cs @@ -0,0 +1,102 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public class GitObjectManipulationTests : TestsWithLongRunningEnlistment + { + private FileSystemRunner fileSystem; + public GitObjectManipulationTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase] + public void PackFileWritesAreRedirectedToLocalAlternate() + { + string filename = "pack-e145421ff608e7f956de40e77ef948d26432913c.pack"; + string virtualPath = Path.Combine(this.Enlistment.VirtualRepoRoot, ".git", "objects", "pack", filename); + string physicalPath = Path.Combine(this.Enlistment.LocalAlternateRoot, ".git", "objects", "pack", filename); + + virtualPath.ShouldNotExistOnDisk(this.fileSystem); + physicalPath.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.WriteAllText(virtualPath, "any ol' contents"); + + virtualPath.ShouldBeAFile(this.fileSystem); + physicalPath.ShouldBeAFile(this.fileSystem); + + string repoPath = Path.Combine(this.Enlistment.PhysicalRepoRoot, ".git", "objects", "pack", filename); + repoPath.ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.DeleteFile(virtualPath); + } + + [TestCase] + public void LooseObjectWritesAreRedirectedToLocalAlternate() + { + string firstTwoletters = "e1"; + string rest = "45421ff608e7f956de40e77ef948d26432913c"; + + // Assert that creating a two letter folder in .git\objects ends up only in the local alternate + string virtualFolder = Path.Combine(this.Enlistment.VirtualRepoRoot, ".git", "objects", firstTwoletters); + string physicalFolder = Path.Combine(this.Enlistment.LocalAlternateRoot, ".git", "objects", firstTwoletters); + + virtualFolder.ShouldNotExistOnDisk(this.fileSystem); + physicalFolder.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.CreateDirectory(virtualFolder); + + virtualFolder.ShouldBeAFile(this.fileSystem); + physicalFolder.ShouldBeAFile(this.fileSystem); + + string repoFolder = Path.Combine(this.Enlistment.PhysicalRepoRoot, ".git", "objects", "pack", firstTwoletters, rest); + repoFolder.ShouldNotExistOnDisk(this.fileSystem); + + // Assert that creating a file in the folder above ends up only in the local alternate + string virtualPath = Path.Combine(virtualFolder, rest); + string physicalPath = Path.Combine(physicalFolder, rest); + + virtualPath.ShouldNotExistOnDisk(this.fileSystem); + physicalPath.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.WriteAllText(virtualPath, "any ol' contents"); + + virtualPath.ShouldBeAFile(this.fileSystem); + physicalPath.ShouldBeAFile(this.fileSystem); + + Path.Combine(repoFolder, rest).ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.DeleteFile(virtualPath); + this.fileSystem.DeleteDirectory(virtualFolder); + } + + [TestCase] + public void NormalFileWritesAreNotRedirectedToLocalAlternate() + { + string filename = "AWeirdRandom.GitObjectsfile"; + string virtualPath = Path.Combine(this.Enlistment.VirtualRepoRoot, ".git", "objects", filename); + string alternatePath = Path.Combine(this.Enlistment.LocalAlternateRoot, ".git", "objects", filename); + string repoPath = Path.Combine(this.Enlistment.PhysicalRepoRoot, ".git", "objects", filename); + + virtualPath.ShouldNotExistOnDisk(this.fileSystem); + alternatePath.ShouldNotExistOnDisk(this.fileSystem); + repoPath.ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.WriteAllText(virtualPath, "any ol' contents"); + + virtualPath.ShouldBeAFile(this.fileSystem); + alternatePath.ShouldNotExistOnDisk(this.fileSystem); + repoPath.ShouldBeAFile(this.fileSystem); + + this.fileSystem.DeleteFile(virtualPath); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitReadAndGitLockTests.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitReadAndGitLockTests.cs new file mode 100644 index 00000000..4464bb2e --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/GitReadAndGitLockTests.cs @@ -0,0 +1,64 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixture] + public class GitReadAndGitLockTests : TestsWithLongRunningEnlistment + { + private FileSystemRunner fileSystem; + + public GitReadAndGitLockTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase, Order(1)] + public void GitStatus() + { + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "nothing to commit, working tree clean"); + } + + [TestCase, Order(2)] + public void GitLog() + { + GitHelpers.CheckGitCommand(this.Enlistment.RepoRoot, "log -n1", "commit", "Author:", "Date:"); + } + + [TestCase, Order(3)] + public void GitBranch() + { + GitHelpers.CheckGitCommand( + this.Enlistment.RepoRoot, + "branch -a", + "* " + Properties.Settings.Default.Commitish, + "remotes/origin/" + Properties.Settings.Default.Commitish); + } + + [TestCase, Order(4)] + public void GitCommandWaitsWhileAnotherIsRunning() + { + GitHelpers.AcquireGVFSLock(this.Enlistment, resetTimeout: 3000); + + ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status", cleanOutput: false); + statusWait.Output.ShouldContain("Waiting for 'git hash-object --stdin' to release the lock."); + } + + [TestCase, Order(5)] + public void GitAliasNamedAfterKnownCommandAcquiresLock() + { + string alias = nameof(this.GitAliasNamedAfterKnownCommandAcquiresLock); + + GitHelpers.AcquireGVFSLock(this.Enlistment, resetTimeout: 3000); + GitHelpers.CheckGitCommand(this.Enlistment.RepoRoot, "config --local alias." + alias + " status"); + ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, alias, cleanOutput: false); + statusWait.Output.ShouldContain("Waiting for 'git hash-object --stdin' to release the lock."); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/LongRunningSetup.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/LongRunningSetup.cs new file mode 100644 index 00000000..103097fa --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/LongRunningSetup.cs @@ -0,0 +1,32 @@ +using GVFS.FunctionalTests.Tools; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [SetUpFixture] + public class LongRunningSetup + { + public static GVFSFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [OneTimeSetUp] + public void CreateEnlistmentAndMount() + { + string pathToGvfs = Path.Combine(TestContext.CurrentContext.TestDirectory, Properties.Settings.Default.PathToGVFS); + LongRunningSetup.Enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(pathToGvfs); + } + + [OneTimeTearDown] + public void UnmountAndDeleteEnlistment() + { + if (LongRunningSetup.Enlistment != null) + { + LongRunningSetup.Enlistment.UnmountAndDeleteAll(); + LongRunningSetup.Enlistment = null; + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/MultithreadedReadWriteTests.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/MultithreadedReadWriteTests.cs new file mode 100644 index 00000000..d4ceb8a9 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/MultithreadedReadWriteTests.cs @@ -0,0 +1,143 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.IO; +using System.Text; +using System.Threading; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixture] + public class MultithreadedReadWriteTests : TestsWithLongRunningEnlistment + { + [TestCase] + public void CanReadUnhydratedFileInParallelWithoutTearing() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string fileName = @"GVFS\GVFS.FunctionalTests\Tests\LongRunningEnlistment\WorkingDirectoryTests.cs"; + string virtualPath = this.Enlistment.GetVirtualPathTo(fileName); + virtualPath.ShouldBeAFile(fileSystem); + + // Not using the runner because reading specific bytes isn't common + // Can't use ReadAllText because it will remove some bytes that the stream won't. + byte[] actualContents = File.ReadAllBytes(virtualPath); + + Thread[] threads = new Thread[4]; + + // Readers + bool keepRunning = true; + for (int i = 0; i < threads.Length; ++i) + { + int myIndex = i; + threads[i] = new Thread(() => + { + // Create random seeks (seeded for repeatability) + Random randy = new Random(myIndex); + + // Small buffer so we hit the drive a lot. + // Block larger than the buffer to hit the drive more + const int SmallBufferSize = 128; + const int LargerBlockSize = SmallBufferSize * 10; + + using (Stream reader = new FileStream(virtualPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, SmallBufferSize, false)) + { + while (keepRunning) + { + byte[] block = new byte[LargerBlockSize]; + + // Always try to grab a full block (easier for asserting) + int position = randy.Next((int)reader.Length - block.Length - 1); + + reader.Position = position; + reader.Read(block, 0, block.Length).ShouldEqual(block.Length); + block.ShouldEqual(actualContents, position, block.Length); + } + } + }); + + threads[i].Start(); + } + + Thread.Sleep(2500); + keepRunning = false; + + for (int i = 0; i < threads.Length; ++i) + { + threads[i].Join(); + } + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void CanReadWriteAFileInParallel(FileSystemRunner fileSystem) + { + string fileName = @"CanReadWriteAFileInParallel"; + string virtualPath = this.Enlistment.GetVirtualPathTo(fileName); + + // Create the file new each time. + virtualPath.ShouldNotExistOnDisk(fileSystem); + File.Create(virtualPath).Dispose(); + + bool keepRunning = true; + Thread[] threads = new Thread[4]; + StringBuilder[] fileContents = new StringBuilder[4]; + + // Writer + fileContents[0] = new StringBuilder(); + threads[0] = new Thread(() => + { + DateTime start = DateTime.Now; + Random r = new Random(0); // Seeded for repeatability + while ((DateTime.Now - start).TotalSeconds < 2.5) + { + string newChar = r.Next(10).ToString(); + fileSystem.AppendAllText(virtualPath, newChar); + fileContents[0].Append(newChar); + Thread.Yield(); + } + + keepRunning = false; + }); + + // Readers + for (int i = 1; i < threads.Length; ++i) + { + int myIndex = i; + fileContents[i] = new StringBuilder(); + threads[i] = new Thread(() => + { + using (Stream readStream = File.Open(virtualPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (StreamReader reader = new StreamReader(readStream, true)) + { + while (keepRunning) + { + Thread.Yield(); + fileContents[myIndex].Append(reader.ReadToEnd()); + } + + // Catch the last write that might have escaped us + fileContents[myIndex].Append(reader.ReadToEnd()); + } + }); + } + + foreach (Thread thread in threads) + { + thread.Start(); + } + + foreach (Thread thread in threads) + { + thread.Join(); + } + + for (int i = 1; i < threads.Length; ++i) + { + fileContents[i].ToString().ShouldEqual(fileContents[0].ToString()); + } + + fileSystem.DeleteFile(virtualPath); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/TestsWithLongRunningEnlistment.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/TestsWithLongRunningEnlistment.cs new file mode 100644 index 00000000..4197da8c --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/TestsWithLongRunningEnlistment.cs @@ -0,0 +1,14 @@ +using GVFS.FunctionalTests.Tools; +using NUnit.Framework; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixture] + public class TestsWithLongRunningEnlistment + { + public GVFSFunctionalTestEnlistment Enlistment + { + get { return LongRunningSetup.Enlistment; } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/WorkingDirectoryTests.cs b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/WorkingDirectoryTests.cs new file mode 100644 index 00000000..371830e3 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/LongRunningEnlistment/WorkingDirectoryTests.cs @@ -0,0 +1,2520 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace GVFS.FunctionalTests.Tests.LongRunningEnlistment +{ + [TestFixture] + public class WorkingDirectoryTests : TestsWithLongRunningEnlistment + { + private enum CreationDisposition + { + CreateNew = 1, // CREATE_NEW + CreateAlways = 2, // CREATE_ALWAYS + OpenExisting = 3, // OPEN_EXISTING + OpenAlways = 4, // OPEN_ALWAYS + TruncateExisting = 5 // TRUNCATE_EXISTING + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void ShrinkFileContents(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "ShrinkFileContents"); + string originalVirtualContents = "0123456789"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), originalVirtualContents); + this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(originalVirtualContents); + + string newText = "112233"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), newText); + this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(newText); + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(filename)); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void GrowFileContents(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "GrowFileContents"); + string originalVirtualContents = "112233"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), originalVirtualContents); + this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(originalVirtualContents); + + string newText = "0123456789"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(filename), newText); + this.Enlistment.GetVirtualPathTo(filename).ShouldBeAFile(fileSystem).WithContents(newText); + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(filename)); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void FilesAreBufferedAndCanBeFlushed(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "FilesAreBufferedAndCanBeFlushed"); + string filePath = this.Enlistment.GetVirtualPathTo(filename); + + byte[] buffer = System.Text.Encoding.ASCII.GetBytes("Some test data"); + + using (FileStream writeStream = File.Open(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + writeStream.Write(buffer, 0, buffer.Length); + + using (FileStream readStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + readStream.Length.ShouldEqual(0); + writeStream.Flush(); + readStream.Length.ShouldEqual(buffer.Length); + + byte[] readBuffer = new byte[buffer.Length]; + readStream.Read(readBuffer, 0, readBuffer.Length).ShouldEqual(readBuffer.Length); + readBuffer.ShouldMatchInOrder(buffer); + } + } + + fileSystem.DeleteFile(filePath); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void FileAttributesAreUpdated(string parentFolder) + { + string filename = Path.Combine(parentFolder, "FileAttributesAreUpdated"); + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string virtualFile = this.Enlistment.GetVirtualPathTo(filename); + virtualFile.ShouldNotExistOnDisk(fileSystem); + + File.Create(virtualFile).Dispose(); + virtualFile.ShouldBeAFile(fileSystem); + + // Update defaults. FileInfo is not batched, so each of these will create a separate Open-Update-Close set. + FileInfo before = new FileInfo(virtualFile); + DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); + before.CreationTime = testValue; + before.LastAccessTime = testValue; + before.LastWriteTime = testValue; + before.Attributes = FileAttributes.Hidden; + + // FileInfo caches information. We can refresh, but just to be absolutely sure... + virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue, FileAttributes.Hidden); + + File.Delete(virtualFile); + virtualFile.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void FolderAttributesAreUpdated(string parentFolder) + { + string folderName = Path.Combine(parentFolder, "FolderAttributesAreUpdated"); + string virtualFolder = this.Enlistment.GetVirtualPathTo(folderName); + Directory.CreateDirectory(virtualFolder); + + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + virtualFolder.ShouldBeADirectory(fileSystem); + + // Update defaults. DirectoryInfo is not batched, so each of these will create a separate Open-Update-Close set. + DirectoryInfo before = new DirectoryInfo(virtualFolder); + DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); + before.CreationTime = testValue; + before.LastAccessTime = testValue; + before.LastWriteTime = testValue; + before.Attributes = FileAttributes.Hidden; + + // DirectoryInfo caches information. We can refresh, but just to be absolutely sure... + virtualFolder.ShouldBeADirectory(fileSystem) + .WithInfo(testValue, testValue, testValue, FileAttributes.Hidden | FileAttributes.Directory); + + Directory.Delete(virtualFolder); + } + + [TestCase] + public void UnhydratedFileAttributesAreUpdated() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = @"GVFS\GVFS\GVFS.csproj"; + string virtualFile = this.Enlistment.GetVirtualPathTo(filename); + + // Update defaults. FileInfo is not batched, so each of these will create a separate Open-Update-Close set. + FileInfo before = new FileInfo(virtualFile); + DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); + before.CreationTime = testValue; + before.LastAccessTime = testValue; + before.LastWriteTime = testValue; + before.Attributes = FileAttributes.Hidden; + + // FileInfo caches information. We can refresh, but just to be absolutely sure... + virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue, FileAttributes.Hidden); + } + + [TestCase] + public void UnhydratedFolderAttributesAreUpdated() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string folderName = @"GVFS\GVFS\CommandLine"; + string virtualFolder = this.Enlistment.GetVirtualPathTo(folderName); + + // Update defaults. DirectoryInfo is not batched, so each of these will create a separate Open-Update-Close set. + DirectoryInfo before = new DirectoryInfo(virtualFolder); + DateTime testValue = DateTime.Now + TimeSpan.FromDays(1); + before.CreationTime = testValue; + before.LastAccessTime = testValue; + before.LastWriteTime = testValue; + before.Attributes = FileAttributes.Hidden; + + // DirectoryInfo caches information. We can refresh, but just to be absolutely sure... + virtualFolder.ShouldBeADirectory(fileSystem) + .WithInfo(testValue, testValue, testValue, FileAttributes.Hidden | FileAttributes.Directory); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void CannotWriteToReadOnlyFile(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "CannotWriteToReadOnlyFile"); + string virtualFilePath = this.Enlistment.GetVirtualPathTo(filename); + virtualFilePath.ShouldNotExistOnDisk(fileSystem); + + // Write initial contents + string originalContents = "Contents of ReadOnly file"; + fileSystem.WriteAllText(virtualFilePath, originalContents); + virtualFilePath.ShouldBeAFile(fileSystem).WithContents(originalContents); + + // Make file read only + FileInfo fileInfo = new FileInfo(virtualFilePath); + fileInfo.Attributes = FileAttributes.ReadOnly; + + // Verify that file cannot be written to + string newContents = "New contents for file"; + fileSystem.WriteAllTextShouldFail(virtualFilePath, newContents); + virtualFilePath.ShouldBeAFile(fileSystem).WithContents(originalContents); + + // Cleanup + fileInfo.Attributes = FileAttributes.Normal; + fileSystem.DeleteFile(virtualFilePath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void ReadonlyCanBeSetAndUnset(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "ReadonlyCanBeSetAndUnset"); + string virtualFilePath = this.Enlistment.GetVirtualPathTo(filename); + virtualFilePath.ShouldNotExistOnDisk(fileSystem); + + string originalContents = "Contents of ReadOnly file"; + fileSystem.WriteAllText(virtualFilePath, originalContents); + + // Make file read only + FileInfo fileInfo = new FileInfo(virtualFilePath); + fileInfo.Attributes = FileAttributes.ReadOnly; + virtualFilePath.ShouldBeAFile(fileSystem).WithAttribute(FileAttributes.ReadOnly); + + // Clear read only + fileInfo.Attributes = FileAttributes.Normal; + virtualFilePath.ShouldBeAFile(fileSystem).WithoutAttribute(FileAttributes.ReadOnly); + + // Cleanup + fileSystem.DeleteFile(virtualFilePath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void ChangeVirtualNTFSFileNameCase(FileSystemRunner fileSystem, string parentFolder) + { + string oldFilename = Path.Combine(parentFolder, "ChangePhysicalFileNameCase.txt"); + string newFilename = Path.Combine(parentFolder, "changephysicalfilenamecase.txt"); + string fileContents = "Hello World"; + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); + + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(oldFilename), fileContents); + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithCaseMatchingName(Path.GetFileName(oldFilename)); + + fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithCaseMatchingName(Path.GetFileName(newFilename)); + + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(newFilename)); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void ChangeVirtualNTFSFileName(FileSystemRunner fileSystem, string parentFolder) + { + string oldFilename = Path.Combine(parentFolder, "ChangePhysicalFileName.txt"); + string newFilename = Path.Combine(parentFolder, "NewFileName.txt"); + string fileContents = "Hello World"; + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); + + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(oldFilename), fileContents); + this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldNotExistOnDisk(fileSystem); + + fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + this.Enlistment.GetVirtualPathTo(newFilename).ShouldBeAFile(fileSystem).WithContents(fileContents); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, oldFilename, parentFolder); + + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(newFilename)); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, newFilename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void MoveVirtualNTFSFileToVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); + + string testFileName = Path.Combine(parentFolder, "test.txt"); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); + this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newTestFileVirtualPath = Path.Combine( + this.Enlistment.GetVirtualPathTo(testFolderName), + Path.GetFileName(testFileName)); + + fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFileName, parentFolder); + newTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.DeleteFile(newTestFileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, Path.Combine(testFolderName, Path.GetFileName(testFileName)), parentFolder); + + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void MoveWorkingDirectoryFileToDotGitFolder(FileSystemRunner fileSystem) + { + string testFolderName = ".git"; + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); + + string testFileName = "test.txt"; + this.Enlistment.GetVirtualPathTo(testFileName).ShouldNotExistOnDisk(fileSystem); + + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); + this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFileName); + fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); + this.Enlistment.GetVirtualPathTo(testFileName).ShouldNotExistOnDisk(fileSystem); + newTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.DeleteFile(newTestFileVirtualPath); + newTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void MoveDotGitFileToWorkingDirectoryFolder(FileSystemRunner fileSystem) + { + string testFolderName = "test_folder"; + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldBeADirectory(fileSystem); + + string sourceFileFolder = ".git"; + string testFileName = "config"; + string sourceFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(sourceFileFolder), testFileName); + string testFileContents = sourceFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(); + + string targetTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFileName); + + fileSystem.MoveFile(sourceFileVirtualPath, targetTestFileVirtualPath); + sourceFileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + targetTestFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.MoveFile(targetTestFileVirtualPath, sourceFileVirtualPath); + sourceFileVirtualPath.ShouldBeAFile(fileSystem).WithContents(testFileContents); + targetTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderName)); + this.Enlistment.GetVirtualPathTo(testFolderName).ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void MoveVirtualNTFSFileToOverwriteVirtualNTFSFile(FileSystemRunner fileSystem, string parentFolder) + { + string targetFilename = Path.Combine(parentFolder, "TargetFile.txt"); + string sourceFilename = Path.Combine(parentFolder, "SourceFile.txt"); + string targetFileContents = "The Target"; + string sourceFileContents = "The Source"; + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFilename, parentFolder); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, sourceFilename, parentFolder); + + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(targetFilename), targetFileContents); + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(fileSystem).WithContents(targetFileContents); + + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(sourceFilename), sourceFileContents); + this.Enlistment.GetVirtualPathTo(sourceFilename).ShouldBeAFile(fileSystem).WithContents(sourceFileContents); + + fileSystem.ReplaceFile(this.Enlistment.GetVirtualPathTo(sourceFilename), this.Enlistment.GetVirtualPathTo(targetFilename)); + + this.Enlistment.GetVirtualPathTo(targetFilename).ShouldBeAFile(fileSystem).WithContents(sourceFileContents); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, sourceFilename, parentFolder); + + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(targetFilename)); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFilename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void MoveVirtualNTFSFileToInvalidFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + + string testFileName = Path.Combine(parentFolder, "test.txt"); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); + this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newTestFileVirtualPath = Path.Combine( + this.Enlistment.GetVirtualPathTo(testFolderName), + Path.GetFileName(testFileName)); + + fileSystem.MoveFileShouldFail(this.Enlistment.GetVirtualPathTo(testFileName), newTestFileVirtualPath); + newTestFileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(testFileName)); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFileName, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void DeletedFilesCanBeImmediatelyRecreated(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "DeletedFilesCanBeImmediatelyRecreated"); + string filePath = this.Enlistment.GetVirtualPathTo(filename); + filePath.ShouldNotExistOnDisk(fileSystem); + + string testData = "Some test data"; + + fileSystem.WriteAllText(filePath, testData); + + fileSystem.DeleteFile(filePath); + + // Do not check for delete. Doing so removes a race between deleting and writing. + // This write will throw if the problem exists. + fileSystem.WriteAllText(filePath, testData); + + filePath.ShouldBeAFile(fileSystem).WithContents().ShouldEqual(testData); + fileSystem.DeleteFile(filePath); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestCanDeleteFilesWhileTheyAreOpenRunners)] + public void CanDeleteFilesWhileTheyAreOpen(FileSystemRunner fileSystem, string parentFolder) + { + string filename = Path.Combine(parentFolder, "CanDeleteFilesWhileTheyAreOpen"); + string filePath = this.Enlistment.GetVirtualPathTo(filename); + + byte[] buffer = System.Text.Encoding.ASCII.GetBytes("Some test data for writing"); + + using (FileStream deletableWriteStream = File.Open(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) + { + deletableWriteStream.Write(buffer, 0, buffer.Length); + deletableWriteStream.Flush(); + + using (FileStream deletableReadStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) + { + byte[] readBuffer = new byte[buffer.Length]; + + deletableReadStream.Read(readBuffer, 0, readBuffer.Length).ShouldEqual(readBuffer.Length); + readBuffer.ShouldMatchInOrder(buffer); + + fileSystem.DeleteFile(filePath); + filePath.ShouldBeAFile(fileSystem); + + deletableWriteStream.Write(buffer, 0, buffer.Length); + deletableWriteStream.Flush(); + } + } + + filePath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCase] + public void CanDeleteHydratedFilesWhileTheyAreOpenForWrite() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string fileName = "GVFS.sln"; + string virtualPath = this.Enlistment.GetVirtualPathTo(fileName); + + virtualPath.ShouldBeAFile(fileSystem); + + using (Stream stream = new FileStream(virtualPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) + using (StreamReader reader = new StreamReader(stream)) + { + // First line is empty, so read two lines + string line = reader.ReadLine() + reader.ReadLine(); + line.Length.ShouldNotEqual(0); + + File.Delete(virtualPath); + + // Open deleted files should still exist + virtualPath.ShouldBeAFile(fileSystem); + + using (StreamWriter writer = new StreamWriter(stream)) + { + writer.WriteLine("newline!"); + writer.Flush(); + + virtualPath.ShouldBeAFile(fileSystem); + } + } + + virtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCase] + public void ProjectedBlobFileTimesMatchHead() + { + // TODO: 467539 - Update all runners to support getting create/modify/access times + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = "AuthoringTests.md"; + string headFileName = ".git\\logs\\HEAD"; + this.Enlistment.GetVirtualPathTo(headFileName).ShouldBeAFile(fileSystem); + + FileInfo headFileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(headFileName)); + FileInfo fileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(filename)); + + fileInfo.CreationTime.ShouldEqual(headFileInfo.CreationTime); + + // Last access and last write can get set outside the test, make sure that are at least + // as recent as the creation time on the HEAD file, and no later than now + fileInfo.LastAccessTime.ShouldBeAtLeast(headFileInfo.CreationTime); + fileInfo.LastWriteTime.ShouldBeAtLeast(headFileInfo.CreationTime); + fileInfo.LastAccessTime.ShouldBeAtMost(DateTime.Now); + fileInfo.LastWriteTime.ShouldBeAtMost(DateTime.Now); + } + + [TestCase] + public void ProjectedBlobFolderTimesMatchHead() + { + // TODO: 467539 - Update all runners to support getting create/modify/access times + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string folderName = @"GVFS\GVFS.Tests"; + string headFileName = ".git\\logs\\HEAD"; + this.Enlistment.GetVirtualPathTo(headFileName).ShouldBeAFile(fileSystem); + + FileInfo headFileInfo = new FileInfo(this.Enlistment.GetVirtualPathTo(headFileName)); + DirectoryInfo folderInfo = new DirectoryInfo(this.Enlistment.GetVirtualPathTo(folderName)); + + folderInfo.CreationTime.ShouldEqual(headFileInfo.CreationTime); + + // Last access and last write can get set outside the test, make sure that are at least + // as recent as the creation time on the HEAD file, and no later than now + folderInfo.LastAccessTime.ShouldBeAtLeast(headFileInfo.CreationTime); + folderInfo.LastWriteTime.ShouldBeAtLeast(headFileInfo.CreationTime); + folderInfo.LastAccessTime.ShouldBeAtMost(DateTime.Now); + folderInfo.LastWriteTime.ShouldBeAtMost(DateTime.Now); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void NonExistentItemBehaviorIsCorrect(FileSystemRunner fileSystem, string parentFolder) + { + string nonExistentItem = Path.Combine(parentFolder, "BadFolderName"); + string nonExistentItem2 = Path.Combine(parentFolder, "BadFolderName2"); + + string virtualPathToNonExistentItem = this.Enlistment.GetVirtualPathTo(nonExistentItem).ShouldNotExistOnDisk(fileSystem); + string virtualPathToNonExistentItem2 = this.Enlistment.GetVirtualPathTo(nonExistentItem2).ShouldNotExistOnDisk(fileSystem); + + fileSystem.MoveFile_FileShouldNotBeFound(virtualPathToNonExistentItem, virtualPathToNonExistentItem2); + fileSystem.DeleteFile_FileShouldNotBeFound(virtualPathToNonExistentItem); + fileSystem.ReplaceFile_FileShouldNotBeFound(virtualPathToNonExistentItem, virtualPathToNonExistentItem2); + fileSystem.ReadAllText_FileShouldNotBeFound(virtualPathToNonExistentItem); + + // TODO #457434 + // fileSystem.MoveDirectoryShouldNotBeFound(nonExistentItem, true) + fileSystem.DeleteDirectory_DirectoryShouldNotBeFound(virtualPathToNonExistentItem); + + // TODO #457434 + // fileSystem.ReplaceDirectoryShouldNotBeFound(nonExistentItem, true) + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void RenameEmptyVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newFolderName = Path.Combine(parentFolder, "test_folder_renamed"); + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName); + newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); + testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + newFolderVirtualPath.ShouldBeADirectory(fileSystem); + + fileSystem.DeleteDirectory(newFolderVirtualPath); + newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void CaseOnlyRenameEmptyVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newFolderName = Path.Combine(parentFolder, "test_FOLDER"); + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName); + + // Use NativeMethods.MoveFile instead of the runner because it supports case only rename + NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath); + + newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(Path.GetFileName(newFolderName)); + + fileSystem.DeleteDirectory(newFolderVirtualPath); + newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void CaseOnlyRenameToAllCapsEmptyVirtualNTFSFolder(FileSystemRunner fileSystem) + { + string testFolderName = Path.Combine("test_folder"); + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newFolderName = Path.Combine("TEST_FOLDER"); + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newFolderName); + + // Use NativeMethods.MoveFile instead of the runner because it supports case only rename + NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath); + + newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(Path.GetFileName(newFolderName)); + + fileSystem.DeleteDirectory(newFolderVirtualPath); + newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void CaseOnlyRenameTopOfVirtualNTFSFolderTree(FileSystemRunner fileSystem) + { + string testFolderParent = "test_folder_parent"; + string testFolderChild = "test_folder_child"; + string testFolderGrandChild = "test_folder_grandchild"; + string testFile = "test.txt"; + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + + // Create the folder tree + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + + string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + + string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + + string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newFolderParentName = "test_FOLDER_PARENT"; + + // Use NativeMethods.MoveFile instead of the runner because it supports case only rename + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(newFolderParentName)); + + this.Enlistment.GetVirtualPathTo(newFolderParentName).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderParentName); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Cleanup + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void CaseOnlyRenameFullDotGitFolder(FileSystemRunner fileSystem) + { + string testFolderName = ".git\\test_folder"; + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + testFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newFolderName = "test_FOLDER"; + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderName)); + + // Use NativeMethods.MoveFile instead of the runner because it supports case only rename + NativeMethods.MoveFile(testFolderVirtualPath, newFolderVirtualPath); + + newFolderVirtualPath.ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderName); + + fileSystem.DeleteDirectory(newFolderVirtualPath); + newFolderVirtualPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void CaseOnlyRenameTopOfDotGitFullFolderTree(FileSystemRunner fileSystem) + { + string testFolderParent = ".git\\test_folder_parent"; + string testFolderChild = "test_folder_child"; + string testFolderGrandChild = "test_folder_grandchild"; + string testFile = "test.txt"; + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + + // Create the folder tree + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + + string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + + string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + + string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newFolderParentName = "test_FOLDER_PARENT"; + + // Use NativeMethods.MoveFile instead of the runner because it supports case only rename + NativeMethods.MoveFile(this.Enlistment.GetVirtualPathTo(testFolderParent), this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName))); + + this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)).ShouldBeADirectory(fileSystem).WithCaseMatchingName(newFolderParentName); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Cleanup + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName))); + + this.Enlistment.GetVirtualPathTo(Path.Combine(".git", newFolderParentName)).ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void MoveVirtualNTFSFolderIntoVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string targetFolderName = Path.Combine(parentFolder, "target_folder"); + string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); + + fileSystem.CreateDirectory(targetFolderVirtualPath); + targetFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string testFileName = Path.Combine(testFolderName, "test.txt"); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFileName), testFileContents); + this.Enlistment.GetVirtualPathTo(testFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newTestFolder = Path.Combine(targetFolderName, Path.GetFileName(testFolderName)); + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newTestFolder); + + fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + newFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newTestFileName = Path.Combine(newTestFolder, Path.GetFileName(testFileName)); + this.Enlistment.GetVirtualPathTo(newTestFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.DeleteDirectory(targetFolderVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void RenameAndMoveVirtualNTFSFolderIntoVirtualNTFSFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderName = Path.Combine(parentFolder, "test_folder"); + string testFolderVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + + fileSystem.CreateDirectory(testFolderVirtualPath); + testFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string targetFolderName = Path.Combine(parentFolder, "target_folder"); + string targetFolderVirtualPath = this.Enlistment.GetVirtualPathTo(targetFolderName); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); + + fileSystem.CreateDirectory(targetFolderVirtualPath); + targetFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string testFileName = "test.txt"; + string testFilePartialPath = Path.Combine(testFolderName, testFileName); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(testFilePartialPath), testFileContents); + this.Enlistment.GetVirtualPathTo(testFilePartialPath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string newTestFolder = Path.Combine(targetFolderName, "test_folder_renamed"); + string newFolderVirtualPath = this.Enlistment.GetVirtualPathTo(newTestFolder); + + fileSystem.MoveDirectory(testFolderVirtualPath, newFolderVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderName, parentFolder); + newFolderVirtualPath.ShouldBeADirectory(fileSystem); + + string newTestFileName = Path.Combine(newTestFolder, testFileName); + this.Enlistment.GetVirtualPathTo(newTestFileName).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + fileSystem.DeleteDirectory(targetFolderVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, targetFolderName, parentFolder); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void MoveVirtualNTFSFolderTreeIntoVirtualNTFSFolder(FileSystemRunner fileSystem) + { + string testFolderParent = "test_folder_parent"; + string testFolderChild = "test_folder_child"; + string testFolderGrandChild = "test_folder_grandchild"; + string testFile = "test.txt"; + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + + // Create the folder tree (to move) + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + + string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + + string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + + string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Create the target + string targetFolder = "target_folder"; + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldBeADirectory(fileSystem); + + fileSystem.MoveDirectory( + this.Enlistment.GetVirtualPathTo(testFolderParent), + this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, testFolderParent))); + + // The old tree structure should be gone + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); + + // The tree should have been moved under the target folder + testFolderParent = Path.Combine(targetFolder, testFolderParent); + realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Cleanup + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); + + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void MoveDotGitFullFolderTreeToDotGitFullFolder(FileSystemRunner fileSystem) + { + string testFolderRoot = ".git"; + string testFolderParent = "test_folder_parent"; + string testFolderChild = "test_folder_child"; + string testFolderGrandChild = "test_folder_grandchild"; + string testFile = "test.txt"; + this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldNotExistOnDisk(fileSystem); + + // Create the folder tree (to move) + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent))); + this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldBeADirectory(fileSystem); + + string realtiveChildFolderPath = Path.Combine(testFolderRoot, testFolderParent, testFolderChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + + string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + + string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Create the target + string targetFolder = ".git\\target_folder"; + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); + + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldBeADirectory(fileSystem); + + fileSystem.MoveDirectory( + this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)), + this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, testFolderParent))); + + // The old tree structure should be gone + this.Enlistment.GetVirtualPathTo(Path.Combine(testFolderRoot, testFolderParent)).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); + + // The tree should have been moved under the target folder + testFolderParent = Path.Combine(targetFolder, testFolderParent); + realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Cleanup + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(targetFolder)); + + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldNotExistOnDisk(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldNotExistOnDisk(fileSystem); + } + + [TestCaseSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] + public void DeleteIndexFileFails(FileSystemRunner fileSystem) + { + string indexFilePath = this.Enlistment.GetVirtualPathTo(@".git\index"); + indexFilePath.ShouldBeAFile(fileSystem); + fileSystem.DeleteFile_AccessShouldBeDenied(indexFilePath); + indexFilePath.ShouldBeAFile(fileSystem); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestRunners)] + public void MoveVirtualNTFSFolderIntoInvalidFolder(FileSystemRunner fileSystem, string parentFolder) + { + string testFolderParent = Path.Combine(parentFolder, "test_folder_parent"); + string testFolderChild = "test_folder_child"; + string testFolderGrandChild = "test_folder_grandchild"; + string testFile = "test.txt"; + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderParent, parentFolder); + + // Create the folder tree (to move) + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + + string realtiveChildFolderPath = Path.Combine(testFolderParent, testFolderChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + + string realtiveGrandChildFolderPath = Path.Combine(realtiveChildFolderPath, testFolderGrandChild); + fileSystem.CreateDirectory(this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath)); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + + string relativeTestFilePath = Path.Combine(realtiveGrandChildFolderPath, testFile); + string testFileContents = "This is the contents of a test file"; + fileSystem.WriteAllText(this.Enlistment.GetVirtualPathTo(relativeTestFilePath), testFileContents); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + string targetFolder = Path.Combine(parentFolder, "target_folder_does_not_exists"); + this.Enlistment.GetVirtualPathTo(targetFolder).ShouldNotExistOnDisk(fileSystem); + + // This move should fail + fileSystem.MoveDirectory_TargetShouldBeInvalid( + this.Enlistment.GetVirtualPathTo(testFolderParent), + this.Enlistment.GetVirtualPathTo(Path.Combine(targetFolder, Path.GetFileName(testFolderParent)))); + + // The old tree structure should still be there + this.Enlistment.GetVirtualPathTo(testFolderParent).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(realtiveGrandChildFolderPath).ShouldBeADirectory(fileSystem); + this.Enlistment.GetVirtualPathTo(relativeTestFilePath).ShouldBeAFile(fileSystem).WithContents(testFileContents); + + // Cleanup + fileSystem.DeleteDirectory(this.Enlistment.GetVirtualPathTo(testFolderParent)); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, testFolderParent, parentFolder); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, realtiveChildFolderPath, parentFolder); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, realtiveGrandChildFolderPath, parentFolder); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, relativeTestFilePath, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void CreateFileInheritsParentDirectoryAttributes(string parentFolder) + { + string parentDirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(parentFolder, "CreateFileInheritsParentDirectoryAttributes")); + FileSystemRunner.DefaultRunner.CreateDirectory(parentDirectoryPath); + DirectoryInfo parentDirInfo = new DirectoryInfo(parentDirectoryPath); + parentDirInfo.Attributes |= FileAttributes.NoScrubData; + parentDirInfo.Attributes.HasFlag(FileAttributes.NoScrubData).ShouldEqual(true); + + string targetFilePath = Path.Combine(parentDirectoryPath, "TargetFile"); + FileSystemRunner.DefaultRunner.WriteAllText(targetFilePath, "Some contents that don't matter"); + targetFilePath.ShouldBeAFile(FileSystemRunner.DefaultRunner).WithAttribute(FileAttributes.NoScrubData); + + FileSystemRunner.DefaultRunner.DeleteDirectory(parentDirectoryPath); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void CreateDirectoryInheritsParentDirectoryAttributes(string parentFolder) + { + string parentDirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(parentFolder, "CreateDirectoryInheritsParentDirectoryAttributes")); + FileSystemRunner.DefaultRunner.CreateDirectory(parentDirectoryPath); + DirectoryInfo parentDirInfo = new DirectoryInfo(parentDirectoryPath); + parentDirInfo.Attributes |= FileAttributes.NoScrubData; + parentDirInfo.Attributes.HasFlag(FileAttributes.NoScrubData).ShouldEqual(true); + + string targetDirPath = Path.Combine(parentDirectoryPath, "TargetDir"); + FileSystemRunner.DefaultRunner.CreateDirectory(targetDirPath); + targetDirPath.ShouldBeADirectory(FileSystemRunner.DefaultRunner).WithAttribute(FileAttributes.NoScrubData); + + FileSystemRunner.DefaultRunner.DeleteDirectory(parentDirectoryPath); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void StreamAccessReadFromMemoryMappedVirtualNTFSFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "StreamAccessReadFromMemoryMappedVirtualNTFSFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + int offset = 0; + int size = contents.Length; + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size)) + { + streamAccessor.CanRead.ShouldEqual(true); + + for (int i = 0; i < size; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contents[i]); + } + } + } + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void RandomAccessReadFromMemoryMappedVirtualNTFSFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "RandomAccessReadFromMemoryMappedVirtualNTFSFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + int offset = 0; + int size = contents.Length; + using (MemoryMappedViewAccessor randAccessor = mmf.CreateViewAccessor(offset, size)) + { + randAccessor.CanRead.ShouldEqual(true); + + for (int i = 0; i < size; ++i) + + { + ((char)randAccessor.ReadByte(i)).ShouldEqual(contents[i]); + } + + for (int i = size - 1; i >= 0; --i) + { + ((char)randAccessor.ReadByte(i)).ShouldEqual(contents[i]); + } + } + } + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void StreamAccessReadWriteFromMemoryMappedVirtualNTFSFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "StreamAccessReadWriteFromMemoryMappedVirtualNTFSFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + int offset = 64; + int size = contents.Length; + string newContent = "**NEWCONTENT**"; + + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset, size - offset)) + { + streamAccessor.CanRead.ShouldEqual(true); + streamAccessor.CanWrite.ShouldEqual(true); + + for (int i = offset; i < size - offset; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contents[i]); + } + + // Reset to the start of the stream (which will place the streamAccessor at offset in the memory file) + streamAccessor.Seek(0, SeekOrigin.Begin); + byte[] newContentBuffer = Encoding.ASCII.GetBytes(newContent); + + streamAccessor.Write(newContentBuffer, 0, newContent.Length); + + for (int i = 0; i < newContent.Length; ++i) + { + contentsBuilder[offset + i] = newContent[i]; + } + + contents = contentsBuilder.ToString(); + } + + // Verify the file has the new contents inserted into it + using (MemoryMappedViewStream streamAccessor = mmf.CreateViewStream(offset: 0, size: size)) + { + for (int i = 0; i < size; ++i) + { + streamAccessor.ReadByte().ShouldEqual(contents[i]); + } + } + } + + // Confirm the new contents was written to disk + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void RandomAccessReadWriteFromMemoryMappedVirtualNTFSFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "RandomAccessReadWriteFromMemoryMappedVirtualNTFSFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath)) + { + int offset = 64; + int size = contents.Length; + string newContent = "**NEWCONTENT**"; + + using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset, size - offset)) + { + randomAccessor.CanRead.ShouldEqual(true); + randomAccessor.CanWrite.ShouldEqual(true); + + for (int i = 0; i < size - offset; ++i) + { + ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i + offset]); + } + + for (int i = 0; i < newContent.Length; ++i) + { + // Convert to byte before writing rather than writing as char, because char version will write a 16-bit + // unicode char + randomAccessor.Write(i, Convert.ToByte(newContent[i])); + ((char)randomAccessor.ReadByte(i)).ShouldEqual(newContent[i]); + } + + for (int i = 0; i < newContent.Length; ++i) + { + contentsBuilder[offset + i] = newContent[i]; + } + + contents = contentsBuilder.ToString(); + } + + // Verify the file has the new contents inserted into it + using (MemoryMappedViewAccessor randomAccessor = mmf.CreateViewAccessor(offset: 0, size: size)) + { + for (int i = 0; i < size; ++i) + { + ((char)randomAccessor.ReadByte(i)).ShouldEqual(contents[i]); + } + } + } + + // Confirm the new contents was written to disk + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void StreamAccessToExistingMemoryMappedFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "StreamAccessToExistingMemoryMappedFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + int size = contents.Length; + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + string memoryMapFileName = "StreamAccessFile"; + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath, FileMode.Open, memoryMapFileName)) + { + Thread[] threads = new Thread[4]; + bool keepRunning = true; + for (int i = 0; i < threads.Length; ++i) + { + int myIndex = i; + threads[i] = new Thread(() => + { + // Create random seeks (seeded for repeatability) + Random randNum = new Random(myIndex); + + using (MemoryMappedFile threadFile = MemoryMappedFile.OpenExisting(memoryMapFileName)) + { + while (keepRunning) + { + // Pick an offset somewhere in the first half of the file + int offset = randNum.Next(size / 2); + + using (MemoryMappedViewStream streamAccessor = threadFile.CreateViewStream(offset, size - offset)) + { + for (int j = 0; j < size - offset; ++j) + { + streamAccessor.ReadByte().ShouldEqual(contents[j + offset]); + } + } + } + } + }); + + threads[i].Start(); + } + + Thread.Sleep(500); + keepRunning = false; + + for (int i = 0; i < threads.Length; ++i) + { + threads[i].Join(); + } + } + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void RandomAccessToExistingMemoryMappedFile(string parentFolder) + { + // Use SystemIORunner as the text we are writing is too long to pass to the command line + FileSystemRunner fileSystem = new SystemIORunner(); + + string filename = Path.Combine(parentFolder, "RandomAccessToExistingMemoryMappedFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + StringBuilder contentsBuilder = new StringBuilder(); + while (contentsBuilder.Length < 4096 * 2) + { + contentsBuilder.Append(Guid.NewGuid().ToString()); + } + + string contents = contentsBuilder.ToString(); + int size = contents.Length; + + fileSystem.WriteAllText(fileVirtualPath, contents); + fileVirtualPath.ShouldBeAFile(fileSystem).WithContents(contents); + + string memoryMapFileName = "RandomAccessFile"; + using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileVirtualPath, FileMode.Open, memoryMapFileName)) + { + Thread[] threads = new Thread[4]; + bool keepRunning = true; + for (int i = 0; i < threads.Length; ++i) + { + int myIndex = i; + threads[i] = new Thread(() => + { + // Create random seeks (seeded for repeatability) + Random randNum = new Random(myIndex); + + using (MemoryMappedFile threadFile = MemoryMappedFile.OpenExisting(memoryMapFileName)) + { + while (keepRunning) + { + // Pick an offset somewhere in the first half of the file + int offset = randNum.Next(size / 2); + + using (MemoryMappedViewAccessor randomAccessor = threadFile.CreateViewAccessor(offset, size - offset)) + { + for (int j = 0; j < size - offset; ++j) + { + ((char)randomAccessor.ReadByte(j)).ShouldEqual(contents[j + offset]); + } + } + } + } + }); + + threads[i].Start(); + } + + Thread.Sleep(500); + keepRunning = false; + + for (int i = 0; i < threads.Length; ++i) + { + threads[i].Join(); + } + } + + fileSystem.DeleteFile(fileVirtualPath); + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void NativeReadAndWriteSeparateHandles(string parentFolder) + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = Path.Combine(parentFolder, "NativeReadAndWriteSeparateHandles"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.ReadAndWriteSeparateHandles(fileVirtualPath).ShouldEqual(true); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void NativeReadAndWriteSameHandle(string parentFolder) + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = Path.Combine(parentFolder, "NativeReadAndWriteSameHandle"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.ReadAndWriteSameHandle(fileVirtualPath, synchronousIO: false).ShouldEqual(true); + + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.ReadAndWriteSameHandle(fileVirtualPath, synchronousIO: true).ShouldEqual(true); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void NativeReadAndWriteRepeatedly(string parentFolder) + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = Path.Combine(parentFolder, "NativeReadAndWriteRepeatedly"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.ReadAndWriteRepeatedly(fileVirtualPath, synchronousIO: false).ShouldEqual(true); + + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.ReadAndWriteRepeatedly(fileVirtualPath, synchronousIO: true).ShouldEqual(true); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void NativeRemoveReadOnlyAttribute(string parentFolder) + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = Path.Combine(parentFolder, "NativeRemoveReadOnlyAttribute"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.RemoveReadOnlyAttribute(fileVirtualPath).ShouldEqual(true); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCaseSource(typeof(FileRunnersAndFolders), FileRunnersAndFolders.TestFolders)] + public void NativeCannotWriteToReadOnlyFile(string parentFolder) + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string filename = Path.Combine(parentFolder, "NativeCannotWriteToReadOnlyFile"); + string fileVirtualPath = this.Enlistment.GetVirtualPathTo(filename); + fileVirtualPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.CannotWriteToReadOnlyFile(fileVirtualPath).ShouldEqual(true); + + FileRunnersAndFolders.ShouldNotExistOnDisk(this.Enlistment, fileSystem, filename, parentFolder); + } + + [TestCase] + public void NativeEnumerationErrorsMatchNTFS() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo("this_does_not_exist"); + nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem); + string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist"); + nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true); + } + + [TestCase] + public void NativeEnumerationErrorsMatchNTFSForNestedFolder() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + this.Enlistment.GetVirtualPathTo("GVFS").ShouldBeADirectory(fileSystem); + string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo("GVFS\\this_does_not_exist"); + nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem); + + this.Enlistment.DotGVFSRoot.ShouldBeADirectory(fileSystem); + string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist"); + nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true); + } + + [TestCase] + public void NativeEnumerationDotGitFolderErrorsMatchNTFS() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string nonExistentVirtualPath = this.Enlistment.GetVirtualPathTo(".git\\this_does_not_exist"); + nonExistentVirtualPath.ShouldNotExistOnDisk(fileSystem); + string nonExistentPhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "this_does_not_exist"); + nonExistentPhysicalPath.ShouldNotExistOnDisk(fileSystem); + + NativeTests.EnumerationErrorsMatchNTFSForNonExistentFolder(nonExistentVirtualPath, nonExistentPhysicalPath).ShouldEqual(true); + } + + [TestCase] + public void NativeEnumerationErrorsMatchNTFSForEmptyNewFolder() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string newVirtualFolderPath = this.Enlistment.GetVirtualPathTo("new_folder"); + newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem); + fileSystem.CreateDirectory(newVirtualFolderPath); + newVirtualFolderPath.ShouldBeADirectory(fileSystem); + + string newPhysicalFolderPath = Path.Combine(this.Enlistment.DotGVFSRoot, "new_folder"); + newPhysicalFolderPath.ShouldNotExistOnDisk(fileSystem); + fileSystem.CreateDirectory(newPhysicalFolderPath); + newPhysicalFolderPath.ShouldBeADirectory(fileSystem); + + NativeTests.EnumerationErrorsMatchNTFSForEmptyFolder(newVirtualFolderPath, newPhysicalFolderPath).ShouldEqual(true); + + fileSystem.DeleteDirectory(newVirtualFolderPath); + newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem); + fileSystem.DeleteDirectory(newPhysicalFolderPath); + newPhysicalFolderPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCase] + public void NativeDeleteEmptyFolderWithFileDispositionOnClose() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string newVirtualFolderPath = this.Enlistment.GetVirtualPathTo("new_folder"); + newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem); + fileSystem.CreateDirectory(newVirtualFolderPath); + newVirtualFolderPath.ShouldBeADirectory(fileSystem); + + NativeTests.CanDeleteEmptyFolderWithFileDispositionOnClose(newVirtualFolderPath).ShouldEqual(true); + + newVirtualFolderPath.ShouldNotExistOnDisk(fileSystem); + } + + [TestCase] + public void NativeQueryDirectoryFileRestartScanResetsFilter() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string folderPath = this.Enlistment.GetVirtualPathTo("EnumerateAndReadTestFiles"); + folderPath.ShouldBeADirectory(fileSystem); + + NativeTests.QueryDirectoryFileRestartScanResetsFilter(folderPath).ShouldEqual(true); + } + + [TestCase] + public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_VirtualGVFltPath() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\virtual"); + string existingFilePhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "GVFS_HEAD"); + existingFilePhysicalPath.ShouldBeAFile(fileSystem); + + foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition))) + { + NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true); + } + } + + [TestCase] + public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_PartialGVFltPath() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\partial"); + existingFileVirtualPath.ShouldBeAFile(fileSystem); + fileSystem.ReadAllText(existingFileVirtualPath); + string existingFilePhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "GVFS_HEAD"); + existingFilePhysicalPath.ShouldBeAFile(fileSystem); + + foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition))) + { + NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true); + } + } + + [TestCase] + public void ErrorWhenPathTreatsFileAsFolderMatchesNTFS_FullGVFltPath() + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + + string existingFileVirtualPath = this.Enlistment.GetVirtualPathTo("ErrorWhenPathTreatsFileAsFolderMatchesNTFS\\full"); + existingFileVirtualPath.ShouldBeAFile(fileSystem); + fileSystem.AppendAllText(existingFileVirtualPath, "extra text"); + string existingFilePhysicalPath = Path.Combine(this.Enlistment.DotGVFSRoot, "GVFS_HEAD"); + existingFilePhysicalPath.ShouldBeAFile(fileSystem); + + foreach (CreationDisposition creationDispostion in Enum.GetValues(typeof(CreationDisposition))) + { + NativeTests.ErrorWhenPathTreatsFileAsFolderMatchesNTFS(existingFileVirtualPath, existingFilePhysicalPath, (int)creationDispostion).ShouldEqual(true); + } + } + + [TestCase] + public void EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete() + { + NativeTrailingSlashTests.EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_ModifyFileInScratchAndDir() + { + GVFlt_BugRegressionTest.GVFlt_ModifyFileInScratchAndDir(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_RMDIRTest1() + { + GVFlt_BugRegressionTest.GVFlt_RMDIRTest1(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_RMDIRTest2() + { + GVFlt_BugRegressionTest.GVFlt_RMDIRTest2(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_RMDIRTest3() + { + GVFlt_BugRegressionTest.GVFlt_RMDIRTest3(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_RMDIRTest4() + { + GVFlt_BugRegressionTest.GVFlt_RMDIRTest4(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_RMDIRTest5() + { + GVFlt_BugRegressionTest.GVFlt_RMDIRTest5(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeepNonExistFileUnderPartial() + { + GVFlt_BugRegressionTest.GVFlt_DeepNonExistFileUnderPartial(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SupersededReparsePoint() + { + GVFlt_BugRegressionTest.GVFlt_SupersededReparsePoint(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteVirtualFile_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteVirtualFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteVirtualFile_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteVirtualFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeletePlaceholder_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeletePlaceholder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeletePlaceholder_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeletePlaceholder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteFullFile_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteFullFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteFullFile_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteFullFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteLocalFile_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteLocalFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteLocalFile_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteLocalFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNotExistFile_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteNotExistFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNotExistFile_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteNotExistFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNonRootVirtualFile_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteNonRootVirtualFile_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNonRootVirtualFile_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteNonRootVirtualFile_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteFileOutsideVRoot_SetDisposition() + { + GVFlt_DeleteFileTest.GVFlt_DeleteFileOutsideVRoot_SetDisposition(Path.GetDirectoryName(this.Enlistment.RepoRoot)).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteFileOutsideVRoot_DeleteOnClose() + { + GVFlt_DeleteFileTest.GVFlt_DeleteFileOutsideVRoot_DeleteOnClose(Path.GetDirectoryName(this.Enlistment.RepoRoot)).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition() + { + GVFlt_DeleteFolderTest.GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose() + { + GVFlt_DeleteFolderTest.GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteLocalEmptyFolder_SetDisposition() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteLocalEmptyFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteLocalEmptyFolder_DeleteOnClose() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteLocalEmptyFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNonRootVirtualFolder_SetDisposition() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteNonRootVirtualFolder_SetDisposition(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose() + { + GVFlt_DeleteFolderTest.GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumEmptyFolder() + { + GVFlt_DirEnumTest.GVFlt_EnumEmptyFolder(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumFolderWithOneFileInRepo() + { + GVFlt_DirEnumTest.GVFlt_EnumFolderWithOneFileInPackage(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumFolderWithOneFileInRepoBeforeScratchFile() + { + GVFlt_DirEnumTest.GVFlt_EnumFolderWithOneFileInBoth(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumFolderWithOneFileInRepoAfterScratchFile() + { + GVFlt_DirEnumTest.GVFlt_EnumFolderWithOneFileInBoth1(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumFolderDeleteExistingFile() + { + GVFlt_DirEnumTest.GVFlt_EnumFolderDeleteExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_EnumFolderSmallBuffer() + { + GVFlt_DirEnumTest.GVFlt_EnumFolderSmallBuffer(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_ModifyFileInScratchAndCheckLastWriteTime() + { + GVFlt_FileAttributeTest.GVFlt_ModifyFileInScratchAndCheckLastWriteTime(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_FileSize() + { + GVFlt_FileAttributeTest.GVFlt_FileSize(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_ModifyFileInScratchAndCheckFileSize() + { + GVFlt_FileAttributeTest.GVFlt_ModifyFileInScratchAndCheckFileSize(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_FileAttributes() + { + GVFlt_FileAttributeTest.GVFlt_FileAttributes(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_OneEAAttributeWillPass() + { + GVFlt_FileEATest.GVFlt_OneEAAttributeWillPass(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_OpenRootFolder() + { + GVFlt_FileOperationTest.GVFlt_OpenRootFolder(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_WriteAndVerify() + { + GVFlt_FileOperationTest.GVFlt_WriteAndVerify(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_DeleteExistingFile() + { + GVFlt_FileOperationTest.GVFlt_DeleteExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_OpenNonExistingFile() + { + GVFlt_FileOperationTest.GVFlt_OpenNonExistingFile(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_NoneToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_NoneToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_PartialToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_PartialToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_FullToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_FullToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_LocalToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_LocalToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToVirtual() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToVirtualFileNameChanged() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToVirtualFileNameChanged(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToPartial() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToPartial(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_PartialToPartial() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_PartialToPartial(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_LocalToVirtual() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_LocalToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToVirtualIntermidiateDirNotExist() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToVirtualIntermidiateDirNotExist(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToNoneIntermidiateDirNotExist() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToNoneIntermidiateDirNotExist(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_OutsideToNone() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_OutsideToNone(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_OutsideToVirtual() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_OutsideToVirtual(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_OutsideToPartial() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_OutsideToPartial(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_NoneToOutside() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_NoneToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_VirtualToOutside() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_VirtualToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_PartialToOutside() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_PartialToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_OutsideToOutside() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_OutsideToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFile_LongFileName() + { + GVFlt_MoveFileTest.GVFlt_MoveFile_LongFileName(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_NoneToNone() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_NoneToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_VirtualToNone() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_VirtualToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_PartialToNone() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_PartialToNone(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_VirtualToVirtual() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_VirtualToVirtual(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_VirtualToPartial() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_VirtualToPartial(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_OutsideToNone() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_OutsideToNone(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_OutsideToVirtual() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_OutsideToVirtual(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_NoneToOutside() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_NoneToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_VirtualToOutside() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_VirtualToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_MoveFolder_OutsideToOutside() + { + GVFlt_MoveFolderTest.GVFlt_MoveFolder_OutsideToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_OpenForReadsSameTime() + { + GVFlt_MultiThreadTest.GVFlt_OpenForReadsSameTime(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_OpenForWritesSameTime() + { + GVFlt_MultiThreadTest.GVFlt_OpenForWritesSameTime(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_ToVirtualFile() + { + GVFlt_SetLinkTest.GVFlt_SetLink_ToVirtualFile(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_ToPlaceHolder() + { + GVFlt_SetLinkTest.GVFlt_SetLink_ToPlaceHolder(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_ToFullFile() + { + GVFlt_SetLinkTest.GVFlt_SetLink_ToFullFile(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_ToNonExistFileWillFail() + { + GVFlt_SetLinkTest.GVFlt_SetLink_ToNonExistFileWillFail(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_NameAlreadyExistWillFail() + { + GVFlt_SetLinkTest.GVFlt_SetLink_NameAlreadyExistWillFail(this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_FromOutside() + { + GVFlt_SetLinkTest.GVFlt_SetLink_FromOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + [TestCase] + public void Native_GVFlt_SetLink_ToOutside() + { + GVFlt_SetLinkTest.GVFlt_SetLink_ToOutside(Path.GetDirectoryName(this.Enlistment.RepoRoot), this.Enlistment.RepoRoot).ShouldEqual(true); + } + + private class NativeTests + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool ReadAndWriteSeparateHandles(string fileVirtualPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool ReadAndWriteSameHandle(string fileVirtualPath, bool synchronousIO); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool ReadAndWriteRepeatedly(string fileVirtualPath, bool synchronousIO); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool RemoveReadOnlyAttribute(string fileVirtualPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool CannotWriteToReadOnlyFile(string fileVirtualPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool EnumerationErrorsMatchNTFSForNonExistentFolder(string nonExistentVirtualPath, string nonExistentPhysicalPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool EnumerationErrorsMatchNTFSForEmptyFolder(string emptyFolderVirtualPath, string emptyFolderPhysicalPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool CanDeleteEmptyFolderWithFileDispositionOnClose(string emptyFolderPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool QueryDirectoryFileRestartScanResetsFilter(string folderPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(string filePath, string fileNTFSPath, int creationDisposition); + } + + private class NativeTrailingSlashTests + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(string virtualRootPath); + } + + private class GVFlt_BugRegressionTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_ModifyFileInScratchAndDir(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_RMDIRTest1(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_RMDIRTest2(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_RMDIRTest3(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_RMDIRTest4(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_RMDIRTest5(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeepNonExistFileUnderPartial(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SupersededReparsePoint(string virtualRootPath); + } + + private class GVFlt_DeleteFileTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteVirtualFile_SetDisposition(string enumFolderSmallBufferPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteVirtualFile_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeletePlaceholder_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeletePlaceholder_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteFullFile_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteFullFile_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteLocalFile_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteLocalFile_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNotExistFile_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNotExistFile_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNonRootVirtualFile_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNonRootVirtualFile_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteFileOutsideVRoot_SetDisposition(string pathOutsideRepo); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteFileOutsideVRoot_DeleteOnClose(string pathOutsideRepo); + } + + private class GVFlt_DeleteFolderTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteLocalEmptyFolder_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteLocalEmptyFolder_DeleteOnClose(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNonRootVirtualFolder_SetDisposition(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose(string virtualRootPath); + } + + private class GVFlt_DirEnumTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumEmptyFolder(string emptyFolderPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumFolderWithOneFileInPackage(string enumFolderWithOneFileInRepoPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumFolderWithOneFileInBoth(string enumFolderWithOneFileInRepoBeforeScratchPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumFolderWithOneFileInBoth1(string enumFolderWithOneFileInRepoAfterScratchPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumFolderDeleteExistingFile(string enumFolderDeleteExistingFilePath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_EnumFolderSmallBuffer(string enumFolderSmallBufferPath); + } + + private class GVFlt_FileAttributeTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_ModifyFileInScratchAndCheckLastWriteTime(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_FileSize(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_ModifyFileInScratchAndCheckFileSize(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_FileAttributes(string virtualRootPath); + } + + private class GVFlt_FileEATest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_OneEAAttributeWillPass(string virtualRootPath); + } + + private class GVFlt_FileOperationTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_OpenRootFolder(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_WriteAndVerify(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_DeleteExistingFile(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_OpenNonExistingFile(string virtualRootPath); + } + + private class GVFlt_MoveFileTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_NoneToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_PartialToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_FullToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_LocalToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToVirtual(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToVirtualFileNameChanged(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToPartial(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_PartialToPartial(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_LocalToVirtual(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToVirtualIntermidiateDirNotExist(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToNoneIntermidiateDirNotExist(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_OutsideToNone(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_OutsideToVirtual(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_OutsideToPartial(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_NoneToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_VirtualToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_PartialToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_OutsideToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFile_LongFileName(string virtualRootPath); + } + + private class GVFlt_MoveFolderTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_NoneToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_VirtualToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_PartialToNone(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_VirtualToVirtual(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_VirtualToPartial(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_OutsideToNone(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_OutsideToVirtual(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_NoneToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_VirtualToOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_MoveFolder_OutsideToOutside(string pathOutsideRepo, string virtualRootPath); + } + + private class GVFlt_MultiThreadTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_OpenForReadsSameTime(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_OpenForWritesSameTime(string virtualRootPath); + } + + private class GVFlt_SetLinkTest + { + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_ToVirtualFile(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_ToPlaceHolder(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_ToFullFile(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_ToNonExistFileWillFail(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_NameAlreadyExistWillFail(string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_FromOutside(string pathOutsideRepo, string virtualRootPath); + + [DllImport("GVFS.NativeTests.dll")] + public static extern bool GVFlt_SetLink_ToOutside(string pathOutsideRepo, string virtualRootPath); + } + + private class FileRunnersAndFolders + { + public const string TestFolders = "Folders"; + public const string TestRunners = "Runners"; + public const string TestCanDeleteFilesWhileTheyAreOpenRunners = "CanDeleteFilesWhileTheyAreOpenRunners"; + public const string DotGitFolder = ".git"; + + private static object[] allFolders = + { + new object[] { string.Empty }, + new object[] { DotGitFolder }, + }; + + public static object[] Runners + { + get + { + List runnersAndParentFolders = new List(); + foreach (object[] runner in FileSystemRunner.Runners.ToList()) + { + runnersAndParentFolders.Add(new object[] { runner.ToList().First(), string.Empty }); + runnersAndParentFolders.Add(new object[] { runner.ToList().First(), DotGitFolder }); + } + + return runnersAndParentFolders.ToArray(); + } + } + + public static object[] CanDeleteFilesWhileTheyAreOpenRunners + { + get + { + // Don't use the BashRunner for the CanDeleteFilesWhileTheyAreOpen test as bash.exe (rm command) moves + // the file to the recycle bin rather than deleting it if the file that is getting removed is currently open. + List runnersAndParentFolders = new List(); + foreach (object[] runner in FileSystemRunner.Runners.ToList()) + { + if (!(runner.ToList().First() is BashRunner)) + { + runnersAndParentFolders.Add(new object[] { runner.ToList().First(), string.Empty }); + runnersAndParentFolders.Add(new object[] { runner.ToList().First(), DotGitFolder }); + } + } + + return runnersAndParentFolders.ToArray(); + } + } + + public static object[] Folders + { + get + { + return allFolders; + } + } + + public static void ShouldNotExistOnDisk(GVFSFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem, string filename, string parentFolder) + { + enlistment.GetVirtualPathTo(filename).ShouldNotExistOnDisk(fileSystem); + } + } + + private class DeleteDotGitTestsRunners + { + public const string TestRunners = "Runners"; + + public static object[] Runners + { + get + { + // Don't use the BashRunner or SystemIORunner for the CanDeleteFilesWhileTheyAreOpen test as they start + // recursively deleting inside of the directory junction (before attempting to delete the junction itself) + List runners = new List(); + foreach (object[] runner in FileSystemRunner.Runners.ToList()) + { + if (!(runner.ToList().First() is BashRunner) && !(runner.ToList().First() is SystemIORunner)) + { + runners.Add(new object[] { runner.ToList().First() }); + } + } + + return runners.ToArray(); + } + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/PrintTestCaseStats.cs b/GVFS/GVFS.FunctionalTests/Tests/PrintTestCaseStats.cs new file mode 100644 index 00000000..db855ce1 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/PrintTestCaseStats.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using System; + +[assembly: GVFS.FunctionalTests.Tests.PrintTestCaseStats] + +namespace GVFS.FunctionalTests.Tests +{ + public class PrintTestCaseStats : TestActionAttribute + { + public override ActionTargets Targets + { + get { return ActionTargets.Test; } + } + + public override void BeforeTest(ITest test) + { + Console.WriteLine("Test " + test.FullName.Substring("GVFS.FunctionalTests.Tests.".Length)); + Console.WriteLine("Started at {0:hh:mm:ss}", DateTime.Now); + } + + public override void AfterTest(ITest test) + { + Console.WriteLine("Completed at {0:hh:mm:ss}", DateTime.Now); + Console.WriteLine(); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/Tools/ControlGitRepo.cs b/GVFS/GVFS.FunctionalTests/Tools/ControlGitRepo.cs new file mode 100644 index 00000000..c8911abf --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/ControlGitRepo.cs @@ -0,0 +1,60 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using System.IO; + +namespace GVFS.FunctionalTests.Tools +{ + public class ControlGitRepo + { + public ControlGitRepo(string repoUrl, string rootPath, string commitish) + { + this.RootPath = rootPath; + this.RepoUrl = repoUrl; + this.Commitish = commitish; + } + + public string RootPath { get; private set; } + public string RepoUrl { get; private set; } + public string Commitish { get; private set; } + + public static ControlGitRepo Create() + { + return new ControlGitRepo( + Properties.Settings.Default.RepoToClone, + Properties.Settings.Default.ControlGitRepoRoot, + Properties.Settings.Default.Commitish); + } + + public void Initialize() + { + if (Directory.Exists(this.RootPath)) + { + this.Delete(); + } + + Directory.CreateDirectory(this.RootPath); + GitProcess.Invoke(this.RootPath, "init"); + GitProcess.Invoke(this.RootPath, "config core.autocrlf false"); + GitProcess.Invoke(this.RootPath, "config merge.stat false"); + GitProcess.Invoke(this.RootPath, "config advice.statusUoption false"); + GitProcess.Invoke(this.RootPath, "config core.abbrev 12"); + GitProcess.Invoke(this.RootPath, "remote add origin " + this.RepoUrl); + this.Fetch(this.Commitish); + GitProcess.Invoke(this.RootPath, "branch --set-upstream " + this.Commitish + " origin/" + this.Commitish); + GitProcess.Invoke(this.RootPath, "checkout " + this.Commitish); + GitProcess.Invoke(this.RootPath, "branch --unset-upstream"); + } + + public void Fetch(string commitish) + { + GitProcess.Invoke(this.RootPath, "fetch origin " + commitish); + } + + public void Delete() + { + if (Directory.Exists(this.RootPath)) + { + SystemIORunner.RecursiveDelete(this.RootPath); + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs new file mode 100644 index 00000000..41d01243 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSFunctionalTestEnlistment.cs @@ -0,0 +1,339 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace GVFS.FunctionalTests.Tools +{ + public enum LocalHistoryOptions + { + Invalid = 0, + + FullLocalHistory, + + // Coming soon: + // ShallowLocalHistory, + // NoLocalHistory, + } + + public enum PhysicalCheckoutOptions + { + Invalid = 0, + + FullPhysicalCheckout, + NoPhysicalCheckout, + } + + public class GVFSFunctionalTestEnlistment + { + private const string ZeroBackgroundOperations = "Background operations: 0\r\n"; + private const string LockHeldByGit = "GVFS Lock: Held by {0}"; + private const int SleepMSWaitingForStatusCheck = 100; + private const int DefaultMaxWaitMSForStatusCheck = 5000; + + private GVFSProcess gvfsProcess; + + public GVFSFunctionalTestEnlistment(string pathToGVFS, string enlistmentRoot, string repoUrl, string commitish) + { + this.EnlistmentRoot = enlistmentRoot; + this.RepoUrl = repoUrl; + this.Commitish = commitish; + + this.gvfsProcess = new GVFSProcess(pathToGVFS, this.EnlistmentRoot); + } + + private enum BreadcrumbType + { + Invalid = 0, + + Restart, + + BeginRecurseIntoDirectory, + EndRecurseIntoDirectory, + + TryDeleteFile, + TryDeleteEmptyDirectory, + + DeleteSucceeded, + DeleteFailed, + + SilentFailure, + } + + public string EnlistmentRoot + { + get; private set; + } + + public string RepoUrl + { + get; private set; + } + + public string RepoRoot + { + get { return Path.Combine(this.EnlistmentRoot, "src"); } + } + + public string DotGVFSRoot + { + get { return Path.Combine(this.EnlistmentRoot, ".gvfs"); } + } + + public string DiagnosticsRoot + { + get { return Path.Combine(this.DotGVFSRoot, "diagnostics"); } + } + + public string Commitish + { + get; private set; + } + + public static GVFSFunctionalTestEnlistment Create(string pathToGvfs) + { + return new GVFSFunctionalTestEnlistment( + pathToGvfs, + Properties.Settings.Default.EnlistmentRoot, + Properties.Settings.Default.RepoToClone, + Properties.Settings.Default.Commitish); + } + + public static GVFSFunctionalTestEnlistment CloneAndMount(string pathToGvfs) + { + GVFSFunctionalTestEnlistment enlistment = GVFSFunctionalTestEnlistment.Create(pathToGvfs); + enlistment.UnmountAndDeleteAll(); + enlistment.CloneAndMount(LocalHistoryOptions.FullLocalHistory, PhysicalCheckoutOptions.NoPhysicalCheckout); + + return enlistment; + } + + public void DeleteEnlistment() + { + if (Directory.Exists(this.EnlistmentRoot)) + { + List breadcrumbs = new List(); + RecursiveFolderDeleteRetryForever(breadcrumbs, this.EnlistmentRoot); + } + } + + public void CloneAndMount(LocalHistoryOptions localHistoryOptions, PhysicalCheckoutOptions checkoutOptions) + { + this.gvfsProcess.Clone(this.RepoUrl, this.Commitish); + + this.MountGVFS(); + GitProcess.Invoke(this.RepoRoot, "checkout " + this.Commitish); + GitProcess.Invoke(this.RepoRoot, "branch --unset-upstream"); + } + + public void MountGVFS() + { + this.gvfsProcess.Mount(); + } + + public string PrefetchFolder(string folderPath) + { + return this.gvfsProcess.Prefetch(folderPath); + } + + public string PrefetchFolderBasedOnFile(string filterFilePath) + { + return this.gvfsProcess.PrefetchFolderBasedOnFile(filterFilePath); + } + + public string Diagnose() + { + return this.gvfsProcess.Diagnose(); + } + + public string Status() + { + return this.gvfsProcess.Status(); + } + + public bool WaitForBackgroundOperations(int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) + { + return this.WaitForStatus(maxWaitMilliseconds, ZeroBackgroundOperations); + } + + public bool WaitForLock(string lockCommand, int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) + { + return this.WaitForStatus(maxWaitMilliseconds, string.Format(LockHeldByGit, lockCommand)); + } + + public void UnmountGVFS() + { + this.gvfsProcess.Unmount(); + } + + public void UnmountAndDeleteAll() + { + try + { + this.UnmountGVFS(); + } + finally + { + this.DeleteEnlistment(); + } + } + + public string GetVirtualPathTo(string pathInRepo) + { + return Path.Combine(this.RepoRoot, pathInRepo); + } + + public string GetObjectPathTo(string objectHash) + { + return Path.Combine( + this.RepoRoot, + TestConstants.DotGit.Objects.Root, + objectHash.Substring(0, 2), + objectHash.Substring(2)); + } + + private static void RecursiveFolderDeleteRetryForever(List breadcrumbs, string path) + { + while (true) + { + if (TryRecursiveFolderDelete(breadcrumbs, path)) + { + return; + } + + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.Restart, null)); + Thread.Sleep(500); + } + } + + private static bool TryRecursiveFolderDelete(List breadcrumbs, string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.BeginRecurseIntoDirectory, path)); + + try + { + try + { + foreach (FileInfo file in directory.GetFiles()) + { + try + { + file.Attributes = FileAttributes.Normal; + } + catch (ArgumentException) + { + // Setting the attributes will throw an ArgumentException in situations where + // it really ought to throw IOExceptions, e.g. because the file is currently locked + return false; + } + + if (!TryDelete(breadcrumbs, file)) + { + return false; + } + } + + foreach (DirectoryInfo subDirectory in directory.GetDirectories()) + { + if (!TryRecursiveFolderDelete(breadcrumbs, subDirectory.FullName)) + { + return false; + } + } + } + catch (DirectoryNotFoundException e) + { + // For junctions directory.GetFiles() or .GetDirectories() can throw DirectoryNotFoundException + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.DeleteFailed, path, e)); + } + catch (IOException e) + { + // There is a race when enumerating while a virtualization instance is being shut down + // If GVFlt receives the enumeration request before it knows that GVFS has been shut down + // (and then GVFS does not handle the request because it is shut down) we can get an IOException + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.DeleteFailed, path, e)); + return false; + } + + if (!TryDelete(breadcrumbs, directory)) + { + return false; + } + + return true; + } + finally + { + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.EndRecurseIntoDirectory, path)); + } + } + + private static bool TryDelete(List breadcrumbs, FileSystemInfo fileOrFolder) + { + bool isFile = fileOrFolder is FileInfo; + + breadcrumbs.Add(new Breadcrumb( + isFile ? BreadcrumbType.TryDeleteFile : BreadcrumbType.TryDeleteEmptyDirectory, + fileOrFolder.FullName)); + + try + { + fileOrFolder.Delete(); + if ((isFile && File.Exists(fileOrFolder.FullName)) || + (!isFile && Directory.Exists(fileOrFolder.FullName))) + { + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.SilentFailure, fileOrFolder.FullName)); + return false; + } + else + { + breadcrumbs.Add(new Breadcrumb(BreadcrumbType.DeleteSucceeded, fileOrFolder.FullName)); + return true; + } + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + } + + private bool WaitForStatus(int maxWaitMilliseconds, string statusShouldContain) + { + string status = null; + int totalWaitMilliseconds = 0; + while (totalWaitMilliseconds <= maxWaitMilliseconds && (status == null || !status.Contains(statusShouldContain))) + { + Thread.Sleep(SleepMSWaitingForStatusCheck); + status = this.Status(); + totalWaitMilliseconds += SleepMSWaitingForStatusCheck; + } + + return totalWaitMilliseconds <= maxWaitMilliseconds; + } + + private class Breadcrumb + { + public Breadcrumb(BreadcrumbType type, string path, Exception exception = null) + { + this.BreadcrumbType = type; + this.Path = path; + this.Exception = exception; + } + + public BreadcrumbType BreadcrumbType { get; private set; } + public string Path { get; private set; } + public Exception Exception { get; private set; } + + public override string ToString() + { + return this.BreadcrumbType + ":" + this.Path; + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/GVFSProcess.cs b/GVFS/GVFS.FunctionalTests/Tools/GVFSProcess.cs new file mode 100644 index 00000000..10e9af5b --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/GVFSProcess.cs @@ -0,0 +1,92 @@ +using GVFS.Tests.Should; +using System; +using System.Diagnostics; + +namespace GVFS.FunctionalTests.Tools +{ + public class GVFSProcess + { + private readonly string pathToGVFS; + private readonly string enlistmentRoot; + + public GVFSProcess(string pathToGVFS, string enlistmentRoot) + { + this.pathToGVFS = pathToGVFS; + this.enlistmentRoot = enlistmentRoot; + } + + public void Clone(string repositorySource, string branchToCheckout) + { + string args = string.Format( + "clone \"{0}\" \"{1}\" --branch \"{2}\" --no-mount --no-prefetch", + repositorySource, + this.enlistmentRoot, + branchToCheckout); + this.CallGVFS(args, failOnError: true); + } + + public void Mount() + { + string mountCommand = "mount " + this.enlistmentRoot; + + this.IsGVFSMounted().ShouldEqual(false, "GVFS is already mounted"); + this.CallGVFS(mountCommand); + this.IsGVFSMounted().ShouldEqual(true, "GVFS did not mount"); + } + + public string Prefetch(string folderPath) + { + string args = "prefetch --folders \"" + folderPath + "\" " + this.enlistmentRoot; + return this.CallGVFS(args); + } + + public string PrefetchFolderBasedOnFile(string filterFilePath) + { + string args = "prefetch --folders-list \"" + filterFilePath + "\" " + this.enlistmentRoot; + return this.CallGVFS(args); + } + + public string Diagnose() + { + return this.CallGVFS("diagnose " + this.enlistmentRoot); + } + + public string Status() + { + return this.CallGVFS("status " + this.enlistmentRoot); + } + + public void Unmount() + { + this.CallGVFS("unmount " + this.enlistmentRoot); + } + + private bool IsGVFSMounted() + { + string statusResult = this.CallGVFS("status " + this.enlistmentRoot); + return statusResult.Contains("Mount status: Ready"); + } + + private string CallGVFS(string args, bool failOnError = false) + { + ProcessStartInfo processInfo = new ProcessStartInfo(this.pathToGVFS); + processInfo.Arguments = args; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + using (Process process = Process.Start(processInfo)) + { + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (failOnError) + { + process.ExitCode.ShouldEqual(0, result); + } + + return result; + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs new file mode 100644 index 00000000..4c3e80c1 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs @@ -0,0 +1,122 @@ +using GVFS.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace GVFS.FunctionalTests.Tools +{ + public static class GitHelpers + { + public const string ExcludeFilePath = @".git\info\exclude"; + private const int MaxRetries = 10; + private const int ThreadSleepMS = 1500; + + public static void CheckGitCommand(string virtualRepoRoot, string command, params string[] expectedLinesInResult) + { + ProcessResult result = GitProcess.InvokeProcess(virtualRepoRoot, command); + + foreach (string line in expectedLinesInResult) + { + result.Output.ShouldContain(line); + } + + result.Errors.ShouldBeEmpty(); + } + + public static void CheckNotInGitCommand(string virtualRepoRoot, string command, params string[] unexpectedLinesInResult) + { + ProcessResult result = GitProcess.InvokeProcess(virtualRepoRoot, command); + + foreach (string line in unexpectedLinesInResult) + { + result.Output.ShouldNotContain(line); + } + + result.Errors.ShouldBeEmpty(); + } + + public static ProcessResult InvokeGitAgainstGVFSRepo(string gvfsRepoRoot, string command, bool cleanOutput = true) + { + ProcessResult result = GitProcess.InvokeProcess(gvfsRepoRoot, command); + + string output = result.Output; + if (cleanOutput) + { + string[] lines = output.Split(new string[] { "\r\n" }, StringSplitOptions.None); + output = string.Join("\r\n", lines.Where(line => !line.StartsWith("Waiting for "))); + } + + return new ProcessResult( + output, + result.Errors, + result.ExitCode); + } + + public static void ValidateGitCommand( + GVFSFunctionalTestEnlistment enlistment, + ControlGitRepo controlGitRepo, + string command, + params object[] args) + { + string controlRepoRoot = controlGitRepo.RootPath; + string gvfsRepoRoot = enlistment.RepoRoot; + + command = string.Format(command, args); + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstGVFSRepo(gvfsRepoRoot, command); + actualResult.Output.ShouldEqual(expectedResult.Output); + actualResult.Errors.ShouldEqual(expectedResult.Errors); + + if (command != "status") + { + ValidateGitCommand(enlistment, controlGitRepo, "status"); + } + } + + public static ManualResetEventSlim AcquireGVFSLock(GVFSFunctionalTestEnlistment enlistment, int resetTimeout = Timeout.Infinite) + { + ManualResetEventSlim resetEvent = new ManualResetEventSlim(initialState: false); + + ProcessStartInfo processInfo = new ProcessStartInfo(Properties.Settings.Default.PathToGit); + processInfo.WorkingDirectory = enlistment.RepoRoot; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + processInfo.RedirectStandardInput = true; + processInfo.Arguments = "hash-object --stdin"; + + Process holdingProcess = Process.Start(processInfo); + StreamWriter stdin = holdingProcess.StandardInput; + + enlistment.WaitForLock("git hash-object --stdin"); + + Task.Run( + () => + { + resetEvent.Wait(resetTimeout); + + // Make sure to let the holding process end. + if (stdin != null) + { + stdin.WriteLine("dummy"); + stdin.Close(); + } + + if (holdingProcess != null) + { + if (!holdingProcess.HasExited) + { + holdingProcess.Kill(); + } + + holdingProcess.Dispose(); + } + }); + + return resetEvent; + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/GitProcess.cs b/GVFS/GVFS.FunctionalTests/Tools/GitProcess.cs new file mode 100644 index 00000000..64dbd83c --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/GitProcess.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; + +namespace GVFS.FunctionalTests.Tools +{ + public class GitProcess + { + public static string Invoke(string executionWorkingDirectory, string command) + { + return InvokeProcess(executionWorkingDirectory, command).Output; + } + + public static ProcessResult InvokeProcess(string executionWorkingDirectory, string command) + { + ProcessStartInfo processInfo = new ProcessStartInfo(Properties.Settings.Default.PathToGit); + processInfo.WorkingDirectory = executionWorkingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + processInfo.Arguments = command; + + processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; + + return ProcessHelper.Run(processInfo); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/NativeMethods.cs b/GVFS/GVFS.FunctionalTests/Tools/NativeMethods.cs new file mode 100644 index 00000000..26ac54a3 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/NativeMethods.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace GVFS.FunctionalTests.Tools +{ + public class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool MoveFile(string lpExistingFileName, string lpNewFileName); + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs b/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs new file mode 100644 index 00000000..14ad3ed9 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/ProcessHelper.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; + +namespace GVFS.FunctionalTests.Tools +{ + public static class ProcessHelper + { + public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null) + { + using (Process executingProcess = new Process()) + { + string output = string.Empty; + string errors = string.Empty; + + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + executingProcess.StartInfo = processInfo; + executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors = errors + args.Data + errorMsgDelimeter; + } + }; + + if (executionLock != null) + { + lock (executionLock) + { + output = StartProcess(executingProcess); + } + } + else + { + output = StartProcess(executingProcess); + } + + return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); + } + } + + private static string StartProcess(Process executingProcess) + { + executingProcess.Start(); + + if (executingProcess.StartInfo.RedirectStandardError) + { + executingProcess.BeginErrorReadLine(); + } + + string output = string.Empty; + if (executingProcess.StartInfo.RedirectStandardOutput) + { + output = executingProcess.StandardOutput.ReadToEnd(); + } + + executingProcess.WaitForExit(); + + return output; + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/ProcessResult.cs b/GVFS/GVFS.FunctionalTests/Tools/ProcessResult.cs new file mode 100644 index 00000000..21ed1162 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/ProcessResult.cs @@ -0,0 +1,16 @@ +namespace GVFS.FunctionalTests.Tools +{ + public class ProcessResult + { + public ProcessResult(string output, string errors, int exitCode) + { + this.Output = output; + this.Errors = errors; + this.ExitCode = exitCode; + } + + public string Output { get; } + public string Errors { get; } + public int ExitCode { get; } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tools/TestConstants.cs b/GVFS/GVFS.FunctionalTests/Tools/TestConstants.cs new file mode 100644 index 00000000..f16fb04c --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tools/TestConstants.cs @@ -0,0 +1,24 @@ +using System.IO; + +namespace GVFS.FunctionalTests.Tools +{ + public static class TestConstants + { + public static class DotGit + { + public const string Root = ".git"; + public static readonly string Head = Path.Combine(DotGit.Root, "HEAD"); + + public static class Objects + { + public static readonly string Root = Path.Combine(DotGit.Root, "objects"); + } + + public static class Info + { + public static readonly string Root = Path.Combine(DotGit.Root, "info"); + public static readonly string SparseCheckout = Path.Combine(Root, "sparse-checkout"); + } + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/app.config b/GVFS/GVFS.FunctionalTests/app.config new file mode 100644 index 00000000..9317915b --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/app.config @@ -0,0 +1,48 @@ + + + + +
+ + + + + + + + + GVFS.exe + + + C:\Repos\GVFSFunctionalTests\enlistment + + + URL of a VSTS git repo that contains the Commitish below + + + 1 + + + + + + C:\Program Files\Git\bin\bash.exe + + + FunctionalTests/20170130 + + + C:\Repos\GVFSFunctionalTests\ControlRepo + + + C:\Repos\GVFSFunctionalTests\FastFetch\Test + + + C:\Repos\GVFSFunctionalTests\FastFetch\Control + + + C:\Program Files\Git\cmd\git.exe + + + + \ No newline at end of file diff --git a/GVFS/GVFS.FunctionalTests/packages.config b/GVFS/GVFS.FunctionalTests/packages.config new file mode 100644 index 00000000..305e60a4 --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.GVFlt/DotGit/ExcludeFile.cs b/GVFS/GVFS.GVFlt/DotGit/ExcludeFile.cs new file mode 100644 index 00000000..e9608f1d --- /dev/null +++ b/GVFS/GVFS.GVFlt/DotGit/ExcludeFile.cs @@ -0,0 +1,95 @@ +using GVFS.Common; +using GVFS.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace GVFS.GVFlt.DotGit +{ + public class ExcludeFile + { + private const string DefaultEntry = "*"; + private HashSet entries; + private FileSerializer fileSerializer; + private GVFSContext context; + + public ExcludeFile(GVFSContext context, string virtualExcludeFilePath) + { + this.entries = new HashSet(StringComparer.OrdinalIgnoreCase); + this.fileSerializer = new FileSerializer(context, virtualExcludeFilePath); + this.context = context; + } + + public void LoadOrCreate() + { + foreach (string line in this.fileSerializer.ReadAll()) + { + string sanitizedFileLine; + if (GitConfigFileUtils.TrySanitizeConfigFileLine(line, out sanitizedFileLine)) + { + this.entries.Add(sanitizedFileLine); + } + } + + // Ensure the default entry is always in the exclude file + if (this.entries.Add(DefaultEntry)) + { + this.fileSerializer.AppendLine(DefaultEntry); + this.fileSerializer.Close(); + } + } + + public void Close() + { + this.fileSerializer.Close(); + } + + public CallbackResult FolderChanged(string virtualPath) + { + try + { + string[] pathParts = virtualPath.Split(new char[] { GVFSConstants.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + + StringBuilder path = new StringBuilder("!"); + for (int i = 0; i < pathParts.Length; i++) + { + path.Append(GVFSConstants.GitPathSeparatorString + pathParts[i]); + string entry = path.ToString(); + if (this.entries.Add(entry)) + { + this.fileSerializer.AppendLine(entry); + } + } + + string finalEntry = path.ToString() + GVFSConstants.GitPathSeparatorString + "*"; + if (this.entries.Add(finalEntry)) + { + this.fileSerializer.AppendLine(finalEntry); + } + } + catch (IOException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "ExcludeFile"); + metadata.Add("virtualPath", virtualPath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "IOException caught while processing FolderChanged"); + this.context.Tracer.RelatedError(metadata); + return CallbackResult.RetryableError; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "ExcludeFile"); + metadata.Add("virtualPath", virtualPath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Exception caught while processing FolderChanged"); + this.context.Tracer.RelatedError(metadata); + return CallbackResult.FatalError; + } + + return CallbackResult.Success; + } + } +} diff --git a/GVFS/GVFS.GVFlt/DotGit/FileSerializer.cs b/GVFS/GVFS.GVFlt/DotGit/FileSerializer.cs new file mode 100644 index 00000000..6d8959b3 --- /dev/null +++ b/GVFS/GVFS.GVFlt/DotGit/FileSerializer.cs @@ -0,0 +1,89 @@ +using GVFS.Common; +using System; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.GVFlt.DotGit +{ + public class FileSerializer + { + private readonly string filePath; + private GVFSContext context; + private Stream fileStream; + private StreamWriter fileWriter; + + public FileSerializer(GVFSContext context, string filePath) + { + this.context = context; + this.filePath = filePath; + } + + public IEnumerable ReadAll() + { + using (Stream stream = this.context.FileSystem.OpenFileStream( + this.filePath, + FileMode.OpenOrCreate, + FileAccess.Read, + FileShare.None)) + { + using (StreamReader reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + yield return reader.ReadLine(); + } + } + } + } + + /// + /// Appends the specified line to the file (using \n line breaks). + /// AppendLine will open the file (if the file was not previously opened by a call to AppendLine). + /// Callers must call Close() after calling AppendLine, as AppendLine leaves the file open. + /// + /// Line to append + public void AppendLine(string line) + { + try + { + if (this.fileStream == null) + { + this.fileStream = this.context.FileSystem.OpenFileStream( + this.filePath, + FileMode.Open, + FileAccess.ReadWrite, + FileShare.Read); + this.fileStream.Position = this.fileStream.Length; + } + + if (this.fileWriter == null) + { + this.fileWriter = new StreamWriter(this.fileStream); + this.fileWriter.AutoFlush = true; + } + } + catch (Exception) + { + this.Close(); + throw; + } + + this.fileWriter.Write(line + "\n"); + } + + public void Close() + { + if (this.fileWriter != null) + { + this.fileWriter.Dispose(); + this.fileWriter = null; + } + + if (this.fileStream != null) + { + this.fileStream.Dispose(); + this.fileStream = null; + } + } + } +} diff --git a/GVFS/GVFS.GVFlt/DotGit/GitConfigFileUtils.cs b/GVFS/GVFS.GVFlt/DotGit/GitConfigFileUtils.cs new file mode 100644 index 00000000..48103c25 --- /dev/null +++ b/GVFS/GVFS.GVFlt/DotGit/GitConfigFileUtils.cs @@ -0,0 +1,27 @@ +namespace GVFS.GVFlt.DotGit +{ + public class GitConfigFileUtils + { + /// + /// Sanitizes lines read from Git config files: + /// - Removes leading and trailing whitespace + /// - Removes comments + /// + /// Input line from config file + /// Sanitized config file line + /// true if sanitizedLine has content, false if there is no content left after sanitizing + public static bool TrySanitizeConfigFileLine(string fileLine, out string sanitizedLine) + { + sanitizedLine = fileLine; + int commentIndex = sanitizedLine.IndexOf('#'); + if (commentIndex >= 0) + { + sanitizedLine = sanitizedLine.Substring(0, commentIndex); + } + + sanitizedLine = sanitizedLine.Trim(); + + return !string.IsNullOrWhiteSpace(sanitizedLine); + } + } +} diff --git a/GVFS/GVFS.GVFlt/DotGit/SparseCheckoutAndDoNotProject.cs b/GVFS/GVFS.GVFlt/DotGit/SparseCheckoutAndDoNotProject.cs new file mode 100644 index 00000000..7748bdb9 --- /dev/null +++ b/GVFS/GVFS.GVFlt/DotGit/SparseCheckoutAndDoNotProject.cs @@ -0,0 +1,204 @@ +using GVFS.Common; +using GVFS.Common.Tracing; +using Microsoft.Isam.Esent.Collections.Generic; +using System; +using System.IO; + +namespace GVFS.GVFlt.DotGit +{ + public class SparseCheckoutAndDoNotProject : IDisposable + { + private FileSerializer sparseCheckoutSerializer; + + // sparseCheckoutEntries + // - Mirror of what’s on disk in the sparse-checkout file + // - Files and folder paths in sparseCheckoutEntries should not be projected + private ConcurrentHashSet sparseCheckoutEntries; + + // additionalDoNotProject + // - File and folder paths that should not be projected, but git.exe does not need to know about (and + // so they are not in the sparse-checkout) + private PersistentDictionary additionalDoNotProject; + private GVFSContext context; + + public SparseCheckoutAndDoNotProject(GVFSContext context, string virtualSparseCheckoutFilePath, string databaseName) + { + this.sparseCheckoutEntries = new ConcurrentHashSet(StringComparer.OrdinalIgnoreCase); + this.sparseCheckoutSerializer = new FileSerializer(context, virtualSparseCheckoutFilePath); + + this.additionalDoNotProject = new PersistentDictionary( + Path.Combine(context.Enlistment.DotGVFSRoot, databaseName)); + this.context = context; + } + + public void LoadOrCreate() + { + foreach (string line in this.sparseCheckoutSerializer.ReadAll()) + { + string sanitizedFileLine; + if (GitConfigFileUtils.TrySanitizeConfigFileLine(line, out sanitizedFileLine)) + { + this.sparseCheckoutEntries.Add(sanitizedFileLine); + } + } + + this.sparseCheckoutSerializer.Close(); + } + + public void Close() + { + this.sparseCheckoutSerializer.Close(); + } + + /// + /// Checks if the specified path is in either the sparse-checkout file or the additionalDoNotProject + /// database. If the path is in neither of these, then it should be projected. + /// + /// True if the path should be projected, and false if it should not (i.e. because the + /// path is in the sparse-checkout or additionalDoNotProject collections) + public bool ShouldPathBeProjected(string virtualPath, bool isFolder) + { + string entry = this.NormalizeEntryString(virtualPath, isFolder); + return !(this.sparseCheckoutEntries.Contains(entry) || this.additionalDoNotProject.ContainsKey(entry)); + } + + public void StopProjecting(string virtualPath, bool isFolder) + { + string entry = this.NormalizeEntryString(virtualPath, isFolder); + if (!this.additionalDoNotProject.ContainsKey(entry)) + { + // Use [] rather than Add to avoid ArgumentException if the key already exists. + this.additionalDoNotProject[entry] = true; + this.additionalDoNotProject.Flush(); + } + } + + public CallbackResult OnFolderCreated(string virtualPath) + { + return this.AddSparseCheckoutEntry(virtualPath, isFolder: true); + } + + public CallbackResult OnFolderRenamed(string newVirtualPath) + { + return this.AddSparseCheckoutEntry(newVirtualPath, isFolder: true); + } + + public CallbackResult OnFolderDeleted(string newVirtualPath) + { + return this.AddSparseCheckoutEntry(newVirtualPath, isFolder: true); + } + + public CallbackResult OnPartialPlaceholderFolderCreated(string virtualPath) + { + this.StopProjecting(virtualPath, isFolder: true); + return CallbackResult.Success; + } + + public CallbackResult OnPlaceholderFileCreated(string virtualPath, DateTime createTimeUtc, DateTime lastWriteTimeUtc, long fileSize) + { + return this.AddFileEntryAndClearSkipWorktreeBit(virtualPath, createTimeUtc, lastWriteTimeUtc, fileSize); + } + + public CallbackResult OnFileCreated(string virtualPath) + { + return this.AddFileEntryAndClearSkipWorktreeBit(virtualPath, createTimeUtc: DateTime.MinValue, lastWriteTimeUtc: DateTime.MinValue, fileSize: 0); + } + + public CallbackResult OnFileRenamed(string virtualPath) + { + return this.AddFileEntryAndClearSkipWorktreeBit(virtualPath, createTimeUtc: DateTime.MinValue, lastWriteTimeUtc: DateTime.MinValue, fileSize: 0); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (this.additionalDoNotProject != null) + { + this.additionalDoNotProject.Dispose(); + this.additionalDoNotProject = null; + } + } + + private string NormalizeEntryString(string virtualPath, bool isFolder) + { + return GVFSConstants.GitPathSeparatorString + + virtualPath.TrimStart(GVFSConstants.PathSeparator).Replace(GVFSConstants.PathSeparator, GVFSConstants.GitPathSeparator) + + (isFolder ? GVFSConstants.GitPathSeparatorString : string.Empty); + } + + private CallbackResult AddFileEntryAndClearSkipWorktreeBit( + string virtualPath, + DateTime createTimeUtc, + DateTime lastWriteTimeUtc, + long fileSize) + { + string fileName = Path.GetFileName(virtualPath); + CallbackResult result = this.AddSparseCheckoutEntry(virtualPath, isFolder: false); + if (result != CallbackResult.Success) + { + return result; + } + + return this.context.Repository.Index.ClearSkipWorktreeAndUpdateEntry(virtualPath, createTimeUtc, lastWriteTimeUtc, (uint)fileSize); + } + + private CallbackResult AddSparseCheckoutEntry(string virtualPath, bool isFolder) + { + string entry = this.NormalizeEntryString(virtualPath, isFolder); + if (this.sparseCheckoutEntries.Add(entry)) + { + try + { + this.sparseCheckoutSerializer.AppendLine(entry); + } + catch (IOException e) + { + CallbackResult result = CallbackResult.RetryableError; + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "SparseCheckoutAndDoNotProject"); + metadata.Add("virtualFolderPath", virtualPath); + metadata.Add("isFolder", isFolder); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "IOException caught while processing AddSparseCheckoutEntry"); + + // Remove the entry so that if AddRecursiveSparseCheckoutEntry is called again + // we'll try to append to the file again + if (!this.sparseCheckoutEntries.TryRemove(entry)) + { + metadata["ErrorMessage"] += ", failed to undo addition to sparseCheckoutEntries"; + result = CallbackResult.FatalError; + } + + this.context.Tracer.RelatedError(metadata); + return result; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "SparseCheckoutAndDoNotProject"); + metadata.Add("virtualFolderPath", virtualPath); + metadata.Add("isFolder", isFolder); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Exception caught while processing AddSparseCheckoutEntry"); + + // Remove the entry so that if AddRecursiveSparseCheckoutEntry is called again + // we'll try to append to the file again + if (!this.sparseCheckoutEntries.TryRemove(entry)) + { + metadata["ErrorMessage"] += ", failed to undo addition to sparseCheckoutEntries"; + } + + this.context.Tracer.RelatedError(metadata); + return CallbackResult.FatalError; + } + } + + return CallbackResult.Success; + } + } +} diff --git a/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj b/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj new file mode 100644 index 00000000..72d89ec5 --- /dev/null +++ b/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj @@ -0,0 +1,124 @@ + + + + + Debug + AnyCPU + {1118B427-7063-422F-83B9-5023C8EC5A7A} + Library + Properties + GVFS.GVFlt + GVFS.GVFlt + v4.5.2 + 512 + + + + + true + ..\..\..\BuildOutput\GVFS.GVFlt\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.GVFlt\obj\x64\Debug\ + DEBUG;TRACE + true + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + ..\..\..\BuildOutput\GVFS.GVFlt\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.GVFlt\obj\x64\Release\ + TRACE + true + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + False + ..\..\..\packages\Microsoft.Database.Collections.Generic.1.9.4\lib\net40\Esent.Collections.dll + True + + + False + ..\..\..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll + True + + + False + ..\..\..\packages\Microsoft.Database.Isam.1.9.4\lib\net40\Esent.Isam.dll + True + + + False + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + False + ..\..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + true + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + GVFS.Common + + + {fb0831ae-9997-401b-b31f-3a065fdbeb20} + GVFS.GvFltWrapper + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.GVFlt/GVFltActiveEnumeration.cs b/GVFS/GVFS.GVFlt/GVFltActiveEnumeration.cs new file mode 100644 index 00000000..89e86b4b --- /dev/null +++ b/GVFS/GVFS.GVFlt/GVFltActiveEnumeration.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; + +namespace GVFS.GVFlt +{ + public class GVFltActiveEnumeration : IDisposable + { + private readonly IEnumerable fileInfos; + private IEnumerator fileInfoEnumerator; + private bool disposed = false; + private string filterString = null; + + public GVFltActiveEnumeration(IEnumerable fileInfos) + { + this.fileInfos = fileInfos; + this.ResetEnumerator(); + this.MoveNext(); + } + + /// + /// true if Current refers to an element in the enumeration, false if Current is past the end of the collection + /// + public bool IsCurrentValid { get; private set; } + + /// + /// Gets the element in the collection at the current position of the enumerator + /// + public GVFltFileInfo Current + { + get { return this.fileInfoEnumerator.Current; } + } + + /// + /// Resets the enumerator and advances it to the first GVFltFileInfo in the enumeration + /// + /// Filter string to save. Can be null. + public void RestartEnumeration(string filter) + { + this.ResetEnumerator(); + this.IsCurrentValid = this.fileInfoEnumerator.MoveNext(); + this.SaveFilter(filter); + } + + /// + /// Advances the enumerator to the next element of the collection (that is being projected). + /// If a filter string is set, MoveNext will advance to the next entry that matches the filter. + /// + /// + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection + /// + public bool MoveNext() + { + this.IsCurrentValid = this.fileInfoEnumerator.MoveNext(); + while (this.IsCurrentValid && this.IsCurrentHidden()) + { + this.IsCurrentValid = this.fileInfoEnumerator.MoveNext(); + } + + return this.IsCurrentValid; + } + + /// + /// Attempts to save the filter string for this enumeration. When setting a filter string, if Current is valid + /// and does not match the specified filter, the enumerator will be advanced until an element is found that + /// matches the filter (or the end of the collection is reached). + /// + /// Filter string to save. Can be null. + /// True if the filter string was saved. False if the filter string was not saved (because a filter string + /// was previously saved). + /// + /// + /// Per MSDN (https://msdn.microsoft.com/en-us/library/windows/hardware/ff567047(v=vs.85).aspx, the filter string + /// specified in the first call to ZwQueryDirectoryFile will be used for all subsequent calls for the handle (and + /// the string specified in subsequent calls should be ignored) + /// + public bool TrySaveFilterString(string filter) + { + if (this.filterString == null) + { + this.SaveFilter(filter); + return true; + } + + return false; + } + + /// + /// Returns the current filter string or null if no filter string has been saved + /// + /// The current filter string or null if no filter string has been saved + public string GetFilterString() + { + return this.filterString; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.fileInfoEnumerator.Dispose(); + } + + this.disposed = true; + } + } + + private static bool FileNameMatchesFilter(string name, string filter) + { + if (string.IsNullOrEmpty(filter)) + { + return true; + } + + return PatternMatcher.StrictMatchPattern(filter.ToUpperInvariant(), name.ToUpperInvariant()); + } + + private void SaveFilter(string filter) + { + if (string.IsNullOrEmpty(filter)) + { + this.filterString = string.Empty; + } + else + { + this.filterString = filter; + if (this.IsCurrentValid && this.IsCurrentHidden()) + { + this.MoveNext(); + } + } + } + + private bool IsCurrentHidden() + { + return !this.Current.IsProjected || !FileNameMatchesFilter(this.Current.Name, this.GetFilterString()); + } + + private void ResetEnumerator() + { + this.fileInfoEnumerator = this.fileInfos.GetEnumerator(); + } + } +} diff --git a/GVFS/GVFS.GVFlt/GVFltCallbacks.cs b/GVFS/GVFS.GVFlt/GVFltCallbacks.cs new file mode 100644 index 00000000..b7058b47 --- /dev/null +++ b/GVFS/GVFS.GVFlt/GVFltCallbacks.cs @@ -0,0 +1,1630 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using GVFS.GVFlt.DotGit; +using GVFSGvFltWrapper; +using Microsoft.Database.Isam.Config; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Isam.Esent.Collections.Generic; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace GVFS.GVFlt +{ + public class GVFltCallbacks : IDisposable + { + private const string RefMarker = "ref:"; + private const int BlockSize = 64 * 1024; + private const long AllocationSize = 1L << 10; + private const int AcquireGitLockRetries = 50; + private const int AcquireGitLockWaitPerTryMillis = 600; + + private const int MinGvFltThreads = 3; + + private static readonly string RefsHeadsPath = GVFSConstants.DotGit.Refs.Heads.Root + GVFSConstants.PathSeparator; + private readonly string logsHeadPath; + + private GvFltWrapper gvflt; + private object stopLock = new object(); + private bool gvfltIsStarted = false; + private bool isMountComplete = false; + private ConcurrentDictionary activeEnumerations; + private ConcurrentDictionary workingDirectoryFolders; + private GVFSGitObjects gvfsGitObjects; + private SparseCheckoutAndDoNotProject sparseCheckoutAndDoNotProject; + private ExcludeFile excludeFile; + private PersistentDictionary blobSizes; + private string projectedCommitId = null; + private IDisposable folderCreateWatcher; + private IDisposable fileCreateWatcher; + + private ConcurrentHashSet createdByGVFS = new ConcurrentHashSet(StringComparer.OrdinalIgnoreCase); + + private ReliableBackgroundOperations background; + private GVFSContext context; + private FileProperties logsHeadFileProperties; + + public GVFltCallbacks(GVFSContext context, GVFSGitObjects gitObjects) + { + this.context = context; + this.logsHeadFileProperties = null; + this.gvflt = new GvFltWrapper(); + this.activeEnumerations = new ConcurrentDictionary(); + this.workingDirectoryFolders = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + this.sparseCheckoutAndDoNotProject = new SparseCheckoutAndDoNotProject( + this.context, + Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Info.SparseCheckoutPath), + GVFSConstants.DatabaseNames.DoNotProject); + this.excludeFile = new ExcludeFile(this.context, Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Info.ExcludePath)); + this.blobSizes = new PersistentDictionary( + Path.Combine(this.context.Enlistment.DotGVFSRoot, GVFSConstants.DatabaseNames.BlobSizes), + new DatabaseConfig() + { + CacheSizeMax = 500 * 1024 * 1024, // 500 MB + }); + this.gvfsGitObjects = gitObjects; + + this.background = new ReliableBackgroundOperations( + this.context, + this.PreBackgroundOperation, + this.ExecuteBackgroundOperation, + this.PostBackgroundOperation, + GVFSConstants.DatabaseNames.BackgroundGitUpdates); + + this.logsHeadPath = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Logs.Head); + } + + public static bool TryPrepareFolderForGVFltCallbacks(string folderPath, out string error) + { + error = string.Empty; + Guid virtualizationInstanceGuid = Guid.NewGuid(); + HResult result = GvFltWrapper.GvConvertDirectoryToVirtualizationRoot(virtualizationInstanceGuid, folderPath); + if (result != HResult.Ok) + { + error = "Failed to prepare \"" + folderPath + "\" for callbacks, error: " + result.ToString("F"); + return false; + } + + return true; + } + + public static bool DoesPathAllowDelete(string virtualPath) + { + if (virtualPath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + public static bool IsPathMonitoredForWrites(string virtualPath) + { + if (virtualPath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase) || + virtualPath.Equals(GVFSConstants.DotGit.Head, StringComparison.OrdinalIgnoreCase) || + virtualPath.Equals(GVFSConstants.DotGit.Logs.Head, StringComparison.OrdinalIgnoreCase) || + virtualPath.StartsWith(RefsHeadsPath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + public int GetBackgroundOperationCount() + { + return this.background.Count; + } + + public bool TryStart(out string error) + { + error = string.Empty; + + this.sparseCheckoutAndDoNotProject.LoadOrCreate(); + this.excludeFile.LoadOrCreate(); + this.context.Repository.Initialize(); + + // Callbacks + this.gvflt.OnStartDirectoryEnumeration = this.GVFltStartDirectoryEnumerationHandler; + this.gvflt.OnEndDirectoryEnumeration = this.GVFltEndDirectoryEnumerationHandler; + this.gvflt.OnGetDirectoryEnumeration = this.GVFltGetDirectoryEnumerationHandler; + this.gvflt.OnQueryFileName = this.GVFltQueryFileNameHandler; + this.gvflt.OnGetPlaceHolderInformation = this.GVFltGetPlaceHolderInformationHandler; + this.gvflt.OnGetFileStream = this.GVFltGetFileStreamHandler; + this.gvflt.OnNotifyFirstWrite = this.GVFltNotifyFirstWriteHandler; + + this.gvflt.OnNotifyCreate = this.GVFltNotifyCreateHandler; + this.gvflt.OnNotifyPreDelete = this.GVFltNotifyPreDeleteHandler; + this.gvflt.OnNotifyPreRename = null; + this.gvflt.OnNotifyPreSetHardlink = null; + this.gvflt.OnNotifyFileRenamed = this.GVFltNotifyFileRenamedHandler; + this.gvflt.OnNotifyHardlinkCreated = null; + this.gvflt.OnNotifyFileHandleClosed = this.GVFltNotifyFileHandleClosedHandler; + + uint threadCount = (uint)Math.Max(MinGvFltThreads, Environment.ProcessorCount * 2); + + // We currently use twice as many threads as connections to allow for + // non-network operations to possibly succeed despite the connection limit + HResult result = this.gvflt.GvStartVirtualizationInstance( + this.context.Tracer, + this.context.Enlistment.WorkingDirectoryRoot, + poolThreadCount: threadCount, + concurrentThreadCount: threadCount); + + if (result != HResult.Ok) + { + this.context.Tracer.RelatedError("GvStartVirtualizationInstance failed: " + result.ToString("X") + "(" + result.ToString("G") + ")"); + error = "Failed to start virtualization instance (" + result.ToString() + ")"; + return false; + } + + bool gvfsHeadFileFound; + string parseGVFSHeadFileErrors; + if (!this.context.Enlistment.TryParseGVFSHeadFile(out gvfsHeadFileFound, out parseGVFSHeadFileErrors, out this.projectedCommitId)) + { + if (gvfsHeadFileFound) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("parseGVFSHeadFileErrors", parseGVFSHeadFileErrors); + metadata.Add("ErrorMessage", "TryStart: Failed to parse GVFSHeadFile"); + this.context.Tracer.RelatedError(metadata); + } + } + + if (string.IsNullOrEmpty(this.projectedCommitId)) + { + this.UpdateGVFSHead(this.GetHeadCommitId()); + } + + if (this.projectedCommitId == null) + { + throw new GvFltException("Failed to start virtualiation instance, error: Failed to retreive projected commit ID"); + } + + // TODO 694569: Replace file system watcher with GVFlt callbacks + this.folderCreateWatcher = this.context.FileSystem.MonitorChanges( + this.context.Enlistment.WorkingDirectoryRoot, + notifyFilter: NotifyFilters.DirectoryName, + onCreate: e => + { + if (!PathUtil.IsPathInsideDotGit(e.Name) && !this.createdByGVFS.Contains(e.Name)) + { + this.StopProjecting(e.Name, isFolder: true); + this.background.Enqueue(BackgroundGitUpdate.OnFolderCreated(e.Name)); + } + }, + onRename: e => + { + if (!PathUtil.IsPathInsideDotGit(e.Name)) + { + this.background.Enqueue(BackgroundGitUpdate.OnFolderRenamed(e.OldName, e.Name)); + } + }, + onDelete: e => + { + if (!PathUtil.IsPathInsideDotGit(e.Name)) + { + this.background.Enqueue(BackgroundGitUpdate.OnFolderDeleted(e.Name)); + } + }); + + this.fileCreateWatcher = this.context.FileSystem.MonitorChanges( + this.context.Enlistment.WorkingDirectoryRoot, + notifyFilter: NotifyFilters.FileName, + onCreate: e => + { + if (!PathUtil.IsPathInsideDotGit(e.Name) && !this.createdByGVFS.Contains(e.Name)) + { + this.StopProjecting(e.Name, isFolder: false); + this.background.Enqueue(BackgroundGitUpdate.OnFileCreated(e.Name)); + } + }, + onRename: e => + { + if (!PathUtil.IsPathInsideDotGit(e.Name)) + { + this.background.Enqueue(BackgroundGitUpdate.OnFileRenamed(e.OldName, e.Name)); + } + }, + onDelete: null); + + this.gvfltIsStarted = true; + this.background.Start(); + this.isMountComplete = true; + + return true; + } + + public void Stop() + { + lock (this.stopLock) + { + // Stop the background thread first since some of its operations might require that the GVFlt + // Virtualization Instance still be present + this.background.Shutdown(); + + if (this.gvfltIsStarted) + { + this.gvflt.GvStopVirtualizationInstance(); + this.gvflt.GvDetachDriver(); + Console.WriteLine("GVFlt callbacks stopped"); + this.gvfltIsStarted = false; + } + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (this.folderCreateWatcher != null) + { + this.folderCreateWatcher.Dispose(); + this.folderCreateWatcher = null; + } + + if (this.fileCreateWatcher != null) + { + this.fileCreateWatcher.Dispose(); + this.fileCreateWatcher = null; + } + + if (this.sparseCheckoutAndDoNotProject != null) + { + this.sparseCheckoutAndDoNotProject.Dispose(); + this.sparseCheckoutAndDoNotProject = null; + } + + if (this.blobSizes != null) + { + this.blobSizes.Dispose(); + this.blobSizes = null; + } + + if (this.background != null) + { + this.background.Dispose(); + this.background = null; + } + + if (this.context != null) + { + this.context.Dispose(); + this.context = null; + } + } + } + + private void UpdateGVFSHead(string commitId) + { + this.projectedCommitId = commitId; + string gvfsHeadFile = this.context.Enlistment.GVFSHeadFile; + this.context.FileSystem.WriteAllText(gvfsHeadFile, this.projectedCommitId); + } + + private void OnIndexFileChange() + { + this.context.Repository.Index.Invalidate(); + } + + private void OnHeadChange() + { + string repoHeadCommitId = this.GetHeadCommitId(); + + if (repoHeadCommitId == null) + { + // This will happen if the ref mentioned in .git\HEAD does not exist. This happens during "git branch -m", + // because deletes the old ref before creating the new one. It can also happen if a user simply deletes + // the ref that they're currently on. + + // In this situation, we will continue projecting the last commit we were at until HEAD changes again. + return; + } + + if (!this.projectedCommitId.Equals(repoHeadCommitId)) + { + // We need to capture what the git command is when the HEAD is changed + // so that some other command doesn't run and change it before we have a chance to read it + string lockedGitCommand = this.context.Repository.GVFSLock.GetLockedGitCommand(); + if (string.IsNullOrEmpty(lockedGitCommand)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "WorkingDirectoryCallbacks"); + metadata.Add("Message", "gvfs lock not held."); + this.context.Tracer.RelatedEvent(EventLevel.Warning, "OnHeadChange", metadata); + } + + if (string.IsNullOrEmpty(lockedGitCommand) || + (GitHelper.IsVerb(lockedGitCommand, "reset") && + !lockedGitCommand.Contains("--hard") && + !lockedGitCommand.Contains("--merge") && + !lockedGitCommand.Contains("--keep"))) + { + // If there were any files that were added, the paths need to be in the + // exclude file so they will show up as untracked + this.background.Enqueue(BackgroundGitUpdate.OnHeadChangeForNonHardReset(repoHeadCommitId, this.projectedCommitId)); + } + else if (GitHelper.IsVerb(lockedGitCommand, "commit")) + { + this.UpdateGVFSHead(repoHeadCommitId); + } + else + { + this.UpdateGVFSHead(repoHeadCommitId); + this.workingDirectoryFolders.Clear(); + } + } + } + + private void OnLogsHeadChange() + { + // Don't open the .git\logs\HEAD file here to check its attributes as we're in a callback for the .git folder + this.logsHeadFileProperties = null; + } + + private StatusCode GVFltStartDirectoryEnumerationHandler(Guid enumerationId, string virtualPath) + { + virtualPath = PathUtil.RemoveTrailingSlashIfPresent(virtualPath); + + if (!this.isMountComplete) + { + EventMetadata metadata = this.CreateEventMetadata( + "GVFltStartDirectoryEnumerationHandler: Failed to start enumeration, mount has not yet completed", + virtualPath); + metadata.Add("enumerationId", enumerationId); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "StartDirectoryEnum_MountNotComplete", metadata); + + return StatusCode.StatusDeviceNotReady; + } + + GVFltFolder folder; + try + { + folder = this.workingDirectoryFolders.GetOrAdd( + virtualPath, + path => new GVFltFolder(this.context, this.gvfsGitObjects, this.sparseCheckoutAndDoNotProject, this.blobSizes, path, this.projectedCommitId)); + } + catch (TimeoutException e) + { + EventMetadata metadata = this.CreateEventMetadata( + "GVFltStartDirectoryEnumerationHandler: Timeout while creating GVFltFolder", + virtualPath, + e, + errorMessage: true); + metadata.Add("enumerationId", enumerationId); + this.context.Tracer.RelatedError(metadata); + + return StatusCode.StatusTimeout; + } + + GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(folder.GetItems()); + if (!this.activeEnumerations.TryAdd(enumerationId, activeEnumeration)) + { + EventMetadata metadata = this.CreateEventMetadata( + "GVFltStartDirectoryEnumerationHandler: Failed to add enumeration ID to active collection", + virtualPath, + exception: null, + errorMessage: true); + metadata.Add("enumerationId", enumerationId); + this.context.Tracer.RelatedError(metadata); + + activeEnumeration.Dispose(); + return StatusCode.StatusInvalidParameter; + } + + return StatusCode.StatusSucccess; + } + + private StatusCode GVFltEndDirectoryEnumerationHandler(Guid enumerationId) + { + GVFltActiveEnumeration activeEnumeration; + if (this.activeEnumerations.TryRemove(enumerationId, out activeEnumeration)) + { + activeEnumeration.Dispose(); + } + else + { + EventMetadata metadata = this.CreateEventMetadata( + "GVFltEndDirectoryEnumerationHandler: Failed to remove enumeration ID from active collection", + virtualPath: null, + exception: null, + errorMessage: true); + + metadata.Add("enumerationId", enumerationId); + this.context.Tracer.RelatedError(metadata); + return StatusCode.StatusInvalidParameter; + } + + return StatusCode.StatusSucccess; + } + + private StatusCode GVFltGetDirectoryEnumerationHandler( + Guid enumerationId, + string filterFileName, + bool restartScan, + GvDirectoryEnumerationResult result) + { + GVFltActiveEnumeration activeEnumeration = null; + if (!this.activeEnumerations.TryGetValue(enumerationId, out activeEnumeration)) + { + EventMetadata metadata = this.CreateEventMetadata( + "GVFltGetDirectoryEnumerationHandler: Failed to find active enumeration ID", + virtualPath: null, + exception: null, + errorMessage: true); + metadata.Add("filterFileName", filterFileName); + metadata.Add("enumerationId", enumerationId); + metadata.Add("restartScan", restartScan); + this.context.Tracer.RelatedError(metadata); + + return StatusCode.StatusInternalError; + } + + bool initialRequest; + if (restartScan) + { + activeEnumeration.RestartEnumeration(filterFileName); + initialRequest = true; + } + else + { + initialRequest = activeEnumeration.TrySaveFilterString(filterFileName); + } + + if (activeEnumeration.IsCurrentValid) + { + GVFltFileInfo fileInfo = activeEnumeration.Current; + FileProperties properties = this.GetLogsHeadFileProperties(); + + result.ChangeTime = properties.LastWriteTimeUTC; + result.CreationTime = properties.CreationTimeUTC; + result.LastAccessTime = properties.LastAccessTimeUTC; + result.LastWriteTime = properties.LastWriteTimeUTC; + result.AllocationSize = AllocationSize; + + if (fileInfo.IsFolder) + { + result.EndOfFile = 0; + result.FileAttributes = (uint)NativeMethods.FileAttributes.FILE_ATTRIBUTE_DIRECTORY; + } + else + { + result.EndOfFile = fileInfo.Size; + result.FileAttributes = (uint)NativeMethods.FileAttributes.FILE_ATTRIBUTE_ARCHIVE; + } + + if (result.TrySetFileName(fileInfo.Name)) + { + // Only advance the enumeration if the file name fit in the GvDirectoryEnumerationResult + activeEnumeration.MoveNext(); + return StatusCode.StatusSucccess; + } + else + { + // Return StatusBufferOverflow to indicate that the file name had to be truncated + return StatusCode.StatusBufferOverflow; + } + } + + StatusCode statusCode = (initialRequest && PathUtil.IsEnumerationFilterSet(filterFileName)) ? StatusCode.StatusNoSuchFile : StatusCode.StatusNoMoreFiles; + return statusCode; + } + + /// + /// GVFltQueryFileNameHandler is called by GVFlt when a file is being deleted or renamed. It is an optimiation so that GVFlt + /// can avoid calling Start\Get\End enumeration to check if GVFS is still projecting a file. This method uses the same + /// rules for deciding what is projected as the enumeration callbacks. + /// + private StatusCode GVFltQueryFileNameHandler(string virtualPath) + { + if (PathUtil.IsPathInsideDotGit(virtualPath)) + { + return StatusCode.StatusObjectNameNotFound; + } + + virtualPath = PathUtil.RemoveTrailingSlashIfPresent(virtualPath); + + if (!this.isMountComplete) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltQueryFileNameHandler: Mount has not yet completed", virtualPath); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "QueryFileName_MountNotComplete", metadata); + return StatusCode.StatusDeviceNotReady; + } + + GVFltFileInfo fileInfo; + try + { + fileInfo = this.GetGVFltFileInfo(virtualPath); + } + catch (TimeoutException e) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltQueryFileNameHandler: Timeout while getting GVFltFileInfo", virtualPath, e, errorMessage: true); + this.context.Tracer.RelatedError(metadata); + return StatusCode.StatusTimeout; + } + + if (fileInfo == null || !fileInfo.IsProjected) + { + return StatusCode.StatusObjectNameNotFound; + } + + return StatusCode.StatusSucccess; + } + + private StatusCode GVFltGetPlaceHolderInformationHandler( + string virtualPath, + uint desiredAccess, + uint shareMode, + uint createDisposition, + uint createOptions, + uint triggeringProcessId, + string triggeringProcessImageFileName) + { + virtualPath = PathUtil.RemoveTrailingSlashIfPresent(virtualPath); + + if (!this.isMountComplete) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltGetPlaceHolderInformationHandler: Mount has not yet completed", virtualPath); + metadata.Add("desiredAccess", desiredAccess); + metadata.Add("shareMode", shareMode); + metadata.Add("createDisposition", createDisposition); + metadata.Add("createOptions", createOptions); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "GetPlaceHolder_MountNotComplete", metadata); + + return StatusCode.StatusDeviceNotReady; + } + + GVFltFileInfo fileInfo; + try + { + fileInfo = this.GetGVFltFileInfo(virtualPath); + } + catch (TimeoutException e) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltGetPlaceHolderInformationHandler: Timeout while getting GVFltFileInfo", virtualPath, e, errorMessage: true); + metadata.Add("desiredAccess", desiredAccess); + metadata.Add("shareMode", shareMode); + metadata.Add("createDisposition", createDisposition); + metadata.Add("createOptions", createOptions); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + this.context.Tracer.RelatedError(metadata); + + return StatusCode.StatusTimeout; + } + + if (fileInfo == null || !fileInfo.IsProjected) + { + return StatusCode.StatusObjectNameNotFound; + } + + try + { + if (!fileInfo.IsFolder && + !this.IsSpecialGitFile(fileInfo) && + !this.CanDeferGitLockAcquisition() && + !this.TryAcquireGitLock()) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltGetPlaceHolderInformationHandler: Failed to acquire lock for placeholder creation", virtualPath); + metadata.Add("desiredAccess", desiredAccess); + metadata.Add("shareMode", shareMode); + metadata.Add("createDisposition", createDisposition); + metadata.Add("createOptions", createOptions); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + this.context.Tracer.RelatedEvent(EventLevel.Verbose, nameof(this.GVFltGetPlaceHolderInformationHandler), metadata); + + // Another process is modifying the working directory so we cannot modify it + // until they are done. + return StatusCode.StatusObjectNameNotFound; + } + + // The file name case in the virtualPath parameter might be different than the file name case in the repo. + // Build a new virtualPath that preserves the case in the repo so that the placeholder file is created + // with proper case. + string gitCaseVirtualPath = Path.Combine(Path.GetDirectoryName(virtualPath), fileInfo.Name); + + string sha = string.Empty; + uint fileAttributes; + if (fileInfo.IsFolder) + { + fileAttributes = (uint)NativeMethods.FileAttributes.FILE_ATTRIBUTE_DIRECTORY; + } + else + { + if (!this.context.Repository.TryGetFileSha(this.projectedCommitId, gitCaseVirtualPath, out sha)) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltGetPlaceHolderInformationHandler: TryGetFileSha failed", virtualPath, exception: null, errorMessage: true); + metadata.Add("gitCaseVirtualPath", gitCaseVirtualPath); + metadata.Add("desiredAccess", desiredAccess); + metadata.Add("shareMode", shareMode); + metadata.Add("createDisposition", createDisposition); + metadata.Add("createOptions", createOptions); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + this.context.Tracer.RelatedError(metadata); + return StatusCode.StatusFileNotAvailable; + } + + fileAttributes = (uint)NativeMethods.FileAttributes.FILE_ATTRIBUTE_ARCHIVE; + } + + FileProperties properties = this.GetLogsHeadFileProperties(); + this.createdByGVFS.Add(gitCaseVirtualPath); + StatusCode result = this.gvflt.GvWritePlaceholderInformation( + gitCaseVirtualPath, + properties.CreationTimeUTC, + properties.LastAccessTimeUTC, + properties.LastWriteTimeUTC, + changeTime: properties.LastWriteTimeUTC, + fileAttributes: fileAttributes, + allocationSize: AllocationSize, + endOfFile: fileInfo.Size, + directory: fileInfo.IsFolder, + contentId: sha, + epochId: this.projectedCommitId); + + if (result != StatusCode.StatusSucccess) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltGetPlaceHolderInformationHandler: GvWritePlaceholderInformation failed", virtualPath, exception: null, errorMessage: true); + metadata.Add("gitCaseVirtualPath", gitCaseVirtualPath); + metadata.Add("desiredAccess", desiredAccess); + metadata.Add("shareMode", shareMode); + metadata.Add("createDisposition", createDisposition); + metadata.Add("createOptions", createOptions); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + metadata.Add("FileName", fileInfo.Name); + metadata.Add("IsFolder", fileInfo.IsFolder); + metadata.Add("StatusCode", result.ToString("X") + "(" + result.ToString("G") + ")"); + this.context.Tracer.RelatedError(metadata); + } + else + { + this.background.Enqueue(BackgroundGitUpdate.OnPlaceholderCreated(gitCaseVirtualPath, fileInfo.IsFolder)); + + if (!fileInfo.IsFolder) + { + // Note: Folder will have IsProjected set to false in GVFltNotifyFirstWriteHandler. We can't update folders + // here because GVFltGetPlaceHolderInformationHandler is not synchronized across threads and it is common for + // multiple threads of a build to open handles to the same folder in parallel + fileInfo.IsProjected = false; + } + } + + return result; + } + finally + { + this.background.ReleaseAcquisitionLock(); + } + } + + private StatusCode GVFltGetFileStreamHandler( + string virtualPath, + long byteOffset, + uint length, + Guid streamGuid, + string contentId, + uint triggeringProcessId, + string triggeringProcessImageFileName, + GVFltWriteBuffer targetBuffer) + { + string sha = contentId; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("originalVirtualPath", virtualPath); + metadata.Add("byteOffset", byteOffset); + metadata.Add("length", length); + metadata.Add("streamGuid", streamGuid); + metadata.Add("triggeringProcessId", triggeringProcessId); + metadata.Add("triggeringProcessImageFileName", triggeringProcessImageFileName); + metadata.Add("sha", sha); + using (ITracer activity = this.context.Tracer.StartActivity("GetFileStream", EventLevel.Verbose, metadata)) + { + if (!this.isMountComplete) + { + metadata.Add("Message", "GVFltGetFileStreamHandler failed, mount has not yet completed"); + activity.RelatedEvent(EventLevel.Informational, "GetFileStream_MountNotComplete", metadata); + return StatusCode.StatusDeviceNotReady; + } + + if (byteOffset != 0) + { + metadata.Add("ErrorMessage", "Invalid Parameter: byteOffset must be 0"); + activity.RelatedError(metadata); + return StatusCode.StatusInvalidParameter; + } + + try + { + if (!this.gvfsGitObjects.TryCopyBlobContentStream( + sha, + (reader, blobLength) => + { + if (blobLength != length) + { + metadata.Add("blobLength", blobLength); + metadata.Add("ErrorMessage", "Actual file length (blobLength) does not match requested length"); + activity.RelatedError(metadata); + + // Clear out the stream to leave it in a good state. + reader.CopyBlockTo(StreamWriter.Null, blobLength); + + throw new GvFltException(StatusCode.StatusInvalidParameter); + } + + using (StreamWriter writer = new StreamWriter(targetBuffer.Stream, reader.CurrentEncoding, (int)targetBuffer.Length, leaveOpen: true)) + { + writer.AutoFlush = true; + + long remainingData = blobLength; + while (remainingData > 0) + { + uint bytesToCopy = (uint)Math.Min(remainingData, targetBuffer.Length); + writer.BaseStream.Seek(0, SeekOrigin.Begin); + reader.CopyBlockTo(writer, bytesToCopy); + long writeOffset = length - remainingData; + + StatusCode writeResult = this.gvflt.GvWriteFile(streamGuid, targetBuffer, (ulong)writeOffset, bytesToCopy); + remainingData -= bytesToCopy; + + if (writeResult != StatusCode.StatusSucccess) + { + switch (writeResult) + { + case StatusCode.StatusFileClosed: + // StatusFileClosed is expected, and occurs when an application closes a file handle before OnGetFileStream + // is complete + break; + + case StatusCode.StatusObjectNameNotFound: + // GvWriteFile may return STATUS_OBJECT_NAME_NOT_FOUND if the stream guid provided is not valid (doesn’t exist in the stream table). + // For each file expansion, GVFlt creates a new get stream session with a new stream guid, the session starts at the beginning of the + // file expansion, and ends after the GetFileStream command returns or times out. + // + // If we hit this in GVFS, the most common explanation is that we're calling GvWriteFile after the GVFlt thread waiting on the respose + // from GetFileStream has already timed out + metadata.Add("Message", "GvWriteFile returned StatusObjectNameNotFound"); + activity.RelatedEvent(EventLevel.Informational, "GetFileStream_ObjectNameNotFound", metadata); + break; + + default: + metadata.Add("ErrorMessage", "GvWriteFile failed, error: " + writeResult.ToString("X") + "(" + writeResult.ToString("G") + ")"); + activity.RelatedError(metadata); + break; + } + + // Clear out the stream to leave it in a good state. + if (remainingData > 0) + { + reader.CopyBlockTo(StreamWriter.Null, remainingData); + } + + throw new GvFltException(writeResult); + } + } + } + })) + { + metadata.Add("ErrorMessage", "TryCopyBlobContentStream failed"); + activity.RelatedError(metadata); + return StatusCode.StatusFileNotAvailable; + } + } + catch (TimeoutException) + { + metadata.Add("Message", "GVFltGetFileStreamHandler: Timeout while getting file stream"); + activity.RelatedEvent(EventLevel.Warning, "Warning", metadata); + return StatusCode.StatusTimeout; + } + + return StatusCode.StatusSucccess; + } + } + + private StatusCode GVFltNotifyFirstWriteHandler(string virtualPath) + { + virtualPath = PathUtil.RemoveTrailingSlashIfPresent(virtualPath); + + if (!this.isMountComplete) + { + EventMetadata metadata = this.CreateEventMetadata("GVFltNotifyFirstWriteHandler: Mount has not yet completed", virtualPath); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "NotifyFirstWrite_MountNotComplete", metadata); + return StatusCode.StatusDeviceNotReady; + } + + if (string.Equals(virtualPath, string.Empty)) + { + // Empty path is the root folder + this.background.Enqueue(BackgroundGitUpdate.OnFolderFirstWrite(virtualPath, isFolder: true)); + } + else + { + GVFltFileInfo fileInfo = this.GetGVFltFileInfo(virtualPath, readOnly: true); + if (fileInfo == null) + { + this.background.Enqueue(BackgroundGitUpdate.OnFolderFirstWrite(virtualPath, isFolder: false)); + } + else if (fileInfo.IsFolder) + { + fileInfo.IsProjected = false; + this.background.Enqueue(BackgroundGitUpdate.OnFolderFirstWrite(virtualPath, isFolder: true)); + } + } + + return StatusCode.StatusSucccess; + } + + private void GVFltNotifyCreateHandler( + string virtualPath, + uint desiredAccess, + uint shareMode, + uint createDisposition, + uint createOptions, + uint iostatusBlock, + ref uint notificationMask) + { + if (PathUtil.IsPathInsideDotGit(virtualPath)) + { + notificationMask = this.GetDotGitNotificationMask(virtualPath); + } + } + + private StatusCode GVFltNotifyPreDeleteHandler(string virtualPath) + { + if (PathUtil.IsPathInsideDotGit(virtualPath)) + { + virtualPath = PathUtil.RemoveTrailingSlashIfPresent(virtualPath); + if (!DoesPathAllowDelete(virtualPath)) + { + return StatusCode.StatusAccessDenied; + } + } + + return StatusCode.StatusSucccess; + } + + private void GVFltNotifyFileRenamedHandler( + string virtualPath, + string destinationPath, + ref uint notificationMask) + { + if (PathUtil.IsPathInsideDotGit(virtualPath)) + { + notificationMask = this.GetDotGitNotificationMask(destinationPath); + this.OnDotGitFileChanged(destinationPath); + } + } + + private void GVFltNotifyFileHandleClosedHandler( + string virtualPath, + bool fileModified, + bool fileDeleted) + { + if (fileModified) + { + if (PathUtil.IsPathInsideDotGit(virtualPath)) + { + this.OnDotGitFileChanged(virtualPath); + } + } + } + + private void OnDotGitFileChanged(string virtualPath) + { + if (virtualPath.Equals(GVFSConstants.DotGit.Index, StringComparison.OrdinalIgnoreCase)) + { + this.OnIndexFileChange(); + } + else if (virtualPath.Equals(GVFSConstants.DotGit.Head, StringComparison.OrdinalIgnoreCase) || + virtualPath.StartsWith(RefsHeadsPath, StringComparison.OrdinalIgnoreCase)) + { + this.OnHeadChange(); + } + else if (virtualPath.Equals(GVFSConstants.DotGit.Logs.Head, StringComparison.OrdinalIgnoreCase)) + { + this.OnLogsHeadChange(); + } + } + + /// If true, GetGVFltFileInfo will only check the entries + /// already present in GVFS's collection. If false, GetGVFltFileInfo will create a + /// new GVFltFolder for virtualPath's parent (if there's not already an entry in GVFS's collection). + private GVFltFileInfo GetGVFltFileInfo(string virtualPath, bool readOnly = false) + { + string parentFolderVirtualPath; + try + { + parentFolderVirtualPath = Path.GetDirectoryName(virtualPath); + } + catch (ArgumentException) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("virtualPath", virtualPath); + metadata.Add("ErrorMessage", "GetGVFltFileInfo: file name contains illegal characters"); + this.context.Tracer.RelatedError(metadata); + + throw new GvFltException(StatusCode.StatusObjectNameInvalid); + } + catch (PathTooLongException) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("virtualPath", virtualPath); + metadata.Add("ErrorMessage", "GetGVFltFileInfo: PathTooLongException, virtualPath is too long for GetDirectoryName"); + this.context.Tracer.RelatedError(metadata); + return null; + } + + string fileName = Path.GetFileName(virtualPath); + + GVFltFileInfo fileInfo = null; + if (readOnly) + { + GVFltFolder folder; + if (this.workingDirectoryFolders.TryGetValue(parentFolderVirtualPath, out folder)) + { + fileInfo = folder.GetFileInfo(fileName); + } + } + else + { + GVFltFolder folder = this.workingDirectoryFolders.GetOrAdd( + parentFolderVirtualPath, + path => new GVFltFolder(this.context, this.gvfsGitObjects, this.sparseCheckoutAndDoNotProject, this.blobSizes, parentFolderVirtualPath, this.projectedCommitId)); + fileInfo = folder.GetFileInfo(fileName); + } + + return fileInfo; + } + + private uint GetDotGitNotificationMask(string virtualPath) + { + uint notificationMask = (uint)GvNotificationType.NotificationFileRenamed; + + if (!DoesPathAllowDelete(virtualPath)) + { + notificationMask |= (uint)GvNotificationType.NotificationPreDelete; + } + + if (IsPathMonitoredForWrites(virtualPath)) + { + notificationMask |= (uint)GvNotificationType.NotificationFileHandleClosed; + } + + return notificationMask; + } + + private CallbackResult PreBackgroundOperation() + { + return this.context.Repository.Index.Open(); + } + + private CallbackResult ExecuteBackgroundOperation(BackgroundGitUpdate gitUpdate) + { + EventMetadata metadata = new EventMetadata(); + CallbackResult result; + + switch (gitUpdate.Operation) + { + case BackgroundGitUpdate.OperationType.OnPlaceholderCreated: + result = CallbackResult.Success; + metadata.Add("virtualPath", gitUpdate.VirtualPath); + metadata.Add("IsFolder", gitUpdate.IsFolder); + if (gitUpdate.IsFolder) + { + if (gitUpdate.VirtualPath != string.Empty) + { + result = this.sparseCheckoutAndDoNotProject.OnPartialPlaceholderFolderCreated(gitUpdate.VirtualPath); + } + } + else + { + long fileSize = 0; + GVFltFileInfo fileInfo = null; + try + { + fileInfo = this.GetGVFltFileInfo(gitUpdate.VirtualPath); + } + catch (TimeoutException e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Area", "ExecuteBackgroundOperation"); + exceptionMetadata.Add("Operation", gitUpdate.Operation.ToString()); + exceptionMetadata.Add("virtualPath", gitUpdate.VirtualPath); + exceptionMetadata.Add("Message", "ExecuteBackgroundOperation: Timeout while getting GVFltFileInfo for index update."); + exceptionMetadata.Add("Exception", e.ToString()); + this.context.Tracer.RelatedError(exceptionMetadata); + } + + if (fileInfo != null) + { + fileSize = fileInfo.Size; + } + + FileProperties properties = this.GetLogsHeadFileProperties(); + result = this.sparseCheckoutAndDoNotProject.OnPlaceholderFileCreated(gitUpdate.VirtualPath, properties.CreationTimeUTC, properties.LastAccessTimeUTC, fileSize); + } + + break; + + case BackgroundGitUpdate.OperationType.OnFolderFirstWrite: + metadata.Add("virtualPath", gitUpdate.VirtualPath); + metadata.Add("isFolder", gitUpdate.IsFolder); + result = CallbackResult.Success; + + // For OnFolderFirstWrite: + // (gitUpdate.IsFolder == true) => The first write callback confirmed that gitUpdate.VirtualPath is a folder path + // (gitUpdate.IsFolder == false) => The first write callback was unable to confirm that gitUpdate.VirtualPath is a folder path, + // the background thread needs to check if the path is for a folder + bool confirmedFolder = gitUpdate.IsFolder; + + if (confirmedFolder) + { + result = this.excludeFile.FolderChanged(gitUpdate.VirtualPath); + } + else + { + // If, when the first write callback was received, the file info for this path was not in workingDirectoryFolders the background + // thread needs to check if a placeholder has been created for a folder at this path (and if so, the exclude file needs to be updated) + if (!this.sparseCheckoutAndDoNotProject.ShouldPathBeProjected(gitUpdate.VirtualPath, isFolder: true)) + { + result = this.excludeFile.FolderChanged(gitUpdate.VirtualPath); + } + } + + break; + + case BackgroundGitUpdate.OperationType.OnFileCreated: + metadata.Add("virtualPath", gitUpdate.VirtualPath); + result = this.sparseCheckoutAndDoNotProject.OnFileCreated(gitUpdate.VirtualPath); + + break; + + case BackgroundGitUpdate.OperationType.OnFileRenamed: + metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath); + metadata.Add("virtualPath", gitUpdate.VirtualPath); + result = this.sparseCheckoutAndDoNotProject.OnFileRenamed(gitUpdate.VirtualPath); + + break; + + case BackgroundGitUpdate.OperationType.OnFolderCreated: + metadata.Add("virtualPath", gitUpdate.VirtualPath); + result = this.excludeFile.FolderChanged(gitUpdate.VirtualPath); + if (result == CallbackResult.Success) + { + result = this.sparseCheckoutAndDoNotProject.OnFolderCreated(gitUpdate.VirtualPath); + } + + break; + + case BackgroundGitUpdate.OperationType.OnFolderRenamed: + result = CallbackResult.Success; + metadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath); + metadata.Add("virtualPath", gitUpdate.VirtualPath); + + Queue relativeFolderPaths = new Queue(); + relativeFolderPaths.Enqueue(gitUpdate.VirtualPath); + result = CallbackResult.Success; + + // Add the renamed folder and all of its subfolders to the exclude file + while (relativeFolderPaths.Count > 0) + { + string folderPath = relativeFolderPaths.Dequeue(); + result = this.excludeFile.FolderChanged(folderPath); + if (result == CallbackResult.Success) + { + try + { + foreach (DirectoryItemInfo itemInfo in this.context.FileSystem.ItemsInDirectory(Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, folderPath))) + { + if (itemInfo.IsDirectory) + { + string itemVirtualPath = Path.Combine(folderPath, itemInfo.Name); + relativeFolderPaths.Enqueue(itemVirtualPath); + } + } + } + catch (DirectoryNotFoundException) + { + // DirectoryNotFoundException can occur when the renamed folder (or one of its children) is + // deleted prior to the background thread running + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Area", "ExecuteBackgroundOperation"); + exceptionMetadata.Add("Operation", gitUpdate.Operation.ToString()); + exceptionMetadata.Add("oldVirtualPath", gitUpdate.OldVirtualPath); + exceptionMetadata.Add("virtualPath", gitUpdate.VirtualPath); + exceptionMetadata.Add("Message", "DirectoryNotFoundException while traversing folder path"); + exceptionMetadata.Add("folderPath", folderPath); + this.context.Tracer.RelatedEvent(EventLevel.Informational, "DirectoryNotFoundWhileUpdatingExclude", exceptionMetadata); + } + catch (IOException e) + { + metadata.Add("Details", "IOException while traversing folder path"); + metadata.Add("folderPath", folderPath); + metadata.Add("Exception", e.ToString()); + result = CallbackResult.RetryableError; + break; + } + } + else + { + break; + } + } + + if (result == CallbackResult.Success) + { + result = this.sparseCheckoutAndDoNotProject.OnFolderRenamed(gitUpdate.VirtualPath); + } + + break; + + case BackgroundGitUpdate.OperationType.OnFolderDeleted: + metadata.Add("virtualPath", gitUpdate.VirtualPath); + result = this.sparseCheckoutAndDoNotProject.OnFolderDeleted(gitUpdate.VirtualPath); + + break; + + // This case is only for a HEAD change after a non-hard reset + case BackgroundGitUpdate.OperationType.OnHeadChange: + result = CallbackResult.Success; + GitProcess.Result gitResult = new GitProcess(this.context.Enlistment).DiffWithNameOnlyAndFilterForAddedAndReanamedFiles(gitUpdate.NewCommitId, gitUpdate.OldCommitId); + if (!gitResult.HasErrors && !string.IsNullOrWhiteSpace(gitResult.Output)) + { + string[] addedFiles = gitResult.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string addedFile in addedFiles) + { + // Convert octets that git uses to display paths with unicode characters + string cleanedFilePath = GitPathConverter.ConvertPathOctetsToUtf8(addedFile.Trim('"')).Replace(GVFSConstants.GitPathSeparator, GVFSConstants.PathSeparator); + + int lastSlash = cleanedFilePath.LastIndexOf(GVFSConstants.PathSeparator); + string folderToAdd = string.Empty; + if (lastSlash != -1) + { + folderToAdd = cleanedFilePath.Substring(0, lastSlash); + } + + this.excludeFile.FolderChanged(folderToAdd); + + string fullPath = Path.Combine(this.context.Enlistment.WorkingDirectoryRoot, cleanedFilePath); + + try + { + using (FileStream forceHydrate = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + // We have to open the file and force it to get hydrated, otherwise a subsequent reset --hard + // will lose these new files because it will no longer know which commit to project them from + + // In the future, we can simply lay down a placeholder with the right blob id, and ensure that the + // index has the correct mtime and size, so that the files will be present without necessarily being hydrated + + forceHydrate.ReadByte(); + } + } + catch (FileNotFoundException) + { + // FileNotFoundException can occur when addedFile was deleted prior to HEAD changes + } + catch (DirectoryNotFoundException) + { + // DirectoryNotFoundException can occur when addedFile's parent folder was deleted prior to HEAD changes + } + catch (IOException e) + { + metadata.Add("Exception", e.ToString()); + result = CallbackResult.RetryableError; + break; + } + } + } + else if (gitResult.HasErrors) + { + metadata.Add("gitResult.Errors", gitResult.Errors); + result = CallbackResult.RetryableError; + } + + break; + + default: + throw new InvalidOperationException("Invalid background operation"); + } + + if (result != CallbackResult.Success) + { + metadata.Add("Area", "ExecuteBackgroundOperation"); + metadata.Add("Operation", gitUpdate.Operation.ToString()); + metadata.Add("Message", "Background operation failed"); + metadata.Add("result", result.ToString()); + this.context.Tracer.RelatedEvent(EventLevel.Warning, "FailedBackgroundOperation", metadata); + } + + return result; + } + + private CallbackResult PostBackgroundOperation() + { + this.sparseCheckoutAndDoNotProject.Close(); + this.excludeFile.Close(); + return this.context.Repository.Index.Close(); + } + + private string GetFullFileContents(string relativeFilePath) + { + string fileContents = string.Empty; + try + { + GvFltWrapper.OnDiskStatus fileStatus = this.gvflt.GetFileOnDiskStatus(relativeFilePath); + switch (fileStatus) + { + case GvFltWrapper.OnDiskStatus.Full: + fileContents = this.gvflt.ReadFullFileContents(relativeFilePath); + break; + case GvFltWrapper.OnDiskStatus.Partial: + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetFullFileContents"); + metadata.Add("relativeFilePath", relativeFilePath); + metadata.Add("ErrorMessage", "GetFullFileContents: Attempted to read file contents for partial file"); + this.context.Tracer.RelatedError(metadata); + + fileContents = null; + break; + case GvFltWrapper.OnDiskStatus.NotOnDisk: + break; + } + } + catch (GvFltException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetFullFileContents"); + metadata.Add("relativeFilePath", relativeFilePath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "GvFltException caught while trying to read file"); + this.context.Tracer.RelatedError(metadata); + + return null; + } + + return fileContents; + } + + private string GetHeadCommitId() + { + string headFileContents = this.GetFullFileContents(GVFSConstants.DotGit.Head); + if (string.IsNullOrEmpty(headFileContents)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetHeadCommitId"); + metadata.Add("headFileContents", headFileContents); + metadata.Add("ErrorMessage", "HEAD file is missing or empty"); + this.context.Tracer.RelatedError(metadata); + + return null; + } + + headFileContents = headFileContents.Trim(); + + if (GitHelper.IsValidFullSHA(headFileContents)) + { + return headFileContents; + } + + if (!headFileContents.StartsWith(RefMarker, StringComparison.OrdinalIgnoreCase)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetHeadCommitId"); + metadata.Add("headFileContents", headFileContents); + metadata.Add("ErrorMessage", "headContents does not contain SHA or ref marker"); + this.context.Tracer.RelatedError(metadata); + + return null; + } + + string symRef = headFileContents.Substring(RefMarker.Length).Trim(); + string refFilePath = Path.Combine(GVFSConstants.DotGit.Root, symRef.Replace('/', '\\')); + string commitId = this.GetFullFileContents(refFilePath); + if (commitId == null) + { + return null; + } + + if (commitId.Length > 0) + { + commitId = commitId.Trim(); + if (GitHelper.IsValidFullSHA(commitId)) + { + return commitId; + } + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetHeadCommitId"); + metadata.Add("symRef", symRef); + metadata.Add("commitId", commitId); + metadata.Add("ErrorMessage", "commitId in sym ref file is not a valid commit ID"); + this.context.Tracer.RelatedError(metadata); + + return null; + } + + try + { + string packedRefFileContents = this.GetFullFileContents(GVFSConstants.DotGit.PackedRefs); + if (packedRefFileContents == null) + { + return null; + } + + foreach (string packedRefLine in packedRefFileContents.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (packedRefLine.Contains(symRef)) + { + commitId = packedRefLine.Substring(0, 40); + + if (GitHelper.IsValidFullSHA(commitId)) + { + return commitId; + } + else + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetHeadCommitId"); + metadata.Add("symRef", symRef); + metadata.Add("commitId", commitId); + metadata.Add("Message", "Commit ID found in packed-refs that is not a valid hex string"); + this.context.Tracer.RelatedEvent(EventLevel.Warning, "GetHeadCommitId_BadPackedRefsCommit", metadata); + } + } + } + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GetHeadCommitId"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Exception caught while trying to parse packed-refs file"); + + return null; + } + + return null; + } + + private bool IsSpecialGitFile(GVFltFileInfo fileInfo) + { + if (fileInfo.IsFolder) + { + return false; + } + + return + fileInfo.Name.Equals(GVFSConstants.SpecialGitFiles.GitAttributes, StringComparison.OrdinalIgnoreCase) || + fileInfo.Name.Equals(GVFSConstants.SpecialGitFiles.GitIgnore, StringComparison.OrdinalIgnoreCase); + } + + private void StopProjecting(string virtualPath, bool isFolder) + { + this.sparseCheckoutAndDoNotProject.StopProjecting(virtualPath, isFolder); + GVFltFileInfo fileInfo = this.GetGVFltFileInfo(virtualPath); + if (fileInfo != null) + { + fileInfo.IsProjected = false; + } + } + + /// + /// Try to acquire the global lock. Retry but ensure that we don't reach the GVFlt callback timeout./> + /// + /// True if the lock was acquired, false otherwise. + private bool TryAcquireGitLock() + { + this.background.ObtainAcquisitionLock(); + int numRetries = 0; + + int maxGitLockRetries = this.GetMaxGitLockRetries(); + + while (numRetries < maxGitLockRetries) + { + if (this.context.Repository.GVFSLock.TryAcquireLock()) + { + return true; + } + else + { + Thread.Sleep(AcquireGitLockWaitPerTryMillis); + numRetries++; + } + } + + return false; + } + + private EventMetadata CreateEventMetadata( + string message = null, + string virtualPath = null, + Exception exception = null, + bool errorMessage = false) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GVFltCallbacks"); + + if (virtualPath != null) + { + metadata.Add("virtualPath", virtualPath); + } + + if (message != null) + { + metadata.Add(errorMessage ? "ErrorMessage" : "Message", message); + } + + if (exception != null) + { + metadata.Add("Exception", exception.ToString()); + } + + return metadata; + } + + private int GetMaxGitLockRetries() + { + if (this.context.Repository.GVFSLock.IsLockedByGitVerb("commit")) + { + return AcquireGitLockRetries; + } + else + { + return 1; + } + } + + private FileProperties GetLogsHeadFileProperties() + { + // Use a temporary FileProperties in case another thread sets this.logsHeadFileProperties before this + // method returns + FileProperties properties = this.logsHeadFileProperties; + if (properties == null) + { + try + { + properties = this.context.FileSystem.GetFileProperties(this.logsHeadPath); + this.logsHeadFileProperties = properties; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "GVFltCallbacks"); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "GetLogsHeadFileProperties: Exception thrown from GetFileProperties"); + this.context.Tracer.RelatedError("GetLogsHeadFileProperties_GetFilePropertiesException", metadata); + + properties = FileProperties.DefaultFile; + + // Leave logsHeadFileProperties null to indicate that it is still needs to be refreshed + this.logsHeadFileProperties = null; + } + } + + return properties; + } + + /// + /// If a git-status or git-add is running, we don't want to fail placeholder creation because users will + /// want to be able to run those commands during long running builds. Allow lock acquisition to be deferred + /// until background thread actually needs it. + /// + private bool CanDeferGitLockAcquisition() + { + return this.context.Repository.GVFSLock.IsLockedByGitVerb("status", "add"); + } + + [Serializable] + public struct BackgroundGitUpdate : IBackgroundOperation + { + public BackgroundGitUpdate(OperationType operation, string virtualPath, string oldVirtualPath, bool isFolder) + { + this.Id = Guid.NewGuid(); + this.Operation = operation; + this.VirtualPath = virtualPath; + this.OldVirtualPath = oldVirtualPath; + this.IsFolder = isFolder; + this.NewCommitId = null; + this.OldCommitId = null; + } + + public BackgroundGitUpdate(OperationType operation, string newCommitId, string oldCommitId) + { + this.Id = Guid.NewGuid(); + this.VirtualPath = null; + this.OldVirtualPath = null; + this.IsFolder = false; + this.Operation = operation; + this.NewCommitId = newCommitId; + this.OldCommitId = oldCommitId; + } + + public enum OperationType + { + Invalid = 0, + + OnHeadChange, + + OnPlaceholderCreated, + OnFolderFirstWrite, + + OnFileCreated, + OnFileRenamed, + OnFolderCreated, + OnFolderRenamed, + OnFolderDeleted, + } + + public OperationType Operation { get; set; } + + public string VirtualPath { get; set; } + public string OldVirtualPath { get; set; } + public bool IsFolder { get; set; } + public string NewCommitId { get; set; } + public string OldCommitId { get; set; } + public Guid Id { get; set; } + + public static BackgroundGitUpdate OnHeadChangeForNonHardReset(string newCommitId, string oldCommitId) + { + return new BackgroundGitUpdate(OperationType.OnHeadChange, newCommitId, oldCommitId); + } + + public static BackgroundGitUpdate OnPlaceholderCreated(string virtualPath, bool isFolder) + { + return new BackgroundGitUpdate(OperationType.OnPlaceholderCreated, virtualPath, null, isFolder); + } + + public static BackgroundGitUpdate OnFolderFirstWrite(string virtualPath, bool isFolder) + { + return new BackgroundGitUpdate(OperationType.OnFolderFirstWrite, virtualPath, null, isFolder); + } + + public static BackgroundGitUpdate OnFileCreated(string virtualPath) + { + return new BackgroundGitUpdate(OperationType.OnFileCreated, virtualPath, null, false); + } + + public static BackgroundGitUpdate OnFileRenamed(string oldVirtualPath, string newVirtualPath) + { + return new BackgroundGitUpdate(OperationType.OnFileRenamed, newVirtualPath, oldVirtualPath, false); + } + + public static BackgroundGitUpdate OnFolderCreated(string virtualPath) + { + return new BackgroundGitUpdate(OperationType.OnFolderCreated, virtualPath, null, true); + } + + public static BackgroundGitUpdate OnFolderRenamed(string oldVirtualPath, string newVirtualPath) + { + return new BackgroundGitUpdate(OperationType.OnFolderRenamed, newVirtualPath, oldVirtualPath, true); + } + + public static BackgroundGitUpdate OnFolderDeleted(string virtualPath) + { + return new BackgroundGitUpdate(OperationType.OnFolderDeleted, virtualPath, null, true); + } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } + } + } +} diff --git a/GVFS/GVFS.GVFlt/GVFltFileInfo.cs b/GVFS/GVFS.GVFlt/GVFltFileInfo.cs new file mode 100644 index 00000000..5067f13a --- /dev/null +++ b/GVFS/GVFS.GVFlt/GVFltFileInfo.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace GVFS.GVFlt +{ + public class GVFltFileInfo + { + private volatile bool isProjected; + + public GVFltFileInfo(string name, long size, bool isFolder) + { + this.Name = name; + this.Size = size; + this.IsFolder = isFolder; + this.IsProjected = true; + } + + public string Name { get; } + public long Size { get; } + public bool IsFolder { get; } + public bool IsProjected + { + get { return this.isProjected; } + set { this.isProjected = value; } + } + + public static IComparer SortAlphabeticallyIgnoreCase() + { + return new SortFileInfoAlphabetically(); + } + + private class SortFileInfoAlphabetically : IComparer + { + public int Compare(GVFltFileInfo a, GVFltFileInfo b) + { + if (a == null) + { + if (b == null) + { + return 0; + } + else + { + return -1; + } + } + else + { + if (b == null) + { + return 1; + } + + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + } + } + } + } +} diff --git a/GVFS/GVFS.GVFlt/GVFltFolder.cs b/GVFS/GVFS.GVFlt/GVFltFolder.cs new file mode 100644 index 00000000..1dffadf5 --- /dev/null +++ b/GVFS/GVFS.GVFlt/GVFltFolder.cs @@ -0,0 +1,116 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using GVFSGvFltWrapper; +using Microsoft.Isam.Esent.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GVFS.GVFlt +{ + public class GVFltFolder + { + private List entries; + + public GVFltFolder( + GVFSContext context, + GVFSGitObjects gitObjects, + DotGit.SparseCheckoutAndDoNotProject sparseCheckoutAndDoNotProject, + PersistentDictionary blobSizes, + string virtualPath, + string projectedCommitId) + { + List treeEntries = context.Repository.GetTreeEntries(projectedCommitId, virtualPath).ToList(); + treeEntries = GetProjectedEntries(context, sparseCheckoutAndDoNotProject, virtualPath, treeEntries); + this.PopulateNamedEntrySizes(gitObjects, blobSizes, virtualPath, context, treeEntries); + + this.entries = new List(); + foreach (GitTreeEntry entry in treeEntries) + { + this.entries.Add(new GVFltFileInfo(entry.Name, entry.IsBlob ? entry.Size : 0, entry.IsTree)); + } + + this.entries.Sort(GVFltFileInfo.SortAlphabeticallyIgnoreCase()); + } + + public IEnumerable GetItems() + { + return this.entries; + } + + public GVFltFileInfo GetFileInfo(string name) + { + return this.entries.Find(fileInfo => fileInfo.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + private static List GetProjectedEntries(GVFSContext context, DotGit.SparseCheckoutAndDoNotProject sparseCheckoutAndDoNotProject, string virtualPath, List treeEntries) + { + List projectedTreeEntries = new List(); + foreach (GitTreeEntry entry in treeEntries) + { + string entryVirtualPath = Path.Combine(virtualPath, entry.Name); + if (sparseCheckoutAndDoNotProject.ShouldPathBeProjected(entryVirtualPath, entry.IsTree)) + { + projectedTreeEntries.Add(entry); + } + } + + return projectedTreeEntries; + } + + private void PopulateNamedEntrySizes( + GVFSGitObjects gitObjects, + PersistentDictionary blobSizes, + string parentVirtualPath, + GVFSContext context, + IEnumerable entries) + { + List blobs = entries.Where(e => e.IsBlob).ToList(); + + // Then try to find as many blob sizes locally as possible. + List entriesMissingSizes = new List(); + foreach (GitTreeEntry namedEntry in blobs.Where(b => b.Size == 0)) + { + long blobLength = 0; + if (blobSizes.TryGetValue(namedEntry.Sha, out blobLength)) + { + namedEntry.Size = blobLength; + } + else if (gitObjects.TryGetBlobSizeLocally(namedEntry.Sha, out blobLength)) + { + namedEntry.Size = blobLength; + blobSizes[namedEntry.Sha] = blobLength; + } + else + { + entriesMissingSizes.Add(namedEntry); + } + } + + // Anything remaining should come from the remote. + if (entriesMissingSizes.Count > 0) + { + Dictionary objectLengths = gitObjects.GetFileSizes(entriesMissingSizes.Select(e => e.Sha).Distinct()).ToDictionary(s => s.Id, s => s.Size, StringComparer.OrdinalIgnoreCase); + foreach (GitTreeEntry namedEntry in entriesMissingSizes) + { + long blobLength = 0; + if (objectLengths.TryGetValue(namedEntry.Sha, out blobLength)) + { + namedEntry.Size = blobLength; + blobSizes[namedEntry.Sha] = blobLength; + } + else + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ErrorMessage", "GvFltException: Failed to download size for: " + namedEntry.Name + ", SHA: " + namedEntry.Sha); + context.Tracer.RelatedError(metadata, Keywords.Network); + throw new GvFltException(StatusCode.StatusFileNotAvailable); + } + } + } + } + } +} diff --git a/GVFS/GVFS.GVFlt/PathUtil.cs b/GVFS/GVFS.GVFlt/PathUtil.cs new file mode 100644 index 00000000..4e9cc0b7 --- /dev/null +++ b/GVFS/GVFS.GVFlt/PathUtil.cs @@ -0,0 +1,32 @@ +using GVFS.Common; +using System; +using System.Linq; + +namespace GVFS.GVFlt +{ + public class PathUtil + { + /// + /// Returns true for paths that begin with ".git\" (regardless of case) + /// + public static bool IsPathInsideDotGit(string virtualPath) + { + return virtualPath.StartsWith(GVFSConstants.DotGit.Root + GVFSConstants.PathSeparator, StringComparison.OrdinalIgnoreCase); + } + + public static string RemoveTrailingSlashIfPresent(string path) + { + return path.TrimEnd('\\'); + } + + public static bool IsEnumerationFilterSet(string filter) + { + if (string.IsNullOrWhiteSpace(filter) || filter == "*") + { + return false; + } + + return true; + } + } +} diff --git a/GVFS/GVFS.GVFlt/PatternMatcher.cs b/GVFS/GVFS.GVFlt/PatternMatcher.cs new file mode 100644 index 00000000..6ad3871a --- /dev/null +++ b/GVFS/GVFS.GVFlt/PatternMatcher.cs @@ -0,0 +1,512 @@ +using System; + +namespace GVFS.GVFlt +{ + // From http://referencesource.microsoft.com/#System/services/io/system/io/PatternMatcher.cs + // + // Changes made after copying from above URL: + // + // - Changed ANSI_DOS_STAR to '<' and ANSI_DOS_QM to '>' (these are the correct values, see ntifs.h) + // + internal static class PatternMatcher + { + /// + /// Private constants (directly from C header files) + /// + private const int MATCHES_ARRAY_SIZE = 16; + private const char ANSI_DOS_STAR = '<'; + private const char ANSI_DOS_QM = '>'; + private const char DOS_DOT = '"'; + + /// + /// Tells whether a given name matches the expression given with a strict (i.e. UNIX like) + /// semantics. This code is a port of unmanaged code. Original code comment follows: + /// + /// Routine Description: + /// + /// This routine compares a Dbcs name and an expression and tells the caller + /// if the name is in the language defined by the expression. The input name + /// cannot contain wildcards, while the expression may contain wildcards. + /// + /// Expression wild cards are evaluated as shown in the nondeterministic + /// finite automatons below. Note that ~* and ~? are DOS_STAR and DOS_QM. + /// + /// + /// ~* is DOS_STAR, ~? is DOS_QM, and ~. is DOS_DOT + /// + /// + /// S + /// <-----< + /// X | | e Y + /// X * Y == (0)----->-(1)->-----(2)-----(3) + /// + /// + /// S-. + /// <-----< + /// X | | e Y + /// X ~* Y == (0)----->-(1)->-----(2)-----(3) + /// + /// + /// + /// X S S Y + /// X ?? Y == (0)---(1)---(2)---(3)---(4) + /// + /// + /// + /// X . . Y + /// X ~.~. Y == (0)---(1)----(2)------(3)---(4) + /// | |________| + /// | ^ | + /// |_______________| + /// ^EOF or .^ + /// + /// + /// X S-. S-. Y + /// X ~?~? Y == (0)---(1)-----(2)-----(3)---(4) + /// | |________| + /// | ^ | + /// |_______________| + /// ^EOF or .^ + /// + /// + /// + /// where S is any single character + /// + /// S-. is any single character except the final . + /// + /// e is a null character transition + /// + /// EOF is the end of the name string + /// + /// In words: + /// + /// * matches 0 or more characters. + /// + /// ? matches exactly 1 character. + /// + /// DOS_STAR matches 0 or more characters until encountering and matching + /// the final . in the name. + /// + /// DOS_QM matches any single character, or upon encountering a period or + /// end of name string, advances the expression to the end of the + /// set of contiguous DOS_QMs. + /// + /// DOS_DOT matches either a . or zero characters beyond name string. + /// + /// Arguments: + /// + /// Expression - Supplies the input expression to check against + /// + /// Name - Supplies the input name to check for. + /// + /// Return Value: + /// + /// BOOLEAN - TRUE if Name is an element in the set of strings denoted + /// by the input Expression and FALSE otherwise. + /// + /// + public static bool StrictMatchPattern(string expression, string name) + { + int nameOffset; + int exprOffset; + int length; + + int srcCount; + int destCount; + int previousDestCount; + int matchesCount; + + char nameChar = '\0'; + char exprChar = '\0'; + + int[] previousMatches = new int[MATCHES_ARRAY_SIZE]; + int[] currentMatches = new int[MATCHES_ARRAY_SIZE]; + + int maxState; + int currentState; + + bool nameFinished = false; + + // + // The idea behind the algorithm is pretty simple. We keep track of + // all possible locations in the regular expression that are matching + // the name. If when the name has been exhausted one of the locations + // in the expression is also just exhausted, the name is in the language + // defined by the regular expression. + // + + if (name == null || name.Length == 0 || expression == null || expression.Length == 0) + { + return false; + } + + // + // Special case by far the most common wild card search of * or *.* + // + + if (expression.Equals("*") || expression.Equals("*.*")) + { + return true; + } + + // If this class is ever exposed for generic use, + // we need to make sure that name doesn't contain wildcards. Currently + // the only component that calls this method is FileSystemWatcher and + // it will never pass a name that contains a wildcard. + + + // + // Also special case expressions of the form *X. With this and the prior + // case we have covered virtually all normal queries. + // + if (expression[0] == '*' && expression.IndexOf('*', 1) == -1) + { + int rightLength = expression.Length - 1; + // if name is shorter that the stuff to the right of * in expression, we don't + // need to do the string compare, otherwise we compare rightlength characters + // and the end of both strings. + if (name.Length >= rightLength && String.Compare(expression, 1, name, name.Length - rightLength, rightLength, StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + } + + // + // Walk through the name string, picking off characters. We go one + // character beyond the end because some wild cards are able to match + // zero characters beyond the end of the string. + // + // With each new name character we determine a new set of states that + // match the name so far. We use two arrays that we swap back and forth + // for this purpose. One array lists the possible expression states for + // all name characters up to but not including the current one, and other + // array is used to build up the list of states considering the current + // name character as well. The arrays are then switched and the process + // repeated. + // + // There is not a one-to-one correspondence between state number and + // offset into the expression. This is evident from the NFAs in the + // initial comment to this function. State numbering is not continuous. + // This allows a simple conversion between state number and expression + // offset. Each character in the expression can represent one or two + // states. * and DOS_STAR generate two states: ExprOffset*2 and + // ExprOffset*2 + 1. All other expreesion characters can produce only + // a single state. Thus ExprOffset = State/2. + // + // + // Here is a short description of the variables involved: + // + // NameOffset - The offset of the current name char being processed. + // + // ExprOffset - The offset of the current expression char being processed. + // + // SrcCount - Prior match being investigated with current name char + // + // DestCount - Next location to put a matching assuming current name char + // + // NameFinished - Allows one more itteration through the Matches array + // after the name is exhusted (to come *s for example) + // + // PreviousDestCount - This is used to prevent entry duplication, see coment + // + // PreviousMatches - Holds the previous set of matches (the Src array) + // + // CurrentMatches - Holds the current set of matches (the Dest array) + // + // AuxBuffer, LocalBuffer - the storage for the Matches arrays + // + + // + // Set up the initial variables + // + + previousMatches[0] = 0; + matchesCount = 1; + + nameOffset = 0; + maxState = expression.Length * 2; + + while (!nameFinished) + { + if (nameOffset < name.Length) + { + nameChar = name[nameOffset]; + length = 1; + nameOffset++; + } + else + { + nameFinished = true; + + // + // if we have already exhasted the expression, C#. Don't + // continue. + // + if (previousMatches[matchesCount - 1] == maxState) + { + break; + } + } + + // + // Now, for each of the previous stored expression matches, see what + // we can do with this name character. + // + srcCount = 0; + destCount = 0; + previousDestCount = 0; + + while (srcCount < matchesCount) + { + + // + // We have to carry on our expression analysis as far as possible + // for each character of name, so we loop here until the + // expression stops matching. A clue here is that expression + // cases that can match zero or more characters end with a + // continue, while those that can accept only a single character + // end with a break. + // + exprOffset = ((previousMatches[srcCount++] + 1) / 2); + length = 0; + + while (true) + { + if (exprOffset == expression.Length) + { + break; + } + + // + // The first time through the loop we don't want + // to increment ExprOffset. + // + + exprOffset += length; + + currentState = exprOffset * 2; + + if (exprOffset == expression.Length) + { + currentMatches[destCount++] = maxState; + break; + } + + exprChar = expression[exprOffset]; + length = 1; + + // + // Before we get started, we have to check for something + // really gross. We may be about to exhaust the local + // space for ExpressionMatches[][], so we have to allocate + // some pool if this is the case. Yuk! + // + + if (destCount >= MATCHES_ARRAY_SIZE - 2) + { + int newSize = currentMatches.Length * 2; + int[] tmp = new int[newSize]; + Array.Copy(currentMatches, tmp, currentMatches.Length); + currentMatches = tmp; + + tmp = new int[newSize]; + Array.Copy(previousMatches, tmp, previousMatches.Length); + previousMatches = tmp; + } + + // + // * matches any character zero or more times. + // + + if (exprChar == '*') + { + currentMatches[destCount++] = currentState; + currentMatches[destCount++] = (currentState + 1); + continue; + } + + // + // DOS_STAR matches any character except . zero or more times. + // + + if (exprChar == ANSI_DOS_STAR) + { + bool iCanEatADot = false; + + // + // If we are at a period, determine if we are allowed to + // consume it, ie. make sure it is not the last one. + // + if (!nameFinished && (nameChar == '.')) + { + char tmpChar; + int offset; + + int nameLength = name.Length; + for (offset = nameOffset; offset < nameLength; offset++) + { + tmpChar = name[offset]; + length = 1; + + if (tmpChar == '.') + { + iCanEatADot = true; + break; + } + } + } + + if (nameFinished || (nameChar != '.') || iCanEatADot) + { + currentMatches[destCount++] = currentState; + currentMatches[destCount++] = (currentState + 1); + continue; + } + else + { + + // + // We are at a period. We can only match zero + // characters (ie. the epsilon transition). + // + currentMatches[destCount++] = (currentState + 1); + continue; + } + } + + // + // The following expreesion characters all match by consuming + // a character, thus force the expression, and thus state + // forward. + // + currentState += length * 2; + + // + // DOS_QM is the most complicated. If the name is finished, + // we can match zero characters. If this name is a '.', we + // don't match, but look at the next expression. Otherwise + // we match a single character. + // + if (exprChar == ANSI_DOS_QM) + { + + if (nameFinished || (nameChar == '.')) + { + continue; + } + + currentMatches[destCount++] = currentState; + break; + } + + // + // A DOS_DOT can match either a period, or zero characters + // beyond the end of name. + // + if (exprChar == DOS_DOT) + { + + if (nameFinished) + { + continue; + } + + if (nameChar == '.') + { + currentMatches[destCount++] = currentState; + break; + } + } + + // + // From this point on a name character is required to even + // continue, let alone make a match. + // + if (nameFinished) + { + break; + } + + // + // If this expression was a '?' we can match it once. + // + if (exprChar == '?') + { + currentMatches[destCount++] = currentState; + break; + } + + // + // Finally, check if the expression char matches the name char + // + if (exprChar == nameChar) + { + currentMatches[destCount++] = currentState; + break; + } + + // + // The expression didn't match so go look at the next + // previous match. + // + + break; + } + + + // + // Prevent duplication in the destination array. + // + // Each of the arrays is montonically increasing and non- + // duplicating, thus we skip over any source element in the src + // array if we just added the same element to the destination + // array. This guarentees non-duplication in the dest. array. + // + + if ((srcCount < matchesCount) && (previousDestCount < destCount)) + { + while (previousDestCount < destCount) + { + int previousLength = previousMatches.Length; + while ((srcCount < previousLength) && (previousMatches[srcCount] < currentMatches[previousDestCount])) + { + srcCount += 1; + } + previousDestCount += 1; + } + } + } + + // + // If we found no matches in the just finished itteration, it's time + // to bail. + // + + if (destCount == 0) + { + return false; + } + + // + // Swap the meaning the two arrays + // + + { + int[] tmp; + + tmp = previousMatches; + + previousMatches = currentMatches; + + currentMatches = tmp; + } + + matchesCount = destCount; + } + + currentState = previousMatches[matchesCount - 1]; + + return currentState == maxState; + } + } +} diff --git a/GVFS/GVFS.GVFlt/Properties/AssemblyInfo.cs b/GVFS/GVFS.GVFlt/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8f1a8580 --- /dev/null +++ b/GVFS/GVFS.GVFlt/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.GVFlt")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.GVFlt")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1118b427-7063-422f-83b9-5023c8ec5a7a")] diff --git a/GVFS/GVFS.GVFlt/packages.config b/GVFS/GVFS.GVFlt/packages.config new file mode 100644 index 00000000..a4c3a96d --- /dev/null +++ b/GVFS/GVFS.GVFlt/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/AssemblyInfo.cpp b/GVFS/GVFS.GvFltWrapper/AssemblyInfo.cpp new file mode 100644 index 00000000..cf63278a --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/AssemblyInfo.cpp @@ -0,0 +1,25 @@ +#include "stdafx.h" +#include "CommonAssemblyVersion.h" + +using namespace System; +using namespace System::Reflection; +using namespace System::Runtime::CompilerServices; +using namespace System::Runtime::InteropServices; +using namespace System::Security::Permissions; + +// +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// +[assembly:AssemblyTitleAttribute(L"GVFSGvFltWrapper")]; +[assembly:AssemblyDescriptionAttribute(L"")]; +[assembly:AssemblyConfigurationAttribute(L"")]; +[assembly:AssemblyCompanyAttribute(L"")]; +[assembly:AssemblyProductAttribute(L"GVFSGvFltWrapper")]; +[assembly:AssemblyCopyrightAttribute(L"Copyright (c) Microsoft 2016")]; +[assembly:AssemblyTrademarkAttribute(L"")]; +[assembly:AssemblyCultureAttribute(L"")]; + +[assembly:ComVisible(false)]; +[assembly:CLSCompliantAttribute(true)]; \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj b/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj new file mode 100644 index 00000000..4bfdc03e --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj @@ -0,0 +1,156 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {FB0831AE-9997-401B-B31F-3A065FDBEB20} + v4.5.2 + ManagedCProj + GVFSGvFltWrapper + 8.1 + + + + DynamicLibrary + true + v140 + true + Unicode + + + DynamicLibrary + false + v140 + true + Unicode + + + 0.2.173.2 + + + + + + + + + + + + + + + true + $(SolutionDir)..\BuildOutput\$(ProjectName)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + false + $(SolutionDir)..\BuildOutput\$(ProjectName)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + + Level4 + Disabled + _DEBUG;%(PreprocessorDefinitions) + Use + true + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\header;$(SolutionDir)\..\BuildOutput + + + gvlib.lib;fltlib.lib;Ole32.lib;Advapi32.lib + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\lib + + + + + + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + $(SolutionDir)\Scripts\CreateCommonCliAssemblyVersion.bat $(GVFSVersion) $(SolutionDir)\.. + + + $(SolutionDir)\..\BuildOutput + + + + + Level4 + NDEBUG;%(PreprocessorDefinitions) + Use + true + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\header;$(SolutionDir)\..\BuildOutput + + + gvlib.lib;fltlib.lib;Ole32.lib;Advapi32.lib + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\lib + + + + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + $(SolutionDir)\Scripts\CreateCommonCliAssemblyVersion.bat $(GVFSVersion) $(SolutionDir)\.. + + + $(SolutionDir)\..\BuildOutput + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + + + + + Designer + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj.filters b/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj.filters new file mode 100644 index 00000000..81722cc3 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GVFS.GvFltWrapper.vcxproj.filters @@ -0,0 +1,85 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {c42f0003-79e9-4a34-9c03-c74c9242f1d8} + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + + + Resource Files + + + \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.cpp b/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.cpp new file mode 100644 index 00000000..53effc4b --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.cpp @@ -0,0 +1,43 @@ +#include "stdafx.h" +#include "GvFltWriteBuffer.h" + +using namespace System; +using namespace System::IO; +using namespace GVFSGvFltWrapper; + +GVFltWriteBuffer::GVFltWriteBuffer(int bufferSize) +{ + this->buffer = (unsigned char*)malloc(sizeof(unsigned char) * bufferSize); + if (this->buffer == nullptr) + { + throw gcnew InvalidOperationException("Unable to allocate GVFltWriteBuffer"); + } + + this->stream = gcnew UnmanagedMemoryStream(buffer, bufferSize, bufferSize, FileAccess::Write); +} + +GVFltWriteBuffer::~GVFltWriteBuffer() +{ + delete this->stream; + this->!GVFltWriteBuffer(); +} + +GVFltWriteBuffer::!GVFltWriteBuffer() +{ + free(this->buffer); +} + +long long GVFltWriteBuffer::Length::get(void) +{ + return this->stream->Length; +} + +UnmanagedMemoryStream^ GVFltWriteBuffer::Stream::get(void) +{ + return this->stream; +} + +IntPtr GVFltWriteBuffer::Pointer::get(void) +{ + return IntPtr(this->buffer); +} diff --git a/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.h b/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.h new file mode 100644 index 00000000..ada1a4f5 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GVFltWriteBuffer.h @@ -0,0 +1,33 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + public ref class GVFltWriteBuffer + { + public: + GVFltWriteBuffer(int bufferSize); + ~GVFltWriteBuffer(); + + property long long Length + { + long long get(void); + }; + + property System::IO::UnmanagedMemoryStream^ Stream + { + System::IO::UnmanagedMemoryStream^ get(void); + }; + + property System::IntPtr Pointer + { + System::IntPtr get(void); + } + + protected: + !GVFltWriteBuffer(); + + private: + System::IO::UnmanagedMemoryStream^ stream; + unsigned char* buffer; + }; +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationFileNamesResult.h b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationFileNamesResult.h new file mode 100644 index 00000000..b73c5b2a --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationFileNamesResult.h @@ -0,0 +1,69 @@ +#pragma once + +#include "GvDirectoryEnumerationResult.h" +#include "NativeEnumerationResultUtils.h" + +namespace GVFSGvFltWrapper +{ + public ref class GvDirectoryEnumerationFileNamesResult : public GvDirectoryEnumerationResult + { + public: + GvDirectoryEnumerationFileNamesResult(PFILE_NAMES_INFORMATION enumerationData, unsigned long maxEnumerationDataLength); + + property System::DateTime CreationTime + { + virtual void set(System::DateTime value) override { UNREFERENCED_PARAMETER(value); } + }; + + property System::DateTime LastAccessTime + { + virtual void set(System::DateTime value) override { UNREFERENCED_PARAMETER(value); } + }; + + property System::DateTime LastWriteTime + { + virtual void set(System::DateTime value) override { UNREFERENCED_PARAMETER(value); } + }; + + property System::DateTime ChangeTime + { + virtual void set(System::DateTime value) override { UNREFERENCED_PARAMETER(value); } + }; + + property long long EndOfFile + { + virtual void set(long long value) override { UNREFERENCED_PARAMETER(value); } + }; + + property long long AllocationSize + { + virtual void set(long long value) override { UNREFERENCED_PARAMETER(value); } + }; + + property unsigned int FileAttributes + { + virtual void set(unsigned int value) override { UNREFERENCED_PARAMETER(value); } + }; + + virtual bool TrySetFileName(System::String^ value) override; + + private: + PFILE_NAMES_INFORMATION enumerationData; + unsigned long maxEnumerationDataLength; + }; + + + inline GvDirectoryEnumerationFileNamesResult::GvDirectoryEnumerationFileNamesResult(PFILE_NAMES_INFORMATION enumerationData, unsigned long maxEnumerationDataLength) + : enumerationData(enumerationData) + , maxEnumerationDataLength(maxEnumerationDataLength) + { + this->bytesWritten = FIELD_OFFSET(FILE_NAMES_INFORMATION, FileName); + } + + inline bool GvDirectoryEnumerationFileNamesResult::TrySetFileName(System::String^ value) + { + bool nameTruncated = false; + this->bytesWritten = PopulateNameInEnumerationData(this->enumerationData, this->maxEnumerationDataLength, value, nameTruncated); + return !nameTruncated; + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResult.h b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResult.h new file mode 100644 index 00000000..5fcd2aea --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResult.h @@ -0,0 +1,68 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + public ref class GvDirectoryEnumerationResult abstract + { + public: + GvDirectoryEnumerationResult(); + + property System::DateTime CreationTime + { + virtual void set(System::DateTime value) abstract; + }; + + property System::DateTime LastAccessTime + { + virtual void set(System::DateTime value) abstract; + }; + + property System::DateTime LastWriteTime + { + virtual void set(System::DateTime value) abstract; + }; + + property System::DateTime ChangeTime + { + virtual void set(System::DateTime value) abstract; + }; + + property long long EndOfFile + { + virtual void set(long long value) abstract; + }; + + property long long AllocationSize + { + virtual void set(long long value) abstract; + }; + + property unsigned int FileAttributes + { + virtual void set(unsigned int value) abstract; + }; + + property unsigned long BytesWritten + { + unsigned long get(void); + } + + // Returns true if the entire name could be set, and false if the name had to be truncated + // due to insufficient space + virtual bool TrySetFileName(System::String^ value) abstract; + + protected: + unsigned long bytesWritten; + + }; + + inline GvDirectoryEnumerationResult::GvDirectoryEnumerationResult() + : bytesWritten(0) + { + } + + inline unsigned long GvDirectoryEnumerationResult::BytesWritten::get(void) + { + return this->bytesWritten; + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResultImpl.h b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResultImpl.h new file mode 100644 index 00000000..1c8dc6d9 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvDirectoryEnumerationResultImpl.h @@ -0,0 +1,113 @@ +#pragma once + +#include "GvDirectoryEnumerationResult.h" + +namespace GVFSGvFltWrapper +{ + template + public ref class GvDirectoryEnumerationResultImpl : public GvDirectoryEnumerationResult + { + public: + GvDirectoryEnumerationResultImpl(NativeEnumerationDataStruct* enumerationData, unsigned long maxEnumerationDataLength); + + property System::DateTime CreationTime + { + virtual void set(System::DateTime value) override; + }; + + property System::DateTime LastAccessTime + { + virtual void set(System::DateTime value) override; + }; + + property System::DateTime LastWriteTime + { + virtual void set(System::DateTime value) override; + }; + + property System::DateTime ChangeTime + { + virtual void set(System::DateTime value) override; + }; + + property long long EndOfFile + { + virtual void set(long long value) override; + }; + + property long long AllocationSize + { + virtual void set(long long value) override; + }; + + property unsigned int FileAttributes + { + virtual void set(unsigned int value) override; + }; + + virtual bool TrySetFileName(System::String^ value) override; + + private: + NativeEnumerationDataStruct* enumerationData; + unsigned long maxEnumerationDataLength; + }; + + + template + inline GvDirectoryEnumerationResultImpl::GvDirectoryEnumerationResultImpl(NativeEnumerationDataStruct* enumerationData, unsigned long maxEnumerationDataLength) + : enumerationData(enumerationData) + , maxEnumerationDataLength(maxEnumerationDataLength) + { + this->bytesWritten = FIELD_OFFSET(NativeEnumerationDataStruct, FileName); + } + + template + inline void GvDirectoryEnumerationResultImpl::CreationTime::set(System::DateTime value) + { + this->enumerationData->CreationTime.QuadPart = value.ToFileTime(); + } + + template + inline void GvDirectoryEnumerationResultImpl::LastAccessTime::set(System::DateTime value) + { + this->enumerationData->LastAccessTime.QuadPart = value.ToFileTime(); + } + + template + inline void GvDirectoryEnumerationResultImpl::LastWriteTime::set(System::DateTime value) + { + this->enumerationData->LastWriteTime.QuadPart = value.ToFileTime(); + } + + template + inline void GvDirectoryEnumerationResultImpl::ChangeTime::set(System::DateTime value) + { + this->enumerationData->ChangeTime.QuadPart = value.ToFileTime(); + } + + template + inline void GvDirectoryEnumerationResultImpl::EndOfFile::set(long long value) + { + this->enumerationData->EndOfFile.QuadPart = value; + } + + template + inline void GvDirectoryEnumerationResultImpl::AllocationSize::set(long long value) + { + this->enumerationData->AllocationSize.QuadPart = value; + } + + template + inline void GvDirectoryEnumerationResultImpl::FileAttributes::set(unsigned int value) + { + this->enumerationData->FileAttributes = value; + } + + template + inline bool GvDirectoryEnumerationResultImpl::TrySetFileName(System::String^ value) + { + bool nameTruncated = false; + this->bytesWritten = PopulateNameInEnumerationData(this->enumerationData, this->maxEnumerationDataLength, value, nameTruncated); + return !nameTruncated; + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvFltCallbackDelegates.h b/GVFS/GVFS.GvFltWrapper/GvFltCallbackDelegates.h new file mode 100644 index 00000000..019d2fe4 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvFltCallbackDelegates.h @@ -0,0 +1,64 @@ +#pragma once + +#include "GvDirectoryEnumerationResult.h" +#include "GVFltWriteBuffer.h" + +namespace GVFSGvFltWrapper +{ + public delegate StatusCode GvStartDirectoryEnumerationEvent(System::Guid enumerationId, System::String^ relativePath); + + public delegate StatusCode GvEndDirectoryEnumerationEvent(System::Guid enumerationId); + + public delegate StatusCode GvGetDirectoryEnumerationEvent( + System::Guid enumerationId, + System::String^ filterFileName, + bool restartScan, + GvDirectoryEnumerationResult^ result); + + public delegate StatusCode GvQueryFileNameEvent(System::String^ relativePath); + + public delegate StatusCode GvGetPlaceHolderInformationEvent( + System::String^ relativePath, + unsigned long desiredAccess, + unsigned long shareMode, + unsigned long createDisposition, + unsigned long createOptions, + unsigned long triggeringProcessId, + System::String^ triggeringProcessImageFileName); + + public delegate StatusCode GvGetFileStreamEvent( + System::String^ relativePath, + long long byteOffset, + unsigned long length, + System::Guid streamGuid, + System::String^ contentId, + unsigned long triggeringProcessId, + System::String^ triggeringProcessImageFileName, + GVFltWriteBuffer^ targetBuffer); + + public delegate StatusCode GvNotifyFirstWriteEvent(System::String^ relativePath); + + public delegate void GvNotifyCreateEvent( + System::String^ relativePath, + unsigned long desiredAccess, + unsigned long shareMode, + unsigned long createDisposition, + unsigned long createOptions, + unsigned long ioStatusBlock, + unsigned long% notificationMask); + + public delegate StatusCode GvNotifyPreDeleteEvent(System::String^ relativePath); + + public delegate StatusCode GvNotifyPreRenameEvent(System::String^ relativePath, System::String^ destinationPath); + + public delegate StatusCode GvNotifyPreSetHardlinkEvent(System::String^ relativePath, System::String^ destinationPath); + + public delegate void GvNotifyFileRenamedEvent(System::String^ relativePath, System::String^ destinationPath, unsigned long% notificationMask); + + public delegate void GvNotifyHardlinkCreatedEvent(System::String^ relativePath, System::String^ destinationPath); + + public delegate void GvNotifyFileHandleClosedEvent( + System::String^ relativePath, + bool fileModified, + bool fileDeleted); +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvFltException.cpp b/GVFS/GVFS.GvFltWrapper/GvFltException.cpp new file mode 100644 index 00000000..449af2b5 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvFltException.cpp @@ -0,0 +1,31 @@ +#include "stdafx.h" +#include "GvFltException.h" + +using namespace System; +using namespace GVFSGvFltWrapper; + +GvFltException::GvFltException(String^ errorMessage) + : GvFltException(errorMessage, StatusCode::StatusInternalError) +{ +} + +GvFltException::GvFltException(StatusCode errorCode) + : GvFltException("GvFltException exception, error: " + errorCode.ToString(), errorCode) +{ +} + +GvFltException::GvFltException(String^ errorMessage, StatusCode errorCode) + : Exception(errorMessage) + , errorCode(errorCode) +{ +} + +String^ GvFltException::ToString() +{ + return String::Format("GvFltException ErrorCode: {0}, {1}", + this->errorCode, this->Exception::ToString()); +} + +StatusCode GvFltException::ErrorCode::get(void) +{ + return this->errorCode; +}; \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvFltException.h b/GVFS/GVFS.GvFltWrapper/GvFltException.h new file mode 100644 index 00000000..a80c7b91 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvFltException.h @@ -0,0 +1,24 @@ +#pragma once +#include "StatusCode.h" + +namespace GVFSGvFltWrapper +{ + [System::Serializable()] + public ref class GvFltException : System::Exception + { + public: + GvFltException(System::String^ errorMessage); + GvFltException(StatusCode errorCode); + GvFltException(System::String^ errorMessage, StatusCode errorCode); + + virtual System::String^ ToString() override; + + virtual property StatusCode ErrorCode + { + StatusCode get(void); + }; + + private: + StatusCode errorCode; + }; +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvFltWrapper.cpp b/GVFS/GVFS.GvFltWrapper/GvFltWrapper.cpp new file mode 100644 index 00000000..9e6c8ce8 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvFltWrapper.cpp @@ -0,0 +1,1188 @@ +#include "stdafx.h" +#include "GvFltException.h" +#include "GvFltWrapper.h" +#include "GvDirectoryEnumerationResultImpl.h" +#include "GvDirectoryEnumerationFileNamesResult.h" +#include "Utils.h" + +using namespace GVFSGvFltWrapper; +using namespace System; +using namespace System::ComponentModel; +using namespace System::Text; + +namespace +{ + const int BLOCK_SIZE = 64 * 1024; + const UCHAR CURRENT_PLACEHOLDER_VERSION = 1; + const int EPOCH_RESERVED_BYTES = 4; + + ref class ActiveGvFltManager + { + public: + // Handle to the active GvFltWrapper instance. + // In the future if we support multiple GvFltWrappers per GVFS instance, this can be a map + // of GV_VIRTUALIZATIONINSTANCE_HANDLE to GvFltWrapper. Then in each callback the + // appropriate GvFltWrapper instance can be found (and the callback is delivered). + static GvFltWrapper^ activeGvFltWrapper; + }; + + // GvFlt callback functions that forward the request from GvFlt to the active + // GvFltWrapper (ActiveGvFltManager::activeGvFltWrapper) + NTSTATUS GvStartDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId, + _In_ LPCWSTR pathName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo); + + NTSTATUS GvEndDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId); + + NTSTATUS GvGetDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId, + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _Inout_ PULONG length, + _In_ LPCWSTR filterFileName, + _In_ BOOLEAN returnSingleEntry, + _In_ BOOLEAN restartScan, + _Out_ PVOID fileInformation); + + NTSTATUS GvQueryFileNameCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName + ); + + NTSTATUS GvGetPlaceholderInformationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO parentDirectoryVersionInfo, + _In_ DWORD desiredAccess, + _In_ DWORD shareMode, + _In_ DWORD createDisposition, + _In_ DWORD createOptions, + _In_ LPCWSTR destinationFileName, + _In_ DWORD triggeringProcessId, + _In_ LPCWSTR triggeringProcessImageFileName + ); + + NTSTATUS GvGetFileStreamCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo, + _In_ LARGE_INTEGER byteOffset, + _In_ DWORD length, + _In_ ULONG flags, + _In_ GUID streamGuid, + _In_ DWORD triggeringProcessId, + _In_ LPCWSTR triggeringProcessImageFileName + ); + + NTSTATUS GvNotifyFirstWriteCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo + ); + + NTSTATUS GvNotifyOperationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo, + _In_ GUID streamGuid, + _In_ GUID handleGuid, + _In_ GV_NOTIFICATION_TYPE notificationType, + _In_opt_ LPCWSTR destinationFileName, + _Inout_ PGV_OPERATION_PARAMETERS operationParameters + ); + + // Internal helper functions used by the above callbacks + GvDirectoryEnumerationResult^ CreateEnumerationResult( + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _In_ PVOID buffer, + _In_ ULONG bufferLength, + _Out_ size_t& fileInfoSize); + + void SetNextEntryOffset( + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _In_ PVOID buffer, + _In_ ULONG offset); + + size_t GetRequiredAlignment(_In_ FILE_INFORMATION_CLASS fileInformationClass); + + UCHAR GetPlaceHolderVersion(const GV_PLACEHOLDER_VERSION_INFO& versionInfo); + void SetPlaceHolderVersion(GV_PLACEHOLDER_VERSION_INFO& versionInfo, UCHAR version); + + String^ GetContentId(const GV_PLACEHOLDER_VERSION_INFO& versionInfo); + void SetContentId(GV_PLACEHOLDER_VERSION_INFO& versionInfo, String^ contentId); + + void SetEpochId(GV_PLACEHOLDER_VERSION_INFO& versionInfo, String^ epochId); + +} + +GvFltWrapper::GvFltWrapper() + : virtualizationInstanceHandle(nullptr) + , virtualRootPath(nullptr) +{ +} + +GvStartDirectoryEnumerationEvent^ GvFltWrapper::OnStartDirectoryEnumeration::get(void) +{ + return this->gvStartDirectoryEnumerationEvent; +} + +void GvFltWrapper::OnStartDirectoryEnumeration::set(GvStartDirectoryEnumerationEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvStartDirectoryEnumerationEvent = eventCB; +} + +GvEndDirectoryEnumerationEvent^ GvFltWrapper::OnEndDirectoryEnumeration::get(void) +{ + return this->gvEndDirectoryEnumerationEvent; +} + +void GvFltWrapper::OnEndDirectoryEnumeration::set(GvEndDirectoryEnumerationEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvEndDirectoryEnumerationEvent = eventCB; +} + +GvGetDirectoryEnumerationEvent^ GvFltWrapper::OnGetDirectoryEnumeration::get(void) +{ + return this->gvGetDirectoryEnumerationEvent; +} + +void GvFltWrapper::OnGetDirectoryEnumeration::set(GvGetDirectoryEnumerationEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvGetDirectoryEnumerationEvent = eventCB; +} + +GvQueryFileNameEvent^ GvFltWrapper::OnQueryFileName::get(void) +{ + return this->gvQueryFileNameEvent; +} + +void GvFltWrapper::OnQueryFileName::set(GvQueryFileNameEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvQueryFileNameEvent = eventCB; +} + +GvGetPlaceHolderInformationEvent^ GvFltWrapper::OnGetPlaceHolderInformation::get(void) +{ + return this->gvGetPlaceHolderInformationEvent; +} + +void GvFltWrapper::OnGetPlaceHolderInformation::set(GvGetPlaceHolderInformationEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvGetPlaceHolderInformationEvent = eventCB; +} + +GvGetFileStreamEvent^ GvFltWrapper::OnGetFileStream::get(void) +{ + return this->gvGetFileStreamEvent; +} + +void GvFltWrapper::OnGetFileStream::set(GvGetFileStreamEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvGetFileStreamEvent = eventCB; +} + +GvNotifyFirstWriteEvent^ GvFltWrapper::OnNotifyFirstWrite::get(void) +{ + return this->gvNotifyFirstWriteEvent; +} + +void GvFltWrapper::OnNotifyFirstWrite::set(GvNotifyFirstWriteEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyFirstWriteEvent = eventCB; +} + +GvNotifyCreateEvent^ GvFltWrapper::OnNotifyCreate::get(void) +{ + return this->gvNotifyCreateEvent; +} + +void GvFltWrapper::OnNotifyCreate::set(GvNotifyCreateEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyCreateEvent = eventCB; +} + +GvNotifyPreDeleteEvent^ GvFltWrapper::OnNotifyPreDelete::get(void) +{ + return this->gvNotifyPreDeleteEvent; +} + +void GvFltWrapper::OnNotifyPreDelete::set(GvNotifyPreDeleteEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyPreDeleteEvent = eventCB; +} + +GvNotifyPreRenameEvent^ GvFltWrapper::OnNotifyPreRename::get(void) +{ + return this->gvNotifyPreRenameEvent; +} + +void GvFltWrapper::OnNotifyPreRename::set(GvNotifyPreRenameEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyPreRenameEvent = eventCB; +} + +GvNotifyPreSetHardlinkEvent^ GvFltWrapper::OnNotifyPreSetHardlink::get(void) +{ + return this->gvNotifyPreSetHardlinkEvent; +} + +void GvFltWrapper::OnNotifyPreSetHardlink::set(GvNotifyPreSetHardlinkEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyPreSetHardlinkEvent = eventCB; +} + +GvNotifyFileRenamedEvent^ GvFltWrapper::OnNotifyFileRenamed::get(void) +{ + return this->gvNotifyFileRenamedEvent; +} + +void GvFltWrapper::OnNotifyFileRenamed::set(GvNotifyFileRenamedEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyFileRenamedEvent = eventCB; +} + +GvNotifyHardlinkCreatedEvent^ GvFltWrapper::OnNotifyHardlinkCreated::get(void) +{ + return this->gvNotifyHardlinkCreatedEvent; +} + +void GvFltWrapper::OnNotifyHardlinkCreated::set(GvNotifyHardlinkCreatedEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyHardlinkCreatedEvent = eventCB; +} + +GvNotifyFileHandleClosedEvent^ GvFltWrapper::OnNotifyFileHandleClosed::get(void) +{ + return this->gvNotifyFileHandleClosedEvent; +} + +void GvFltWrapper::OnNotifyFileHandleClosed::set(GvNotifyFileHandleClosedEvent^ eventCB) +{ + ConfirmNotStarted(); + this->gvNotifyFileHandleClosedEvent = eventCB; +} + +GVFS::Common::Tracing::ITracer^ GvFltWrapper::Tracer::get(void) +{ + return this->tracer; +} + +HResult GvFltWrapper::GvStartVirtualizationInstance( + GVFS::Common::Tracing::ITracer^ tracerImpl, + System::String^ virtualizationRootPath, + unsigned long poolThreadCount, + unsigned long concurrentThreadCount) +{ + ConfirmNotStarted(); + + if (virtualizationRootPath == nullptr) + { + throw gcnew ArgumentNullException(gcnew String("virtualizationRootPath")); + } + + ActiveGvFltManager::activeGvFltWrapper = this; + + this->tracer = tracerImpl; + this->virtualRootPath = virtualizationRootPath; + pin_ptr rootPath = PtrToStringChars(this->virtualRootPath); + GV_COMMAND_CALLBACKS callbacks; + GvCommandCallbacksInit(&callbacks); + callbacks.GvStartDirectoryEnumeration = GvStartDirectoryEnumerationCB; + callbacks.GvEndDirectoryEnumeration = GvEndDirectoryEnumerationCB; + callbacks.GvGetDirectoryEnumeration = GvGetDirectoryEnumerationCB; + callbacks.GvQueryFileName = GvQueryFileNameCB; + callbacks.GvGetPlaceholderInformation = GvGetPlaceholderInformationCB; + callbacks.GvGetFileStream = GvGetFileStreamCB; + callbacks.GvNotifyFirstWrite = GvNotifyFirstWriteCB; + callbacks.GvNotifyOperation = GvNotifyOperationCB; + + pin_ptr instanceHandle = &(this->virtualizationInstanceHandle); + return static_cast(::GvStartVirtualizationInstance( + rootPath, + &callbacks, + 0, // flags + poolThreadCount, + concurrentThreadCount, + instanceHandle + )); +} + +HResult GvFltWrapper::GvStopVirtualizationInstance() +{ + long result = ::GvStopVirtualizationInstance(this->virtualizationInstanceHandle); + if (result == STATUS_SUCCESS) + { + this->tracer = nullptr; + this->virtualizationInstanceHandle = nullptr; + ActiveGvFltManager::activeGvFltWrapper = nullptr; + } + + return static_cast(result); +} + +HResult GvFltWrapper::GvDetachDriver() +{ + pin_ptr rootPath = PtrToStringChars(this->virtualRootPath); + return static_cast(::GvDetachDriver(rootPath)); +} + + +StatusCode GvFltWrapper::GvWriteFile( + Guid streamGuid, + GVFltWriteBuffer^ targetBuffer, + unsigned long long byteOffset, + unsigned long length + ) +{ + array^ guidData = streamGuid.ToByteArray(); + pin_ptr data = &(guidData[0]); + pin_ptr instanceHandle = &(this->virtualizationInstanceHandle); + + return static_cast(::GvWriteFile( + *instanceHandle, + *(GUID*)data, + targetBuffer->Pointer.ToPointer(), + byteOffset, + length + )); +} + +StatusCode GvFltWrapper::GvWritePlaceholderInformation( + String^ targetRelPathName, + DateTime creationTime, + DateTime lastAccessTime, + DateTime lastWriteTime, + DateTime changeTime, + unsigned long fileAttributes, + long long allocationSize, + long long endOfFile, + bool directory, + String^ contentId, + String^ epochId) +{ + pin_ptr instanceHandle = &(this->virtualizationInstanceHandle); + pin_ptr path = PtrToStringChars(targetRelPathName); + std::shared_ptr fileInformation(static_cast(malloc(sizeof(GV_PLACEHOLDER_INFORMATION))), free); + + memset(&fileInformation->FileBasicInformation, 0, sizeof(FILE_BASIC_INFORMATION)); + memset(&fileInformation->FileStandardInformation, 0, sizeof(FILE_STANDARD_INFORMATION)); + + fileInformation->FileBasicInformation.CreationTime.QuadPart = creationTime.ToFileTime(); + fileInformation->FileBasicInformation.LastAccessTime.QuadPart = lastAccessTime.ToFileTime(); + fileInformation->FileBasicInformation.LastWriteTime.QuadPart = lastWriteTime.ToFileTime(); + fileInformation->FileBasicInformation.ChangeTime.QuadPart = changeTime.ToFileTime(); + fileInformation->FileBasicInformation.FileAttributes = fileAttributes; + + fileInformation->FileStandardInformation.AllocationSize.QuadPart = allocationSize; + fileInformation->FileStandardInformation.EndOfFile.QuadPart = endOfFile; + fileInformation->FileStandardInformation.Directory = directory; + + fileInformation->EaInformation.EaBufferSize = 0; + fileInformation->EaInformation.OffsetToFirstEa = static_cast(-1); + fileInformation->SecurityInformation.SecurityBufferSize = 0; + fileInformation->SecurityInformation.OffsetToSecurityDescriptor = static_cast(-1); + fileInformation->StreamsInformation.StreamsInfoBufferSize = 0; + fileInformation->StreamsInformation.OffsetToFirstStreamInfo = static_cast(-1); + + // In GVFS, placeholders are always authoritative + fileInformation->Flags = GV_PLACEHOLDER_INFORMATION_AUTHORITATIVE; + + memset(&fileInformation->VersionInfo, 0, sizeof(GV_PLACEHOLDER_VERSION_INFO)); + SetPlaceHolderVersion(fileInformation->VersionInfo, CURRENT_PLACEHOLDER_VERSION); + SetEpochId(fileInformation->VersionInfo, epochId); + SetContentId(fileInformation->VersionInfo, contentId); + + return static_cast(::GvWritePlaceholderInformation( + *instanceHandle, + path, + fileInformation.get(), + FIELD_OFFSET(GV_PLACEHOLDER_INFORMATION, VariableData))); // We have written no variable data +} + +StatusCode GvFltWrapper::GvCreatePlaceholderAsHardlink( + System::String^ destinationFileName, + System::String^ hardLinkTarget) +{ + pin_ptr instanceHandle = &(this->virtualizationInstanceHandle); + pin_ptr targetPath = PtrToStringChars(destinationFileName); + pin_ptr hardLinkPath = PtrToStringChars(hardLinkTarget); + + return static_cast(::GvCreatePlaceholderAsHardlink( + *instanceHandle, + targetPath, + hardLinkPath)); +} + +GvFltWrapper::OnDiskStatus GvFltWrapper::GetFileOnDiskStatus(System::String^ relativePath) +{ + GUID handleGUID; + pin_ptr filePath = PtrToStringChars(relativePath); + NTSTATUS openResult = ::GvOpenFile(this->virtualizationInstanceHandle, filePath, GENERIC_READ, &handleGUID); + if (NT_SUCCESS(openResult)) + { + NTSTATUS closeResult = ::GvCloseFile(this->virtualizationInstanceHandle, handleGUID); + + if (!NT_SUCCESS(closeResult)) + { + this->Tracer->RelatedError(String::Format("FileExists: GvCloseFile failed for {0}: {1}", relativePath, static_cast(closeResult))); + } + + return OnDiskStatus::Full; + } + + switch (openResult) + { + case STATUS_IO_REPARSE_TAG_NOT_HANDLED: + return OnDiskStatus::Partial; + case STATUS_OBJECT_NAME_NOT_FOUND: + return OnDiskStatus::NotOnDisk; + default: + throw gcnew GvFltException("ReadFileContents: GvOpenFile failed", static_cast(openResult)); + break; + } +} + +System::String^ GvFltWrapper::ReadFullFileContents(System::String^ relativePath) +{ + GUID handleGUID; + pin_ptr filePath = PtrToStringChars(relativePath); + NTSTATUS openResult = ::GvOpenFile(this->virtualizationInstanceHandle, filePath, GENERIC_READ, &handleGUID); + + if (!NT_SUCCESS(openResult)) + { + throw gcnew GvFltException("ReadFileContents: GvOpenFile failed", static_cast(openResult)); + } + + StringBuilder^ allLines = gcnew StringBuilder(); + char buffer[BLOCK_SIZE]; + ULONG length = sizeof(buffer) - sizeof(char); // Leave room for null terminator + ULONG bytesRead = 0; + ULONGLONG bytesOffset = 0; + + do + { + NTSTATUS readResult = ::GvReadFile(this->virtualizationInstanceHandle, handleGUID, buffer, bytesOffset, length, &bytesRead); + + if (!NT_SUCCESS(readResult)) + { + NTSTATUS closeResult = ::GvCloseFile(this->virtualizationInstanceHandle, handleGUID); + + if (!NT_SUCCESS(closeResult)) + { + this->Tracer->RelatedError(String::Format("ReadFileContents: GvCloseFile failed while closing file after failed read of {0}: {1}", relativePath, static_cast(closeResult))); + } + + throw gcnew GvFltException("ReadFileContents: GvReadFile failed", static_cast(readResult)); + } + + if (bytesRead > 0) + { + // Add null terminator + *static_cast(static_cast(buffer + bytesRead)) = 0; + allLines->Append(gcnew String(static_cast(static_cast(buffer)))); + bytesOffset += bytesRead; + } + } while (bytesRead > 0); + + NTSTATUS closeResult = ::GvCloseFile(this->virtualizationInstanceHandle, handleGUID); + + if (!NT_SUCCESS(closeResult)) + { + this->Tracer->RelatedError(String::Format("ReadFileContents: GvCloseFile failed for {0}: {1}", relativePath, static_cast(closeResult))); + } + + return allLines->ToString(); +} + +//static +HResult GvFltWrapper::GvConvertDirectoryToVirtualizationRoot(System::Guid virtualizationInstanceGuid, System::String^ rootPathName) +{ + array^ guidArray = virtualizationInstanceGuid.ToByteArray(); + pin_ptr guidData = &(guidArray[0]); + + pin_ptr rootPath = PtrToStringChars(rootPathName); + + GV_PLACEHOLDER_VERSION_INFO versionInfo; + memset(&versionInfo, 0, sizeof(GV_PLACEHOLDER_VERSION_INFO)); + SetPlaceHolderVersion(versionInfo, CURRENT_PLACEHOLDER_VERSION); + + return static_cast(::GvConvertDirectoryToPlaceholder( + rootPath, // RootPathName + L"", // TargetPathName + &versionInfo, // VersionInfo + 0, // ReparseTag + GV_FLAG_VIRTUALIZATION_ROOT, // Flags + *(GUID*)guidData)); // VirtualizationInstanceID +} + +void GvFltWrapper::ConfirmNotStarted() +{ + if (this->virtualizationInstanceHandle) + { + throw gcnew GvFltException("Operation invalid after GvFlt is started"); + } +} + +namespace +{ + NTSTATUS GvStartDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId, + _In_ LPCWSTR pathName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + UNREFERENCED_PARAMETER(versionInfo); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnStartDirectoryEnumeration != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + + try + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnStartDirectoryEnumeration( + GUIDtoGuid(enumerationId), + gcnew String(pathName))); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvStartDirectoryEnumerationCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvStartDirectoryEnumerationCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvStartDirectoryEnumerationCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvEndDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnEndDirectoryEnumeration != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + + try + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnEndDirectoryEnumeration(GUIDtoGuid(enumerationId))); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvEndDirectoryEnumerationCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvEndDirectoryEnumerationCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvEndDirectoryEnumerationCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvGetDirectoryEnumerationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ GUID enumerationId, + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _Inout_ PULONG length, + _In_ LPCWSTR filterFileName, + _In_ BOOLEAN returnSingleEntry, + _In_ BOOLEAN restartScan, + _Out_ PVOID fileInformation + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + + size_t fileInfoSize = 0; + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnGetDirectoryEnumeration != nullptr) + { + memset(fileInformation, 0, *length); + NTSTATUS resultStatus = STATUS_SUCCESS; + ULONG totalBytesWritten = 0; + + try + { + PVOID outputBuffer = fileInformation; + GvDirectoryEnumerationResult^ enumerationData = CreateEnumerationResult(fileInformationClass, outputBuffer, *length, fileInfoSize); + StatusCode callbackResult = ActiveGvFltManager::activeGvFltWrapper->OnGetDirectoryEnumeration( + GUIDtoGuid(enumerationId), + filterFileName != NULL ? gcnew String(filterFileName) : nullptr, + (restartScan != FALSE), + enumerationData); + + totalBytesWritten = enumerationData->BytesWritten; + + if (!returnSingleEntry) + { + bool requestedMultipleEntries = false; + + // Entries must be aligned on the proper boundary (either 8-byte or 4-byte depending on the type) + size_t alignment = GetRequiredAlignment(fileInformationClass); + size_t remainingSpace = static_cast(*length - totalBytesWritten); + PVOID previousEntry = outputBuffer; + PVOID nextEntry = (PUCHAR)outputBuffer + totalBytesWritten; + if (!std::align(alignment, fileInfoSize, nextEntry, remainingSpace)) + { + nextEntry = nullptr; + } + + while (callbackResult == StatusCode::StatusSucccess && nextEntry != nullptr) + { + requestedMultipleEntries = true; + + enumerationData = CreateEnumerationResult(fileInformationClass, nextEntry, static_cast(remainingSpace), fileInfoSize); + + callbackResult = ActiveGvFltManager::activeGvFltWrapper->OnGetDirectoryEnumeration( + GUIDtoGuid(enumerationId), + filterFileName != NULL ? gcnew String(filterFileName) : nullptr, + false, // restartScan + enumerationData); + + if (callbackResult == StatusCode::StatusSucccess) + { + SetNextEntryOffset(fileInformationClass, previousEntry, static_cast((PUCHAR)nextEntry - (PUCHAR)previousEntry)); + + totalBytesWritten = static_cast((PUCHAR)nextEntry - (PUCHAR)outputBuffer) + enumerationData->BytesWritten; + + // Advance nextEntry to the next boundary aligned spot in the buffer + remainingSpace = static_cast(*length - totalBytesWritten); + previousEntry = nextEntry; + nextEntry = (PUCHAR)outputBuffer + totalBytesWritten; + if (!std::align(alignment, fileInfoSize, nextEntry, remainingSpace)) + { + nextEntry = nullptr; + } + } + } + + if (requestedMultipleEntries) + { + if (callbackResult == StatusCode::StatusBufferOverflow) + { + // We attempted to place multiple entries in the buffer, but not all of them fit, return StatusSucccess + // On the next call to GvGetDirectoryEnumerationCB we'll start with the entry that was too + // big to fit + callbackResult = StatusCode::StatusSucccess; + } + else if (callbackResult == StatusCode::StatusNoMoreFiles) + { + // We succeeded in placing all remaining entries in the buffer. Return StatusSucccess to indicate + // that there are entries in the buffer. On the next call to GvGetDirectoryEnumerationCB StatusNoMoreFiles + // will be returned + callbackResult = StatusCode::StatusSucccess; + } + } + } + + resultStatus = static_cast(callbackResult); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetDirectoryEnumerationCB caught GvFltException: " + error->ToString()); + resultStatus = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetDirectoryEnumerationCB caught Win32Exception: " + error->ToString()); + resultStatus = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetDirectoryEnumerationCB fatal exception: " + error->ToString()); + throw; + } + + *length = totalBytesWritten; + + return resultStatus; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvQueryFileNameCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnQueryFileName != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + + try + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnQueryFileName(gcnew String(pathFileName))); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvQueryFileNameCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvQueryFileNameCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvQueryFileNameCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvGetPlaceholderInformationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO parentDirectoryVersionInfo, + _In_ DWORD desiredAccess, + _In_ DWORD shareMode, + _In_ DWORD createDisposition, + _In_ DWORD createOptions, + _In_ LPCWSTR destinationFileName, + _In_ DWORD triggeringProcessId, + _In_ LPCWSTR triggeringProcessImageFileName + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + UNREFERENCED_PARAMETER(parentDirectoryVersionInfo); + UNREFERENCED_PARAMETER(destinationFileName); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnGetPlaceHolderInformation != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + + try + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnGetPlaceHolderInformation( + gcnew String(pathFileName), + desiredAccess, + shareMode, + createDisposition, + createOptions, + triggeringProcessId, + triggeringProcessImageFileName != nullptr ? gcnew String(triggeringProcessImageFileName) : System::String::Empty)); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetPlaceholderInformationCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetPlaceholderInformationCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetPlaceholderInformationCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvGetFileStreamCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo, + _In_ LARGE_INTEGER byteOffset, + _In_ DWORD length, + _In_ ULONG flags, + _In_ GUID streamGuid, + _In_ DWORD triggeringProcessId, + _In_ LPCWSTR triggeringProcessImageFileName + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + UNREFERENCED_PARAMETER(flags); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr) + { + if (versionInfo == NULL) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetFileStreamCB called with null versionInfo, path: " + gcnew String(pathFileName)); + return static_cast(StatusCode::StatusInternalError); + } + else if (GetPlaceHolderVersion(*versionInfo) != CURRENT_PLACEHOLDER_VERSION) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError( + "GvGetFileStreamCB: Unexpected placeholder version " + gcnew String(std::to_wstring(GetPlaceHolderVersion(*versionInfo)).c_str()) + " for file " + gcnew String(pathFileName)); + return static_cast(StatusCode::StatusInternalError); + } + + if (ActiveGvFltManager::activeGvFltWrapper->OnGetFileStream != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + try + { + GVFltWriteBuffer targetBuffer(BLOCK_SIZE); + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnGetFileStream( + gcnew String(pathFileName), + byteOffset.QuadPart, + length, + GUIDtoGuid(streamGuid), + GetContentId(*versionInfo), + triggeringProcessId, + triggeringProcessImageFileName != nullptr ? gcnew String(triggeringProcessImageFileName) : System::String::Empty, + %targetBuffer)); + } + catch (GvFltException^ error) + { + switch (error->ErrorCode) + { + case StatusCode::StatusFileClosed: + // StatusFileClosed is expected, and occurs when an application closes a file handle before OnGetFileStream + // is complete + break; + + case StatusCode::StatusObjectNameNotFound: + // GvWriteFile may return STATUS_OBJECT_NAME_NOT_FOUND if the stream guid provided is not valid (doesn’t exist in the stream table). + // For each file expansion, GVFlt creates a new get stream session with a new stream guid, the session starts at the beginning of the + // file expansion, and ends after the GetFileStream command returns or times out. + // + // If we hit this in GVFS, the most common explanation is that we're calling GvWriteFile after the GVFlt thread waiting on the respose + // from GetFileStream has already timed out + break; + + default: + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetFileStreamCB caught GvFltException: " + error->ToString()); + break; + } + + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetFileStreamCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvGetFileStreamCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvNotifyFirstWriteCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + UNREFERENCED_PARAMETER(versionInfo); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr && + ActiveGvFltManager::activeGvFltWrapper->OnNotifyFirstWrite != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + + try + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnNotifyFirstWrite(gcnew String(pathFileName))); + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyFirstWriteCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyFirstWriteCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyFirstWriteCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + NTSTATUS GvNotifyOperationCB( + _In_ GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle, + _In_ LPCWSTR pathFileName, + _In_ PGV_PLACEHOLDER_VERSION_INFO versionInfo, + _In_ GUID streamGuid, + _In_ GUID handleGuid, + _In_ GV_NOTIFICATION_TYPE notificationType, + _In_opt_ LPCWSTR destinationFileName, + _Inout_ PGV_OPERATION_PARAMETERS operationParameters + ) + { + UNREFERENCED_PARAMETER(virtualizationInstanceHandle); + UNREFERENCED_PARAMETER(versionInfo); + UNREFERENCED_PARAMETER(streamGuid); + UNREFERENCED_PARAMETER(handleGuid); + + if (ActiveGvFltManager::activeGvFltWrapper != nullptr) + { + NTSTATUS result = STATUS_SUCCESS; + try + { + // NOTE: Post callbacks have void return type. The return type is void because + // they are not allowed to fail as the operation has already taken place. If, in the + // future, we were to allow post callbacks to fail the application would need to take + // all necessary actions to undo the operation that has succeeded. + switch (notificationType) + { + case GV_NOTIFICATION_POST_CREATE: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyCreate != nullptr) + { + ActiveGvFltManager::activeGvFltWrapper->OnNotifyCreate( + gcnew String(pathFileName), + operationParameters->PostCreate.DesiredAccess, + operationParameters->PostCreate.ShareMode, + operationParameters->PostCreate.CreateDisposition, + operationParameters->PostCreate.CreateOptions, + operationParameters->PostCreate.IoStatusBlock, + operationParameters->PostCreate.NotificationMask); + } + break; + + case GV_NOTIFICATION_PRE_DELETE: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreDelete != nullptr) + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreDelete(gcnew String(pathFileName))); + } + break; + + case GV_NOTIFICATION_PRE_RENAME: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreRename != nullptr) + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreRename( + gcnew String(pathFileName), + gcnew String(destinationFileName))); + } + break; + + case GV_NOTIFICATION_PRE_SET_HARDLINK: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreSetHardlink != nullptr) + { + result = static_cast(ActiveGvFltManager::activeGvFltWrapper->OnNotifyPreSetHardlink( + gcnew String(pathFileName), + gcnew String(destinationFileName))); + } + break; + + case GV_NOTIFICATION_FILE_RENAMED: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyFileRenamed != nullptr) + { + ActiveGvFltManager::activeGvFltWrapper->OnNotifyFileRenamed( + gcnew String(pathFileName), + gcnew String(destinationFileName), + operationParameters->FileRenamed.NotificationMask); + } + break; + + case GV_NOTIFICATION_HARDLINK_CREATED: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyHardlinkCreated != nullptr) + { + ActiveGvFltManager::activeGvFltWrapper->OnNotifyHardlinkCreated( + gcnew String(pathFileName), + gcnew String(destinationFileName)); + } + break; + + case GV_NOTIFICATION_FILE_HANDLE_CLOSED: + if (ActiveGvFltManager::activeGvFltWrapper->OnNotifyFileHandleClosed != nullptr) + { + ActiveGvFltManager::activeGvFltWrapper->OnNotifyFileHandleClosed( + gcnew String(pathFileName), + (operationParameters->HandleClosed.FileModified != FALSE), + (operationParameters->HandleClosed.FileDeleted != FALSE)); + } + break; + + default: + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyOperationCB unexpected notification type: " + gcnew String(std::to_string(notificationType).c_str())); + break; + } + } + catch (GvFltException^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyOperationCB caught GvFltException: " + error->ToString()); + result = static_cast(error->ErrorCode); + } + catch (Win32Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyOperationCB caught Win32Exception: " + error->ToString()); + result = Win32ErrorToNtStatus(error->NativeErrorCode); + } + catch (Exception^ error) + { + ActiveGvFltManager::activeGvFltWrapper->Tracer->RelatedError("GvNotifyOperationCB fatal exception: " + error->ToString()); + throw; + } + + return result; + } + + return STATUS_INVALID_DEVICE_STATE; + } + + inline GvDirectoryEnumerationResult^ CreateEnumerationResult( + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _In_ PVOID buffer, + _In_ ULONG bufferLength, + _Out_ size_t& fileInfoSize) + { + switch (fileInformationClass) + { + case FileNamesInformation: + fileInfoSize = FIELD_OFFSET(FILE_NAMES_INFORMATION, FileName); + return gcnew GvDirectoryEnumerationFileNamesResult(static_cast(buffer), bufferLength); + case FileIdExtdDirectoryInformation: + fileInfoSize = FIELD_OFFSET(FILE_ID_EXTD_DIR_INFORMATION, FileName); + return gcnew GvDirectoryEnumerationResultImpl(static_cast(buffer), bufferLength); + case FileIdExtdBothDirectoryInformation: + fileInfoSize = FIELD_OFFSET(FILE_ID_EXTD_BOTH_DIR_INFORMATION, FileName); + return gcnew GvDirectoryEnumerationResultImpl(static_cast(buffer), bufferLength); + default: + throw gcnew GvFltException(StatusCode::StatusInvalidDeviceRequest); + } + } + + inline void SetNextEntryOffset( + _In_ FILE_INFORMATION_CLASS fileInformationClass, + _In_ PVOID buffer, + _In_ ULONG offset) + { + switch (fileInformationClass) + { + case FileNamesInformation: + static_cast(buffer)->NextEntryOffset = offset; + break; + case FileIdExtdDirectoryInformation: + static_cast(buffer)->NextEntryOffset = offset; + break; + case FileIdExtdBothDirectoryInformation: + static_cast(buffer)->NextEntryOffset = offset; + break; + default: + throw gcnew GvFltException(StatusCode::StatusInvalidDeviceRequest);; + } + } + + inline size_t GetRequiredAlignment(_In_ FILE_INFORMATION_CLASS fileInformationClass) + { + switch (fileInformationClass) + { + case FileIdExtdDirectoryInformation: // Could not find offset for FILE_ID_EXTD_DIR_INFORMATION in MSDN, assuming it is 8 for now + case FileIdExtdBothDirectoryInformation: + return 8; + case FileNamesInformation: + return 4; + break; + default: + throw gcnew GvFltException(StatusCode::StatusInvalidDeviceRequest);; + } + } + + inline UCHAR GetPlaceHolderVersion(const GV_PLACEHOLDER_VERSION_INFO& versionInfo) + { + return versionInfo.EpochID[0]; + } + + inline void SetPlaceHolderVersion(GV_PLACEHOLDER_VERSION_INFO& versionInfo, UCHAR version) + { + // Use the first byte of VersionInfo.EpochID to store GVFS's version number for placeholders + versionInfo.EpochID[0] = version; + } + + inline String^ GetContentId(const GV_PLACEHOLDER_VERSION_INFO& versionInfo) + { + return gcnew String( + static_cast(static_cast(const_cast(versionInfo).ContentID))); + } + + inline void SetContentId(GV_PLACEHOLDER_VERSION_INFO& versionInfo, String^ contentId) + { + if (contentId->Length > 0) + { + pin_ptr unmangedContentId = PtrToStringChars(contentId); + memcpy( + versionInfo.ContentID, + unmangedContentId, + min(contentId->Length * sizeof(WCHAR), GV_PLACEHOLDER_ID_LENGTH - sizeof(WCHAR))); + } + } + + inline void SetEpochId(GV_PLACEHOLDER_VERSION_INFO& versionInfo, String^ epochId) + { + if (epochId->Length > 0) + { + pin_ptr unmangedEpochId = PtrToStringChars(epochId); + memcpy( + versionInfo.EpochID + EPOCH_RESERVED_BYTES, + unmangedEpochId, + min(epochId->Length * sizeof(WCHAR), (GV_PLACEHOLDER_ID_LENGTH - sizeof(WCHAR)) - sizeof(UCHAR))); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/GvFltWrapper.h b/GVFS/GVFS.GvFltWrapper/GvFltWrapper.h new file mode 100644 index 00000000..81e31ad1 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvFltWrapper.h @@ -0,0 +1,189 @@ +#pragma once + +#include "GvFltCallbackDelegates.h" +#include "HResult.h" + +namespace GVFSGvFltWrapper +{ + public ref class GvFltWrapper + { + public: + GvFltWrapper(); + + property GvStartDirectoryEnumerationEvent^ OnStartDirectoryEnumeration + { + GvStartDirectoryEnumerationEvent^ get(void); + void set(GvStartDirectoryEnumerationEvent^ eventCB); + }; + + property GvEndDirectoryEnumerationEvent^ OnEndDirectoryEnumeration + { + GvEndDirectoryEnumerationEvent^ get(void); + void set(GvEndDirectoryEnumerationEvent^ eventCB); + }; + + property GvGetDirectoryEnumerationEvent^ OnGetDirectoryEnumeration + { + GvGetDirectoryEnumerationEvent^ get(void); + void set(GvGetDirectoryEnumerationEvent^ eventCB); + }; + + property GvQueryFileNameEvent^ OnQueryFileName + { + GvQueryFileNameEvent^ get(void); + void set(GvQueryFileNameEvent^ eventCB); + } + + property GvGetPlaceHolderInformationEvent^ OnGetPlaceHolderInformation + { + GvGetPlaceHolderInformationEvent^ get(void); + void set(GvGetPlaceHolderInformationEvent^ eventCB); + }; + + property GvGetFileStreamEvent^ OnGetFileStream + { + GvGetFileStreamEvent^ get(void); + void set(GvGetFileStreamEvent^ eventCB); + }; + + property GvNotifyFirstWriteEvent^ OnNotifyFirstWrite + { + GvNotifyFirstWriteEvent^ get(void); + void set(GvNotifyFirstWriteEvent^ eventCB); + }; + + property GvNotifyCreateEvent^ OnNotifyCreate + { + GvNotifyCreateEvent^ get(void); + void set(GvNotifyCreateEvent^ eventCB); + }; + + property GvNotifyPreDeleteEvent^ OnNotifyPreDelete + { + GvNotifyPreDeleteEvent^ get(void); + void set(GvNotifyPreDeleteEvent^ eventCB); + } + + property GvNotifyPreRenameEvent^ OnNotifyPreRename + { + GvNotifyPreRenameEvent^ get(void); + void set(GvNotifyPreRenameEvent^ eventCB); + } + + property GvNotifyPreSetHardlinkEvent^ OnNotifyPreSetHardlink + { + GvNotifyPreSetHardlinkEvent^ get(void); + void set(GvNotifyPreSetHardlinkEvent^ eventCB); + } + + property GvNotifyFileRenamedEvent^ OnNotifyFileRenamed + { + GvNotifyFileRenamedEvent^ get(void); + void set(GvNotifyFileRenamedEvent^ eventCB); + } + + property GvNotifyHardlinkCreatedEvent^ OnNotifyHardlinkCreated + { + GvNotifyHardlinkCreatedEvent^ get(void); + void set(GvNotifyHardlinkCreatedEvent^ eventCB); + } + + property GvNotifyFileHandleClosedEvent^ OnNotifyFileHandleClosed + { + GvNotifyFileHandleClosedEvent^ get(void); + void set(GvNotifyFileHandleClosedEvent^ eventCB); + } + + property GVFS::Common::Tracing::ITracer^ Tracer + { + GVFS::Common::Tracing::ITracer^ get(void); + }; + + HResult GvStartVirtualizationInstance( + GVFS::Common::Tracing::ITracer^ tracerImpl, + System::String^ virtualizationRootPath, + unsigned long poolThreadCount, + unsigned long concurrentThreadCount); + + HResult GvStopVirtualizationInstance(); + + HResult GvDetachDriver(); + + StatusCode GvWriteFile( + System::Guid streamGuid, + GVFltWriteBuffer^ targetBuffer, + unsigned long long byteOffset, + unsigned long length + ); + + StatusCode GvWritePlaceholderInformation( + System::String^ targetRelPathName, + System::DateTime creationTime, + System::DateTime lastAccessTime, + System::DateTime lastWriteTime, + System::DateTime changeTime, + unsigned long fileAttributes, + long long allocationSize, + long long endOfFile, + bool directory, + System::String^ contentId, + System::String^ epochId); + + StatusCode GvCreatePlaceholderAsHardlink( + System::String^ destinationFileName, + System::String^ hardLinkTarget); + + enum class OnDiskStatus : long + { + NotOnDisk = 0, + Partial = 1, + Full = 2 + }; + + // FileExists + // + // Returns: + // OnDiskStatus indicating if the file is not on disk, a partial file, or a full file. + // + // Throws: + // GvFltException + // + // Notes: + // This function cannot be used to determine if a folder is partial or full, and cannot be + // used to determine if a path is a file or a folder. + OnDiskStatus GetFileOnDiskStatus(System::String^ relativePath); + + // ReadFullFileContents + // + // Returns: + // Contents of the specified full file. BOM, if present, is not removed. + // + // Throws: + // - GvFltException + System::String^ ReadFullFileContents(System::String^ relativePath); + + static HResult GvConvertDirectoryToVirtualizationRoot(System::Guid virtualizationInstanceGuid, System::String^ rootPathName); + + private: + void ConfirmNotStarted(); + + GvStartDirectoryEnumerationEvent^ gvStartDirectoryEnumerationEvent; + GvEndDirectoryEnumerationEvent^ gvEndDirectoryEnumerationEvent; + GvGetDirectoryEnumerationEvent^ gvGetDirectoryEnumerationEvent; + GvQueryFileNameEvent^ gvQueryFileNameEvent; + GvGetPlaceHolderInformationEvent^ gvGetPlaceHolderInformationEvent; + GvGetFileStreamEvent^ gvGetFileStreamEvent; + GvNotifyFirstWriteEvent^ gvNotifyFirstWriteEvent; + GvNotifyCreateEvent^ gvNotifyCreateEvent; + GvNotifyPreDeleteEvent^ gvNotifyPreDeleteEvent; + GvNotifyPreRenameEvent^ gvNotifyPreRenameEvent; + GvNotifyPreSetHardlinkEvent^ gvNotifyPreSetHardlinkEvent; + GvNotifyFileRenamedEvent^ gvNotifyFileRenamedEvent; + GvNotifyHardlinkCreatedEvent^ gvNotifyHardlinkCreatedEvent; + GvNotifyFileHandleClosedEvent^ gvNotifyFileHandleClosedEvent; + + GV_VIRTUALIZATIONINSTANCE_HANDLE virtualizationInstanceHandle; + System::String^ virtualRootPath; + GVFS::Common::Tracing::ITracer^ tracer; + }; +} diff --git a/GVFS/GVFS.GvFltWrapper/GvNotificationType.h b/GVFS/GVFS.GvFltWrapper/GvNotificationType.h new file mode 100644 index 00000000..6e0721fb --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/GvNotificationType.h @@ -0,0 +1,17 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + [System::FlagsAttribute] + public enum class GvNotificationType : unsigned long + { + NotificationNone = GV_NOTIFICATION_NONE, + NotificationPostCreate = GV_NOTIFICATION_POST_CREATE, + NotificationPreDelete = GV_NOTIFICATION_PRE_DELETE, + NotificationPreRename = GV_NOTIFICATION_PRE_RENAME, + NotificationPreSetHardlink = GV_NOTIFICATION_PRE_SET_HARDLINK, + NotificationFileRenamed = GV_NOTIFICATION_FILE_RENAMED, + NotificationHardlinkCreated = GV_NOTIFICATION_HARDLINK_CREATED, + NotificationFileHandleClosed = GV_NOTIFICATION_FILE_HANDLE_CLOSED, + }; +} diff --git a/GVFS/GVFS.GvFltWrapper/HResult.h b/GVFS/GVFS.GvFltWrapper/HResult.h new file mode 100644 index 00000000..24bbfde6 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/HResult.h @@ -0,0 +1,22 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + public enum class HResult : long + { + // Subset of HRESULT values. Add more values as needed. + + Ok = S_OK, // Operation successful + Abort = E_ABORT, // Operation aborted + AccessDenied = E_ACCESSDENIED, // General access denied error + Fail = E_FAIL, // Unspecified failure + Handle = E_HANDLE, // Handle that is not valid + InvalidArg = E_INVALIDARG, // One or more arguments are not valid + NoInterface = E_NOINTERFACE, // No such interface supported + NotImpl = E_NOTIMPL, // Not implemented + OutOfMemory = E_OUTOFMEMORY, // Failed to allocate necessary memory + Pointer = E_POINTER, // Pointer that is not valid + Unexpected = E_UNEXPECTED, // Unexpected failure + ReparsePointEncountered = __HRESULT_FROM_WIN32(ERROR_REPARSE_POINT_ENCOUNTERED) // The object manager encountered a reparse point while retrieving an object. + }; +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/NativeEnumerationResultUtils.h b/GVFS/GVFS.GvFltWrapper/NativeEnumerationResultUtils.h new file mode 100644 index 00000000..92910687 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/NativeEnumerationResultUtils.h @@ -0,0 +1,40 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + // PopulateNameInEnumerationData + // + // Populates the specified name in NativeEnumerationDataStruct. If there is not enough free space in the buffer + // for the entire name, the name is truncated and nameTruncated is set to true + // + // Parameters: + // + // enumerationData -> Pointer to the native struct that contains enumeration data + // maxEnumerationDataLength -> Maximum size of enumeration data. This is the total + // available size, and includes both the fixed and variable length + // portions of NativeEnumerationDataStruct + // fileName -> Name that is to be stored in enumeration data + // nameTruncated [out] -> True if the name was truncated, false if it was not + // + // Returns: Total size of enumerationData (includes both fixed length and variable length portions) + template + inline unsigned long PopulateNameInEnumerationData(NativeEnumerationDataStruct* enumerationData, unsigned long maxEnumerationDataLength, System::String^ fileName, bool& nameTruncated) + { + ULONG fileNameOffsetBytes = FIELD_OFFSET(NativeEnumerationDataStruct, FileName); + ULONG maxFileNameLengthBytes = maxEnumerationDataLength < fileNameOffsetBytes ? 0 : maxEnumerationDataLength - fileNameOffsetBytes; + ULONG maxFileNameCharacters = maxFileNameLengthBytes / sizeof(WCHAR); + ULONG numBytesToCopy = min(maxFileNameCharacters, static_cast(fileName->Length)) * sizeof(WCHAR); + + pin_ptr name = PtrToStringChars(fileName); + + // Use memcpy rather than strcpy as tests have shown that strings are NOT null terminated in NativeEnumerationDataStruct + memcpy(enumerationData->FileName, name, numBytesToCopy); + + // FileNameLength is in bytes, not number of characters + enumerationData->FileNameLength = numBytesToCopy; + + nameTruncated = maxFileNameCharacters < static_cast(fileName->Length); + + return (fileNameOffsetBytes + numBytesToCopy); + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/StatusCode.h b/GVFS/GVFS.GvFltWrapper/StatusCode.h new file mode 100644 index 00000000..cfbe1691 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/StatusCode.h @@ -0,0 +1,30 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + public enum class StatusCode : long + { + // Subset of NTSTATUS values. Add more values as needed. + StatusSucccess = STATUS_SUCCESS, + StatusTimeout = STATUS_TIMEOUT, + StatusFileNotAvailable = STATUS_FILE_NOT_AVAILABLE, + StatusUnsuccessful = STATUS_UNSUCCESSFUL, + StatusNotImplemented = STATUS_NOT_IMPLEMENTED, + StatusInvalidHandle = STATUS_INVALID_HANDLE, + StatusInvalidParameter = STATUS_INVALID_PARAMETER, + StatusObjectNameNotFound = STATUS_OBJECT_NAME_NOT_FOUND, + StatusInvalidDeviceRequest = STATUS_INVALID_DEVICE_REQUEST, + StatusEndOfFile = STATUS_END_OF_FILE, + StatusBufferOverflow = STATUS_BUFFER_OVERFLOW, + StatusInternalError = STATUS_INTERNAL_ERROR, + StatusNoMemory = STATUS_NO_MEMORY, + StatusNoMoreFiles = STATUS_NO_MORE_FILES, + StatusNoSuchFile = STATUS_NO_SUCH_FILE, + StatusRequestAborted = STATUS_REQUEST_ABORTED, + StatusAccessDenied = STATUS_ACCESS_DENIED, + StatusNoInterface = STATUS_NOINTERFACE, + StatusDeviceNotReady = STATUS_DEVICE_NOT_READY, + StatusFileClosed = STATUS_FILE_CLOSED, + StatusObjectNameInvalid = STATUS_OBJECT_NAME_INVALID, + }; +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/Stdafx.cpp b/GVFS/GVFS.GvFltWrapper/Stdafx.cpp new file mode 100644 index 00000000..2e96c7ad --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/Stdafx.cpp @@ -0,0 +1,5 @@ +// stdafx.cpp : source file that includes just the standard includes +// GVFS.GvFltWrapper.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/Stdafx.h b/GVFS/GVFS.GvFltWrapper/Stdafx.h new file mode 100644 index 00000000..6a2eebd5 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/Stdafx.h @@ -0,0 +1,199 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, +// but are changed infrequently + +#pragma once + + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include +#define WIN32_NO_STATUS +#include +#include + +#ifndef _NTDEF_ +typedef _Return_type_success_(return >= 0) LONG NTSTATUS; +typedef NTSTATUS *PNTSTATUS; +#endif + +#ifndef _WDMDDK_ +// +// Define the file information class values +// +// WARNING: The order of the following values are assumed by the I/O system. +// Any changes made here should be reflected there as well. +// + +typedef enum _FILE_INFORMATION_CLASS { + FileDirectoryInformation = 1, + FileFullDirectoryInformation, // 2 + FileBothDirectoryInformation, // 3 + FileBasicInformation, // 4 + FileStandardInformation, // 5 + FileInternalInformation, // 6 + FileEaInformation, // 7 + FileAccessInformation, // 8 + FileNameInformation, // 9 + FileRenameInformation, // 10 + FileLinkInformation, // 11 + FileNamesInformation, // 12 + FileDispositionInformation, // 13 + FilePositionInformation, // 14 + FileFullEaInformation, // 15 + FileModeInformation, // 16 + FileAlignmentInformation, // 17 + FileAllInformation, // 18 + FileAllocationInformation, // 19 + FileEndOfFileInformation, // 20 + FileAlternateNameInformation, // 21 + FileStreamInformation, // 22 + FilePipeInformation, // 23 + FilePipeLocalInformation, // 24 + FilePipeRemoteInformation, // 25 + FileMailslotQueryInformation, // 26 + FileMailslotSetInformation, // 27 + FileCompressionInformation, // 28 + FileObjectIdInformation, // 29 + FileCompletionInformation, // 30 + FileMoveClusterInformation, // 31 + FileQuotaInformation, // 32 + FileReparsePointInformation, // 33 + FileNetworkOpenInformation, // 34 + FileAttributeTagInformation, // 35 + FileTrackingInformation, // 36 + FileIdBothDirectoryInformation, // 37 + FileIdFullDirectoryInformation, // 38 + FileValidDataLengthInformation, // 39 + FileShortNameInformation, // 40 + FileIoCompletionNotificationInformation, // 41 + FileIoStatusBlockRangeInformation, // 42 + FileIoPriorityHintInformation, // 43 + FileSfioReserveInformation, // 44 + FileSfioVolumeInformation, // 45 + FileHardLinkInformation, // 46 + FileProcessIdsUsingFileInformation, // 47 + FileNormalizedNameInformation, // 48 + FileNetworkPhysicalNameInformation, // 49 + FileIdGlobalTxDirectoryInformation, // 50 + FileIsRemoteDeviceInformation, // 51 + FileUnusedInformation, // 52 + FileNumaNodeInformation, // 53 + FileStandardLinkInformation, // 54 + FileRemoteProtocolInformation, // 55 + + // + // These are special versions of these operations (defined earlier) + // which can be used by kernel mode drivers only to bypass security + // access checks for Rename and HardLink operations. These operations + // are only recognized by the IOManager, a file system should never + // receive these. + // + FileRenameInformationBypassAccessCheck, // 56 + FileLinkInformationBypassAccessCheck, // 57 + FileVolumeNameInformation, // 58 + FileIdInformation, // 59 + FileIdExtdDirectoryInformation, // 60 + FileReplaceCompletionInformation, // 61 + FileHardLinkFullIdInformation, // 62 + FileIdExtdBothDirectoryInformation, // 63 + FileMaximumInformation +} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS; + +// +// Define the various structures which are returned on query operations +// + +typedef struct _FILE_BASIC_INFORMATION { + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION; + +typedef struct _FILE_STANDARD_INFORMATION { + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG NumberOfLinks; + BOOLEAN DeletePending; + BOOLEAN Directory; +} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION; +#endif + +#ifndef _NTIFS_ + +typedef struct _FILE_NAMES_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; + +typedef struct _FILE_ID_EXTD_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + WCHAR FileName[1]; +} FILE_ID_EXTD_DIR_INFORMATION, *PFILE_ID_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_EXTD_BOTH_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_EXTD_BOTH_DIR_INFORMATION; + +#endif + +#ifndef ERROR_REPARSE_POINT_ENCOUNTERED +// MessageId: ERROR_REPARSE_POINT_ENCOUNTERED +// +// MessageText: +// +// The object manager encountered a reparse point while retrieving an object. +// +#define ERROR_REPARSE_POINT_ENCOUNTERED 4395L +#endif + +#define __HRESULT_FROM_WIN32(x) ((HRESULT)(x) <= 0 ? ((HRESULT)(x)) : ((HRESULT) (((x) & 0x0000FFFF) | (FACILITY_WIN32 << 16) | 0x80000000))) +#define UNREFERENCED_PARAMETER(P) (P) + +// +// Generic test for success on any status value (non-negative numbers +// indicate success). +// + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +#include +#include +#include +#include +#include "gvlib.h" +#include "GvNotificationType.h" + + diff --git a/GVFS/GVFS.GvFltWrapper/Utils.h b/GVFS/GVFS.GvFltWrapper/Utils.h new file mode 100644 index 00000000..93b5264f --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/Utils.h @@ -0,0 +1,70 @@ +#pragma once + +namespace GVFSGvFltWrapper +{ + inline System::Guid GUIDtoGuid(const GUID& guid) + { + return System::Guid( + guid.Data1, + guid.Data2, + guid.Data3, + guid.Data4[0], + guid.Data4[1], + guid.Data4[2], + guid.Data4[3], + guid.Data4[4], + guid.Data4[5], + guid.Data4[6], + guid.Data4[7]); + } + + inline NTSTATUS Win32ErrorToNtStatus(int win32Error) + { + // Mapping is a combination of ToNTStatus in the GVFlt test app, and + // mapping provided at https://support.microsoft.com/en-us/kb/113996. + // Note that mapping on microsoft.com is not 1:1. When a single win32error + // can map to multiple NTSTATUS values, the more general NTSTATUS value is + // returned. + switch (win32Error) + { + case ERROR_INVALID_PARAMETER: + return STATUS_INVALID_PARAMETER; + case ERROR_FILE_NOT_FOUND: + return STATUS_OBJECT_NAME_NOT_FOUND; + case ERROR_ACCESS_DENIED: + return STATUS_ACCESS_DENIED; + case ERROR_NOACCESS: + return STATUS_ACCESS_VIOLATION; + case ERROR_NOT_LOCKED: + return STATUS_NOT_LOCKED; + case ERROR_BAD_LENGTH: + return STATUS_INFO_LENGTH_MISMATCH; + case ERROR_STACK_OVERFLOW: + return STATUS_STACK_OVERFLOW; + case ERROR_PROC_NOT_FOUND: + return STATUS_ENTRYPOINT_NOT_FOUND; + case ERROR_IO_PENDING: + return STATUS_PENDING; + case ERROR_MORE_DATA: + return STATUS_MORE_ENTRIES; + case ERROR_ARITHMETIC_OVERFLOW: + return STATUS_INTEGER_OVERFLOW; + case ERROR_NO_MORE_ITEMS: + return STATUS_NO_MORE_ENTRIES; + case ERROR_INVALID_HANDLE: + return STATUS_INVALID_HANDLE; + case ERROR_PATH_NOT_FOUND: + return STATUS_OBJECT_PATH_NOT_FOUND; + case ERROR_DISK_FULL: + return STATUS_DISK_FULL; + case ERROR_DIRECTORY: + return STATUS_NOT_A_DIRECTORY; + case ERROR_FILE_INVALID: + return STATUS_FILE_INVALID; + case ERROR_IO_DEVICE: + return STATUS_IO_DEVICE_ERROR; + default: + return STATUS_INTERNAL_ERROR; + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/Version.rc b/GVFS/GVFS.GvFltWrapper/Version.rc new file mode 100644 index 00000000..e7d0cf54 Binary files /dev/null and b/GVFS/GVFS.GvFltWrapper/Version.rc differ diff --git a/GVFS/GVFS.GvFltWrapper/packages.config b/GVFS/GVFS.GvFltWrapper/packages.config new file mode 100644 index 00000000..f6d82e53 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/GVFS/GVFS.GvFltWrapper/resource.h b/GVFS/GVFS.GvFltWrapper/resource.h new file mode 100644 index 00000000..c1b5c159 --- /dev/null +++ b/GVFS/GVFS.GvFltWrapper/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Version.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/GVFS/GVFS.Hooks/App.config b/GVFS/GVFS.Hooks/App.config new file mode 100644 index 00000000..d740e886 --- /dev/null +++ b/GVFS/GVFS.Hooks/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj new file mode 100644 index 00000000..4ace9b46 --- /dev/null +++ b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj @@ -0,0 +1,94 @@ + + + + + Debug + AnyCPU + {BDA91EE5-C684-4FC5-A90A-B7D677421917} + Exe + Properties + GVFS.Hooks + GVFS.Hooks + v4.5.2 + 512 + true + + + + + true + ..\..\..\BuildOutput\GVFS.Hooks\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.Hooks\obj\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + ..\..\..\BuildOutput\GVFS.Hooks\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.Hooks\obj\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + + False + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + GVFS.Common + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Hooks/KnownGitCommands.cs b/GVFS/GVFS.Hooks/KnownGitCommands.cs new file mode 100644 index 00000000..cbf9f216 --- /dev/null +++ b/GVFS/GVFS.Hooks/KnownGitCommands.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; + +namespace GVFS.Hooks +{ + internal static class KnownGitCommands + { + private static HashSet knownCommands = new HashSet() + { + "add", + "am", + "annotate", + "apply", + "archive", + "bisect--helper", + "blame", + "branch", + "bundle", + "cat-file", + "check-attr", + "check-ignore", + "check-mailmap", + "check-ref-format", + "checkout", + "checkout-index", + "cherry", + "cherry-pick", + "clean", + "clone", + "column", + "commit", + "commit-tree", + "config", + "count-objects", + "credential", + "describe", + "diff", + "diff-files", + "diff-index", + "diff-tree", + "fast-export", + "fetch", + "fetch-pack", + "fmt-merge-msg", + "for-each-ref", + "format-patch", + "fsck", + "fsck-objects", + "gc", + "get-tar-commit-id", + "grep", + "hash-object", + "help", + "index-pack", + "init", + "init-db", + "interpret-trailers", + "log", + "ls-files", + "ls-remote", + "ls-tree", + "mailinfo", + "mailsplit", + "merge", + "merge-base", + "merge-file", + "merge-index", + "merge-ours", + "merge-recursive", + "merge-recursive-ours", + "merge-recursive-theirs", + "merge-subtree", + "merge-tree", + "mktag", + "mktree", + "mv", + "name-rev", + "notes", + "pack-objects", + "pack-redundant", + "pack-refs", + "patch-id", + "pickaxe", + "prune", + "prune-packed", + "pull", + "push", + "read-tree", + "rebase", + "rebase--helper", + "receive-pack", + "reflog", + "remote", + "remote-ext", + "remote-fd", + "repack", + "replace", + "rerere", + "reset", + "rev-list", + "rev-parse", + "revert", + "rm", + "send-pack", + "shortlog", + "show", + "show-branch", + "show-ref", + "stage", + "status", + "stripspace", + "symbolic-ref", + "tag", + "unpack-file", + "unpack-objects", + "update-index", + "update-ref", + "update-server-info", + "upload-archive", + "upload-archive--writer", + "var", + "verify-commit", + "verify-pack", + "verify-tag", + "version", + "whatchanged", + "worktree", + "write-tree", + + // Externals + "bisect", + "filter-branch", + "gui", + "merge-octopus", + "merge-one-file", + "merge-resolve", + "mergetool", + "parse-remote", + "quiltimport", + "rebase", + "submodule", + }; + + public static bool Contains(string gitCommand) + { + return knownCommands.Contains(gitCommand); + } + } +} diff --git a/GVFS/GVFS.Hooks/Program.cs b/GVFS/GVFS.Hooks/Program.cs new file mode 100644 index 00000000..1e2435dd --- /dev/null +++ b/GVFS/GVFS.Hooks/Program.cs @@ -0,0 +1,366 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace GVFS.Hooks +{ + public class Program + { + private const string PrecommandHook = "pre-command"; + + // Deprecated - keep to support old clones + private const string PostcommandHook = "post-command"; + + private const string GitLockWaitArgName = "--internal-gitlock-waittime-ms"; + private static JsonEtwTracer tracer; + + private static Dictionary specialArgValues = new Dictionary(); + + public static void Main(string[] args) + { + args = ReadAndRemoveSpecialArgValues(args); + + using (tracer = new JsonEtwTracer(GVFSConstants.GVFSEtwProviderName, "GVFS.Hooks")) + { + tracer.WriteStartEvent( + null, + null, + null, + new EventMetadata + { + { "Args", string.Join(" ", args) }, + }); + + try + { + if (args.Length < 2) + { + ExitWithError("Usage: gvfs.hooks []"); + } + + switch (GetHookType(args)) + { + case PrecommandHook: + CheckForLegalCommands(args); + RunPreCommands(args); + AcquireGlobalLock(args); + break; + + case PostcommandHook: + // no-op - keep this handling to support old clones that had the + // post-command hook installed. + break; + + default: + ExitWithError("Unrecognized hook: " + string.Join(" ", args)); + break; + } + } + catch (Exception ex) + { + ExitWithError("Unexpected exception: " + ex.ToString()); + } + } + } + + private static void RunPreCommands(string[] args) + { + string command = GetGitCommand(args); + switch (command) + { + case "fetch": + case "pull": + ProcessHelper.Run("gvfs", "prefetch --commits", redirectOutput: false); + break; + } + } + + private static string[] ReadAndRemoveSpecialArgValues(string[] args) + { + string waitArgValue; + if (TryRemoveArg(ref args, GitLockWaitArgName, out waitArgValue)) + { + specialArgValues.Add(GitLockWaitArgName, waitArgValue); + } + + return args; + } + + private static void ExitWithError(params string[] messages) + { + foreach (string message in messages) + { + Console.Error.WriteLine(message); + } + + tracer.RelatedError(string.Join("\r\n", messages)); + Environment.Exit(1); + } + + private static void CheckForLegalCommands(string[] args) + { + string command = GetGitCommand(args); + switch (command) + { + case "update-index": + if (ContainsArg(args, "--split-index") || + ContainsArg(args, "--no-split-index")) + { + ExitWithError("Split index is not supported on a GVFS repo"); + } + + break; + + case "fsck": + case "gc": + case "repack": + ExitWithError("'git " + command + "' is not supported on a GVFS repo"); + break; + + case "submodule": + ExitWithError("Submodule operations are not supported on a GVFS repo"); + break; + } + } + + private static void AcquireGlobalLock(string[] args) + { + try + { + if (ShouldLock(args)) + { + GVFSEnlistment enlistment = GVFSEnlistment.CreateFromCurrentDirectory(null, GitProcess.GetInstalledGitBinPath()); + if (enlistment == null) + { + ExitWithError("This hook must be run from a GVFS repo"); + } + + if (EnlistmentIsReady(enlistment)) + { + string fullCommand = "git " + string.Join(" ", args.Skip(1)); + int pid = ProcessHelper.GetParentProcessId("git.exe"); + + Process parentProcess = null; + if (pid == GVFSConstants.InvalidProcessId || + !ProcessHelper.TryGetProcess(pid, out parentProcess)) + { + ExitWithError("GVFS.Hooks: Unable to find parent git.exe process " + "(PID: " + pid + ")."); + } + + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + ExitWithError("The enlistment does not appear to be mounted. Use 'gvfs status' to check."); + } + + NamedPipeMessages.AcquireLock.Request request = + new NamedPipeMessages.AcquireLock.Request(pid, fullCommand, ProcessHelper.GetCommandLine(parentProcess)); + + NamedPipeMessages.Message requestMessage = request.CreateMessage(); + pipeClient.SendRequest(requestMessage); + + NamedPipeMessages.AcquireLock.Response response = new NamedPipeMessages.AcquireLock.Response(pipeClient.ReadResponse()); + + if (response.Result == NamedPipeMessages.AcquireLock.AcceptResult) + { + return; + } + else if (response.Result == NamedPipeMessages.AcquireLock.MountNotReadyResult) + { + ExitWithError("GVFS has not finished initializing, please wait a few seconds and try again."); + } + else + { + int retries = 0; + char[] waiting = { '\u2014', '\\', '|', '/' }; + string message = string.Empty; + while (true) + { + if (response.Result == NamedPipeMessages.AcquireLock.AcceptResult) + { + if (!Console.IsOutputRedirected) + { + Console.WriteLine("\r{0}...", message); + } + + return; + } + else if (response.Result == NamedPipeMessages.AcquireLock.DenyGVFSResult) + { + message = "Waiting for GVFS to release the lock"; + } + else if (response.Result == NamedPipeMessages.AcquireLock.DenyGitResult) + { + message = string.Format("Waiting for '{0}' to release the lock", response.ResponseData.ParsedCommand); + } + else + { + ExitWithError("Error when acquiring the lock. Unrecognized response: " + response.CreateMessage()); + tracer.RelatedError("Unknown LockRequestResponse: " + response); + } + + if (Console.IsOutputRedirected && retries == 0) + { + Console.WriteLine("{0}...", message); + } + else if (!Console.IsOutputRedirected) + { + Console.Write("\r{0}..{1}", message, waiting[retries % waiting.Length]); + } + + Thread.Sleep(500); + + pipeClient.SendRequest(requestMessage); + response = new NamedPipeMessages.AcquireLock.Response(pipeClient.ReadResponse()); + retries++; + } + } + } + } + } + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Error", e.ToString()); + tracer.RelatedError(metadata); + + ExitWithError( + "Unable to initialize Git command.", + "Ensure that GVFS is running."); + } + } + + private static bool EnlistmentIsReady(GVFSEnlistment enlistment) + { + bool enlistmentReady = false; + try + { + enlistmentReady = !enlistment.EnlistmentMutex.WaitOne(1); + if (!enlistmentReady) + { + enlistment.EnlistmentMutex.ReleaseMutex(); + } + } + catch (AbandonedMutexException) + { + enlistmentReady = false; + } + + return enlistmentReady; + } + + private static bool TryRemoveArg(ref string[] args, string argName, out string output) + { + output = null; + int argIdx = Array.IndexOf(args, argName); + if (argIdx >= 0) + { + if (argIdx + 1 < args.Length) + { + output = args[argIdx + 1]; + args = args.Take(argIdx).Concat(args.Skip(argIdx + 2)).ToArray(); + return true; + } + else + { + ExitWithError("Missing value for {0}.", argName); + } + } + + return false; + } + + private static bool ShouldLock(string[] args) + { + string gitCommand = GetGitCommand(args); + + switch (gitCommand) + { + // Keep these alphabetically sorted + case "cat-file": + case "config": + case "credential": + case "diff": + case "diff-tree": + case "for-each-ref": + case "help": + case "index-pack": + case "log": + case "ls-tree": + case "mv": + case "name-rev": + case "push": + case "remote": + case "rev-list": + case "rev-parse": + case "show": + case "unpack-objects": + case "version": + case "web--browse": + return false; + } + + if (gitCommand == "reset" && + !args.Contains("--hard") && + !args.Contains("--merge") && + !args.Contains("--keep")) + { + return false; + } + + // Don't acquire the lock if we've been explicitly asked not to. This enables tools, such as the VS Git + // integration, to provide a "best effort" status without writing to the index. We assume that any such + // tools will be constantly polling in the background, so missing a file once isn't a problem. + if (gitCommand == "status" && + args.Contains("--no-lock-index")) + { + return false; + } + + if (!KnownGitCommands.Contains(gitCommand) && + IsAlias(gitCommand)) + { + return false; + } + + return true; + } + + private static bool ContainsArg(string[] actualArgs, string expectedArg) + { + return actualArgs.Contains(expectedArg, StringComparer.OrdinalIgnoreCase); + } + + private static string GetHookType(string[] args) + { + return args[0].ToLowerInvariant(); + } + + private static string GetGitCommand(string[] args) + { + string command = args[1].ToLowerInvariant(); + if (command.StartsWith("git-")) + { + command = command.Substring(4); + } + + return command; + } + + private static bool IsAlias(string command) + { + ProcessResult result = ProcessHelper.Run("git", "config --get alias." + command); + + return !string.IsNullOrEmpty(result.Output); + } + } +} diff --git a/GVFS/GVFS.Hooks/Properties/AssemblyInfo.cs b/GVFS/GVFS.Hooks/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..00ab2853 --- /dev/null +++ b/GVFS/GVFS.Hooks/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.Hooks")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.Hooks")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bda91ee5-c684-4fc5-a90a-b7d677421917")] diff --git a/GVFS/GVFS.Hooks/packages.config b/GVFS/GVFS.Hooks/packages.config new file mode 100644 index 00000000..5d95b04e --- /dev/null +++ b/GVFS/GVFS.Hooks/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Mount/GVFS.Mount.csproj b/GVFS/GVFS.Mount/GVFS.Mount.csproj new file mode 100644 index 00000000..de74c02b --- /dev/null +++ b/GVFS/GVFS.Mount/GVFS.Mount.csproj @@ -0,0 +1,110 @@ + + + + + Debug + AnyCPU + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} + Exe + Properties + GVFS.Mount + GVFS.Mount + v4.5.2 + 512 + true + + + + + x64 + true + full + false + ..\..\..\BuildOutput\GVFS.Mount\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.Mount\obj\x64\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + true + ..\..\..\BuildOutput\GVFS.Mount\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.Mount\obj\x64\Release\ + TRACE + prompt + 4 + true + + + + ..\..\..\packages\CommandLineParser.2.0.275-beta\lib\net45\CommandLine.dll + True + + + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + False + ..\..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + GVFS.Common + + + {1118b427-7063-422f-83b9-5023c8ec5a7a} + GVFS.GVFlt + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + xcopy /Y $(SolutionDir)..\BuildOutput\GVFS.ReadObjectHook\bin\$(Platform)\$(Configuration)\GVFS.ReadObjectHook.* $(TargetDir) + + + \ No newline at end of file diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs new file mode 100644 index 00000000..3c172b60 --- /dev/null +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -0,0 +1,494 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.NamedPipes; +using GVFS.Common.Physical; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using GVFS.GVFlt; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; + +namespace GVFS.Mount +{ + public class InProcessMount + { + // Tests show that 250 is the max supported pipe name length + private const int MaxPipeNameLength = 250; + private const int MutexMaxWaitTimeMS = 500; + + private readonly bool showDebugWindow; + + private GVFltCallbacks gvfltCallbacks; + private GVFSEnlistment enlistment; + private ITracer tracer; + private GVFSGitObjects gitObjects; + private GVFSLock gvfsLock; + + private MountState currentState; + private HeartbeatThread heartbeat; + + private List folderLockHandles; + + public InProcessMount(ITracer tracer, GVFSEnlistment enlistment, bool showDebugWindow) + { + this.tracer = tracer; + this.enlistment = enlistment; + this.showDebugWindow = showDebugWindow; + } + + private enum MountState + { + Invalid = 0, + + Mounting, + Ready, + Unmounting, + MountFailed + } + + public void Mount(EventLevel verbosity, Keywords keywords) + { + this.currentState = MountState.Mounting; + if (Environment.CurrentDirectory != this.enlistment.EnlistmentRoot) + { + Environment.CurrentDirectory = this.enlistment.EnlistmentRoot; + } + + this.StartNamedPipe(); + this.AcquireRepoMutex(); + + // Checking the disk layout version is done before this point in GVFS.CommandLine.MountVerb.PreExecute + using (RepoMetadata repoMetadata = new RepoMetadata(this.enlistment.DotGVFSRoot)) + { + repoMetadata.SaveCurrentDiskLayoutVersion(); + } + + GVFSContext context = this.CreateContext(); + + this.ValidateMountPoints(); + this.UpdateHooks(); + + this.gvfsLock = context.Repository.GVFSLock; + this.MountAndStartWorkingDirectoryCallbacks(context); + + Console.Title = "GVFS " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.EnlistmentRoot; + + this.tracer.RelatedEvent( + EventLevel.Critical, + "Mount", + new EventMetadata + { + { "Message", "Virtual repo is ready" }, + }); + + this.currentState = MountState.Ready; + } + + private GVFSContext CreateContext() + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + string indexPath = Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Index); + string indexLockPath = Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Index + GVFSConstants.DotGit.LockExtension); + GitRepo gitRepo = this.CreateOrReportAndExit( + () => new GitRepo( + this.tracer, + this.enlistment, + fileSystem, + new GitIndex(this.tracer, this.enlistment, indexPath, indexLockPath)), + "Failed to read git repo"); + return new GVFSContext(this.tracer, fileSystem, gitRepo, this.enlistment); + } + + private void ValidateMountPoints() + { + DirectoryInfo workingDirectoryRootInfo = new DirectoryInfo(this.enlistment.WorkingDirectoryRoot); + if (!workingDirectoryRootInfo.Exists) + { + this.FailMountAndExit("Failed to initialize file system callbacks. Directory \"{0}\" must exist.", this.enlistment.WorkingDirectoryRoot); + } + + string dotGitPath = Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Root); + DirectoryInfo dotGitPathInfo = new DirectoryInfo(dotGitPath); + if (!dotGitPathInfo.Exists) + { + this.FailMountAndExit("Failed to mount. Directory \"{0}\" must exist.", dotGitPathInfo); + } + } + + private void UpdateHooks() + { + bool copyReadObjectHook = false; + string enlistmentReadObjectHookPath = Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Hooks.ReadObjectPath + ".exe"); + string installedReadObjectHookPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), GVFSConstants.GVFSReadObjectHookExecutableName); + + if (!File.Exists(installedReadObjectHookPath)) + { + this.FailMountAndExit(GVFSConstants.GVFSReadObjectHookExecutableName + " cannot be found at {0}", installedReadObjectHookPath); + } + + if (!File.Exists(enlistmentReadObjectHookPath)) + { + copyReadObjectHook = true; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add("enlistmentReadObjectHookPath", enlistmentReadObjectHookPath); + metadata.Add("installedReadObjectHookPath", installedReadObjectHookPath); + metadata.Add("Message", GVFSConstants.DotGit.Hooks.ReadObjectName + " not found in enlistment, copying from installation folder"); + this.tracer.RelatedEvent(EventLevel.Warning, "ReadObjectMissingFromEnlistment", metadata); + } + else + { + try + { + FileVersionInfo enlistmentVersion = FileVersionInfo.GetVersionInfo(enlistmentReadObjectHookPath); + FileVersionInfo installedVersion = FileVersionInfo.GetVersionInfo(installedReadObjectHookPath); + copyReadObjectHook = enlistmentVersion.FileVersion != installedVersion.FileVersion; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add("enlistmentReadObjectHookPath", enlistmentReadObjectHookPath); + metadata.Add("installedReadObjectHookPath", installedReadObjectHookPath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Failed to compare " + GVFSConstants.DotGit.Hooks.ReadObjectName + " version"); + this.tracer.RelatedError(metadata); + this.FailMountAndExit("Error comparing " + GVFSConstants.DotGit.Hooks.ReadObjectName + " versions, see log file for details"); + } + } + + if (copyReadObjectHook) + { + try + { + File.Copy(installedReadObjectHookPath, enlistmentReadObjectHookPath, overwrite: true); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add("enlistmentReadObjectHookPath", enlistmentReadObjectHookPath); + metadata.Add("installedReadObjectHookPath", installedReadObjectHookPath); + metadata.Add("Exception", e.ToString()); + metadata.Add("ErrorMessage", "Failed to copy " + GVFSConstants.DotGit.Hooks.ReadObjectName + " to enlistment"); + this.tracer.RelatedError(metadata); + this.FailMountAndExit("Error copying " + GVFSConstants.DotGit.Hooks.ReadObjectName + " to enlistment, see log file for details"); + } + } + } + + private void StartNamedPipe() + { + if (this.enlistment.NamedPipeName.Length > MaxPipeNameLength) + { + this.FailMountAndExit("Failed to create mount point. Mount path exceeds the maximum number of allowed characters"); + } + + NamedPipeServer pipeServer = new NamedPipeServer(this.enlistment.NamedPipeName, this.HandleConnection); + pipeServer.Start(); + } + + private void FailMountAndExit(string error, params object[] args) + { + this.currentState = MountState.MountFailed; + + this.tracer.RelatedError(error, args); + if (this.showDebugWindow) + { + Console.WriteLine("\nPress Enter to Exit"); + Console.ReadLine(); + } + + Environment.Exit((int)ReturnCode.GenericError); + } + + private void AcquireRepoMutex() + { + bool mutexAcquired = false; + + try + { + if (this.enlistment.EnlistmentMutex.WaitOne(MutexMaxWaitTimeMS)) + { + mutexAcquired = true; + } + } + catch (AbandonedMutexException) + { + // "The exception that is thrown when one thread acquires a Mutex object that another thread has abandoned by exiting without releasing it" + // "The next thread to request ownership of the mutex can handle this exception and proceed" + // https://msdn.microsoft.com/en-us/library/system.threading.abandonedmutexexception(v=vs.110).aspx + // + // If we catch AbandonedMutexException here it means that a previous instance of GVFS for this repo was not shut down gracefully. + // Return true as catching this exception means that we have now acquired the mutex. + mutexAcquired = true; + } + catch (Exception) + { + this.FailMountAndExit("Error: Failed to determine if repo is already mounted."); + } + + if (!mutexAcquired) + { + this.FailMountAndExit("Error: GVFS is already mounted for this repo"); + } + } + + private T CreateOrReportAndExit(Func factory, string reportMessage) + { + try + { + return factory(); + } + catch (Exception e) + { + this.FailMountAndExit(reportMessage + " " + e.ToString()); + throw; + } + } + + private void HandleConnection(NamedPipeServer.Connection connection) + { + while (connection.IsConnected) + { + string request = connection.ReadRequest(); + + if (request == null || + !connection.IsConnected) + { + break; + } + + NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); + + switch (message.Header) + { + case NamedPipeMessages.GetStatus.Request: + this.HandleGetStatusRequest(connection); + break; + + case NamedPipeMessages.Unmount.Request: + this.HandleUnmountRequest(connection); + break; + + case NamedPipeMessages.AcquireLock.AcquireRequest: + this.HandleLockRequest(connection, message); + break; + + case NamedPipeMessages.DownloadObject.DownloadRequest: + this.HandleDownloadObjectRequest(connection, message); + break; + + default: + connection.TrySendResponse(NamedPipeMessages.UnknownRequest); + break; + } + } + } + + private void HandleLockRequest(NamedPipeServer.Connection connection, NamedPipeMessages.Message message) + { + NamedPipeMessages.AcquireLock.Response response; + NamedPipeMessages.AcquireLock.Data externalHolder; + + NamedPipeMessages.AcquireLock.Request request = new NamedPipeMessages.AcquireLock.Request(message); + NamedPipeMessages.AcquireLock.Data requester = request.RequestData; + if (request == null) + { + response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.UnknownRequest, requester); + } + else if (this.currentState != MountState.Ready) + { + response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.MountNotReadyResult); + } + else + { + bool lockAcquired = this.gvfsLock.TryAcquireLock(requester, out externalHolder); + + if (lockAcquired) + { + response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.AcceptResult); + } + else if (externalHolder == null) + { + response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.DenyGVFSResult); + } + else + { + response = new NamedPipeMessages.AcquireLock.Response(NamedPipeMessages.AcquireLock.DenyGitResult, externalHolder); + } + } + + connection.TrySendResponse(response.CreateMessage()); + } + + private void HandleDownloadObjectRequest(NamedPipeServer.Connection connection, NamedPipeMessages.Message message) + { + NamedPipeMessages.DownloadObject.Response response; + + NamedPipeMessages.DownloadObject.Request request = new NamedPipeMessages.DownloadObject.Request(message); + string objectSha = request.RequestSha; + if (request == null) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.UnknownRequest); + } + else if (this.currentState != MountState.Ready) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.MountNotReadyResult); + } + else + { + if (!GitHelper.IsValidFullSHA(objectSha)) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.InvalidSHAResult); + } + else + { + if (this.gitObjects.TryDownloadAndSaveObject(objectSha.Substring(0, 2), objectSha.Substring(2))) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); + } + else + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed); + } + } + } + + connection.TrySendResponse(response.CreateMessage()); + } + + private void HandleGetStatusRequest(NamedPipeServer.Connection connection) + { + NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response(); + response.EnlistmentRoot = this.enlistment.EnlistmentRoot; + response.RepoUrl = this.enlistment.RepoUrl; + response.ObjectsUrl = this.enlistment.ObjectsEndpointUrl; + response.LockStatus = this.gvfsLock != null ? this.gvfsLock.GetStatus() : "Unavailable"; + response.DiskLayoutVersion = RepoMetadata.GetCurrentDiskLayoutVersion(); + + switch (this.currentState) + { + case MountState.Mounting: + response.MountStatus = NamedPipeMessages.GetStatus.Mounting; + break; + + case MountState.Ready: + response.MountStatus = NamedPipeMessages.GetStatus.Ready; + response.BackgroundOperationCount = this.gvfltCallbacks.GetBackgroundOperationCount(); + break; + + case MountState.Unmounting: + response.MountStatus = NamedPipeMessages.GetStatus.Unmounting; + break; + + case MountState.MountFailed: + response.MountStatus = NamedPipeMessages.GetStatus.MountFailed; + break; + + default: + response.MountStatus = NamedPipeMessages.UnknownGVFSState; + break; + } + + connection.TrySendResponse(response.ToJson()); + } + + private void HandleUnmountRequest(NamedPipeServer.Connection connection) + { + switch (this.currentState) + { + case MountState.Mounting: + connection.TrySendResponse(NamedPipeMessages.Unmount.NotMounted); + break; + + // Even if the previous mount failed, attempt to unmount anyway. Otherwise the user has no + // recourse but to kill the process. + case MountState.MountFailed: + goto case MountState.Ready; + + case MountState.Ready: + this.currentState = MountState.Unmounting; + + connection.TrySendResponse(NamedPipeMessages.Unmount.Acknowledged); + this.UnmountAndStopWorkingDirectoryCallbacks(); + connection.TrySendResponse(NamedPipeMessages.Unmount.Completed); + + Environment.Exit((int)ReturnCode.Success); + break; + + case MountState.Unmounting: + connection.TrySendResponse(NamedPipeMessages.Unmount.AlreadyUnmounting); + break; + + default: + connection.TrySendResponse(NamedPipeMessages.UnknownGVFSState); + break; + } + } + + private void AcquireFolderLocks(GVFSContext context) + { + this.folderLockHandles = new List(); + this.folderLockHandles.Add(context.FileSystem.LockDirectory(context.Enlistment.DotGVFSRoot)); + } + + private void ReleaseFolderLocks() + { + foreach (SafeFileHandle folderHandle in this.folderLockHandles) + { + folderHandle.Dispose(); + } + } + + private void MountAndStartWorkingDirectoryCallbacks(GVFSContext context) + { + HttpGitObjects httpGitObjects = new HttpGitObjects(context.Tracer, context.Enlistment, Environment.ProcessorCount); + if (!httpGitObjects.TryRefreshCredentials()) + { + this.FailMountAndExit("Failed to obtain git credentials"); + } + + this.gitObjects = new GVFSGitObjects(context, httpGitObjects); + this.gvfltCallbacks = this.CreateOrReportAndExit(() => new GVFltCallbacks(context, this.gitObjects), "Failed to create src folder callbacks"); + + try + { + string error; + if (!this.gvfltCallbacks.TryStart(out error)) + { + this.FailMountAndExit("Error: {0}. \r\nPlease confirm that gvfs clone completed without error.", error); + } + } + catch (Exception e) + { + this.FailMountAndExit("Failed to initialize src folder callbacks. {0}", e.ToString()); + } + + this.AcquireFolderLocks(context); + + this.heartbeat = new HeartbeatThread(this.tracer); + this.heartbeat.Start(); + } + + private void UnmountAndStopWorkingDirectoryCallbacks() + { + this.ReleaseFolderLocks(); + + if (this.gvfltCallbacks != null) + { + this.gvfltCallbacks.Stop(); + this.gvfltCallbacks.Dispose(); + this.gvfltCallbacks = null; + } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Mount/MountAbortedException.cs b/GVFS/GVFS.Mount/MountAbortedException.cs new file mode 100644 index 00000000..3ab5153c --- /dev/null +++ b/GVFS/GVFS.Mount/MountAbortedException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Mount +{ + public class MountAbortedException : Exception + { + public MountAbortedException(MountVerb verb) + { + this.Verb = verb; + } + + public MountVerb Verb { get; } + } +} diff --git a/GVFS/GVFS.Mount/MountVerb.cs b/GVFS/GVFS.Mount/MountVerb.cs new file mode 100644 index 00000000..00d22775 --- /dev/null +++ b/GVFS/GVFS.Mount/MountVerb.cs @@ -0,0 +1,160 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.IO; +using System.Threading; + +namespace GVFS.Mount +{ + [Verb("mount", HelpText = "Mount a GVFS virtual repo")] + public class MountVerb + { + private TextWriter output; + + public MountVerb() + { + this.output = Console.Out; + this.ReturnCode = ReturnCode.Success; + + this.InitializeDefaultParameterValues(); + } + + public ReturnCode ReturnCode { get; private set; } + + [Option( + 'v', + MountParameters.Verbosity, + Default = MountParameters.DefaultVerbosity, + Required = false, + HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] + public string Verbosity { get; set; } + + [Option( + 'k', + MountParameters.Keywords, + Default = MountParameters.DefaultKeywords, + Required = false, + HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] + public string KeywordsCsv { get; set; } + + [Option( + 'd', + MountParameters.DebugWindow, + Default = false, + Required = false, + HelpText = "Show the debug window. By default, all output is written to a log file and no debug window is shown.")] + public bool ShowDebugWindow { get; set; } + + [Value( + 0, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the GVFS enlistment root")] + public string EnlistmentRootPath { get; set; } + + public void InitializeDefaultParameterValues() + { + this.Verbosity = MountParameters.DefaultVerbosity; + this.KeywordsCsv = MountParameters.DefaultKeywords; + } + + public void Execute() + { + GVFSEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPath); + + EventLevel verbosity; + Keywords keywords; + this.ParseEnumArgs(out verbosity, out keywords); + + ITracer tracer = this.CreateTracer(enlistment, verbosity, keywords); + InProcessMount mountHelper = new InProcessMount(tracer, enlistment, this.ShowDebugWindow); + mountHelper.Mount(verbosity, keywords); + } + + private ITracer CreateTracer(GVFSEnlistment enlistment, EventLevel verbosity, Keywords keywords) + { + JsonEtwTracer tracer = new JsonEtwTracer(GVFSConstants.GVFSEtwProviderName, "GVFSMount"); + tracer.AddLogFileEventListener(GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot), verbosity, keywords); + if (this.ShowDebugWindow) + { + tracer.AddConsoleEventListener(verbosity, keywords); + } + + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + enlistment.CacheServerUrl); + return tracer; + } + + private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + { + if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + { + this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); + } + + if (!Enum.TryParse(this.Verbosity, out verbosity)) + { + this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); + } + } + + private GVFSEnlistment CreateEnlistment(string enlistmentRootPath) + { + string gitBinPath = GitProcess.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + this.ReportErrorAndExit("Error: " + GVFSConstants.GitIsNotInstalledError); + } + + if (string.IsNullOrWhiteSpace(enlistmentRootPath)) + { + enlistmentRootPath = Environment.CurrentDirectory; + } + + string hooksPath = ProcessHelper.WhereDirectory(GVFSConstants.GVFSHooksExecutableName); + if (hooksPath == null) + { + this.ReportErrorAndExit("Could not find " + GVFSConstants.GVFSHooksExecutableName); + } + + GVFSEnlistment enlistment = null; + try + { + enlistment = GVFSEnlistment.CreateFromDirectory(enlistmentRootPath, null, gitBinPath, hooksPath); + if (enlistment == null) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid GVFS enlistment", + enlistmentRootPath); + } + } + catch (InvalidRepoException e) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid GVFS enlistment. {1}", + enlistmentRootPath, + e.Message); + } + + return enlistment; + } + + private void ReportErrorAndExit(string error, params object[] args) + { + if (error != null) + { + this.output.WriteLine(error, args); + } + + this.ReturnCode = ReturnCode.GenericError; + throw new MountAbortedException(this); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Mount/Program.cs b/GVFS/GVFS.Mount/Program.cs new file mode 100644 index 00000000..4b868a4c --- /dev/null +++ b/GVFS/GVFS.Mount/Program.cs @@ -0,0 +1,22 @@ +using CommandLine; +using System; + +namespace GVFS.Mount +{ + public class Program + { + public static void Main(string[] args) + { + try + { + Parser.Default.ParseArguments(args) + .WithParsed(mount => mount.Execute()); + } + catch (MountAbortedException e) + { + // Calling Environment.Exit() is required, to force all background threads to exit as well + Environment.Exit((int)e.Verb.ReturnCode); + } + } + } +} diff --git a/GVFS/GVFS.Mount/Properties/AssemblyInfo.cs b/GVFS/GVFS.Mount/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..98b5d914 --- /dev/null +++ b/GVFS/GVFS.Mount/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.Mount")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.Mount")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b8c1dfbd-cafd-4f7e-a1a3-e11907b5467b")] diff --git a/GVFS/GVFS.Mount/packages.config b/GVFS/GVFS.Mount/packages.config new file mode 100644 index 00000000..3738a2b7 --- /dev/null +++ b/GVFS/GVFS.Mount/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj new file mode 100644 index 00000000..2f92a862 --- /dev/null +++ b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj @@ -0,0 +1,157 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {3771C555-B5C1-45E2-B8B7-2CEF1619CDC5} + Win32Proj + GVFSNativeTests + 8.1 + + + + DynamicLibrary + true + v140 + NotSet + + + DynamicLibrary + false + v140 + true + NotSet + + + + + + + + + + + + + + + true + $(SolutionDir)..\BuildOutput\GVFS.FunctionalTests\bin\$(Platform)\$(Configuration)\ + ..\..\..\BuildOutput\$(ProjectName)\$(Platform)\$(Configuration)\ + + + false + $(SolutionDir)..\BuildOutput\GVFS.FunctionalTests\bin\$(Platform)\$(Configuration)\ + ..\..\..\BuildOutput\$(ProjectName)\$(Platform)\$(Configuration)\ + + + + Use + Level4 + Disabled + _DEBUG;_WINDOWS;_USRDLL;GVFSNATIVETESTS_EXPORTS;%(PreprocessorDefinitions) + true + true + $(SolutionDir)\GVFS\$(Projectname)\include;$(SolutionDir)\GVFS\$(Projectname)\interface;%(AdditionalIncludeDirectories) + + + Windows + true + gvlib.lib;fltlib.lib;Shlwapi.lib;%(AdditionalDependencies) + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\lib + + + + + Level4 + Use + MaxSpeed + true + true + NDEBUG;_WINDOWS;_USRDLL;GVFSNATIVETESTS_EXPORTS;%(PreprocessorDefinitions) + true + true + $(SolutionDir)\GVFS\$(Projectname)\include;$(SolutionDir)\GVFS\$(Projectname)\interface;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + gvlib.lib;fltlib.lib;Shlwapi.lib;%(AdditionalDependencies) + $(SolutionDir)\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\lib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + + + + + + + + + + + + + + + + + + + Create + Create + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj.filters b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj.filters new file mode 100644 index 00000000..d6893b72 --- /dev/null +++ b/GVFS/GVFS.NativeTests/GVFS.NativeTests.vcxproj.filters @@ -0,0 +1,157 @@ + + + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {58670690-8e03-4f0a-bd9d-b0f82f02ff5f} + + + {93586a9a-4ffd-42b8-966e-28d0fa780751} + + + {fa2786cf-49d1-44fc-b4b0-1a77e860d7b8} + + + + + + + + include + + + include + + + include + + + include + + + interface + + + include + + + include + + + interface + + + interface + + + include + + + interface + + + include + + + include + + + include + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + interface + + + include + + + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + source + + + \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/ReadMe.txt b/GVFS/GVFS.NativeTests/ReadMe.txt new file mode 100644 index 00000000..0858587a --- /dev/null +++ b/GVFS/GVFS.NativeTests/ReadMe.txt @@ -0,0 +1,25 @@ +======================================================================== +GVFS.NativeTests +======================================================================== + +Summary: + +GVFS.NativeTests is a library used by GVFS.FunctionalTests to run GVFS +functional tests using the native WinAPI. + +The GVFS.NativeTests dll is output into the appropriate GVFS.FunctionalTests +directory so that the dll can be found when it is DllImported by GVFS.NativeTests. + + +Folder Structure: + +interface -> Header files that are consumable by projects outside of GVFS.NativeTests +include -> Header files that are internal to GVFS.NativeTests +source -> GVFS.NativeTests source code + + +Debugging: + +To step through tests in GVFS.NativeTests and to set breakpoints, ensure that the +"Enable native code debugging" setting is checked in the GVFS.FunctionalTests +project properites (Debug tab) \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/NtFunctions.h b/GVFS/GVFS.NativeTests/include/NtFunctions.h new file mode 100644 index 00000000..2688e3ad --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/NtFunctions.h @@ -0,0 +1,15 @@ +#pragma once + +NTSTATUS NtQueryDirectoryFile( + _In_ HANDLE FileHandle, + _In_opt_ HANDLE Event, + _In_opt_ PIO_APC_ROUTINE ApcRoutine, + _In_opt_ PVOID ApcContext, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _Out_ PVOID FileInformation, + _In_ ULONG Length, + _In_ FILE_INFORMATION_CLASS FileInformationClass, + _In_ BOOLEAN ReturnSingleEntry, + _In_opt_ PUNICODE_STRING FileName, + _In_ BOOLEAN RestartScan +); \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/SafeHandle.h b/GVFS/GVFS.NativeTests/include/SafeHandle.h new file mode 100644 index 00000000..3fea5a70 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/SafeHandle.h @@ -0,0 +1,42 @@ +#pragma once + +// Wrapper for HANDLE that calls CloseHandle when destroyed +class SafeHandle +{ +public: + SafeHandle(HANDLE handle); + ~SafeHandle(); + + HANDLE GetHandle(); + void CloseHandle(); + +private: + HANDLE handle; +}; + +inline SafeHandle::SafeHandle(HANDLE handle) +{ + this->handle = handle; +} + +inline SafeHandle::~SafeHandle() +{ + if (this->handle != NULL) + { + this->CloseHandle(); + } +} + +inline HANDLE SafeHandle::GetHandle() +{ + return this->handle; +} + +inline void SafeHandle::CloseHandle() +{ + if (this->handle != NULL && this->handle != INVALID_HANDLE_VALUE) + { + ::CloseHandle(this->handle); + this->handle = NULL; + } +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/SafeOverlapped.h b/GVFS/GVFS.NativeTests/include/SafeOverlapped.h new file mode 100644 index 00000000..dbd60dcd --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/SafeOverlapped.h @@ -0,0 +1,23 @@ +#pragma once + +// Wrapper for OVERLAPPED that calls CloseHandle on the OVERLAPPED's hEvent when destroyed +struct SafeOverlapped +{ + SafeOverlapped(); + ~SafeOverlapped(); + + OVERLAPPED overlapped; +}; + +inline SafeOverlapped::SafeOverlapped() +{ + memset(&this->overlapped, 0, sizeof(OVERLAPPED)); +} + +inline SafeOverlapped::~SafeOverlapped() +{ + if (this->overlapped.hEvent != NULL) + { + CloseHandle(this->overlapped.hEvent); + } +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/Should.h b/GVFS/GVFS.NativeTests/include/Should.h new file mode 100644 index 00000000..fb4cb712 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/Should.h @@ -0,0 +1,30 @@ +#pragma once + +#include "TestException.h" + +#define STRINGIFY(X) #X +#define EXPAND_AND_STRINGIFY(X) STRINGIFY(X) + +#define SHOULD_BE_TRUE(expr) \ +do { \ + if(!(expr)) \ + { \ + if(IsDebuggerPresent()) \ + { \ + assert(expr); \ + } \ + throw TestException("Failure on line:" EXPAND_AND_STRINGIFY(__LINE__) ", in function:" __FUNCTION__); \ + } \ +} while (0) + +#define SHOULD_EQUAL(P1, P2) SHOULD_BE_TRUE((P1) == (P2)) +#define SHOULD_NOT_EQUAL(P1, P2) SHOULD_BE_TRUE((P1) != (P2)) + +#define FAIL_TEST(msg) \ +do { \ + if (IsDebuggerPresent()) \ + { \ + assert(false); \ + } \ + throw TestException(msg); \ +} while (0) diff --git a/GVFS/GVFS.NativeTests/include/TestException.h b/GVFS/GVFS.NativeTests/include/TestException.h new file mode 100644 index 00000000..968d22f0 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/TestException.h @@ -0,0 +1,27 @@ +#pragma once + +class TestException : public std::exception +{ + +public: + TestException(const std::string& message); + virtual ~TestException(); + virtual const char* what() const override; + +private: + std::string message; +}; + +inline TestException::TestException(const std::string& message) + : message(message) +{ +} + +inline TestException::~TestException() +{ +} + +inline const char* TestException::what() const +{ + return this->message.c_str(); +} diff --git a/GVFS/GVFS.NativeTests/include/TestHelpers.h b/GVFS/GVFS.NativeTests/include/TestHelpers.h new file mode 100644 index 00000000..124e107b --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/TestHelpers.h @@ -0,0 +1,611 @@ +#pragma once + +#include "Should.h" +#include "gvlib_internal.h" + +// Map GVFlt testing macros to GVFS testing macros +#define VERIFY_ARE_EQUAL SHOULD_EQUAL +#define VERIFY_ARE_NOT_EQUAL SHOULD_NOT_EQUAL +#define VERIFY_FAIL FAIL_TEST + +static const DWORD MAX_BUF_SIZE = 256; + +struct FileInfo +{ + std::string Name; + bool IsFile = true; + DWORD FileSize = 0; +}; + +namespace TestHelpers +{ + +inline std::shared_ptr OpenForRead(const std::string& path) +{ + std::shared_ptr handle( + CreateFile(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL), + CloseHandle); + + if (INVALID_HANDLE_VALUE == handle.get()) { + VERIFY_FAIL("failed to open file for read"); + } + + VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, handle.get()); + return handle; +} + +inline std::vector EnumDirectory(const std::string& path) +{ + WIN32_FIND_DATA ffd; + + std::vector result; + + std::string query = path + "*"; + + HANDLE hFind = FindFirstFile(query.c_str(), &ffd); + + if (hFind == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("FindFirstFile failed"); + } + + do + { + FileInfo fileInfo; + fileInfo.Name = ffd.cFileName; + + if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + fileInfo.IsFile = false; + } + else + { + fileInfo.FileSize = ffd.nFileSizeLow; + } + + result.push_back(fileInfo); + } while (FindNextFile(hFind, &ffd) != 0); + + DWORD dwError = GetLastError(); + if (dwError != ERROR_NO_MORE_FILES) + { + VERIFY_FAIL("FindNextFile failed"); + } + + FindClose(hFind); + + return result; +} + +inline void WriteToFile(const std::string& path, const std::string content, bool isNewFile = false) +{ + HANDLE hFile = CreateFile(path.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + isNewFile ? CREATE_NEW : OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile failed"); + } + + if (content.empty()) + { + goto CleanUp; + } + + DWORD dwBytesToWrite = (DWORD)content.size() * sizeof(content[0]); + DWORD dwBytesWritten = 0; + + VERIFY_ARE_EQUAL(TRUE, WriteFile( + hFile, // open file handle + content.c_str(), // start of data to write + dwBytesToWrite, // number of bytes to write + &dwBytesWritten, // number of bytes that were written + NULL)); + + VERIFY_ARE_EQUAL(dwBytesToWrite, dwBytesWritten); + + VERIFY_ARE_EQUAL(TRUE, FlushFileBuffers(hFile)); + +CleanUp: + CloseHandle(hFile); +} + +inline void CreateNewFile(const std::string& path, const std::string content) +{ + WriteToFile(path, content, true); +} + +inline void CreateNewFile(const std::string& path) +{ + CreateNewFile(path, ""); +} + +inline DWORD DelFile(const std::string& path, bool isSetDisposition = true) +{ + if (isSetDisposition) { + BOOL success = DeleteFile(path.c_str()); + if (success) { + return ERROR_SUCCESS; + } + else { + return GetLastError(); + } + } + + // delete on close + HANDLE handle = CreateFile( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, + 0, + OPEN_EXISTING, + FILE_FLAG_DELETE_ON_CLOSE, + NULL); + + if (handle == INVALID_HANDLE_VALUE) { + return GetLastError(); + } + + BOOL success = CloseHandle(handle); + if (!success) { + return GetLastError(); + } + + return ERROR_SUCCESS; +} + +inline std::shared_ptr GetReparseInfo(const std::string& path) +{ + USHORT dataSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; + std::shared_ptr reparseInfo((PGV_REPARSE_INFO)calloc(1, dataSize), free); + + std::wstring_convert> utf16conv; + + HRESULT hr = GvReadGvReparsePointData(utf16conv.from_bytes(path).c_str(), reparseInfo.get(), &dataSize); + if (FAILED(hr)) { + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_A_REPARSE_POINT)) { + // ERROR: target is not a reparse point + return false; + } + else { + // ERROR: failed to read reparse point + return false; + } + } + + return reparseInfo; +} + +inline bool IsFullFolder(const std::string& path) +{ + unsigned long flag = GetReparseInfo(path)->Flags & GV_FLAG_FULLY_POPULATED; + + return flag != 0; +} + +inline bool DoesFileExist(const std::string& path) +{ + HANDLE handle = CreateFile(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + return true; + } + + if (ERROR_FILE_NOT_FOUND == GetLastError()) { + return false; + } + + return false; +} + +inline std::string ReadFileAsString(const std::string& path) +{ + std::shared_ptr hFile = OpenForRead(path); + + char DataBuffer[MAX_BUF_SIZE] = { 0 }; + DWORD dwbytesRead; + + VERIFY_ARE_NOT_EQUAL(ReadFile( + hFile.get(), + DataBuffer, + MAX_BUF_SIZE, + &dwbytesRead, + NULL + ), FALSE); + + return std::string(DataBuffer); +} + +inline DWORD DelFolder(const std::string& path, bool isSetDisposition = true) +{ + if (isSetDisposition) { + auto success = RemoveDirectory(path.c_str()); + if (success) { + return ERROR_SUCCESS; + } + else { + return GetLastError(); + } + } + + // delete on close + HANDLE handle = CreateFile( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_DELETE_ON_CLOSE, + NULL); + + if (handle == INVALID_HANDLE_VALUE) { + return GetLastError(); + } + + BOOL success = CloseHandle(handle); + if (!success) { + return GetLastError(); + } + + return ERROR_SUCCESS; +} + +inline HRESULT CreateDirectoryWithIntermediates( + _In_ const std::string& directoryName +) +{ + if (!CreateDirectory(directoryName.c_str(), nullptr)) { + + int gle = GetLastError(); + if (gle == ERROR_ALREADY_EXISTS) { + + // If the directory already exists just treat that as success. + return S_OK; + + } + else if (gle == ERROR_PATH_NOT_FOUND) { + + // One or more intermediate directories don't exist. Assume + // the incoming path starts with e.g "X:\" + std::string ntPath = "\\\\?\\"; + size_t startPos = 3; + if (directoryName.compare(0, ntPath.length(), ntPath) == 0) { + startPos += ntPath.length(); + } + + std::string::size_type foundPos = directoryName.find_first_of("\\", startPos); + while (foundPos != std::string::npos) { + + if (!CreateDirectory(directoryName.substr(0, foundPos).c_str(), nullptr)) { + + gle = GetLastError(); + if (gle != ERROR_ALREADY_EXISTS) { + return HRESULT_FROM_WIN32(gle); + } + } + + foundPos = directoryName.find_first_of("\\", foundPos + 1); + } + + // The loop created all the intermediate directories. Try creating the final + // part again unless the string ended in a "\". In that case we created everything + // we need. + + if (directoryName.length() - 1 != directoryName.find_last_of("\\")) { + + if (!CreateDirectory(directoryName.c_str(), nullptr)) { + return HRESULT_FROM_WIN32(GetLastError()); + } + } + + } + else { + return HRESULT_FROM_WIN32(gle); + } + } + + return S_OK; +} + +inline std::shared_ptr OpenForQueryAttribute(const std::string& path) +{ + std::shared_ptr handle( + CreateFile(path.c_str(), + FILE_READ_ATTRIBUTES, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL), + CloseHandle); + + VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, handle.get()); + + return handle; +} + +inline FILETIME GetLastWriteTime(const std::string& path) +{ + std::shared_ptr hFile = OpenForQueryAttribute(path); + + FILETIME ftWrite; + VERIFY_ARE_EQUAL(TRUE, GetFileTime(hFile.get(), NULL, NULL, &ftWrite)); + SYSTEMTIME systemTime = { 0 }; + + BOOL success = FileTimeToSystemTime(&ftWrite, &systemTime); + VERIFY_ARE_EQUAL(TRUE, success); + + return ftWrite; +} + +inline LARGE_INTEGER GetFileSize(const std::string& path) +{ + std::shared_ptr hFile = OpenForQueryAttribute(path); + + LARGE_INTEGER size; + VERIFY_ARE_EQUAL(TRUE, GetFileSizeEx(hFile.get(), &size)); + return size; +} + +inline NTSTATUS SetEAInfo(const std::string& path, PFILE_FULL_EA_INFORMATION pbEABuffer, ULONG size, const int attributeNum = 1) { + + HANDLE hFile = CreateFile(path.c_str(), + FILE_WRITE_EA, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (INVALID_HANDLE_VALUE == hFile) + { + printf("\nSetExtendedAttributes: Cannot create handle for file %s", path.c_str()); + VERIFY_FAIL(("SetExtendedAttributes: Cannot create handle for file " + path).c_str()); + } + + NTSTATUS NtStatus = STATUS_SUCCESS; + IO_STATUS_BLOCK IoStatusBlock; + + PFILE_FULL_EA_INFORMATION pEABlock = NULL; + CHAR xAttrName[MAX_PATH] = { 0 }; + CHAR xAttrValue[MAX_PATH] = { 0 }; + + for (int i = 0; iNextEntryOffset = 0; + pEABlock->Flags = 0; + pEABlock->EaNameLength = (UCHAR)(lstrlenA(xAttrName) * sizeof(CHAR)); // in bytes; + pEABlock->EaValueLength = (UCHAR)(lstrlenA(xAttrValue) * sizeof(CHAR)); // in bytes; + + CopyMemory(pEABlock->EaName, xAttrName, lstrlenA(xAttrName) * sizeof(CHAR)); + pEABlock->EaName[pEABlock->EaNameLength] = 0; // IO subsystem checks for this NULL + + CopyMemory(pEABlock->EaName + pEABlock->EaNameLength + 1, xAttrValue, pEABlock->EaValueLength + 1); + pEABlock->EaName[pEABlock->EaNameLength + 1 + pEABlock->EaValueLength + 1] = 0; // IO subsystem checks for this NULL + + HMODULE ntdll = LoadLibrary("ntdll.dll"); + VERIFY_ARE_NOT_EQUAL(ntdll, NULL); + + PSetEaFile NtSetEaFile = (PSetEaFile)GetProcAddress(ntdll, "NtSetEaFile"); + VERIFY_ARE_NOT_EQUAL(NtSetEaFile, NULL); + + NtStatus = NtSetEaFile(hFile, &IoStatusBlock, (PVOID)pEABlock, size); + if (!NT_SUCCESS(NtStatus)) + { + printf("\n\tSetExtendedAttributes: Failed in NtSetEaFile (0x%08x)", NtStatus); + VERIFY_FAIL("SetExtendedAttributes: Failed in NtSetEaFile"); + } + } + + CloseHandle(hFile); + + return NtStatus; +} + +inline NTSTATUS ReadEAInfo(const std::string& path, PFILE_FULL_EA_INFORMATION eaBuffer, PULONG length) { + + NTSTATUS status = STATUS_SUCCESS; + + HANDLE hFile = CreateFile(path.c_str(), + FILE_READ_EA | SYNCHRONIZE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (INVALID_HANDLE_VALUE == hFile) + { + VERIFY_FAIL("ReadEAInfo: Cannot create handle for file"); + } + + IO_STATUS_BLOCK IoStatusBlock; + + // In the GVFlt tests, Index of 0 is used, however, per minkernel\fs\ntfs\ea.c + // "If the index value is zero, there are no Eas to return" Confirmed index of 1 + // properly reads EAs created using ea.exe test tool provided by GVFlt (\\craigba-dev\Bin\amd64\Ea.exe) + ULONG Index = 1; + FILE_EA_INFORMATION eaInfo = { 0 }; + + HMODULE ntdll = LoadLibrary("ntdll.dll"); + VERIFY_ARE_NOT_EQUAL(ntdll, NULL); + + PQueryInformationFile NtQueryInformationFile = (PQueryInformationFile)GetProcAddress(ntdll, "NtQueryInformationFile"); + VERIFY_ARE_NOT_EQUAL(NtQueryInformationFile, NULL); + + status = NtQueryInformationFile( + hFile, + &IoStatusBlock, + &eaInfo, + sizeof(eaInfo), + FileEaInformation + ); + + if (!NT_SUCCESS(status)) { + printf("\n\tError: NtQueryInformationFile failed, status = 0x%lx\n", status); + goto Cleanup; + } + + if (eaInfo.EaSize) { + + if (*length < eaInfo.EaSize) { + printf("\n\tNtQueryEaFile failed, buffer is too small\n"); + status = ERROR_NOT_ENOUGH_MEMORY; + goto Cleanup; + } + + *length = eaInfo.EaSize; + + PQueryEaFile NtQueryEaFile = (PQueryEaFile)GetProcAddress(ntdll, "NtQueryEaFile"); + VERIFY_ARE_NOT_EQUAL(NtQueryEaFile, NULL); + + status = NtQueryEaFile( + hFile, + &IoStatusBlock, + eaBuffer, + *length, + FALSE, + NULL, + 0, + &Index, + TRUE); + + if (!NT_SUCCESS(status)) { + printf("\n\tNtQueryEaFile failed, status = 0x%lx\n", status); + goto Cleanup; + } + } + +Cleanup: + CloseHandle(hFile); + + return status; +} + +inline std::string CombinePath(const std::string& root, const std::string& relPath) +{ + std::string fullPath = root; + + if (root.empty() || root == "\\") { + return relPath; + } + + if (fullPath.back() == '\\') { + fullPath.pop_back(); + } + + if (!relPath.empty()) { + fullPath += '\\'; + fullPath += relPath; + } + + if (fullPath.back() == '\\') { + fullPath.pop_back(); + } + + return fullPath; +} + +inline std::string ReadFileAsStringUncached(const std::string& path) +{ + HANDLE hFile = CreateFile( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, + NULL, + OPEN_EXISTING, + FILE_FLAG_RANDOM_ACCESS, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) { + VERIFY_FAIL("CreateFile failed"); + } + + VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError()); + + HANDLE hMapFile = CreateFileMapping( + hFile, + NULL, // default security + PAGE_READWRITE | FILE_MAP_READ, // read/write access + 0, // maximum object size (high-order DWORD) + 0, // maximum object size (low-order DWORD) + NULL); // name of mapping object + + VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hMapFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError()); + + LPCTSTR pBuf = (LPTSTR)MapViewOfFile(hMapFile, // handle to map object + FILE_MAP_READ, // read permission + 0, + 0, + 0); + + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, GetLastError()); + VERIFY_ARE_NOT_EQUAL(nullptr, pBuf); + + std::string result(pBuf); + + UnmapViewOfFile(pBuf); + + CloseHandle(hMapFile); + + CloseHandle(hFile); + + return result; +} + +inline int MovFile(const std::string& from, const std::string& to) +{ + int ret = rename(from.c_str(), to.c_str()); + if (ret != 0) { + errno_t err; + _get_errno(&err); + return err; + } + + return ret; +} + +inline bool NewHardLink(const std::string& newlink, const std::string& existingFile) +{ + auto created = CreateHardLink(newlink.c_str(), existingFile.c_str(), NULL); + return created == TRUE; +} + +} // namespace TestHelpers \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/TestVerifiers.h b/GVFS/GVFS.NativeTests/include/TestVerifiers.h new file mode 100644 index 00000000..45b79fde --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/TestVerifiers.h @@ -0,0 +1,48 @@ +#pragma once + +#include "Should.h" +#include "TestHelpers.h" + +namespace TestVerifiers +{ + +inline void ExpectDirEntries(const std::string& path, std::vector& entries) +{ + std::vector result = TestHelpers::EnumDirectory(path); + entries.push_back("."); + entries.push_back(".."); + + VERIFY_ARE_EQUAL(entries.size(), result.size()); + + for (const std::string& entry : entries) + { + bool found = false; + for (std::vector::iterator resultIt = result.begin(); resultIt != result.end(); resultIt++) + { + if (resultIt->Name == entry) + { + result.erase(resultIt); + found = true; + break; + } + } + + if (!found) + { + VERIFY_FAIL((" [" + entry + "] not found").c_str()); + return; + } + } + + if (!result.empty()) + { + VERIFY_FAIL("Some expected results not found"); + } +} + +inline void AreEqual(const std::string& str1, const std::string& str2) +{ + VERIFY_ARE_EQUAL(str1, str2); +} + +} // namespace TestVerifiers \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/gvflt.h b/GVFS/GVFS.NativeTests/include/gvflt.h new file mode 100644 index 00000000..eb320152 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/gvflt.h @@ -0,0 +1,73 @@ +// gvflt.h +// +// Contains a subset of the contents of: +// sdktools\CoreBuild\GVFlt\gvflt.h + +#pragma once + +#define GV_FLAG_IMMUTABLE 0x00000001 +#define GV_FLAG_DIRTY 0x00000002 +#define GV_FLAG_FULLY_POPULATED 0x00000004 +#define GV_FLAG_RENAMED 0x00000008 +#define GV_FLAG_VIRTUALIZATION_ROOT 0x00000010 +#define GV_FLAG_FULL_DATA 0x00000020 +#define GV_FLAG_PLACEHOLDER_AUTHORITATIVE 0x00000100 + +// +// Length of ContentID and EpochID in bytes +// + +#define GVFLT_PLACEHOLDER_ID_LENGTH 128 +// +// Structure that uniquely identifies the version of the attributes, file streams etc for a placeholder file +// + +typedef struct _GVFLT_PLACEHOLDER_VERSION_INFO { + + UCHAR EpochID[GVFLT_PLACEHOLDER_ID_LENGTH]; + + UCHAR ContentID[GVFLT_PLACEHOLDER_ID_LENGTH]; + +} GVFLT_PLACEHOLDER_VERSION_INFO, *PGVFLT_PLACEHOLDER_VERSION_INFO; + + +// +// Data written into on-disk reparse point +// + +typedef struct _GV_REPARSE_INFO { + + // + // Version of this struct for future app compat issues + // + + DWORD Version; + + // + // Additional flags + // + + ULONG Flags; + + // + // ID of the Virtualization Instance associated with the Virtualization Root that contains this reparse point + // + + GUID VirtualizationInstanceID; + + // + // Version info for the placeholder file + // + + GVFLT_PLACEHOLDER_VERSION_INFO versionInfo; + + // + // Virtual (i.e. relative to the Virtualization Instance root) name of the fully expanded file + // The name does not include trailing zero + // The length is in bytes + // + USHORT NameLength; + + WCHAR Name[ANYSIZE_ARRAY]; + +} GV_REPARSE_INFO, *PGV_REPARSE_INFO; \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/gvlib_internal.h b/GVFS/GVFS.NativeTests/include/gvlib_internal.h new file mode 100644 index 00000000..15e0dff5 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/gvlib_internal.h @@ -0,0 +1,28 @@ +// gvlib_internal.h +// +// Function declarations for internal functions in gvlib (used in the GVFlt tests) +// that are not intended to be used by user applications (e.g. GVFS) built on GVFlt +// +// Subset of the contents of: sdktools\CoreBuild\GvFlt\lib\gvlib_internal.h + +#pragma once + +#include "gvflt.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// +// Functions operating on GVFS reparse points +// +HRESULT +GvReadGvReparsePointData( + _In_ LPCWSTR FilePath, + _Out_writes_bytes_(*DataSize) PGV_REPARSE_INFO ReparsePointData, + _Inout_ PUSHORT DataSize +); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/stdafx.h b/GVFS/GVFS.NativeTests/include/stdafx.h new file mode 100644 index 00000000..88cceb28 --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/stdafx.h @@ -0,0 +1,184 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files: +#include +#define WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Shlwapi.h" +#include "Strsafe.h" + +#ifdef GVFSNATIVETESTS_EXPORTS +#define NATIVE_TESTS_EXPORT __declspec(dllexport) +#else +#define NATIVE_TESTS_EXPORT __declspec(dllimport) +#endif + +#define UNREFERENCED_PARAMETER(P) (P) + +template < typename T > +struct delete_array +{ + void operator ()(T const* ptr) + { + delete[] ptr; + } +}; + +typedef LONG NTSTATUS; + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +typedef struct _IO_STATUS_BLOCK { + union { + NTSTATUS Status; + PVOID Pointer; + }; + ULONG_PTR Information; +} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + +typedef struct _LSA_UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING; + +typedef struct _FILE_NAMES_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; + +typedef enum _FILE_INFORMATION_CLASS { + FileDirectoryInformation = 1, + FileFullDirectoryInformation, // 2 + FileBothDirectoryInformation, // 3 + FileBasicInformation, // 4 + FileStandardInformation, // 5 + FileInternalInformation, // 6 + FileEaInformation, // 7 + FileAccessInformation, // 8 + FileNameInformation, // 9 + FileRenameInformation, // 10 + FileLinkInformation, // 11 + FileNamesInformation, // 12 + FileDispositionInformation, // 13 + FilePositionInformation, // 14 + FileFullEaInformation, // 15 + FileModeInformation, // 16 + FileAlignmentInformation, // 17 + FileAllInformation, // 18 + FileAllocationInformation, // 19 + FileEndOfFileInformation, // 20 + FileAlternateNameInformation, // 21 + FileStreamInformation, // 22 + FilePipeInformation, // 23 + FilePipeLocalInformation, // 24 + FilePipeRemoteInformation, // 25 + FileMailslotQueryInformation, // 26 + FileMailslotSetInformation, // 27 + FileCompressionInformation, // 28 + FileObjectIdInformation, // 29 + FileCompletionInformation, // 30 + FileMoveClusterInformation, // 31 + FileQuotaInformation, // 32 + FileReparsePointInformation, // 33 + FileNetworkOpenInformation, // 34 + FileAttributeTagInformation, // 35 + FileTrackingInformation, // 36 + FileIdBothDirectoryInformation, // 37 + FileIdFullDirectoryInformation, // 38 + FileValidDataLengthInformation, // 39 + FileShortNameInformation, // 40 + FileIoCompletionNotificationInformation, // 41 + FileIoStatusBlockRangeInformation, // 42 + FileIoPriorityHintInformation, // 43 + FileSfioReserveInformation, // 44 + FileSfioVolumeInformation, // 45 + FileHardLinkInformation, // 46 + FileProcessIdsUsingFileInformation, // 47 + FileNormalizedNameInformation, // 48 + FileNetworkPhysicalNameInformation, // 49 + FileIdGlobalTxDirectoryInformation, // 50 + FileIsRemoteDeviceInformation, // 51 + FileUnusedInformation, // 52 + FileNumaNodeInformation, // 53 + FileStandardLinkInformation, // 54 + FileRemoteProtocolInformation, // 55 + + // + // These are special versions of these operations (defined earlier) + // which can be used by kernel mode drivers only to bypass security + // access checks for Rename and HardLink operations. These operations + // are only recognized by the IOManager, a file system should never + // receive these. + // + FileRenameInformationBypassAccessCheck, // 56 + FileLinkInformationBypassAccessCheck, // 57 + FileVolumeNameInformation, // 58 + FileIdInformation, // 59 + FileIdExtdDirectoryInformation, // 60 + FileReplaceCompletionInformation, // 61 + FileHardLinkFullIdInformation, // 62 + FileIdExtdBothDirectoryInformation, // 63 + FileMaximumInformation +} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS; + +typedef struct _FILE_BOTH_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION; + +typedef struct _FILE_FULL_EA_INFORMATION { + ULONG NextEntryOffset; + UCHAR Flags; + UCHAR EaNameLength; + USHORT EaValueLength; + CHAR EaName[1]; +} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION; + +typedef struct _FILE_EA_INFORMATION { + ULONG EaSize; +} FILE_EA_INFORMATION, *PFILE_EA_INFORMATION; + +typedef VOID(NTAPI *PIO_APC_ROUTINE) (_In_ PVOID ApcContext, _In_ PIO_STATUS_BLOCK IoStatusBlock, _In_ ULONG Reserved); + +typedef NTSTATUS(NTAPI *PQueryDirectoryFile)(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS, BOOLEAN, PUNICODE_STRING, BOOLEAN); + +typedef NTSTATUS(NTAPI *PSetEaFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG); + +typedef NTSTATUS(NTAPI* PQueryInformationFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS); + +typedef NTSTATUS(NTAPI* PQueryEaFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, BOOLEAN, PVOID, ULONG, PULONG, BOOLEAN); + +#include "NtFunctions.h" \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/include/targetver.h b/GVFS/GVFS.NativeTests/include/targetver.h new file mode 100644 index 00000000..90e767bf --- /dev/null +++ b/GVFS/GVFS.NativeTests/include/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_BugRegressionTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_BugRegressionTest.h new file mode 100644 index 00000000..4b59921a --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_BugRegressionTest.h @@ -0,0 +1,29 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_ModifyFileInScratchAndDir(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_RMDIRTest1(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_RMDIRTest2(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_RMDIRTest3(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_RMDIRTest4(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_RMDIRTest5(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeepNonExistFileUnderPartial(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SupersededReparsePoint(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // StartInstanceAndFreeCallbacks + // QickAttachDetach + // - These timing scenarios don't need to be tested with GVFS + // + // UnableToReadPartialFile + // - This test requires control over the GVFlt callback implementation + // + // DeepNonExistFileUnderFull + // - Currently GVFS does not covert folders to full + + // The following were ported to the managed tests: + // + // CMDHangNoneActiveInstance +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFileTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFileTest.h new file mode 100644 index 00000000..eb21b359 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFileTest.h @@ -0,0 +1,25 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_DeleteVirtualFile_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteVirtualFile_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeletePlaceholder_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeletePlaceholder_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteFullFile_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteFullFile_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteLocalFile_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteLocalFile_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNotExistFile_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNotExistFile_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNonRootVirtualFile_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNonRootVirtualFile_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteFileOutsideVRoot_SetDisposition(const char* pathOutsideRepo); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteFileOutsideVRoot_DeleteOnClose(const char* pathOutsideRepo); + + // Note the following tests were not ported from GVFlt: + // + // DeleteFullFileWithoutFileContext_SetDisposition + // DeleteFullFileWithoutFileContext_DeleteOnClose + // - GVFS will always project new files when its back layer changes +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFolderTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFolderTest.h new file mode 100644 index 00000000..36c0d7eb --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_DeleteFolderTest.h @@ -0,0 +1,23 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteLocalEmptyFolder_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteLocalEmptyFolder_DeleteOnClose(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNonRootVirtualFolder_SetDisposition(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // DeleteVirtualEmptyFolder_SetDisposition + // DeleteVirtualEmptyFolder_DeleteOnClose + // - Git does not support empty folders + // + // DeleteFullNonEmptyFolder_SetDisposition + // DeleteFullNonEmptyFolder_DeleteOnClose + // - GVFS does not allow full folders +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_DirEnumTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_DirEnumTest.h new file mode 100644 index 00000000..6a6057ea --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_DirEnumTest.h @@ -0,0 +1,11 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_EnumEmptyFolder(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_EnumFolderWithOneFileInPackage(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_EnumFolderWithOneFileInBoth(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_EnumFolderWithOneFileInBoth1(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_EnumFolderDeleteExistingFile(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_EnumFolderSmallBuffer(const char* virtualRootPath); +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_FileAttributeTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_FileAttributeTest.h new file mode 100644 index 00000000..2a4b44e9 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_FileAttributeTest.h @@ -0,0 +1,14 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_ModifyFileInScratchAndCheckLastWriteTime(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_FileSize(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_ModifyFileInScratchAndCheckFileSize(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_FileAttributes(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // LastWriteTime + // - There is no last write time in the GVFS layer to compare with +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_FileEATest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_FileEATest.h new file mode 100644 index 00000000..711b2b18 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_FileEATest.h @@ -0,0 +1,6 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_OneEAAttributeWillPass(const char* virtualRootPath); +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_FileOperationTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_FileOperationTest.h new file mode 100644 index 00000000..a0984585 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_FileOperationTest.h @@ -0,0 +1,24 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_OpenRootFolder(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_WriteAndVerify(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_DeleteExistingFile(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_OpenNonExistingFile(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // OpenFileForRead + // - Covered in GVFS.FunctionalTests.Tests.EnlistmentPerFixture.WorkingDirectoryTests.ProjectedFileHasExpectedContents + // OpenFileForWrite + // - Covered in GVFS.FunctionalTests.Tests.LongRunningEnlistment.WorkingDirectoryTests.ShrinkFileContents (and other tests) + // ReadFileAndVerifyContent + // - Covered in GVFS.FunctionalTests.Tests.EnlistmentPerFixture.WorkingDirectoryTests.ProjectedFileHasExpectedContents + // WriteFileAndVerifyFileInScratch + // OverwriteAndVerify + // - Does not apply: Tests that writing scratch layer does not impact backing layer contents + // CreateNewFileInScratch + // CreateNewFileAndWriteInScratch + // - Covered in GVFS.FunctionalTests.Tests.LongRunningEnlistment.WorkingDirectoryTests.ShrinkFileContents (and other tests) +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFileTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFileTest.h new file mode 100644 index 00000000..cff92ebf --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFileTest.h @@ -0,0 +1,30 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_NoneToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_PartialToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_FullToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_LocalToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToVirtual(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToVirtualFileNameChanged(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToPartial(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_PartialToPartial(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_LocalToVirtual(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToVirtualIntermidiateDirNotExist(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToNoneIntermidiateDirNotExist(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_OutsideToPartial(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_PartialToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFile_LongFileName(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // VirtualToFull + // - GVFS does not allow full folders +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFolderTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFolderTest.h new file mode 100644 index 00000000..4ebc2987 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_MoveFolderTest.h @@ -0,0 +1,15 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_NoneToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_VirtualToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_PartialToNone(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_VirtualToVirtual(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_VirtualToPartial(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_MoveFolder_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath); +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_MultiThreadsTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_MultiThreadsTest.h new file mode 100644 index 00000000..8babe066 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_MultiThreadsTest.h @@ -0,0 +1,14 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_OpenForReadsSameTime(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_OpenForWritesSameTime(const char* virtualRootPath); + + // Note the following tests were not ported from GVFlt: + // + // GetPlaceholderInfoAndStopInstance + // GetStreamAndStopInstance + // EnumAndStopInstance + // - These tests require precise control of when the virtualization instance is stopped +} diff --git a/GVFS/GVFS.NativeTests/interface/GVFlt_SetLinkTest.h b/GVFS/GVFS.NativeTests/interface/GVFlt_SetLinkTest.h new file mode 100644 index 00000000..fc962d2a --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/GVFlt_SetLinkTest.h @@ -0,0 +1,12 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_ToVirtualFile(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_ToPlaceHolder(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_ToFullFile(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_ToNonExistFileWillFail(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_NameAlreadyExistWillFail(const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_FromOutside(const char* pathOutsideRepo, const char* virtualRootPath); + NATIVE_TESTS_EXPORT bool GVFlt_SetLink_ToOutside(const char* pathOutsideRepo, const char* virtualRootPath); +} diff --git a/GVFS/GVFS.NativeTests/interface/NtQueryDirectoryFileTests.h b/GVFS/GVFS.NativeTests/interface/NtQueryDirectoryFileTests.h new file mode 100644 index 00000000..c8ed7dcc --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/NtQueryDirectoryFileTests.h @@ -0,0 +1,6 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool QueryDirectoryFileRestartScanResetsFilter(const char* folderPath); +} diff --git a/GVFS/GVFS.NativeTests/interface/PlaceholderUtils.h b/GVFS/GVFS.NativeTests/interface/PlaceholderUtils.h new file mode 100644 index 00000000..734ebf6f --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/PlaceholderUtils.h @@ -0,0 +1,6 @@ +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool PlaceHolderHasVersionInfo(const char* virtualPath, int version, const WCHAR* sha, const WCHAR* commit); +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/interface/ReadAndWriteTests.h b/GVFS/GVFS.NativeTests/interface/ReadAndWriteTests.h new file mode 100644 index 00000000..4dd62d67 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/ReadAndWriteTests.h @@ -0,0 +1,26 @@ +#pragma once + +extern "C" +{ + +NATIVE_TESTS_EXPORT bool ReadAndWriteSeparateHandles(const char* fileVirtualPath); + +NATIVE_TESTS_EXPORT bool ReadAndWriteSameHandle(const char* fileVirtualPath, bool synchronousIO); + +NATIVE_TESTS_EXPORT bool ReadAndWriteRepeatedly(const char* fileVirtualPath, bool synchronousIO); + +NATIVE_TESTS_EXPORT bool RemoveReadOnlyAttribute(const char* fileVirtualPath); + +NATIVE_TESTS_EXPORT bool CannotWriteToReadOnlyFile(const char* fileVirtualPath); + +NATIVE_TESTS_EXPORT bool EnumerateAndReadDoesNotChangeEnumerationOrder(const char* folderVirtualPath); + +NATIVE_TESTS_EXPORT bool EnumerationErrorsMatchNTFSForNonExistentFolder(const char* nonExistentVirtualPath, const char* nonExistentPhysicalPath); + +NATIVE_TESTS_EXPORT bool EnumerationErrorsMatchNTFSForEmptyFolder(const char* emptyFolderVirtualPath, const char* emptyFolderPhysicalPath); + +NATIVE_TESTS_EXPORT bool CanDeleteEmptyFolderWithFileDispositionOnClose(const char* emptyFolderPath); + +NATIVE_TESTS_EXPORT bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(const char* fileVirtualPath, const char* fileNTFSPath, int creationDisposition); + +} diff --git a/GVFS/GVFS.NativeTests/interface/TrailingSlashTests.h b/GVFS/GVFS.NativeTests/interface/TrailingSlashTests.h new file mode 100644 index 00000000..33f54331 --- /dev/null +++ b/GVFS/GVFS.NativeTests/interface/TrailingSlashTests.h @@ -0,0 +1,7 @@ + +#pragma once + +extern "C" +{ + NATIVE_TESTS_EXPORT bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(const char* virtualRootPath); +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_BugRegressionTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_BugRegressionTest.cpp new file mode 100644 index 00000000..dfb57eb1 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_BugRegressionTest.cpp @@ -0,0 +1,299 @@ +#include "stdafx.h" +#include "GVFlt_BugRegressionTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_BugRegressionTest"); + +bool GVFlt_ModifyFileInScratchAndDir(const char* virtualRootPath) +{ + // For bug #7700746 - File size is not updated when writing to a file projected from GVFlt app (e.g. GVFS, test app) + + try + { + std::string testScratch = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_ModifyFileInScratchAndDir\\"); + std::string fileName = "ModifyFileInScratchAndDir.txt"; + + WriteToFile(testScratch + fileName, "ModifyFileInScratchAndDir:test data", false); + + std::vector entries = EnumDirectory(testScratch); + VERIFY_ARE_EQUAL((size_t)3, entries.size()); + VERIFY_ARE_EQUAL(fileName, entries[2].Name); + VERIFY_ARE_EQUAL(true, entries[2].IsFile); + VERIFY_ARE_EQUAL(35, (LONGLONG)entries[2].FileSize); + } + catch (TestException&) + { + return false; + } + + return true; +} + +void RMDIR(const std::string& path) +{ + WIN32_FIND_DATA ffd; + + std::vector folders; + + std::string query = path + "*"; + + HANDLE hFind = FindFirstFile(query.c_str(), &ffd); + + if (hFind == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("FindFirstFile failed"); + } + + do + { + if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + folders.push_back(ffd.cFileName); + } + else { + auto fileName = CombinePath(path, ffd.cFileName); + + if (FALSE == DeleteFile(fileName.c_str())) { + VERIFY_FAIL("DeleteFile failed"); + } + } + } while (FindNextFile(hFind, &ffd) != 0); + + auto dwError = GetLastError(); + if (dwError != ERROR_NO_MORE_FILES) + { + VERIFY_FAIL("FindNextFile failed"); + } + + FindClose(hFind); + + for (auto folder : folders) { + if (folder != "." && folder != "..") { + RMDIR(folder); + } + } + + if (FALSE == RemoveDirectory(path.c_str())) { + VERIFY_FAIL("RemoveDirectory failed"); + } +} + +void RMDIRTEST(const std::string& virtualRootPath, const std::string& testName, const std::vector& fileNamesInScratch) +{ + std::string testCaseScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + testName + "\\"; + + for (const std::string& fileName : fileNamesInScratch) { + CreateNewFile(testCaseScratchRoot + fileName); + } + + RMDIR(testCaseScratchRoot); + + auto handle = CreateFile((TEST_ROOT_FOLDER + "\\" + testName).c_str(), + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, handle); +} + +bool GVFlt_RMDIRTest1(const char* virtualRootPath) +{ + // Bug #7703883 - GvFlt: RMDIR /s against a partial folder returns directory not empty error + + try + { + // layer: 1, 2 + // scratch: 3 + std::vector scratchNames = { "3" }; + RMDIRTEST(virtualRootPath, "RMDIRTest1", scratchNames); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_RMDIRTest2(const char* virtualRootPath) +{ + try + { + // layer: 1 + // scratch: 2, 3 + std::vector scratchNames = { "2", "3" }; + RMDIRTEST(virtualRootPath, "RMDIRTest2", scratchNames); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_RMDIRTest3(const char* virtualRootPath) +{ + try + { + // layer: 1, 3 + // scratch: 2 + std::vector scratchNames = { "2" }; + RMDIRTEST(virtualRootPath, "RMDIRTest3", scratchNames); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_RMDIRTest4(const char* virtualRootPath) +{ + try + { + // layer: 2 + // scratch: 1, 3 + std::vector scratchNames = { "1", "3" }; + RMDIRTEST(virtualRootPath, "RMDIRTest4", scratchNames); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_RMDIRTest5(const char* virtualRootPath) +{ + try + { + // layer: 1, 2, 4 + // scratch: 2, 3 + std::string testCaseScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\RMDIRTest5\\"; + OpenForRead(testCaseScratchRoot + "2"); + + std::vector scratchNames = { "3" }; + RMDIRTEST(virtualRootPath, "RMDIRTest5", scratchNames); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeepNonExistFileUnderPartial(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\DeepNonExistFileUnderPartial\\"; + + // try to open a deep non existing file + CreateFile((testScratchRoot + "a\\b\\c\\d\\e").c_str(), + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + VERIFY_ARE_EQUAL((DWORD)ERROR_PATH_NOT_FOUND, GetLastError()); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SupersededReparsePoint(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\SupersededReparsePoint\\"; + + std::string path = testScratchRoot + "test.txt"; + + std::shared_ptr openThreadHandle; + std::shared_ptr openThread1Handle; + std::shared_ptr truncateThreadHandle; + + std::thread openThread([path, &openThreadHandle]() { + + openThreadHandle = std::shared_ptr( + CreateFile(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL), + CloseHandle); + + if (openThreadHandle.get() == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile for read failed (openThread)"); + } + }); + + std::thread openThread1([path, &openThread1Handle]() { + + openThread1Handle = std::shared_ptr( + CreateFile(path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL), + CloseHandle); + + if (openThread1Handle.get() == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile for read failed (openThread1)"); + } + }); + + std::thread truncateThread([path, &truncateThreadHandle]() { + truncateThreadHandle = std::shared_ptr( + CreateFile(path.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + TRUNCATE_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL), + CloseHandle); + + if (truncateThreadHandle.get() == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile for truncate failed (openThread1)"); + } + }); + + openThread.join(); + openThread1.join(); + truncateThread.join(); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFileTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFileTest.cpp new file mode 100644 index 00000000..03a84584 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFileTest.cpp @@ -0,0 +1,336 @@ +#include "stdafx.h" +#include "GVFlt_DeleteFileTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_DeleteFileTest"); + +bool GVFlt_DeleteVirtualFile_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualFile_SetDisposition\\"); + DWORD error = DelFile(testScratchRoot + "a.txt"); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteVirtualFile_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualFile_DeleteOnClose\\"); + DWORD error = DelFile(testScratchRoot + "a.txt", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeletePlaceholder_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholder_SetDisposition\\"); + + // make a.txt a placeholder + ReadFileAsString(testScratchRoot + "a.txt"); + DWORD error = DelFile(testScratchRoot + "a.txt"); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeletePlaceholder_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholder_DeleteOnClose\\"); + + // make a.txt a placeholder + ReadFileAsString(testScratchRoot + "a.txt"); + DWORD error = DelFile(testScratchRoot + "a.txt", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteFullFile_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteFullFile_SetDisposition\\"); + + // make a.txt a full file + WriteToFile(testScratchRoot + "a.txt", "123123"); + DWORD error = DelFile(testScratchRoot + "a.txt"); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteFullFile_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteFullFile_DeleteOnClose\\"); + + // make a.txt a full file + WriteToFile(testScratchRoot + "a.txt", "123123"); + DWORD error = DelFile(testScratchRoot + "a.txt", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "a.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteLocalFile_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalFile_SetDisposition\\"); + + CreateNewFile(testScratchRoot + "c3.txt", "123123"); + DWORD error = DelFile(testScratchRoot + "c3.txt"); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "a.txt", "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "c3.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteLocalFile_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalFile_DeleteOnClose\\"); + + CreateNewFile(testScratchRoot + "c4.txt", "123123"); + DWORD error = DelFile(testScratchRoot + "c4.txt", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "a.txt", "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "c4.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNotExistFile_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNotExistFile_SetDisposition\\"); + + DWORD error = DelFile(testScratchRoot + "notexist.txt"); + VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, error); + + std::vector expected = { "a.txt", "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNotExistFile_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNotExistFile_DeleteOnClose\\"); + + DWORD error = DelFile(testScratchRoot + "notexist.txt", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, error); + + std::vector expected = { "a.txt", "b.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNonRootVirtualFile_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFile_SetDisposition\\"); + std::string testFolder = "A\\B\\C\\D\\"; + std::string testFile = "test.txt"; + + DWORD error = DelFile(testScratchRoot + testFolder + testFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = {}; + ExpectDirEntries(testScratchRoot + testFolder, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + testFile)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNonRootVirtualFile_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFile_DeleteOnClose\\"); + std::string testFolder = "A1\\B\\C\\D\\"; + std::string testFile = "test.txt"; + + DWORD error = DelFile(testScratchRoot + testFolder + testFile, false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = {}; + ExpectDirEntries(testScratchRoot + testFolder, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + testFile)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteFileOutsideVRoot_SetDisposition(const char* pathOutsideRepo) +{ + try + { + std::string testFile = pathOutsideRepo + std::string("\\GVFlt_DeleteFileOutsideVRoot_SetDisposition.txt"); + CreateNewFile(testFile); + + DWORD error = DelFile(testFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + VERIFY_ARE_EQUAL(false, DoesFileExist(testFile)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteFileOutsideVRoot_DeleteOnClose(const char* pathOutsideRepo) +{ + try + { + std::string testFile = pathOutsideRepo + std::string("\\GVFlt_DeleteFileOutsideVRoot_DeleteOnClose.txt"); + CreateNewFile(testFile); + + DWORD error = DelFile(testFile, false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + VERIFY_ARE_EQUAL(false, DoesFileExist(testFile)); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFolderTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFolderTest.cpp new file mode 100644 index 00000000..7017cc70 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_DeleteFolderTest.cpp @@ -0,0 +1,261 @@ +#include "stdafx.h" +#include "GVFlt_DeleteFolderTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_DeleteFolderTest"); + + +// -------------------- +// +// Special note on "EmptyFolder". In our tests, this folder actually has a single empty file because Git +// does not allow committing empty folders. +// +// -------------------- + + +bool GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualNonEmptyFolder_SetDisposition\\"); + + DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder"); + VERIFY_ARE_EQUAL((DWORD)ERROR_DIR_NOT_EMPTY, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose\\"); + + DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition\\"); + + // make it a placeholder folder + EnumDirectory(testScratchRoot + "NonEmptyFolder"); + + DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder"); + VERIFY_ARE_EQUAL((DWORD)ERROR_DIR_NOT_EMPTY, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose\\"); + + // make it a placeholder folder + EnumDirectory(testScratchRoot + "NonEmptyFolder"); + + DWORD error = DelFolder(testScratchRoot + "NonEmptyFolder", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteLocalEmptyFolder_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalEmptyFolder_SetDisposition\\"); + + // create a new local folder + CreateDirectoryWithIntermediates(testScratchRoot + "localFolder\\"); + + DWORD error = DelFolder(testScratchRoot + "localFolder"); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder\\" + "bar.txt")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteLocalEmptyFolder_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteLocalEmptyFolder_DeleteOnClose\\"); + + // create a new local folder + CreateDirectoryWithIntermediates(testScratchRoot + "localFolder\\"); + + DWORD error = DelFolder(testScratchRoot + "localFolder", false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = { "EmptyFolder", "NonEmptyFolder", "TestFile.txt" }; + ExpectDirEntries(testScratchRoot, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot)); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "EmptyFolder")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "NonEmptyFolder\\" + "bar.txt")); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "NonEmptyFolder")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNonRootVirtualFolder_SetDisposition(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFolder_SetDisposition\\"); + + std::string testFolder = "A\\B\\C\\D\\"; + std::string targetFolder = "E\\"; + std::string testFile = "test.txt"; + + // NOTE: Deviate from GVFlt's DeleteNonRootVirtualFolder_SetDisposition here by deleting a file first + // Git will not allow empty folders to be commited, and so \E must have a file in it + DWORD fileError = DelFile(testScratchRoot + testFolder + targetFolder + testFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, fileError); + + DWORD error = DelFolder(testScratchRoot + testFolder + targetFolder); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = {}; + ExpectDirEntries(testScratchRoot + testFolder, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + targetFolder)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose\\"); + + std::string testFolder = "A\\B\\C\\D\\"; + std::string targetFolder = "E\\"; + std::string testFile = "test.txt"; + + // NOTE: Deviate from GVFlt's DeleteNonRootVirtualFolder_DeleteOnClose here by deleting a file first + // Git will not allow empty folders to be commited, and so \E must have a file in it + DWORD fileError = DelFile(testScratchRoot + testFolder + targetFolder + testFile); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, fileError); + + DWORD error = DelFolder(testScratchRoot + testFolder + targetFolder, false); + VERIFY_ARE_EQUAL((DWORD)ERROR_SUCCESS, error); + + std::vector expected = {}; + ExpectDirEntries(testScratchRoot + testFolder, expected); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + testFolder)); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + testFolder + targetFolder)); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_DirEnumTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_DirEnumTest.cpp new file mode 100644 index 00000000..e20f8c70 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_DirEnumTest.cpp @@ -0,0 +1,211 @@ +#include "stdafx.h" +#include "GVFlt_DirEnumTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "Should.h" + +using namespace TestHelpers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_EnumTest"); + +bool GVFlt_EnumEmptyFolder(const char* virtualRootPath) +{ + try + { + std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumEmptyFolder\\"); + CreateDirectoryWithIntermediates(folderPath); + + std::vector result = EnumDirectory(folderPath); + VERIFY_ARE_EQUAL((size_t)2, result.size()); + VERIFY_ARE_EQUAL(false, result[0].IsFile); + VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0); + VERIFY_ARE_EQUAL(false, result[1].IsFile); + VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_EnumFolderWithOneFileInPackage(const char* virtualRootPath) +{ + try + { + std::vector result = EnumDirectory(virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInPackage\\")); + VERIFY_ARE_EQUAL((size_t)3, result.size()); + VERIFY_ARE_EQUAL(false, result[0].IsFile); + VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0); + VERIFY_ARE_EQUAL(false, result[1].IsFile); + VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0); + VERIFY_ARE_EQUAL(true, result[2].IsFile); + VERIFY_ARE_EQUAL("onlyFileInFolder.txt", result[2].Name); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_EnumFolderWithOneFileInBoth(const char* virtualRootPath) +{ + try + { + std::string existingFileName = "newfileInPackage.txt"; + std::string newFileName = "newfileInScratch.txt"; + std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInBoth\\"); + CreateNewFile(folderPath + newFileName); + + std::vector result = EnumDirectory(folderPath); + VERIFY_ARE_EQUAL((size_t)4, result.size()); + VERIFY_ARE_EQUAL(false, result[0].IsFile); + VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0); + VERIFY_ARE_EQUAL(false, result[1].IsFile); + VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0); + VERIFY_ARE_EQUAL(true, result[2].IsFile); + VERIFY_ARE_EQUAL(existingFileName, result[2].Name); + VERIFY_ARE_EQUAL(true, result[3].IsFile); + VERIFY_ARE_EQUAL(newFileName, result[3].Name); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_EnumFolderWithOneFileInBoth1(const char* virtualRootPath) +{ + try + { + std::string existingFileName = "newfileInPackage.txt"; + std::string newFileName = "123"; + std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderWithOneFileInBoth1\\"); + CreateNewFile(folderPath + newFileName); + + std::vector result = EnumDirectory(folderPath); + VERIFY_ARE_EQUAL((size_t)4, result.size()); + VERIFY_ARE_EQUAL(false, result[0].IsFile); + VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0); + VERIFY_ARE_EQUAL(false, result[1].IsFile); + VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0); + VERIFY_ARE_EQUAL(true, result[2].IsFile); + VERIFY_ARE_EQUAL(newFileName, result[2].Name); + VERIFY_ARE_EQUAL(true, result[3].IsFile); + VERIFY_ARE_EQUAL(existingFileName, result[3].Name); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_EnumFolderDeleteExistingFile(const char* virtualRootPath) +{ + try + { + std::string fileName1 = "fileInPackage1.txt"; + std::string fileName2 = "fileInPackage2.txt"; + std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderDeleteExistingFile\\"); + + VERIFY_ARE_EQUAL(TRUE, DeleteFile((folderPath + fileName1).c_str())); + + std::vector result = EnumDirectory(folderPath); + VERIFY_ARE_EQUAL((size_t)3, result.size()); + VERIFY_ARE_EQUAL(false, result[0].IsFile); + VERIFY_ARE_EQUAL(strcmp(".", result[0].Name.c_str()), 0); + VERIFY_ARE_EQUAL(false, result[1].IsFile); + VERIFY_ARE_EQUAL(strcmp("..", result[1].Name.c_str()), 0); + VERIFY_ARE_EQUAL(true, result[2].IsFile); + VERIFY_ARE_EQUAL(fileName2, result[2].Name); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_EnumFolderSmallBuffer(const char* virtualRootPath) +{ + std::string folderPath = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\GVFlt_EnumFolderSmallBuffer"); + + try + { + UCHAR buffer[512]; + NTSTATUS status; + BOOLEAN restart = TRUE; + IO_STATUS_BLOCK ioStatus; + USHORT count = 0; + + for (USHORT i = 0; i < 26; i += 2) { + + // open every other file to create placeholders + std::string name(1, (char)('a' + i)); + OpenForRead(folderPath + std::string("\\") + name); + } + + std::shared_ptr handle = OpenForRead(folderPath); + + do + { + status = NtQueryDirectoryFile(handle.get(), + NULL, + NULL, + NULL, + &ioStatus, + buffer, + ARRAYSIZE(buffer), + FileBothDirectoryInformation, + FALSE, + NULL, + restart); + + if (status == STATUS_SUCCESS) { + + PFILE_BOTH_DIR_INFORMATION dirInfo; + PUCHAR entry = buffer; + + do { + + dirInfo = (PFILE_BOTH_DIR_INFORMATION)entry; + + std::wstring entryName(dirInfo->FileName, dirInfo->FileNameLength / sizeof(WCHAR)); + + if ((entryName.compare(L".") != 0) && (entryName.compare(L"..") != 0)) { + + std::wstring expectedName(1, L'a' + count); + + VERIFY_ARE_EQUAL(entryName, expectedName); + + count++; + } + + entry = entry + dirInfo->NextEntryOffset; + + } while (dirInfo->NextEntryOffset > 0); + + restart = FALSE; + } + + } while (status == STATUS_SUCCESS); + + VERIFY_ARE_EQUAL(count, 26); + VERIFY_ARE_EQUAL(status, STATUS_NO_MORE_FILES); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_FileAttributeTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_FileAttributeTest.cpp new file mode 100644 index 00000000..3c7c7279 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_FileAttributeTest.cpp @@ -0,0 +1,107 @@ +#include "stdafx.h" +#include "GVFlt_FileAttributeTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "Should.h" + +using namespace TestHelpers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileAttributeTest"); + +bool GVFlt_ModifyFileInScratchAndCheckLastWriteTime(const char* virtualRootPath) +{ + try + { + std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\ModifyFileInScratchAndCheckLastWriteTime.txt"); + + FILETIME lastWriteTime_Package = GetLastWriteTime(fileName); + WriteToFile(fileName, "test data", false); + FILETIME lastWriteTime_scratch = GetLastWriteTime(fileName); + + // last write time is has been updated + // NOTE: This is slightly different than the validate in GVFlt, which tests if the scratch time is different than the layer time + VERIFY_ARE_NOT_EQUAL(0, CompareFileTime(&lastWriteTime_Package, &lastWriteTime_scratch)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_FileSize(const char* virtualRootPath) +{ + try + { + std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\FileSize.txt"); + LARGE_INTEGER file_size_scratch = GetFileSize(fileName); + VERIFY_ARE_EQUAL(7, file_size_scratch.QuadPart); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_ModifyFileInScratchAndCheckFileSize(const char* virtualRootPath) +{ + try + { + std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\ModifyFileInScratchAndCheckFileSize.txt"); + + LARGE_INTEGER fileSize_Package = GetFileSize(fileName); + + WriteToFile(fileName, "ModifyFileInScratchAndCheckFileSize:test data", false); + LARGE_INTEGER file_size_scratch = GetFileSize(fileName); + + VERIFY_ARE_NOT_EQUAL(fileSize_Package.QuadPart, file_size_scratch.QuadPart); + } + catch (TestException&) + { + return false; + } + + return true; +} + +namespace +{ + +void TestFileAttribute(const std::string& fileName, DWORD attribute) +{ + std::string data = "TestFileAttribute: some test data"; + + BOOL success = SetFileAttributes(fileName.c_str(), attribute); + VERIFY_ARE_EQUAL(TRUE, success); + + BOOL attrScratch = GetFileAttributes(fileName.c_str()); + VERIFY_ARE_EQUAL(attribute, attribute&attrScratch); +} + +} + +bool GVFlt_FileAttributes(const char* virtualRootPath) +{ + try + { + std::string testRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\"; + + TestFileAttribute(testRoot + "FileAttributes_ARCHIVE", FILE_ATTRIBUTE_ARCHIVE); + TestFileAttribute(testRoot + "FileAttributes_HIDDEN", FILE_ATTRIBUTE_HIDDEN); + TestFileAttribute(testRoot + "FileAttributes_NOT_CONTENT_INDEXED", FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + //TestFileAttribute(FILE_ATTRIBUTE_OFFLINE); + TestFileAttribute(testRoot + "FileAttributes_READONLY", FILE_ATTRIBUTE_READONLY); + TestFileAttribute(testRoot + "FileAttributes_SYSTEM", FILE_ATTRIBUTE_SYSTEM); + TestFileAttribute(testRoot + "FileAttributes_TEMPORARY", FILE_ATTRIBUTE_TEMPORARY); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_FileEATest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_FileEATest.cpp new file mode 100644 index 00000000..6cbe4551 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_FileEATest.cpp @@ -0,0 +1,35 @@ +#include "stdafx.h" +#include "GVFlt_FileEATest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "Should.h" + +using namespace TestHelpers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileEATest"); + +bool GVFlt_OneEAAttributeWillPass(const char* virtualRootPath) +{ + try + { + std::string fileName = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\OneEAAttributeWillPass.txt"); + + ULONG size = 2 * 65535; + PFILE_FULL_EA_INFORMATION buffer = (PFILE_FULL_EA_INFORMATION)calloc(1, size); + auto status = SetEAInfo(fileName, buffer, size); + VERIFY_ARE_EQUAL(STATUS_SUCCESS, status); + + OpenForRead(fileName); + status = ReadEAInfo(fileName, buffer, &size); + VERIFY_ARE_EQUAL(STATUS_SUCCESS, status); + VERIFY_ARE_NOT_EQUAL((ULONG)0, size); + + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_FileOperationTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_FileOperationTest.cpp new file mode 100644 index 00000000..c33fc7d7 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_FileOperationTest.cpp @@ -0,0 +1,110 @@ +#include "stdafx.h" +#include "GVFlt_FileOperationTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_FileOperationTest"); + +bool GVFlt_OpenRootFolder(const char* virtualRootPath) +{ + try + { + OpenForRead(virtualRootPath); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_WriteAndVerify(const char* virtualRootPath) +{ + try + { + std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\"; + std::string fileName = "WriteAndVerify.txt"; + std::string data = "test data\r\n"; + + // write file in scratch + std::string newData = "new data"; + WriteToFile(scratchRoot + fileName, newData, false); + + std::string newContent = newData + data.substr(newData.size()); + + VERIFY_ARE_EQUAL(newContent, ReadFileAsString(scratchRoot + fileName)); + VERIFY_ARE_EQUAL(newContent, ReadFileAsStringUncached(scratchRoot + fileName)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_DeleteExistingFile(const char* virtualRootPath) +{ + try + { + std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\"; + std::string fileName = "DeleteExistingFile.txt"; + + std::string fileInScratch = scratchRoot + fileName; + // delete in scratch root + VERIFY_ARE_EQUAL(TRUE, DeleteFile(fileInScratch.c_str())); + + // make sure the file can't be opened again + auto hFileInScratch = CreateFile(fileInScratch.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + + VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, hFileInScratch); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_OpenNonExistingFile(const char* virtualRootPath) +{ + try + { + std::string scratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\"; + std::string fileName = "OpenNonExistingFile.txt"; + + std::string fileInScratch = scratchRoot + fileName; + + // make sure the file can't be opened and last error is file not found + HANDLE hFileInScratch = CreateFile(fileInScratch.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + + VERIFY_ARE_EQUAL(INVALID_HANDLE_VALUE, hFileInScratch); + VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, (HRESULT)GetLastError()); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_MoveFileTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_MoveFileTest.cpp new file mode 100644 index 00000000..8401d051 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_MoveFileTest.cpp @@ -0,0 +1,471 @@ +#include "stdafx.h" +#include "GVFlt_MoveFileTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_MoveFileTest"); +static const std::string _lessData = "lessData"; +static const std::string _moreData = "moreData, moreData, moreData"; + +bool GVFlt_MoveFile_NoneToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToNone\\"; + + int error = MovFile(testScratchRoot + "from\\filenotexist", testScratchRoot + "to\\filenotexist"); + VERIFY_ARE_EQUAL((int)ENOENT, error); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "to")); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "ffrom\\filenotexist")); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\filenotexist")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNone\\"; + + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\notexistInTo.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\notexistInTo.txt")); + + VERIFY_ARE_EQUAL(_lessData, ReadFileAsString(testScratchRoot + "to\\notexistInTo.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_PartialToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToNone\\"; + + std::string expected = ReadFileAsString(testScratchRoot + "from\\lessInFrom.txt"); + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\PartialToNone.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\PartialToNone.txt")); + + AreEqual(expected, ReadFileAsString(testScratchRoot + "to\\PartialToNone.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_FullToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "FullToNone\\"; + + WriteToFile(testScratchRoot + "from\\lessInFrom.txt", "testtest"); + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\FullToNone.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\FullToNone.txt")); + + AreEqual("testtest", ReadFileAsString(testScratchRoot + "to\\FullToNone.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_LocalToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LocalToNone\\"; + + CreateNewFile(testScratchRoot + "from\\local.txt", "test"); + int error = MovFile(testScratchRoot + "from\\local.txt", testScratchRoot + "to\\notexistInTo.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\local.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\notexistInTo.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToVirtual(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtual\\"; + + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToVirtualFileNameChanged(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtualFileNameChanged\\"; + + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\moreInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\moreInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToPartial(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToPartial\\"; + + ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"); + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_PartialToPartial(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToPartial\\"; + + ReadFileAsString(testScratchRoot + "from\\lessInFrom.txt"); + ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"); + int error = MovFile(testScratchRoot + "from\\lessInFrom.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_LocalToVirtual(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LocalToVirtual\\"; + + CreateNewFile(testScratchRoot + "from\\local.txt", _lessData); + + int error = MovFile(testScratchRoot + "from\\local.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\local.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToVirtualIntermidiateDirNotExist(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtualIntermidiateDirNotExist\\"; + + int error = MovFile(testScratchRoot + "from\\subFolder\\from.txt", testScratchRoot + "to\\subfolder\\to.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\subFolder\\from.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\subfolder\\to.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_VirtualToNoneIntermidiateDirNotExist(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNoneIntermidiateDirNotExist\\"; + + int error = MovFile(testScratchRoot + "from\\subFolder\\from.txt", testScratchRoot + "to\\notexist\\to.txt"); + VERIFY_ARE_EQUAL((int)ENOENT, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from\\subFolder\\from.txt")); + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\notexist\\to.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + + +bool GVFlt_MoveFile_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToNone\\"; + + std::string outsideFolder = std::string(pathOutsideRepo) + "\\OutsideToNone\\from\\"; + CreateDirectoryWithIntermediates(outsideFolder); + CreateNewFile(outsideFolder + "lessInFrom.txt", _lessData); + + int error = MovFile(outsideFolder + "lessInFrom.txt", testScratchRoot + "to\\less.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\less.txt")); + VERIFY_ARE_EQUAL(false, DoesFileExist(outsideFolder + "lessInFrom.txt")); + + AreEqual(_lessData, ReadFileAsString(testScratchRoot + "to\\less.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + + +bool GVFlt_MoveFile_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToVirtual\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToVirtual\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + CreateNewFile(testLayerRoot + "test.txt"); + + int error = MovFile(testLayerRoot + "test.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "test.txt")); + + AreEqual(_moreData, ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + + +bool GVFlt_MoveFile_OutsideToPartial(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToPartial\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToPartial\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + CreateNewFile(testLayerRoot + "test.txt"); + + ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"); + int error = MovFile(testLayerRoot + "test.txt", testScratchRoot + "to\\lessInFrom.txt"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "test.txt")); + + AreEqual(_moreData, ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + + +bool GVFlt_MoveFile_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToOutside\\"; + + int error = MovFile(testScratchRoot + "to\\less.txt", std::string(pathOutsideRepo) + "\\" + "from\\less.txt"); + VERIFY_ARE_EQUAL((int)ENOENT, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\less.txt")); + VERIFY_ARE_EQUAL(false, DoesFileExist(std::string(pathOutsideRepo) + "\\" + "from\\less.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + + +bool GVFlt_MoveFile_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToOutside\\"; + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "VirtualToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + + int error = MovFile(testScratchRoot + "to\\lessInFrom.txt", testLayerRoot + "less.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + // VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "less.txt")); + + AreEqual(_moreData, ReadFileAsString(testLayerRoot + "less.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_PartialToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToOutside\\"; + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "PartialToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + + ReadFileAsString(testScratchRoot + "to\\lessInFrom.txt"); + int error = MovFile(testScratchRoot + "to\\lessInFrom.txt", testLayerRoot + "less.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "to\\lessInFrom.txt")); + // VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "less.txt")); + + AreEqual(_moreData, ReadFileAsString(testLayerRoot + "less.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToOutside\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + CreateNewFile(testLayerRoot + "from.txt", _lessData); + + int error = MovFile(testLayerRoot + "from.txt", testLayerRoot + "to.txt"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot + "from.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "to.txt")); + + AreEqual(_lessData, ReadFileAsString(testLayerRoot + "to.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFile_LongFileName(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "LongFileName\\"; + std::string filename = "LLLLLLongName0ToRenameFileToWithForChangeJournalABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghojnklmopqrstuvwxyz0123456789aaaaa"; + + int error = MovFile(testScratchRoot + filename, testScratchRoot + filename + "1"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + filename)); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + filename + "1")); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_MoveFolderTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_MoveFolderTest.cpp new file mode 100644 index 00000000..22abf4a4 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_MoveFolderTest.cpp @@ -0,0 +1,237 @@ +#include "stdafx.h" +#include "GVFlt_MoveFolderTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_MoveFolderTest"); + +bool GVFlt_MoveFolder_NoneToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToNone\\"; + + int error = MovFile(testScratchRoot + "fromfolderNotExist", testScratchRoot + "tofolderNotExist"); + VERIFY_ARE_EQUAL((int)ENOENT, error); + + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(false, IsFullFolder(testScratchRoot + "to")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_VirtualToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToNone\\"; + + int error = MovFile(testScratchRoot + "from", testScratchRoot + "tofolderNotExist"); + VERIFY_ARE_EQUAL((int)EINVAL, error); + + // VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\notexistInTo.txt")); + // VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "tofolderNotExist\\notexistInTo.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_PartialToNone(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "PartialToNone\\"; + + ReadFileAsString(testScratchRoot + "from\\notexistInTo.txt"); + int error = MovFile(testScratchRoot + "from", testScratchRoot + "tofolderNotExist"); + VERIFY_ARE_EQUAL((int)EINVAL, error); + + // VERIFY_ARE_EQUAL(false, DoesFileExist(testScratchRoot + "from\\notexistInTo.txt")); + // VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "tofolderNotExist\\notexistInTo.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_VirtualToVirtual(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToVirtual\\"; + + int error = MovFile(testScratchRoot + "from", testScratchRoot + "to"); + VERIFY_ARE_EQUAL((int)EINVAL, error); + /* + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to")); + */ + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_VirtualToPartial(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToPartial\\"; + + ReadFileAsString(testScratchRoot + "to\\notexistInFrom.txt"); + int error = MovFile(testScratchRoot + "from", testScratchRoot + "to"); + VERIFY_ARE_EQUAL((int)EINVAL, error); + + /* + VERIFY_ARE_EQUAL((int)EEXIST, error); + + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to")); + */ + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_OutsideToNone(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToNone\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToNone"; + CreateDirectoryWithIntermediates(testLayerRoot); + + int error = MovFile(testLayerRoot, testScratchRoot + "notexists"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot)); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "notexists")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_OutsideToVirtual(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToVirtual\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToVirtual\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + + int error = MovFile(testLayerRoot, testScratchRoot); + VERIFY_ARE_EQUAL((int)EEXIST, error); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to")); + + error = MovFile(testLayerRoot, testScratchRoot + "to"); + VERIFY_ARE_EQUAL((int)EEXIST, error); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "from")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testScratchRoot + "to")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_NoneToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NoneToOutside\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "NoneToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + + int error = MovFile(testScratchRoot + "NotExist", testLayerRoot); + VERIFY_ARE_EQUAL((int)ENOENT, error); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_VirtualToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "VirtualToOutside\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "VirtualToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot); + + int error = MovFile(testScratchRoot + "from", testLayerRoot); + VERIFY_ARE_EQUAL((int)EINVAL, error); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_MoveFolder_OutsideToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OutsideToOutside\\"; + + std::string testLayerRoot = std::string(pathOutsideRepo) + "\\" + "OutsideToOutside\\"; + CreateDirectoryWithIntermediates(testLayerRoot + "from\\"); + CreateNewFile(testLayerRoot + "from\\" + "test.txt", "test data"); + + int error = MovFile(testLayerRoot + "from\\", testLayerRoot + "to\\"); + VERIFY_ARE_EQUAL((int)0, error); + + VERIFY_ARE_EQUAL(false, DoesFileExist(testLayerRoot + "from\\" + "test.txt")); + VERIFY_ARE_EQUAL(true, DoesFileExist(testLayerRoot + "to\\" + "test.txt")); + + VERIFY_ARE_EQUAL("test data", ReadFileAsString(testLayerRoot + "to\\" + "test.txt")); + } + catch (TestException&) + { + return false; + } + + return true; +} diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_MultiThreadTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_MultiThreadTest.cpp new file mode 100644 index 00000000..31857e27 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_MultiThreadTest.cpp @@ -0,0 +1,104 @@ +#include "stdafx.h" +#include "GVFlt_MultiThreadsTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFLT_MultiThreadTest"); + +static void ReadFileThreadProc(HANDLE& hFile, const std::string& path) +{ + hFile = CreateFile(path.c_str(), + FILE_READ_ATTRIBUTES, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile failed"); + } +} + +bool GVFlt_OpenForReadsSameTime(const char* virtualRootPath) +{ + try + { + std::string scratchTestRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenForReadsSameTime\\"; + + const int threadCount = 10; + std::thread threadList[threadCount]; + std::array handles; + for (auto i = 0; i < threadCount; i++) { + threadList[i] = std::thread(ReadFileThreadProc, std::ref(handles[i]), scratchTestRoot + "test"); + } + + for (auto i = 0; i < threadCount; i++) { + threadList[i].join(); + } + + for (HANDLE hFile : handles) + { + CloseHandle(hFile); + } + } + catch (TestException&) + { + return false; + } + + return true; +} + +static void WriteFileThreadProc(HANDLE& hFile, const std::string& path) +{ + hFile = CreateFile(path.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + VERIFY_FAIL("CreateFile failed"); + } +} + +bool GVFlt_OpenForWritesSameTime(const char* virtualRootPath) +{ + try + { + std::string scratchTestRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "OpenForWritesSameTime\\"; + + const int threadCount = 10; + std::thread threadList[threadCount]; + std::array handles; + for (auto i = 0; i < threadCount; i++) { + threadList[i] = std::thread(WriteFileThreadProc, std::ref(handles[i]), scratchTestRoot + "test"); + } + + for (auto i = 0; i < threadCount; i++) { + threadList[i].join(); + } + + for (HANDLE hFile : handles) + { + CloseHandle(hFile); + } + } + catch (TestException&) + { + return false; + } + + return true; +} diff --git a/GVFS/GVFS.NativeTests/source/GVFlt_SetLinkTest.cpp b/GVFS/GVFS.NativeTests/source/GVFlt_SetLinkTest.cpp new file mode 100644 index 00000000..e22093f9 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/GVFlt_SetLinkTest.cpp @@ -0,0 +1,148 @@ +#include "stdafx.h" +#include "GVFlt_SetLinkTest.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; +using namespace TestVerifiers; + +static const std::string TEST_ROOT_FOLDER("\\GVFlt_SetLinkTest"); +static const std::string _testFile("test.txt"); +static const std::string _data("test data"); + +bool GVFlt_SetLink_ToVirtualFile(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToVirtualFile\\"; + + bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile); + VERIFY_ARE_EQUAL(true, created); + + AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_ToPlaceHolder(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToPlaceHolder\\"; + + ReadFileAsString(testScratchRoot + _testFile); + + bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile); + VERIFY_ARE_EQUAL(true, created); + + AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_ToFullFile(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToFullFile\\"; + + WriteToFile(testScratchRoot + _testFile, "new content"); + + bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + _testFile); + VERIFY_ARE_EQUAL(true, created); + + AreEqual("new content", ReadFileAsString(testScratchRoot + _testFile)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_ToNonExistFileWillFail(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToNonExistFileWillFail\\"; + + bool created = NewHardLink(testScratchRoot + "newlink", testScratchRoot + "nonexist"); + VERIFY_ARE_EQUAL(false, created); + VERIFY_ARE_EQUAL((DWORD)ERROR_FILE_NOT_FOUND, GetLastError()); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_NameAlreadyExistWillFail(const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "NameAlreadyExistWillFail\\"; + + bool created = NewHardLink(testScratchRoot + "foo.txt", testScratchRoot + _testFile); + VERIFY_ARE_EQUAL(false, created); + VERIFY_ARE_EQUAL((DWORD)ERROR_ALREADY_EXISTS, GetLastError()); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_FromOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "FromOutside\\"; + + bool created = NewHardLink(std::string(pathOutsideRepo) + "\\" + "FromOutsideLink", testScratchRoot + _testFile); + VERIFY_ARE_EQUAL(true, created); + AreEqual(_data, ReadFileAsString(std::string(pathOutsideRepo) + "\\" + "FromOutsideLink")); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool GVFlt_SetLink_ToOutside(const char* pathOutsideRepo, const char* virtualRootPath) +{ + try + { + std::string testScratchRoot = virtualRootPath + TEST_ROOT_FOLDER + "\\" + "ToOutside\\"; + + CreateNewFile(std::string(pathOutsideRepo) + "\\" + _testFile, _data); + bool created = NewHardLink(testScratchRoot + "newlink", std::string(pathOutsideRepo) + "\\" + _testFile); + VERIFY_ARE_EQUAL(true, created); + AreEqual(_data, ReadFileAsString(testScratchRoot + "newlink")); + } + catch (TestException&) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/NtFunctions.cpp b/GVFS/GVFS.NativeTests/source/NtFunctions.cpp new file mode 100644 index 00000000..b03a9777 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/NtFunctions.cpp @@ -0,0 +1,45 @@ +#include "stdafx.h" +#include "NtFunctions.h" +#include "Should.h" + +namespace +{ + PQueryDirectoryFile ntQueryDirectoryFile; +} + +NTSTATUS NtQueryDirectoryFile( + _In_ HANDLE FileHandle, + _In_opt_ HANDLE Event, + _In_opt_ PIO_APC_ROUTINE ApcRoutine, + _In_opt_ PVOID ApcContext, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _Out_ PVOID FileInformation, + _In_ ULONG Length, + _In_ FILE_INFORMATION_CLASS FileInformationClass, + _In_ BOOLEAN ReturnSingleEntry, + _In_opt_ PUNICODE_STRING FileName, + _In_ BOOLEAN RestartScan +) +{ + if (ntQueryDirectoryFile == NULL) + { + HMODULE ntdll = LoadLibrary("ntdll.dll"); + SHOULD_NOT_EQUAL(ntdll, NULL); + + ntQueryDirectoryFile = (PQueryDirectoryFile)GetProcAddress(ntdll, "NtQueryDirectoryFile"); + SHOULD_NOT_EQUAL(ntQueryDirectoryFile, NULL); + } + + return ntQueryDirectoryFile( + FileHandle, + Event, + ApcRoutine, + ApcContext, + IoStatusBlock, + FileInformation, + Length, + FileInformationClass, + ReturnSingleEntry, + FileName, + RestartScan); +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/NtQueryDirectoryFileTests.cpp b/GVFS/GVFS.NativeTests/source/NtQueryDirectoryFileTests.cpp new file mode 100644 index 00000000..cb3ad887 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/NtQueryDirectoryFileTests.cpp @@ -0,0 +1,86 @@ +#include "stdafx.h" +#include "NtQueryDirectoryFileTests.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "Should.h" + +bool QueryDirectoryFileRestartScanResetsFilter(const char* folderPath) +{ + try + { + SHOULD_BE_TRUE(PathIsDirectory(folderPath)); + + SafeHandle folderHandle(CreateFile( + folderPath, // lpFileName + (GENERIC_READ), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_FLAG_BACKUP_SEMANTICS, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_NOT_EQUAL(folderHandle.GetHandle(), INVALID_HANDLE_VALUE); + + IO_STATUS_BLOCK ioStatus; + FILE_NAMES_INFORMATION namesInfo[64]; + memset(namesInfo, 0, sizeof(namesInfo)); + + NTSTATUS status = NtQueryDirectoryFile( + folderHandle.GetHandle(), // FileHandle + NULL, // Event + NULL, // ApcRoutine + NULL, // ApcContext + &ioStatus, // IoStatusBlock + namesInfo, // FileInformation + sizeof(namesInfo), // Length + FileNamesInformation, // FileInformationClass + FALSE, // ReturnSingleEntry + NULL, // FileName + FALSE); // RestartScan + + SHOULD_EQUAL(status, STATUS_SUCCESS); + memset(namesInfo, 0, sizeof(namesInfo)); + + status = NtQueryDirectoryFile( + folderHandle.GetHandle(), // FileHandle + NULL, // Event + NULL, // ApcRoutine + NULL, // ApcContext + &ioStatus, // IoStatusBlock + namesInfo, // FileInformation + sizeof(namesInfo), // Length + FileNamesInformation, // FileInformationClass + FALSE, // ReturnSingleEntry + NULL, // FileName + TRUE); // RestartScan + + SHOULD_EQUAL(status, STATUS_SUCCESS); + memset(namesInfo, 0, sizeof(namesInfo)); + + wchar_t nonExistentFileName[] = L"IDontExist"; + UNICODE_STRING nonExistentFileFilter; + nonExistentFileFilter.Buffer = nonExistentFileName; + nonExistentFileFilter.Length = sizeof(nonExistentFileName) - sizeof(wchar_t); // Length should not include null terminator + nonExistentFileFilter.MaximumLength = sizeof(nonExistentFileName); + + status = NtQueryDirectoryFile( + folderHandle.GetHandle(), // FileHandle + NULL, // Event + NULL, // ApcRoutine + NULL, // ApcContext + &ioStatus, // IoStatusBlock + namesInfo, // FileInformation + sizeof(namesInfo), // Length + FileNamesInformation, // FileInformationClass + FALSE, // ReturnSingleEntry + &nonExistentFileFilter, // FileName + TRUE); // RestartScan + + SHOULD_EQUAL(status, STATUS_NO_MORE_FILES); + } + catch (TestException&) + { + return false; + } + + return true; +} diff --git a/GVFS/GVFS.NativeTests/source/PlaceholderUtils.cpp b/GVFS/GVFS.NativeTests/source/PlaceholderUtils.cpp new file mode 100644 index 00000000..a3e47bca --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/PlaceholderUtils.cpp @@ -0,0 +1,32 @@ +#include "stdafx.h" +#include "PlaceholderUtils.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "TestVerifiers.h" +#include "Should.h" + +using namespace TestHelpers; + +bool PlaceHolderHasVersionInfo(const char* virtualPath, int version, const WCHAR* sha, const WCHAR* commit) +{ + try + { + std::string path(virtualPath); + std::shared_ptr reparseInfo = GetReparseInfo(path); + + SHOULD_EQUAL(reparseInfo->versionInfo.EpochID[0], static_cast(version)); + + SHOULD_EQUAL(std::wstring(sha), std::wstring(static_cast(static_cast(reparseInfo->versionInfo.ContentID)))); + + WCHAR* epoch = static_cast(static_cast(reparseInfo->versionInfo.EpochID + 4)); + SHOULD_EQUAL(std::wstring(commit), std::wstring(epoch)); + } + catch (TestException&) + { + return false; + } + + return true; + +} diff --git a/GVFS/GVFS.NativeTests/source/ReadAndWriteTests.cpp b/GVFS/GVFS.NativeTests/source/ReadAndWriteTests.cpp new file mode 100644 index 00000000..79dfc314 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/ReadAndWriteTests.cpp @@ -0,0 +1,972 @@ +#include "stdafx.h" +#include "ReadAndWriteTests.h" +#include "SafeHandle.h" +#include "SafeOverlapped.h" +#include "TestException.h" +#include "Should.h" + +namespace +{ + static const char* TEST_STRING = "*TEST*12345678#TEST#"; + + // ReadOverlapped: Read text from the specified handle using async overlapped IO + // + // handle -> Handle to file + // maxNumberOfBytesToRead -> Maximum number of bytes to read + // expectedNumberOfBytesToRead -> Expected number of bytes to read + // offset -> Offset (from the beginning of the file) where read should start + // expectedContent -> Expected content or nullptr if content should not be validated + // + // + // Returns -> Shared point to the contents that have been read + std::shared_ptr ReadOverlapped(SafeHandle& handle, unsigned long maxNumberOfBytesToRead, unsigned long expectedNumberOfBytesToRead, unsigned long offset); + + // WriteOverlapped: Write text to the specified handle using async overlapped IO + // + // handle -> Handle to file + // buffer -> Data to write to file + // numberOfBytesToWrite -> Number of bytes to write to file + // offset -> Offset (from the beginning of the file) where write should start + void WriteOverlapped(SafeHandle& handle, LPCVOID buffer, unsigned long numberOfBytesToWrite, unsigned long offset); + + // GetAllFiles: Get all of the files in the folder at path, and in any subfolders + // + // path -> Path to folder to enumerate + // files -> [Out] Vector of file names and sizes + void GetAllFiles(const std::string& path, std::vector, DWORD>>* files); + + // OpenAndReadFiles: Open and read files + // + // files -> Files to be opened and read + void OpenAndReadFiles(const std::vector, DWORD>>& files); + + // FindFileShouldSucceed: Confirms that specified path do exists using FindFirstFile + void FindFileShouldSucceed(const std::string& path); + + // FindFileExShouldSucceed: Confirms that specified path do exists using FindFirstFileEx + void FindFileExShouldSucceed(const std::string& path, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp); + + // FindFileErrorsMatch: Confirms that specified paths do not exist, and that the error codes returned for nonExistentVirtualPath + // and nonExistentPhysicalPath are the same. Check is performed using FindFirstFile + // + // nonExistentVirtualPath -> Virtual path that is known to not exist, can contain wildcards + // nonExistentPhysicalPath -> Physical path that is known to not exist, can contain wildcards + void FindFileErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath); + + // FindFileExErrorsMatch: Confirms that specified paths do not exist, and that the error codes returned for nonExistentVirtualPath + // and nonExistentPhysicalPath are the same. Check is performed using FindFirstFileEx + // + // nonExistentVirtualPath -> Virtual path that is known to not exist, can contain wildcards + // nonExistentPhysicalPath -> Physical path that is known to not exist, can contain wildcards + // infoLevelId -> The information level of the returned data (returned by FindFirstFileEx) + // searchOp -> The type of filtering to perform that is different from wildcard matching + void FindFileExErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp); +} + +// Read and write to a file, using synchronous IO and a different +// file handle for each read/write +bool ReadAndWriteSeparateHandles(const char* fileVirtualPath) +{ + try + { + // Build a long test string + std::string writeContent; + while (writeContent.length() < 512) + { + writeContent.append(TEST_STRING); + } + + SafeHandle writeFile(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + CREATE_NEW, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(writeFile.GetHandle(), NULL); + + // Confirm there is nothing to read in the file + const int readBufferLength = 48; + char initialReadBuffer[readBufferLength]; + unsigned long numRead = 0; + SHOULD_NOT_EQUAL(ReadFile(writeFile.GetHandle(), initialReadBuffer, readBufferLength - 1, &numRead, NULL), FALSE); + SHOULD_EQUAL(numRead, 0); + + // Write test string + unsigned long numWritten = 0; + WriteFile(writeFile.GetHandle(), writeContent.data(), static_cast(writeContent.length()), &numWritten, NULL); + SHOULD_EQUAL(numWritten, writeContent.length()); + + writeFile.CloseHandle(); + + // Re-open file for read + SafeHandle readFile(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + // Read test string + unsigned long expectedContentLength = static_cast(writeContent.length()); + numRead = 0; + std::shared_ptr readBuffer(new char[expectedContentLength + 1], delete_array()); + readBuffer.get()[expectedContentLength] = '\0'; + SHOULD_NOT_EQUAL(ReadFile(readFile.GetHandle(), readBuffer.get(), expectedContentLength, &numRead, NULL), FALSE); + SHOULD_EQUAL(numRead, expectedContentLength); + SHOULD_EQUAL(strcmp(writeContent.c_str(), readBuffer.get()), 0); + + readFile.CloseHandle(); + + SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0); + } + catch (TestException&) + { + return false; + } + + return true; +} + +// Read and write to a file, using asynchronous IO and the same +// file handle for each read/write +bool ReadAndWriteSameHandle(const char* fileVirtualPath, bool synchronousIO) +{ + try + { + SafeHandle file(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + CREATE_NEW, // dwCreationDisposition + (FILE_ATTRIBUTE_NORMAL | (synchronousIO ? 0 : FILE_FLAG_OVERLAPPED)), // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(file.GetHandle(), NULL); + + // Confirm there is nothing to read in the file + ReadOverlapped(file, 48 /*maxNumberOfBytesToRead*/, 0 /*expectedNumberOfBytesToRead*/, 0 /*offset*/); + + // Build a long test string + std::string writeContent; + while (writeContent.length() < 512000) + { + writeContent.append(TEST_STRING); + } + + // Write test string + WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0); + + // Read back what was just written + std::shared_ptr readContent = ReadOverlapped( + file, + static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/, + static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/, + 0 /*offset*/); + + SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0); + + // Read back with two async requests, one with offset and one without + { + bool asyncReadNoOffset = false; + SafeOverlapped overlappedRead; + overlappedRead.overlapped.hEvent = CreateEvent( + NULL, // lpEventAttributes + true, // bManualReset + false, // bInitialState + NULL); // lpName + + bool asyncReadWithOffset = false; + const int READ_OFFSET = 48; + SafeOverlapped overlappedReadWithOffset; + overlappedReadWithOffset.overlapped.Offset = READ_OFFSET; + overlappedReadWithOffset.overlapped.hEvent = CreateEvent( + NULL, // lpEventAttributes + true, // bManualReset + false, // bInitialState + NULL); // lpName + + + // Read without offset + unsigned long bytesRead = 0; + std::shared_ptr readBuffer(new char[writeContent.length() + 1], delete_array()); + if (!ReadFile(file.GetHandle(), readBuffer.get(), (DWORD)writeContent.length(), &bytesRead, &overlappedRead.overlapped)) + { + unsigned long lastError = GetLastError(); + SHOULD_EQUAL(lastError, ERROR_IO_PENDING); + + asyncReadNoOffset = true; + } + else + { + SHOULD_EQUAL(bytesRead, writeContent.length()); + } + + // Read with offset + std::shared_ptr readBufferWithOffset(new char[writeContent.length() + 1 - READ_OFFSET], delete_array()); + if (!ReadFile(file.GetHandle(), readBufferWithOffset.get(), (DWORD)writeContent.length() - READ_OFFSET, &bytesRead, &overlappedReadWithOffset.overlapped)) + { + unsigned long lastError = GetLastError(); + SHOULD_EQUAL(lastError, ERROR_IO_PENDING); + + asyncReadWithOffset = true; + } + else + { + SHOULD_EQUAL(bytesRead, writeContent.length() - READ_OFFSET); + } + + // Wait for async result + if (asyncReadNoOffset) + { + GetOverlappedResult(file.GetHandle(), &overlappedRead.overlapped, &bytesRead, true); + SHOULD_EQUAL(bytesRead, writeContent.length()); + } + + if (asyncReadWithOffset) + { + GetOverlappedResult(file.GetHandle(), &overlappedReadWithOffset.overlapped, &bytesRead, true); + SHOULD_EQUAL(bytesRead, writeContent.length() - READ_OFFSET); + } + } + + file.CloseHandle(); + + SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), false); + } + catch (TestException&) + { + return false; + } + + return true; +} + +// Read and write to a file, using the same file handle for each read/write. Reads and writes are done +// repeatedly using a pattern observed with tracer.exe (as part of the Windows Mobile build) +bool ReadAndWriteRepeatedly(const char* fileVirtualPath, bool synchronousIO) +{ + struct TestStep + { + TestStep(unsigned long offset, unsigned long maxBytesToRead, unsigned long expectedBytesToRead, unsigned long writeContentsLength) + : offset(offset) + , maxBytesToRead(maxBytesToRead) + , expectedBytesToRead(expectedBytesToRead) + , writeContentsLength(writeContentsLength) + { + } + + unsigned long offset; + unsigned long maxBytesToRead; + unsigned long expectedBytesToRead; + unsigned long writeContentsLength; + }; + + try + { + SafeHandle file(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + CREATE_NEW, // dwCreationDisposition + (FILE_ATTRIBUTE_NORMAL | (synchronousIO ? 0 : FILE_FLAG_OVERLAPPED)), // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(file.GetHandle(), NULL); + + // Test steps mimic the behavior exhibited by tracer.exe + std::vector testSteps; + + // Start at an offset of 48, try to read some data but there will be nothing to read, then write 512000 bytes of data + testSteps.push_back( + TestStep( + 48 /*offset*/, + 48 /*maxNumberOfBytesToRead*/, + 0 /*expectedNumberOfBytesToRead*/, + 512000 /*writeContentsLength*/) + ); + + // Back up to an offset of 32, try to read as much data as was last written ,and then write 876000 bytes of data + testSteps.push_back( + TestStep( + 32 /*offset*/, + (*testSteps.rbegin()).writeContentsLength /*maxNumberOfBytesToRead*/, + (*testSteps.rbegin()).writeContentsLength /*expectedNumberOfBytesToRead*/, + 876000 /*writeContentsLength*/) + ); + + // Advance to where writing just left off, attempt to read 0 bytes of data, and then write 1000000 bytes of data + testSteps.push_back( + TestStep( + (*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength /*offset */, + 0 /*maxNumberOfBytesToRead*/, + 0 /*expectedNumberOfBytesToRead*/, + 1000000 /*writeContentsLength*/) + ); + + // Advance to where writing just left off, attempt to read 0 bytes of data, and then write 24000 bytes of data + testSteps.push_back( + TestStep((*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength /*offset */, + 0 /*maxNumberOfBytesToRead*/, + 0 /*expectedNumberOfBytesToRead*/, + 24000 /*writeContentsLength*/) + ); + + // Run the above test steps + for (const TestStep& step : testSteps) + { + ReadOverlapped(file, step.maxBytesToRead, step.expectedBytesToRead, step.offset); + + std::string writeContent; + while (writeContent.length() < step.writeContentsLength) + { + writeContent.append(TEST_STRING); + } + + WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), step.offset); + } + + // Final step, back up to an offset of 500000, and read the remainder of the file + unsigned long fileLength = (*testSteps.rbegin()).offset + (*testSteps.rbegin()).writeContentsLength; + unsigned long readOffset = 500000; + ReadOverlapped( + file, + fileLength /*maxNumberOfBytesToRead*/, + fileLength - readOffset /*expectedNumberOfBytesToRead*/, + readOffset /*offset*/); + + file.CloseHandle(); + + SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), false); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool RemoveReadOnlyAttribute(const char* fileVirtualPath) +{ + try + { + // Create a file with ReadOnly attribute + SafeHandle file(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + CREATE_NEW, // dwCreationDisposition + FILE_ATTRIBUTE_READONLY, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(file.GetHandle(), NULL); + + std::string writeContent(TEST_STRING); + + // Write test string + WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0); + + // Read back what was just written + std::shared_ptr readContent = ReadOverlapped( + file, + static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/, + static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/, + 0 /*offset*/); + + SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0); + + file.CloseHandle(); + + // Confirm that FILE_ATTRIBUTE_READONLY is set + DWORD attributes = GetFileAttributes(fileVirtualPath); + SHOULD_EQUAL(attributes & FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_READONLY); + + // Open the file again so that the file is no longer read only + SafeHandle existingFile(CreateFile( + fileVirtualPath, // lpFileName + (FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(existingFile.GetHandle(), NULL); + + // Confirm (by handle) that FILE_ATTRIBUTE_READONLY is set + BY_HANDLE_FILE_INFORMATION fileInfo; + SHOULD_BE_TRUE(GetFileInformationByHandle(existingFile.GetHandle(), &fileInfo)); + SHOULD_EQUAL(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_READONLY); + + // Set the new file info (to clear read only) + FILE_BASIC_INFO newInfo; + memset(&newInfo, 0, sizeof(FILE_BASIC_INFO)); + newInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL; + SHOULD_NOT_EQUAL(SetFileInformationByHandle(existingFile.GetHandle(), FileBasicInfo, &newInfo, sizeof(FILE_BASIC_INFO)), 0); + + // Confirm that FILE_ATTRIBUTE_READONLY has been cleared + SHOULD_NOT_EQUAL(GetFileInformationByHandle(existingFile.GetHandle(), &fileInfo), 0); + SHOULD_EQUAL(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY, 0); + + existingFile.CloseHandle(); + + // Confirm that FILE_ATTRIBUTE_READONLY has been cleared (by file name) + attributes = GetFileAttributes(fileVirtualPath); + SHOULD_EQUAL(attributes & FILE_ATTRIBUTE_READONLY, 0); + + // Cleanup + SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool CannotWriteToReadOnlyFile(const char* fileVirtualPath) +{ + try + { + // Create a file with ReadOnly attribute and confirm that it can be written to (i.e. to + // populate the intial contents) + SafeHandle file(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + CREATE_NEW, // dwCreationDisposition + FILE_ATTRIBUTE_READONLY, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + SHOULD_NOT_EQUAL(file.GetHandle(), NULL); + + std::string writeContent("This file was created with the FILE_ATTRIBUTE_READONLY attribute"); + + // Write test string + WriteOverlapped(file, writeContent.data(), static_cast(writeContent.length()), 0); + + // Read back what was just written + std::shared_ptr readContent = ReadOverlapped( + file, + static_cast(writeContent.length()) /*maxNumberOfBytesToRead*/, + static_cast(writeContent.length()) /*expectedNumberOfBytesToRead*/, + 0 /*offset*/); + + SHOULD_EQUAL(strcmp(writeContent.c_str(), readContent.get()), 0); + + file.CloseHandle(); + + // Try to open the file again for write access + SafeHandle existingFile(CreateFile( + fileVirtualPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + unsigned long lastError = GetLastError(); + + // We should fail to get a handle (since we've requested GENERIC_WRITE access for a read only file) + SHOULD_EQUAL(existingFile.GetHandle(), INVALID_HANDLE_VALUE); + SHOULD_EQUAL(lastError, ERROR_ACCESS_DENIED); + + // Cleanup (remove read only attribute and delete file) + SafeHandle changeAttribHandle(CreateFile( + fileVirtualPath, // lpFileName + (FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + + FILE_BASIC_INFO newInfo; + memset(&newInfo, 0, sizeof(FILE_BASIC_INFO)); + newInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL; + SHOULD_NOT_EQUAL(SetFileInformationByHandle(changeAttribHandle.GetHandle(), FileBasicInfo, &newInfo, sizeof(FILE_BASIC_INFO)), 0); + changeAttribHandle.CloseHandle(); + + SHOULD_NOT_EQUAL(DeleteFile(fileVirtualPath), 0); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool EnumerateAndReadDoesNotChangeEnumerationOrder(const char* folderVirtualPath) +{ + try + { + std::vector, DWORD>> firstEnumerationFiles; + GetAllFiles(folderVirtualPath, &firstEnumerationFiles); + OpenAndReadFiles(firstEnumerationFiles); + + std::vector, DWORD>> secondEnumerationFiles; + GetAllFiles(folderVirtualPath, &secondEnumerationFiles); + + SHOULD_EQUAL(firstEnumerationFiles.size(), secondEnumerationFiles.size()); + for (size_t i = 0; i < firstEnumerationFiles.size(); ++i) + { + SHOULD_EQUAL(firstEnumerationFiles[i].second, secondEnumerationFiles[i].second); + SHOULD_EQUAL(strcmp(firstEnumerationFiles[i].first.data(), secondEnumerationFiles[i].first.data()), 0); + } + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool EnumerationErrorsMatchNTFSForNonExistentFolder(const char* nonExistentVirtualPath, const char* nonExistentPhysicalPath) +{ + try + { + FindFileErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath); + FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(nonExistentVirtualPath, nonExistentPhysicalPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + std::string virtualSubFolderPath = nonExistentVirtualPath + std::string("\\non_existent_sub_item"); + std::string physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\non_existent_sub_item"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = nonExistentVirtualPath + std::string("*"); + physicalSubFolderPath = nonExistentPhysicalPath + std::string("*"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = nonExistentVirtualPath + std::string("?"); + physicalSubFolderPath = nonExistentPhysicalPath + std::string("?"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = nonExistentVirtualPath + std::string("\\*"); + physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\*"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = nonExistentVirtualPath + std::string("\\*.*"); + physicalSubFolderPath = nonExistentPhysicalPath + std::string("\\*.*"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool EnumerationErrorsMatchNTFSForEmptyFolder(const char* emptyFolderVirtualPath, const char* emptyFolderPhysicalPath) +{ + try + { + SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderVirtualPath)); + SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderPhysicalPath)); + + FindFileShouldSucceed(emptyFolderVirtualPath); + FindFileShouldSucceed(emptyFolderPhysicalPath); + + FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(emptyFolderVirtualPath, FindExInfoStandard, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(emptyFolderPhysicalPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + std::string virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\non_existent_sub_item"); + std::string physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\non_existent_sub_item"); + FindFileErrorsMatch(virtualSubFolderPath, physicalSubFolderPath); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExErrorsMatch(virtualSubFolderPath, physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = emptyFolderVirtualPath + std::string("*"); + physicalSubFolderPath = emptyFolderPhysicalPath + std::string("*"); + FindFileShouldSucceed(virtualSubFolderPath); + FindFileShouldSucceed(physicalSubFolderPath); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = emptyFolderVirtualPath + std::string("?"); + physicalSubFolderPath = emptyFolderPhysicalPath + std::string("?"); + FindFileShouldSucceed(virtualSubFolderPath); + FindFileShouldSucceed(physicalSubFolderPath); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\*"); + physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\*"); + FindFileShouldSucceed(virtualSubFolderPath); + FindFileShouldSucceed(physicalSubFolderPath); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + + virtualSubFolderPath = emptyFolderVirtualPath + std::string("\\*.*"); + physicalSubFolderPath = emptyFolderPhysicalPath + std::string("\\*.*"); + FindFileShouldSucceed(virtualSubFolderPath); + FindFileShouldSucceed(physicalSubFolderPath); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(virtualSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchNameMatch); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoBasic, FindExSearchLimitToDirectories); + FindFileExShouldSucceed(physicalSubFolderPath, FindExInfoStandard, FindExSearchLimitToDirectories); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool CanDeleteEmptyFolderWithFileDispositionOnClose(const char* emptyFolderPath) +{ + try + { + SHOULD_BE_TRUE(PathIsDirectoryEmpty(emptyFolderPath)); + + SafeHandle emptyFolder(CreateFile( + emptyFolderPath, // lpFileName + (GENERIC_READ | GENERIC_WRITE | DELETE), // dwDesiredAccess + 0, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_FLAG_BACKUP_SEMANTICS, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_NOT_EQUAL(emptyFolder.GetHandle(), INVALID_HANDLE_VALUE); + + FILE_DISPOSITION_INFO dispositionInfo; + dispositionInfo.DeleteFile = TRUE; + BOOL result = SetFileInformationByHandle(emptyFolder.GetHandle(), FileDispositionInfo, &dispositionInfo, sizeof(FILE_DISPOSITION_INFO)); + SHOULD_BE_TRUE(result); + emptyFolder.CloseHandle(); + SHOULD_BE_TRUE(!PathIsDirectoryEmpty(emptyFolderPath)); + } + catch (TestException&) + { + return false; + } + + return true; +} + +bool ErrorWhenPathTreatsFileAsFolderMatchesNTFS(const char* fileVirtualPath, const char* fileNTFSPath, int creationDisposition) +{ + try + { + // Confirm the files exist + SafeHandle physicalFile(CreateFile( + fileNTFSPath, // lpFileName + GENERIC_READ, // dwDesiredAccess + 0, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_NOT_EQUAL(physicalFile.GetHandle(), INVALID_HANDLE_VALUE); + physicalFile.CloseHandle(); + + SafeHandle virtualFile(CreateFile( + fileVirtualPath, // lpFileName + GENERIC_READ, // dwDesiredAccess + 0, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_NOT_EQUAL(virtualFile.GetHandle(), INVALID_HANDLE_VALUE); + virtualFile.CloseHandle(); + + std::string bogusNTFSPath(fileNTFSPath); + bogusNTFSPath += "\\HEAD"; + SafeHandle bogusNTFSFile(CreateFile( + bogusNTFSPath.c_str(), // lpFileName + GENERIC_READ | GENERIC_WRITE, // dwDesiredAccess + 0, // dwShareMode + NULL, // lpSecurityAttributes + creationDisposition, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_EQUAL(bogusNTFSFile.GetHandle(), INVALID_HANDLE_VALUE); + DWORD bogusNTFSFileError = GetLastError(); + + std::string bogusVirtualPath(fileVirtualPath); + bogusVirtualPath += "\\HEAD"; + SafeHandle bogusVirtualFile(CreateFile( + bogusVirtualPath.c_str(), // lpFileName + GENERIC_READ | GENERIC_WRITE, // dwDesiredAccess + 0, // dwShareMode + NULL, // lpSecurityAttributes + creationDisposition, // dwCreationDisposition + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_EQUAL(bogusVirtualFile.GetHandle(), INVALID_HANDLE_VALUE); + DWORD bogusVirtualFileError = GetLastError(); + + SHOULD_EQUAL(bogusVirtualFileError, bogusNTFSFileError); + } + catch (TestException&) + { + return false; + } + + return true; +} + +namespace +{ + std::shared_ptr ReadOverlapped(SafeHandle& handle, unsigned long maxNumberOfBytesToRead, unsigned long expectedNumberOfBytesToRead, unsigned long offset) + { + SafeOverlapped overlappedRead; + overlappedRead.overlapped.Offset = offset; + overlappedRead.overlapped.hEvent = CreateEvent( + NULL, // lpEventAttributes + true, // bManualReset + false, // bInitialState + NULL); // lpName + + unsigned long bytesRead = 0; + std::shared_ptr readBuffer(new char[maxNumberOfBytesToRead + 1], delete_array()); + readBuffer.get()[0] = '\0'; + readBuffer.get()[maxNumberOfBytesToRead] = '\0'; + if (!ReadFile(handle.GetHandle(), readBuffer.get(), maxNumberOfBytesToRead, &bytesRead, &overlappedRead.overlapped)) + { + unsigned long lastError = GetLastError(); + if (lastError == ERROR_IO_PENDING) + { + GetOverlappedResult(handle.GetHandle(), &overlappedRead.overlapped, &bytesRead, true); + SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead); + } + else if (lastError == ERROR_HANDLE_EOF) + { + SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead); + } + else + { + // Unexpected lastError value + FAIL_TEST("Unexpected lastError value"); + } + } + else + { + SHOULD_EQUAL(bytesRead, expectedNumberOfBytesToRead); + } + + return readBuffer; + } + + void WriteOverlapped(SafeHandle& handle, LPCVOID buffer, unsigned long numberOfBytesToWrite, unsigned long offset) + { + SafeOverlapped overlappedWrite; + overlappedWrite.overlapped.Offset = offset; + overlappedWrite.overlapped.hEvent = CreateEvent( + NULL, // lpEventAttributes + true, // bManualReset + false, // bInitialState + NULL); // lpName + + unsigned long numWritten = 0; + if (!WriteFile(handle.GetHandle(), buffer, numberOfBytesToWrite, &numWritten, &overlappedWrite.overlapped)) + { + unsigned long lastError = GetLastError(); + SHOULD_EQUAL(lastError, ERROR_IO_PENDING); + + GetOverlappedResult(handle.GetHandle(), &overlappedWrite.overlapped, &numWritten, true); + SHOULD_EQUAL(numWritten, numberOfBytesToWrite); + } + else + { + SHOULD_EQUAL(numWritten, numberOfBytesToWrite); + } + } + + void GetAllFiles(const std::string& path, std::vector, DWORD>>* files) + { + WIN32_FIND_DATA ffd; + HANDLE hFind = INVALID_HANDLE_VALUE; + + // List of directories and files + std::list> directories; + + // Walk each directory, pushing new directory entries and file entries to directories and files + directories.push_back(std::array()); + strcpy_s(directories.begin()->data(), MAX_PATH, path.c_str()); + + std::list>::iterator iterDirectories = directories.begin(); + while (iterDirectories != directories.end()) + { + char dirSearchPath[MAX_PATH]; + sprintf_s(dirSearchPath, "%s\\*", (*iterDirectories).data()); + + hFind = FindFirstFile(dirSearchPath, &ffd); + + SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE); + + do + { + if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + if (ffd.cFileName[0] != '.') + { + // Add new directory to the end of the list + directories.push_back(std::array()); + sprintf_s((*directories.rbegin()).data(), MAX_PATH, "%s\\%s", (*iterDirectories).data(), ffd.cFileName); + } + } + else + { + // Add a new file to the end of the list + files->resize(files->size() + 1); + sprintf_s((*files->rbegin()).first.data(), MAX_PATH, "%s\\%s", (*iterDirectories).data(), ffd.cFileName); + (*files->rbegin()).second = /*(ffd.nFileSizeHigh * (MAXDWORD + 1)) +*/ ffd.nFileSizeLow; + } + } while (FindNextFile(hFind, &ffd) != 0); + + FindClose(hFind); + + // Advance to next directory + ++iterDirectories; + } + } + + void OpenAndReadFiles(const std::vector, DWORD>>& files) + { + const unsigned long bytesToRead = 20971520; + std::vector readBuffer; + unsigned long numRead; + unsigned long totalRead; + BOOL result = TRUE; + HANDLE readFile; + + // Read 20MB at a time + readBuffer.resize(bytesToRead); + + for (const std::pair, DWORD>& fileInfo : files) + { + numRead = 0; + totalRead = 0; + result = TRUE; + + readFile = CreateFile( + fileInfo.first.data(), // lpFileName + (GENERIC_READ), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_ALWAYS, // dwCreationDisposition, NOTE: RouteToFile test fails if we use OPEN_EXISTING + FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes + NULL); // hTemplateFile + + // Read the full file in chunks to avoid filling the memory with the files + do { + result = ReadFile(readFile, readBuffer.data(), (fileInfo.second - totalRead > bytesToRead) ? bytesToRead : fileInfo.second - totalRead, &numRead, NULL); + totalRead += numRead; + } while (result && numRead != 0); + + CloseHandle(readFile); + + SHOULD_EQUAL(totalRead, fileInfo.second); + } + } + + void FindFileShouldSucceed(const std::string& path) + { + WIN32_FIND_DATA ffd; + HANDLE hFind = FindFirstFile(path.c_str(), &ffd); + SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE); + FindClose(hFind); + } + + void FindFileExShouldSucceed(const std::string& path, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp) + { + WIN32_FIND_DATA ffd; + HANDLE hFind = FindFirstFileEx(path.c_str(), infoLevelId, &ffd, searchOp, NULL, 0); + SHOULD_NOT_EQUAL(hFind, INVALID_HANDLE_VALUE); + FindClose(hFind); + } + + void FindFileErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath) + { + WIN32_FIND_DATA ffd; + HANDLE hFind = FindFirstFile(nonExistentVirtualPath.c_str(), &ffd); + SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE); + unsigned long lastVirtualError = GetLastError(); + + hFind = FindFirstFile(nonExistentPhysicalPath.c_str(), &ffd); + SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE); + unsigned long lastPhysicalError = GetLastError(); + SHOULD_EQUAL(lastVirtualError, lastPhysicalError); + } + + void FindFileExErrorsMatch(const std::string& nonExistentVirtualPath, const std::string& nonExistentPhysicalPath, FINDEX_INFO_LEVELS infoLevelId, FINDEX_SEARCH_OPS searchOp) + { + WIN32_FIND_DATA ffd; + HANDLE hFind = FindFirstFileEx(nonExistentVirtualPath.c_str(), infoLevelId, &ffd, searchOp, NULL, 0); + SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE); + unsigned long lastVirtualError = GetLastError(); + + hFind = FindFirstFileEx(nonExistentPhysicalPath.c_str(), infoLevelId, &ffd, searchOp, NULL, 0); + SHOULD_EQUAL(hFind, INVALID_HANDLE_VALUE); + unsigned long lastPhysicalError = GetLastError(); + SHOULD_EQUAL(lastVirtualError, lastPhysicalError); + } +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/TrailingSlashTests.cpp b/GVFS/GVFS.NativeTests/source/TrailingSlashTests.cpp new file mode 100644 index 00000000..67856adb --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/TrailingSlashTests.cpp @@ -0,0 +1,105 @@ +#include "stdafx.h" +#include "TrailingSlashTests.h" +#include "SafeHandle.h" +#include "TestException.h" +#include "TestHelpers.h" +#include "Should.h" + +using namespace TestHelpers; + +namespace +{ + const std::string TEST_ROOT_FOLDER("\\TrailingSlashTests"); + void VerifyEnumerationMatches(const std::string& path1, const std::vector& expectedContents); +} + +bool EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete(const char* virtualRootPath) +{ + try + { + std::string testFolder = virtualRootPath + TEST_ROOT_FOLDER + std::string("\\EnumerateWithTrailingSlashMatchesWithoutSlashAfterDelete"); + + // Folder contains "a.txt", "b.txt", and "c.txt" + std::vector expectedResults = { L".", L"..", L"a.txt", L"b.txt", L"c.txt" }; + VerifyEnumerationMatches(testFolder, expectedResults); + VerifyEnumerationMatches(testFolder + "\\", expectedResults); + + // Delete a file + DWORD error = DelFile(testFolder + "\\b.txt"); + SHOULD_EQUAL((DWORD)ERROR_SUCCESS, error); + + expectedResults = { L".", L"..", L"a.txt", L"c.txt" }; + VerifyEnumerationMatches(testFolder, expectedResults); + VerifyEnumerationMatches(testFolder + "\\", expectedResults); + } + catch (TestException&) + { + return false; + } + + return true; +} + +namespace +{ + +void VerifyEnumerationMatches(const std::string& path1, const std::vector& expectedContents) +{ + SafeHandle folderHandle(CreateFile( + path1.c_str(), // lpFileName + (GENERIC_READ), // dwDesiredAccess + FILE_SHARE_READ, // dwShareMode + NULL, // lpSecurityAttributes + OPEN_EXISTING, // dwCreationDisposition + FILE_FLAG_BACKUP_SEMANTICS, // dwFlagsAndAttributes + NULL)); // hTemplateFile + SHOULD_NOT_EQUAL(folderHandle.GetHandle(), INVALID_HANDLE_VALUE); + + UCHAR buffer[2048]; + NTSTATUS status; + IO_STATUS_BLOCK ioStatus; + BOOLEAN restart = TRUE; + size_t expectedIndex = 0; + + do + { + status = NtQueryDirectoryFile(folderHandle.GetHandle(), + NULL, + NULL, + NULL, + &ioStatus, + buffer, + ARRAYSIZE(buffer), + FileBothDirectoryInformation, + FALSE, + NULL, + restart); + + if (status == STATUS_SUCCESS) + { + PFILE_BOTH_DIR_INFORMATION dirInfo; + PUCHAR entry = buffer; + + do + { + dirInfo = (PFILE_BOTH_DIR_INFORMATION)entry; + + std::wstring entryName(dirInfo->FileName, dirInfo->FileNameLength / sizeof(WCHAR)); + + SHOULD_EQUAL(entryName, expectedContents[expectedIndex]); + + entry = entry + dirInfo->NextEntryOffset; + ++expectedIndex; + + } while (dirInfo->NextEntryOffset > 0 && expectedIndex < expectedContents.size()); + + restart = FALSE; + } + + } while (status == STATUS_SUCCESS); + + SHOULD_EQUAL(expectedIndex, expectedContents.size()); + SHOULD_EQUAL(status, STATUS_NO_MORE_FILES); +} + +} \ No newline at end of file diff --git a/GVFS/GVFS.NativeTests/source/dllmain.cpp b/GVFS/GVFS.NativeTests/source/dllmain.cpp new file mode 100644 index 00000000..57879ac4 --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/dllmain.cpp @@ -0,0 +1,22 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "stdafx.h" + +BOOL APIENTRY DllMain( HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + UNREFERENCED_PARAMETER(hModule); + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + diff --git a/GVFS/GVFS.NativeTests/source/stdafx.cpp b/GVFS/GVFS.NativeTests/source/stdafx.cpp new file mode 100644 index 00000000..18a61f7c --- /dev/null +++ b/GVFS/GVFS.NativeTests/source/stdafx.cpp @@ -0,0 +1,7 @@ +// stdafx.cpp : source file that includes just the standard includes +// GVFS.NativeTests.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// Add any additional headers you need in STDAFX.H and not in this file diff --git a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj new file mode 100644 index 00000000..37aceb34 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj @@ -0,0 +1,126 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {5A6656D5-81C7-472C-9DC8-32D071CB2258} + Win32Proj + readobject + 8.1 + + + + Application + true + v140 + Unicode + + + Application + false + v140 + true + Unicode + + + 0.2.173.2 + + + + + + + + + + + + + + + true + $(SolutionDir)..\BuildOutput\$(ProjectName)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + false + $(SolutionDir)..\BuildOutput\$(ProjectName)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + + Use + Level4 + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + Console + true + + + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + $(SolutionDir)\Scripts\CreateCommonVersionHeader.bat $(GVFSVersion) $(SolutionDir)\.. + + + $(SolutionDir)\..\BuildOutput + + + + + Level4 + Use + MaxSpeed + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + Console + true + true + true + + + $(SolutionDir)..\BuildOutput\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + $(SolutionDir)\Scripts\CreateCommonVersionHeader.bat $(GVFSVersion) $(SolutionDir)\.. + + + $(SolutionDir)\..\BuildOutput + + + + + + + + + + + Create + Create + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj.filters b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj.filters new file mode 100644 index 00000000..3b959ee5 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/GVFS.ReadObjectHook.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/GVFS/GVFS.ReadObjectHook/Version.rc b/GVFS/GVFS.ReadObjectHook/Version.rc new file mode 100644 index 00000000..1a197d7a Binary files /dev/null and b/GVFS/GVFS.ReadObjectHook/Version.rc differ diff --git a/GVFS/GVFS.ReadObjectHook/main.cpp b/GVFS/GVFS.ReadObjectHook/main.cpp new file mode 100644 index 00000000..4b583547 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/main.cpp @@ -0,0 +1,204 @@ +// GVFS.ReadObjectHook +// +// GVFS.ReadObjectHook connects to GVFS and asks GVFS to download a git object (to the .git\objects folder). +// GVFS.ReadObjectHook accepts the desired object SHA as an argument, and +// decides which GVFS instance to connect to based on GVFS.ReadObjectHook.exe's +// current working directory. +// +// When GVFS installs GVFS.ReadObjectHook.exe, it copies the file to +// the .git\hooks folder, and renames the executable to read-object.exe +// read-object.exe is called by git.exe when it fails to find the object it's looking for on disk. + +#include "stdafx.h" + +#define REC_BUF_SIZE 512 +#define MAX_REQUEST_CHARS 128 + +enum ReturnCode +{ + Success = 0, + InvalidArgCount = 1, + GetCurrentDirectoryFailure = 2, + NotInGVFSEnlistment = 3, + PipeConnectError = 4, + PipeConnectTimeout = 5, + InvalidSHA = 6, + PipeWriteFailed = 7, + PipeReadFailed = 8 +}; + +std::wstring GetGVFSPipeName(wchar_t* appName); +HANDLE CreatePipeToGVFS(const std::wstring& pipeName); + +int wmain(int argc, WCHAR *argv[]) +{ + if (argc != 2) + { + fwprintf(stderr, L"Usage: %s \n", argv[0]); + exit(ReturnCode::InvalidArgCount); + } + + // Construct download request message + // Format: "DLO|<40 character SHA>" + // Example: "DLO|920C34DCDDFC8F07AC4704C8C0D087D6F2095729" + wchar_t message[MAX_REQUEST_CHARS]; + if (_snwprintf_s(message, _TRUNCATE, L"DLO|%s", argv[1]) < 0) + { + fwprintf(stderr, L"First argument must be a 40 character SHA, actual value: %s\n", argv[1]); + exit(ReturnCode::InvalidSHA); + } + + std::wstring pipeName(GetGVFSPipeName(argv[0])); + + HANDLE pipeHandle = CreatePipeToGVFS(pipeName); + + // GVFS expects message in UTF8 format + std::wstring_convert> utf8Converter; + std::string utf8message(utf8Converter.to_bytes(message)); + utf8message += "\n"; + + DWORD bytesWritten; + BOOL success = WriteFile( + pipeHandle, // pipe handle + utf8message.c_str(), // message + (static_cast(utf8message.size())), // message length + &bytesWritten, // bytes written + NULL); // not overlapped + + if (!success) + { + fwprintf(stderr, L"Failed to write to pipe (%d)\n", GetLastError()); + CloseHandle(pipeHandle); + exit(ReturnCode::PipeWriteFailed); + } + + DWORD bytesRead; + wchar_t receiveBuffer[REC_BUF_SIZE]; + do + { + // Read from the pipe. + success = ReadFile( + pipeHandle, // pipe handle + receiveBuffer, // buffer to receive reply + REC_BUF_SIZE * sizeof(wchar_t), // size of buffer + &bytesRead, // number of bytes read + NULL); // not overlapped + + if (!success && GetLastError() != ERROR_MORE_DATA) + { + break; + } + } while (!success); // repeat loop if ERROR_MORE_DATA + + CloseHandle(pipeHandle); + + if (!success) + { + fwprintf(stderr, L"Read response from pipe failed (%d)\n", GetLastError()); + exit(ReturnCode::PipeReadFailed); + } + + // Treat the hook as successful regardless of the contents of receiveBuffer + // if GVFS did not download the object, then git.exe will see that it's missing when + // it attempts to read from the object again + + return ReturnCode::Success; +} + +inline std::wstring GetGVFSPipeName(wchar_t* appName) +{ + // The pipe name is build using the path of the GVFS enlistment root. + // Start in the current directory and walk up the directory tree + // until we find a folder that contains the ".gvfs" folder + + const size_t dotGVFSRelativePathLength = sizeof(L"\\.gvfs") / sizeof(wchar_t); + + // TODO 640838: Support paths longer than MAX_PATH + wchar_t enlistmentRoot[MAX_PATH]; + DWORD currentDirResult = GetCurrentDirectoryW(MAX_PATH - dotGVFSRelativePathLength, enlistmentRoot); + if (currentDirResult == 0 || currentDirResult > MAX_PATH - dotGVFSRelativePathLength) + { + fwprintf(stderr, L"GetCurrentDirectory failed (%d)\n", GetLastError()); + exit(ReturnCode::GetCurrentDirectoryFailure); + } + + size_t enlistmentRootLength = wcslen(enlistmentRoot); + if ('\\' != enlistmentRoot[enlistmentRootLength - 1]) + { + wcscat_s(enlistmentRoot, L"\\"); + enlistmentRootLength++; + } + + // Walk up enlistmentRoot looking for a folder named .gvfs + wchar_t* lastslash = enlistmentRoot + enlistmentRootLength - 1; + WIN32_FIND_DATAW findFileData; + HANDLE dotGVFSHandle; + while (1) + { + wcscat_s(lastslash, MAX_PATH - (lastslash - enlistmentRoot), L".gvfs"); + dotGVFSHandle = FindFirstFileW(enlistmentRoot, &findFileData); + if (dotGVFSHandle != INVALID_HANDLE_VALUE) + { + FindClose(dotGVFSHandle); + if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + break; + } + } + + lastslash--; + while ((enlistmentRoot != lastslash) && (*lastslash != '\\')) + { + lastslash--; + } + + if (enlistmentRoot == lastslash) + { + fwprintf(stderr, L"%s must be run from inside a GVFS enlistment\n", appName); + exit(ReturnCode::NotInGVFSEnlistment); + } + *(lastslash + 1) = 0; + }; + + *(lastslash) = 0; + + std::wstring namedPipe(CharUpper(enlistmentRoot)); + std::replace(namedPipe.begin(), namedPipe.end(), L':', L'_'); + return L"\\\\.\\pipe\\GVFS_" + namedPipe; +} + +inline HANDLE CreatePipeToGVFS(const std::wstring& pipeName) +{ + HANDLE pipeHandle; + while (1) + { + pipeHandle = CreateFile( + pipeName.c_str(), // pipe name + GENERIC_READ | // read and write access + GENERIC_WRITE, + 0, // no sharing + NULL, // default security attributes + OPEN_EXISTING, // opens existing pipe + 0, // default attributes + NULL); // no template file + + if (pipeHandle != INVALID_HANDLE_VALUE) + { + break; + } + + if (GetLastError() != ERROR_PIPE_BUSY) + { + fwprintf(stderr, L"Could not open pipe. (%d)\n", GetLastError()); + exit(ReturnCode::PipeConnectError); + } + + if (!WaitNamedPipe(pipeName.c_str(), 3000)) + { + fwprintf(stderr, L"Could not open pipe: Timed out."); + exit(ReturnCode::PipeConnectTimeout); + } + } + + return pipeHandle; +} diff --git a/GVFS/GVFS.ReadObjectHook/resource.h b/GVFS/GVFS.ReadObjectHook/resource.h new file mode 100644 index 00000000..c1b5c159 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Version.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/GVFS/GVFS.ReadObjectHook/stdafx.cpp b/GVFS/GVFS.ReadObjectHook/stdafx.cpp new file mode 100644 index 00000000..7f10fcb1 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/stdafx.cpp @@ -0,0 +1,6 @@ +// stdafx.cpp : source file that includes just the standard includes +// GVFS.ReadObjectHook.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + diff --git a/GVFS/GVFS.ReadObjectHook/stdafx.h b/GVFS/GVFS.ReadObjectHook/stdafx.h new file mode 100644 index 00000000..49410608 --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/stdafx.h @@ -0,0 +1,14 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" +#include +#include +#include +#include +#include +#include diff --git a/GVFS/GVFS.ReadObjectHook/targetver.h b/GVFS/GVFS.ReadObjectHook/targetver.h new file mode 100644 index 00000000..90e767bf --- /dev/null +++ b/GVFS/GVFS.ReadObjectHook/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/GVFS/GVFS.Tests/GVFS.Tests.csproj b/GVFS/GVFS.Tests/GVFS.Tests.csproj new file mode 100644 index 00000000..93fa9c0d --- /dev/null +++ b/GVFS/GVFS.Tests/GVFS.Tests.csproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC} + Library + Properties + GVFS.Tests + GVFS.Tests + v4.5.2 + 512 + + + + + true + ..\..\..\BuildOutput\GVFS.Tests\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.Tests\bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + ..\..\..\BuildOutput\GVFS.Tests\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.Tests\bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + + False + ..\..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll + True + + + False + ..\..\..\packages\NUnitLite.3.5.0\lib\net45\nunitlite.dll + True + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.Tests/NUnitRunner.cs b/GVFS/GVFS.Tests/NUnitRunner.cs new file mode 100644 index 00000000..d9b98a75 --- /dev/null +++ b/GVFS/GVFS.Tests/NUnitRunner.cs @@ -0,0 +1,65 @@ +using NUnitLite; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; + +namespace GVFS.Tests +{ + public class NUnitRunner + { + private List args; + private List excludedCategories; + + public NUnitRunner(string[] args) + { + this.args = new List(args); + this.excludedCategories = new List(); + } + + public bool HasCustomArg(string arg) + { + // We also remove it as we're checking, because nunit wouldn't understand what it means + return this.args.Remove(arg); + } + + public void ExcludeCategory(string category) + { + this.excludedCategories.Add("cat!=" + category); + } + + public int RunTests(int repeatCount) + { + if (this.excludedCategories.Count > 0) + { + this.args.Add("--where=" + string.Join("&&", this.excludedCategories)); + } + + int finalResult = 0; + for (int i = 0; i < repeatCount; i++) + { + Console.WriteLine("Starting pass {0}", i + 1); + DateTime now = DateTime.Now; + + finalResult = new AutoRun(Assembly.GetEntryAssembly()).Execute(this.args.ToArray()); + + Console.WriteLine("Completed pass {0} in {1}", i + 1, DateTime.Now - now); + Console.WriteLine(); + + if (i < repeatCount - 1) + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + } + + if (Debugger.IsAttached) + { + Console.WriteLine("Tests completed. Please Enter to exit"); + Console.ReadLine(); + } + + return finalResult; + } + } +} diff --git a/GVFS/GVFS.Tests/Properties/AssemblyInfo.cs b/GVFS/GVFS.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..5911485c --- /dev/null +++ b/GVFS/GVFS.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.Tests")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("72701bc3-5da9-4c7a-bf10-9e98c9fc8eac")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs b/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs new file mode 100644 index 00000000..f6257def --- /dev/null +++ b/GVFS/GVFS.Tests/Should/EnumerableShouldExtensions.cs @@ -0,0 +1,89 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GVFS.Tests.Should +{ + public static class EnumerableShouldExtensions + { + public static IEnumerable ShouldBeEmpty(this IEnumerable group) + { + CollectionAssert.IsEmpty(group); + return group; + } + + public static IEnumerable ShouldBeNonEmpty(this IEnumerable group) + { + CollectionAssert.IsNotEmpty(group); + return group; + } + + public static T ShouldContain(this IEnumerable group, Func predicate) + { + T item = group.FirstOrDefault(predicate); + item.ShouldNotEqual(default(T)); + + return item; + } + + public static T ShouldContainSingle(this IEnumerable group, Func predicate) + { + T item = group.Single(predicate); + item.ShouldNotEqual(default(T)); + + return item; + } + + public static void ShouldNotContain(this IEnumerable group, Func predicate) + { + T item = group.SingleOrDefault(predicate); + item.ShouldEqual(default(T)); + } + + public static IEnumerable ShouldContain(this IEnumerable group, IEnumerable expectedValues, Func predicate) + { + List groupList = new List(group); + + foreach (T expectedValue in expectedValues) + { + Assert.IsTrue(groupList.Any(item => predicate(item, expectedValue))); + } + + return group; + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params Action[] itemCheckers) + { + List groupList = new List(group); + List> itemCheckersList = new List>(itemCheckers); + + for (int i = 0; i < groupList.Count; i++) + { + itemCheckersList[i](groupList[i]); + } + + return group; + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues, Func equals) + { + List groupList = new List(group); + List expectedValuesList = new List(expectedValues); + + groupList.Count.ShouldEqual(expectedValuesList.Count); + + for (int i = 0; i < groupList.Count; i++) + { + Assert.IsTrue(equals(groupList[i], expectedValuesList[i]), "Items at index {0} are not the same", i); + } + + return group; + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues) + { + return group.ShouldMatchInOrder(expectedValues, (t1, t2) => t1.Equals(t2)); + } + } +} diff --git a/GVFS/GVFS.Tests/Should/StringExtensions.cs b/GVFS/GVFS.Tests/Should/StringExtensions.cs new file mode 100644 index 00000000..8046646f --- /dev/null +++ b/GVFS/GVFS.Tests/Should/StringExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.Tests.Should +{ + public static class StringExtensions + { + public static string Repeat(this string self, int count) + { + return string.Join(string.Empty, Enumerable.Range(0, count).Select(x => self).ToArray()); + } + } +} diff --git a/GVFS/GVFS.Tests/Should/StringShouldExtensions.cs b/GVFS/GVFS.Tests/Should/StringShouldExtensions.cs new file mode 100644 index 00000000..fc9c4952 --- /dev/null +++ b/GVFS/GVFS.Tests/Should/StringShouldExtensions.cs @@ -0,0 +1,49 @@ +using NUnit.Framework; + +namespace GVFS.Tests.Should +{ + public static class StringShouldExtensions + { + public static string ShouldContain(this string actualValue, params string[] expectedSubstrings) + { + foreach (string expectedSubstring in expectedSubstrings) + { + Assert.IsTrue( + actualValue.Contains(expectedSubstring), + "Expected substring '{0}' not found in '{1}'", + expectedSubstring, + actualValue); + } + + return actualValue; + } + + public static string ShouldNotContain(this string actualValue, params string[] unexpectedSubstrings) + { + foreach (string expectedSubstring in unexpectedSubstrings) + { + Assert.IsFalse( + actualValue.Contains(expectedSubstring), + "Unexpected substring '{0}' found in '{1}'", + expectedSubstring, + actualValue); + } + + return actualValue; + } + + public static string ShouldContainOneOf(this string actualValue, params string[] expectedSubstrings) + { + for (int i = 0; i < expectedSubstrings.Length; i++) + { + if (actualValue.Contains(expectedSubstrings[i])) + { + return actualValue; + } + } + + Assert.Fail("No expected substrings found in '{0}'", actualValue); + return actualValue; + } + } +} diff --git a/GVFS/GVFS.Tests/Should/ValueShouldExtensions.cs b/GVFS/GVFS.Tests/Should/ValueShouldExtensions.cs new file mode 100644 index 00000000..7a953bec --- /dev/null +++ b/GVFS/GVFS.Tests/Should/ValueShouldExtensions.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using System; + +namespace GVFS.Tests.Should +{ + public static class ValueShouldExtensions + { + public static T ShouldBeAtLeast(this T actualValue, T expectedValue, string message = "") where T : IComparable + { + Assert.GreaterOrEqual(actualValue, expectedValue, message); + return actualValue; + } + + public static T ShouldBeAtMost(this T actualValue, T expectedValue, string message = "") where T : IComparable + { + Assert.LessOrEqual(actualValue, expectedValue, message); + return actualValue; + } + + public static T ShouldEqual(this T actualValue, T expectedValue, string message = "") + { + Assert.AreEqual(expectedValue, actualValue, message); + return actualValue; + } + + public static T[] ShouldEqual(this T[] actualValue, T[] expectedValue, int start, int count) + { + expectedValue.Length.ShouldBeAtLeast(start + count); + for (int i = 0; i < count; ++i) + { + actualValue[i].ShouldEqual(expectedValue[i + start]); + } + + return actualValue; + } + + public static T ShouldNotEqual(this T actualValue, T unexpectedValue, string message = "") + { + Assert.AreNotEqual(unexpectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldBeSameAs(this T actualValue, T expectedValue, string message = "") + { + Assert.AreSame(expectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldNotBeSameAs(this T actualValue, T expectedValue, string message = "") + { + Assert.AreNotSame(expectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldBeOfType(this object obj) + { + Assert.IsTrue(obj is T, "Expected type {0}, but the object is actually of type {1}", typeof(T), obj.GetType()); + return (T)obj; + } + + public static void ShouldBeNull(this T obj, string message = "") + where T : class + { + Assert.IsNull(obj, message); + } + + public static T ShouldNotBeNull(this T obj, string message = "") + where T : class + { + Assert.IsNotNull(obj, message); + return obj; + } + } +} diff --git a/GVFS/GVFS.Tests/packages.config b/GVFS/GVFS.Tests/packages.config new file mode 100644 index 00000000..9dd0d8a6 --- /dev/null +++ b/GVFS/GVFS.Tests/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/App.config b/GVFS/GVFS.UnitTests/App.config new file mode 100644 index 00000000..d740e886 --- /dev/null +++ b/GVFS/GVFS.UnitTests/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/Category/CategoryContants.cs b/GVFS/GVFS.UnitTests/Category/CategoryContants.cs new file mode 100644 index 00000000..2493b8fd --- /dev/null +++ b/GVFS/GVFS.UnitTests/Category/CategoryContants.cs @@ -0,0 +1,7 @@ +namespace GVFS.UnitTests.Category +{ + public static class CategoryContants + { + public const string ExceptionExpected = "ExceptionExpected"; + } +} diff --git a/GVFS/GVFS.UnitTests/Common/GitHelperTests.cs b/GVFS/GVFS.UnitTests/Common/GitHelperTests.cs new file mode 100644 index 00000000..471a791c --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/GitHelperTests.cs @@ -0,0 +1,73 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class GitHelperTests + { + [TestCase] + public void IsVerbTest() + { + GitHelper.IsVerb("git status --no-idea", "status").ShouldEqual(true); + GitHelper.IsVerb("git status", "status").ShouldEqual(true); + GitHelper.IsVerb("git statuses --no-idea", "status").ShouldEqual(false); + GitHelper.IsVerb("git statuses", "status").ShouldEqual(false); + + GitHelper.IsVerb("git add some/file/to/add", "add", "status", "reset").ShouldEqual(true); + GitHelper.IsVerb("git adding add", "add", "status", "reset").ShouldEqual(false); + GitHelper.IsVerb("git add some/file/to/add", "adding", "status", "reset").ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHAIsFalseForEmptyString() + { + GitHelper.IsValidFullSHA(string.Empty).ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHAIsFalseForHexStringsNot40Chars() + { + GitHelper.IsValidFullSHA("1").ShouldEqual(false); + GitHelper.IsValidFullSHA("9").ShouldEqual(false); + GitHelper.IsValidFullSHA("A").ShouldEqual(false); + GitHelper.IsValidFullSHA("a").ShouldEqual(false); + GitHelper.IsValidFullSHA("f").ShouldEqual(false); + GitHelper.IsValidFullSHA("f").ShouldEqual(false); + GitHelper.IsValidFullSHA("1234567890abcdefABCDEF").ShouldEqual(false); + GitHelper.IsValidFullSHA("12345678901234567890123456789012345678901").ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHAFalseForNonHexStrings() + { + GitHelper.IsValidFullSHA("@").ShouldEqual(false); + GitHelper.IsValidFullSHA("g").ShouldEqual(false); + GitHelper.IsValidFullSHA("G").ShouldEqual(false); + GitHelper.IsValidFullSHA("~").ShouldEqual(false); + GitHelper.IsValidFullSHA("_").ShouldEqual(false); + GitHelper.IsValidFullSHA(".").ShouldEqual(false); + GitHelper.IsValidFullSHA("1234567890abcdefABCDEF.tmp").ShouldEqual(false); + GitHelper.IsValidFullSHA("G1234567890abcdefABCDEF.tmp").ShouldEqual(false); + GitHelper.IsValidFullSHA("_G1234567890abcdefABCDEF.tmp").ShouldEqual(false); + GitHelper.IsValidFullSHA("@234567890123456789012345678901234567890").ShouldEqual(false); + GitHelper.IsValidFullSHA("g234567890123456789012345678901234567890").ShouldEqual(false); + GitHelper.IsValidFullSHA("G234567890123456789012345678901234567890").ShouldEqual(false); + GitHelper.IsValidFullSHA("~234567890123456789012345678901234567890").ShouldEqual(false); + GitHelper.IsValidFullSHA("_234567890123456789012345678901234567890").ShouldEqual(false); + GitHelper.IsValidFullSHA(".234567890123456789012345678901234567890").ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHATrueForLength40HexStrings() + { + GitHelper.IsValidFullSHA("1234567890123456789012345678901234567890").ShouldEqual(true); + GitHelper.IsValidFullSHA("abcdef7890123456789012345678901234567890").ShouldEqual(true); + GitHelper.IsValidFullSHA("ABCDEF7890123456789012345678901234567890").ShouldEqual(true); + GitHelper.IsValidFullSHA("1234567890123456789012345678901234ABCDEF").ShouldEqual(true); + GitHelper.IsValidFullSHA("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").ShouldEqual(true); + GitHelper.IsValidFullSHA("ffffffffffffffffffffffffffffffffffffffff").ShouldEqual(true); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/GitPathConverterTests.cs b/GVFS/GVFS.UnitTests/Common/GitPathConverterTests.cs new file mode 100644 index 00000000..e668143f --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/GitPathConverterTests.cs @@ -0,0 +1,56 @@ +using GVFS.Common.Git; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class GitPathConverterTests + { + private const string OctetEncoded = @"\330\261\331\212\331\204\331\214\330\243\331\203\330\252\331\210\330\250\330\261\303\273\331\205\330\247\330\261\330\263\330\243\330\272\330\263\330\267\330\263\302\272\331\260\331\260\333\202\331\227\331\222\333\265\330\261\331\212\331\204\331\214\330\243\331\203"; + private const string Utf8Encoded = @"ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك"; + private const string TestPath = @"/GVFS/"; + + [TestCase] + public void NullFilepathTest() + { + GitPathConverter.ConvertPathOctetsToUtf8(null).ShouldEqual(null); + } + + [TestCase] + public void EmptyFilepathTest() + { + GitPathConverter.ConvertPathOctetsToUtf8(string.Empty).ShouldEqual(string.Empty); + } + + [TestCase] + public void FilepathWithoutOctets() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + "test.cs").ShouldEqual(TestPath + "test.cs"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFilename() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded).ShouldEqual(TestPath + Utf8Encoded); + } + + [TestCase] + public void FilepathWithoutOctetsAsFilenameNoExtension() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + ".txt"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFolder() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + "/file.txt").ShouldEqual(TestPath + Utf8Encoded + "/file.txt"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFileAndFolder() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + TestPath + Utf8Encoded + ".txt"); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs new file mode 100644 index 00000000..7ae9dd85 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/GitVersionTests.cs @@ -0,0 +1,186 @@ +using GVFS.Common.Git; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class GitVersionTests + { + [TestCase] + public void Version_Data_Null_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParse(null, out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Empty_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParse(string.Empty, out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Not_Enough_Numbers_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParse("2.0.1.test", out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Too_Many_Numbers_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParse("2.0.1.test.1.4.3.6", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Version_Data_Valid_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParse("2.0.1.test.1.2", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Compare_Different_Platforms_Returns_False() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test1", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Equal() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Major_Less() + { + GitVersion version1 = new GitVersion(0, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_Major_Greater() + { + GitVersion version1 = new GitVersion(2, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Minor_Less() + { + GitVersion version1 = new GitVersion(1, 1, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_Minor_Greater() + { + GitVersion version1 = new GitVersion(1, 3, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Build_Less() + { + GitVersion version1 = new GitVersion(1, 2, 2, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_Build_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 4, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Revision_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 3, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_Revision_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 5, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_MinorRevision_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 2); + + version1.IsLessThan(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_MinorRevision_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 2); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + } + + [TestCase] + public void Allow_Blank_Minor_Revision() + { + GitVersion version; + GitVersion.TryParse("1.2.3.test.4", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.Revision.ShouldEqual(4); + version.Platform.ShouldEqual("test"); + version.MinorRevision.ShouldEqual(0); + } + + [TestCase] + public void Allow_Invalid_Minor_Revision() + { + GitVersion version; + GitVersion.TryParse("1.2.3.test.4.notint", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.Revision.ShouldEqual(4); + version.Platform.ShouldEqual("test"); + version.MinorRevision.ShouldEqual(0); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/JsonEtwTracerTests.cs b/GVFS/GVFS.UnitTests/Common/JsonEtwTracerTests.cs new file mode 100644 index 00000000..52fe63c4 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/JsonEtwTracerTests.cs @@ -0,0 +1,102 @@ +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using Microsoft.Diagnostics.Tracing; +using NUnit.Framework; +using System.Collections.Generic; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class JsonEtwTracerTests + { + [TestCase] + public void EventsAreFilteredByVerbosity() + { + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity")) + using (MockListener listener = new MockListener(EventLevel.Informational, Keywords.Any)) + { + listener.EnableEvents(tracer.EvtSource, EventLevel.Verbose); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); + listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); + } + + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity")) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) + { + listener.EnableEvents(tracer.EvtSource, EventLevel.Verbose); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); + } + } + + [TestCase] + public void EventsAreFilteredByKeyword() + { + // Network filters all but network out + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity")) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Network)) + { + listener.EnableEvents(tracer.EvtSource, EventLevel.Verbose); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); + listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); + } + + // Any filters nothing out + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity")) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) + { + listener.EnableEvents(tracer.EvtSource, EventLevel.Verbose); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); + } + + // None filters everything out (including events marked as none) + using (JsonEtwTracer tracer = new JsonEtwTracer("Microsoft-GVFS-Test", "EventsAreFilteredByVerbosity")) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.None)) + { + listener.EnableEvents(tracer.EvtSource, EventLevel.Verbose); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldNotReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldBeEmpty(); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoNotReceive", metadata: null); + listener.EventNamesRead.ShouldBeEmpty(); + } + } + + public class MockListener : ConsoleEventListener + { + public readonly List EventNamesRead = new List(); + + public MockListener(EventLevel maxVerbosity, Keywords keywordFilter) : base(maxVerbosity, keywordFilter) + { + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (!this.IsEnabled(eventData.Level, eventData.Keywords)) + { + return; + } + + this.EventNamesRead.Add(eventData.EventName); + } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/ProcessHelperTests.cs b/GVFS/GVFS.UnitTests/Common/ProcessHelperTests.cs new file mode 100644 index 00000000..f3d0066c --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/ProcessHelperTests.cs @@ -0,0 +1,56 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Diagnostics; +using System.IO; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class ProcessHelperTests + { + [TestCase] + public void GetCommandLineTest() + { + Process internalProcess = null; + StreamWriter stdin = null; + + try + { + ProcessStartInfo processInfo = new ProcessStartInfo("git.exe"); + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = false; + processInfo.RedirectStandardError = false; + processInfo.RedirectStandardInput = true; + processInfo.Arguments = "hash-object --stdin"; + + internalProcess = Process.Start(processInfo); + stdin = internalProcess.StandardInput; + + // Get the process as an external process + string commandLine = ProcessHelper.GetCommandLine(Process.GetProcessById(internalProcess.Id)); + + commandLine.EndsWith("\"git.exe\" hash-object --stdin").ShouldEqual(true); + } + finally + { + // End internal process. + if (stdin != null) + { + stdin.WriteLine("dummy"); + stdin.Close(); + } + + if (internalProcess != null) + { + if (!internalProcess.HasExited) + { + internalProcess.Kill(); + } + + internalProcess.Dispose(); + } + } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/RetryWrapperTests.cs b/GVFS/GVFS.UnitTests/Common/RetryWrapperTests.cs new file mode 100644 index 00000000..26d0a1fb --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/RetryWrapperTests.cs @@ -0,0 +1,149 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using NUnit.Framework; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class RetryWrapperTests + { + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void WillRetryOnIOException() + { + const int ExpectedTries = 5; + + RetryWrapper dut = new RetryWrapper(ExpectedTries, exponentialBackoffBase: 0); + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.InvokeAsync( + tryCount => + { + actualTries++; + throw new IOException(); + }).Result; + + output.Succeeded.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void WillNotRetryForGenericExceptions() + { + const int MaxTries = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, exponentialBackoffBase: 0); + + Assert.Throws( + () => + { + RetryWrapper.InvocationResult output = dut.InvokeAsync(tryCount => { throw new Exception(); }).Result; + }); + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void OnFailureIsCalledWhenEventHandlerAttached() + { + const int MaxTries = 5; + const int ExpectedFailures = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + RetryWrapper.InvocationResult output = dut.InvokeAsync( + tryCount => + { + throw new IOException(); + }).Result; + + output.Succeeded.ShouldEqual(false); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void OnSuccessIsOnlyCalledOnce() + { + const int MaxTries = 5; + const int ExpectedFailures = 0; + const int ExpectedTries = 1; + + RetryWrapper dut = new RetryWrapper(MaxTries, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.InvokeAsync( + tryCount => + { + actualTries++; + return Task.Run(() => new RetryWrapper.CallbackResult(true)); + }).Result; + + output.Succeeded.ShouldEqual(true); + output.Result.ShouldEqual(true); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void WillNotRetryWhenNotRequested() + { + const int MaxTries = 5; + const int ExpectedFailures = 1; + const int ExpectedTries = 1; + + RetryWrapper dut = new RetryWrapper(MaxTries, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.InvokeAsync( + tryCount => + { + actualTries++; + return Task.Run(() => new RetryWrapper.CallbackResult(new Exception("Test"), false)); + }).Result; + + output.Succeeded.ShouldEqual(false); + output.Result.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void WillRetryWhenRequested() + { + const int MaxTries = 5; + const int ExpectedFailures = 5; + const int ExpectedTries = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.InvokeAsync( + tryCount => + { + actualTries++; + return Task.Run(() => new RetryWrapper.CallbackResult(new Exception("Test"), true)); + }).Result; + + output.Succeeded.ShouldEqual(false); + output.Result.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/SHA1UtilTests.cs b/GVFS/GVFS.UnitTests/Common/SHA1UtilTests.cs new file mode 100644 index 00000000..5de2f1a6 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/SHA1UtilTests.cs @@ -0,0 +1,28 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.Text; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class SHA1UtilTests + { + private const string TestString = "c:\\Repos\\GVFS\\src\\.gittattributes"; + private const string TestResultSha1 = "ced5ad9680c1a05e9100680c2b3432de23bb7d6d"; + private const string TestResultHex = "633a5c5265706f735c475646535c7372635c2e6769747461747472696275746573"; + + [TestCase] + public void SHA1HashStringForUTF8String() + { + SHA1Util.SHA1HashStringForUTF8String(TestString).ShouldEqual(TestResultSha1); + } + + [TestCase] + public void HexStringFromBytes() + { + byte[] bytes = Encoding.UTF8.GetBytes(TestString); + SHA1Util.HexStringFromBytes(bytes).ShouldEqual(TestResultHex); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Data/backward.txt b/GVFS/GVFS.UnitTests/Data/backward.txt new file mode 100644 index 00000000..5c333e02 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Data/backward.txt @@ -0,0 +1,26 @@ +:040000 000000 34df85f635cb0e04c6d4ecedefe2707951616059 0000000000000000000000000000000000000000 D New folder +:100644 000000 953905e07b8e3fb9f8c9e5b8bd0e6cd1a2b3fbae 0000000000000000000000000000000000000000 D New folder/newFile.txt +:000000 040000 0000000000000000000000000000000000000000 2c877e829ad58c051e5f1b30391e41341d4ff56c A deepfolderdelete +:000000 040000 0000000000000000000000000000000000000000 cd3eb0505948ac13b8b550891be81fdf4def0dae A deepfolderdelete/subfolder +:000000 100644 0000000000000000000000000000000000000000 e02f6e8a454811f5a613743d3e9133afd545e36b A deepfolderdelete/subfolder/necessary.txt +:000000 100644 0000000000000000000000000000000000000000 1e2db0ae91731f7862918bb50b2af43050719cbd A fileToBecomeFolder +:040000 000000 f826e58b21f78935976a9779b27773e55e3b6429 0000000000000000000000000000000000000000 D fileToBecomeFolder +:100644 000000 39d05dc47b628f461ef92a4531db129c5d7ea4fb 0000000000000000000000000000000000000000 D fileToBecomeFolder/newdoc.txt +:000000 100644 0000000000000000000000000000000000000000 05c2b79a2c84782364b0f27244454ea29187bde6 A fileToDelete.txt +:100644 100644 c97bc99f8894348d4a8e4b8146fbac3384ac8cac 3afc1ef879df949927c85b4c849a65e39aa472c7 M fileToEdit.txt +:000000 100644 0000000000000000000000000000000000000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 A fileToRename.txt +:000000 100644 0000000000000000000000000000000000000000 ff236b434e083db032f4ac0f452303f4eeb8f4be A fileToRenameEdit.txt +:100644 000000 382d0750aef7b7e47b08f12befb7274311c4a850 0000000000000000000000000000000000000000 D fileWasRename.txt +:100644 000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 0000000000000000000000000000000000000000 D fileWasRenamed.txt +:100644 000000 c419470b3e2ee3b43d9a13edb180a453b06c0fb5 0000000000000000000000000000000000000000 D folderToBeFile +:000000 040000 0000000000000000000000000000000000000000 184fd663a53e325add265346ddd024b5a8829301 A folderToBeFile +:000000 100644 0000000000000000000000000000000000000000 f51ba4dac00d291e491906a99698c01f802e652d A folderToBeFile/existence.txt +:000000 040000 0000000000000000000000000000000000000000 a5731b081f875263a428c126a3242587fdaea6c9 A folderToDelete +:000000 100644 0000000000000000000000000000000000000000 8724404da33605d27cfa5490d412f0dfea16a277 A folderToDelete/fileToEdit2.txt +:040000 040000 403c21d5d12f6e6a6659460b5cec7e4b66b6c0f2 9a894ca28a2a8d813f2bae305cd94faf366ad5e0 M folderToEdit +:100644 100644 45f57f3292952208ad72232bc2a64038a3d0987f 59b502537b91543cb8aa7e6a26ee235197fa154b M folderToEdit/fileToEdit2.txt +:000000 040000 0000000000000000000000000000000000000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 A folderToRename +:000000 100644 0000000000000000000000000000000000000000 35151eac3bb089589836bfece4dfbb84fae502de A folderToRename/existence.txt +:040000 000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 0000000000000000000000000000000000000000 D folderWasRenamed +:100644 000000 35151eac3bb089589836bfece4dfbb84fae502de 0000000000000000000000000000000000000000 D folderWasRenamed/existence.txt +:100644 000000 9ff3c67e2792e4c17672c6b04a8a6efe732cb07a 0000000000000000000000000000000000000000 D newFile.txt diff --git a/GVFS/GVFS.UnitTests/Data/forward.txt b/GVFS/GVFS.UnitTests/Data/forward.txt new file mode 100644 index 00000000..e3fb40b6 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Data/forward.txt @@ -0,0 +1,26 @@ +:000000 040000 0000000000000000000000000000000000000000 34df85f635cb0e04c6d4ecedefe2707951616059 A New folder +:000000 100644 0000000000000000000000000000000000000000 953905e07b8e3fb9f8c9e5b8bd0e6cd1a2b3fbae A New folder/newFile.txt +:040000 000000 2c877e829ad58c051e5f1b30391e41341d4ff56c 0000000000000000000000000000000000000000 D deepfolderdelete +:040000 000000 cd3eb0505948ac13b8b550891be81fdf4def0dae 0000000000000000000000000000000000000000 D deepfolderdelete/subfolder +:100644 000000 e02f6e8a454811f5a613743d3e9133afd545e36b 0000000000000000000000000000000000000000 D deepfolderdelete/subfolder/necessary.txt +:100644 000000 1e2db0ae91731f7862918bb50b2af43050719cbd 0000000000000000000000000000000000000000 D fileToBecomeFolder +:000000 040000 0000000000000000000000000000000000000000 f826e58b21f78935976a9779b27773e55e3b6429 A fileToBecomeFolder +:000000 100644 0000000000000000000000000000000000000000 39d05dc47b628f461ef92a4531db129c5d7ea4fb A fileToBecomeFolder/newdoc.txt +:100644 000000 05c2b79a2c84782364b0f27244454ea29187bde6 0000000000000000000000000000000000000000 D fileToDelete.txt +:100644 100644 3afc1ef879df949927c85b4c849a65e39aa472c7 c97bc99f8894348d4a8e4b8146fbac3384ac8cac M fileToEdit.txt +:100644 000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 0000000000000000000000000000000000000000 D fileToRename.txt +:100644 000000 ff236b434e083db032f4ac0f452303f4eeb8f4be 0000000000000000000000000000000000000000 D fileToRenameEdit.txt +:000000 100644 0000000000000000000000000000000000000000 382d0750aef7b7e47b08f12befb7274311c4a850 A fileWasRename.txt +:000000 100644 0000000000000000000000000000000000000000 0fb01bbb41482aec747112b58f1a11cf41bb7a11 A fileWasRenamed.txt +:000000 100644 0000000000000000000000000000000000000000 c419470b3e2ee3b43d9a13edb180a453b06c0fb5 A folderToBeFile +:040000 000000 184fd663a53e325add265346ddd024b5a8829301 0000000000000000000000000000000000000000 D folderToBeFile +:100644 000000 f51ba4dac00d291e491906a99698c01f802e652d 0000000000000000000000000000000000000000 D folderToBeFile/existence.txt +:040000 000000 a5731b081f875263a428c126a3242587fdaea6c9 0000000000000000000000000000000000000000 D folderToDelete +:100644 000000 8724404da33605d27cfa5490d412f0dfea16a277 0000000000000000000000000000000000000000 D folderToDelete/fileToEdit2.txt +:040000 040000 9a894ca28a2a8d813f2bae305cd94faf366ad5e0 403c21d5d12f6e6a6659460b5cec7e4b66b6c0f2 M folderToEdit +:100644 100644 59b502537b91543cb8aa7e6a26ee235197fa154b 45f57f3292952208ad72232bc2a64038a3d0987f M folderToEdit/fileToEdit2.txt +:040000 000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 0000000000000000000000000000000000000000 D folderToRename +:100644 000000 35151eac3bb089589836bfece4dfbb84fae502de 0000000000000000000000000000000000000000 D folderToRename/existence.txt +:000000 040000 0000000000000000000000000000000000000000 e30358a677e3a55aa838d0bbe0207f61ce40f401 A folderWasRenamed +:000000 100644 0000000000000000000000000000000000000000 35151eac3bb089589836bfece4dfbb84fae502de A folderWasRenamed/existence.txt +:000000 100644 0000000000000000000000000000000000000000 9ff3c67e2792e4c17672c6b04a8a6efe732cb07a A newFile.txt diff --git a/GVFS/GVFS.UnitTests/Data/index_v2 b/GVFS/GVFS.UnitTests/Data/index_v2 new file mode 100644 index 00000000..d8e03029 Binary files /dev/null and b/GVFS/GVFS.UnitTests/Data/index_v2 differ diff --git a/GVFS/GVFS.UnitTests/Data/index_v3 b/GVFS/GVFS.UnitTests/Data/index_v3 new file mode 100644 index 00000000..172ed9f4 Binary files /dev/null and b/GVFS/GVFS.UnitTests/Data/index_v3 differ diff --git a/GVFS/GVFS.UnitTests/Data/index_v4 b/GVFS/GVFS.UnitTests/Data/index_v4 new file mode 100644 index 00000000..5d7a2e8c Binary files /dev/null and b/GVFS/GVFS.UnitTests/Data/index_v4 differ diff --git a/GVFS/GVFS.UnitTests/FastFetch/BatchObjectDownloadJobTests.cs b/GVFS/GVFS.UnitTests/FastFetch/BatchObjectDownloadJobTests.cs new file mode 100644 index 00000000..c292fa14 --- /dev/null +++ b/GVFS/GVFS.UnitTests/FastFetch/BatchObjectDownloadJobTests.cs @@ -0,0 +1,81 @@ +using FastFetch.Jobs; +using GVFS.Tests.Should; +using GVFS.UnitTests.Mock.Common; +using GVFS.UnitTests.Mock.Physical.Git; +using NUnit.Framework; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Reflection; + +namespace GVFS.UnitTests.FastFetch +{ + [TestFixture] + public class BatchObjectDownloadJobTests + { + private const int MaxParallel = 1; + private const int ChunkSize = 2; + + [TestCase] + public void OnlyRequestsObjectsNotDownloaded() + { + string obj1Sha = new string('1', 40); + string obj2Sha = new string('2', 40); + + BlockingCollection input = new BlockingCollection(); + input.Add(obj1Sha); + input.Add(obj2Sha); + input.CompleteAdding(); + + int obj1Count = 0; + int obj2Count = 0; + + Func objectResolver = (oid) => + { + if (oid.Equals(obj1Sha)) + { + obj1Count++; + return "Object1Contents"; + } + + if (oid.Equals(obj2Sha) && obj2Count++ == 1) + { + return "Object2Contents"; + } + + return null; + }; + + BlockingCollection output = new BlockingCollection(); + MockTracer tracer = new MockTracer(); + MockEnlistment enlistment = new MockEnlistment(); + MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver); + + BatchObjectDownloadJob dut = new BatchObjectDownloadJob( + MaxParallel, + ChunkSize, + input, + output, + tracer, + enlistment, + httpObjects, + new MockPhysicalGitObjects(tracer, enlistment, httpObjects)); + + dut.Start(); + dut.WaitForCompletion(); + + input.Count.ShouldEqual(0); + output.Count.ShouldEqual(2); + output.Take().ShouldEqual(obj1Sha); + output.Take().ShouldEqual(obj2Sha); + obj1Count.ShouldEqual(1); + obj2Count.ShouldEqual(2); + } + + private string GetDataPath(string fileName) + { + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(workingDirectory, "Data", fileName); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/FastFetch/DiffHelperTests.cs b/GVFS/GVFS.UnitTests/FastFetch/DiffHelperTests.cs new file mode 100644 index 00000000..efbe39fb --- /dev/null +++ b/GVFS/GVFS.UnitTests/FastFetch/DiffHelperTests.cs @@ -0,0 +1,102 @@ +using GVFS.Common.Git; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using GVFS.UnitTests.Mock.Common; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.UnitTests.FastFetch +{ + [TestFixture] + public class DiffHelperTests + { + // Make two commits. The first should look like this: + // recursiveDelete + // recursiveDelete/subfolder + // recursiveDelete/subfolder/childFile.txt + // fileToBecomeFolder + // fileToDelete.txt + // fileToEdit.txt + // fileToRename.txt + // fileToRenameEdit.txt + // folderToBeFile + // folderToBeFile/childFile.txt + // folderToDelete + // folderToDelete/childFile.txt + // folderToEdit + // folderToEdit/childFile.txt + // folderToRename + // folderToRename/childFile.txt + // + // The second should follow the action indicated by the file/folder name: + // eg. recursiveDelete should run "rmdir /s/q recursiveDelete" + // eg. folderToBeFile should be deleted and replaced with a file of the same name + // Note that each childFile.txt should have unique contents, but is only a placeholder to force git to add a folder. + // + // Then to generate the diffs, run: + // git diff-tree -r -t Head~1 Head > forward.txt + // git diff-tree -r -t Head Head ~1 > backward.txt + [TestCase] + public void CanParseDiffForwards() + { + MockTracer tracer = new MockTracer(); + DiffHelper diffForwards = new DiffHelper(tracer, null, null, new List()); + diffForwards.ParseDiffFile(this.GetDataPath("forward.txt"), "xx:\\fakeRepo"); + + // File added, file edited, file renamed, folder => file, edit-rename file + // Children of: Add folder, Renamed folder, edited folder, file => folder + diffForwards.RequiredBlobs.Count.ShouldEqual(9); + + // File deleted, folder deleted, file > folder, edit-rename + diffForwards.FileDeleteOperations.Count.ShouldEqual(4); + + // Includes children of: Recursive delete folder, deleted folder, renamed folder, and folder => file + diffForwards.TotalFileDeletes.ShouldEqual(8); + + // Folder created, folder edited, folder deleted, folder renamed (add + delete), + // folder => file, file => folder, recursive delete (top-level only) + diffForwards.DirectoryOperations.Count.ShouldEqual(8); + + // Should also include the deleted folder of recursive delete + diffForwards.TotalDirectoryOperations.ShouldEqual(9); + } + + // Parses Diff B => A + [TestCase] + public void CanParseBackwardsDiff() + { + MockTracer tracer = new MockTracer(); + DiffHelper diffBackwards = new DiffHelper(tracer, null, null, new List()); + diffBackwards.ParseDiffFile(this.GetDataPath("backward.txt"), "xx:\\fakeRepo"); + + // File > folder, deleted file, edited file, renamed file, rename-edit file + // Children of file > folder, renamed folder, deleted folder, recursive delete file, edited folder + diffBackwards.RequiredBlobs.Count.ShouldEqual(10); + + // File added, folder > file, moved folder, added folder + diffBackwards.FileDeleteOperations.Count.ShouldEqual(4); + + // Also includes, the children of: Folder added, folder renamed, file => folder + diffBackwards.TotalFileDeletes.ShouldEqual(7); + + // Folder created, folder edited, folder deleted, folder renamed (add + delete), + // folder => file, file => folder, recursive delete (include subfolder) + diffBackwards.DirectoryOperations.Count.ShouldEqual(9); + + // Should match count above since there were no recursive adds to become recursive deletes + diffBackwards.TotalDirectoryOperations.ShouldEqual(9); + } + + private string GetDataPath(string fileName) + { + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(workingDirectory, "Data", fileName); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/FastFetch/FastFetchTracingTests.cs b/GVFS/GVFS.UnitTests/FastFetch/FastFetchTracingTests.cs new file mode 100644 index 00000000..33735756 --- /dev/null +++ b/GVFS/GVFS.UnitTests/FastFetch/FastFetchTracingTests.cs @@ -0,0 +1,95 @@ +using FastFetch.Jobs; +using FastFetch.Jobs.Data; +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using GVFS.UnitTests.Mock.Common; +using GVFS.UnitTests.Mock.Physical.Git; +using NUnit.Framework; +using System.Collections.Concurrent; + +namespace GVFS.UnitTests.FastFetch +{ + [TestFixture] + public class FastFetchTracingTests + { + private const string FakeSha = "fakesha"; + private const string FakeShaContents = "fakeshacontents"; + + [TestCase] + public void ErrorsForBatchObjectDownloadJob() + { + using (JsonEtwTracer tracer = CreateTracer()) + { + MockEnlistment enlistment = new MockEnlistment(); + MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, enlistment, httpGitObjects); + + BlockingCollection input = new BlockingCollection(); + input.Add(FakeSha); + input.CompleteAdding(); + + BatchObjectDownloadJob dut = new BatchObjectDownloadJob(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + + string sha; + input.TryTake(out sha).ShouldEqual(false); + + IndexPackRequest request; + dut.AvailablePacks.TryTake(out request).ShouldEqual(false); + } + } + + [TestCase] + public void SuccessForBatchObjectDownloadJob() + { + using (JsonEtwTracer tracer = CreateTracer()) + { + MockEnlistment enlistment = new MockEnlistment(); + MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); + httpGitObjects.AddBlobContent(FakeSha, FakeShaContents); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, enlistment, httpGitObjects); + + BlockingCollection input = new BlockingCollection(); + input.Add(FakeSha); + input.CompleteAdding(); + + BatchObjectDownloadJob dut = new BatchObjectDownloadJob(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + + string sha; + input.TryTake(out sha).ShouldEqual(false); + dut.AvailablePacks.Count.ShouldEqual(0); + + dut.AvailableObjects.Count.ShouldEqual(1); + string output = dut.AvailableObjects.Take(); + output.ShouldEqual(FakeSha); + } + } + + [TestCase] + public void ErrorsForIndexPackFile() + { + using (JsonEtwTracer tracer = CreateTracer()) + { + MockEnlistment enlistment = new MockEnlistment(); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, enlistment, null); + + BlockingCollection input = new BlockingCollection(); + BlobDownloadRequest downloadRequest = new BlobDownloadRequest(new string[] { FakeSha }); + input.Add(new IndexPackRequest("mock:\\path\\packFileName", downloadRequest)); + input.CompleteAdding(); + + IndexPackJob dut = new IndexPackJob(1, input, new BlockingCollection(), tracer, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + } + } + + private static JsonEtwTracer CreateTracer() + { + return new JsonEtwTracer("Microsoft-FastFetch-Test", "FastFetchTest"); + } + } +} diff --git a/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj new file mode 100644 index 00000000..5deb1ef3 --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFS.UnitTests.csproj @@ -0,0 +1,173 @@ + + + + + Debug + AnyCPU + {8E0D0989-21F6-4DD8-946C-39F992523CC6} + Exe + Properties + GVFS.UnitTests + GVFS.UnitTests + v4.5.2 + 512 + true + + + + + true + ..\..\..\BuildOutput\GVFS.UnitTests\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS.UnitTests\obj\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + ..\..\..\BuildOutput\GVFS.UnitTests\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS.UnitTests\obj\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + + False + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + False + ..\..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll + True + + + False + ..\..\..\packages\NUnitLite.3.5.0\lib\net45\nunitlite.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Designer + + + + + {07f2a520-2ab7-46dd-97c0-75d8e988d55b} + FastFetch + + + {9ea6ff63-6bb0-4440-9bfb-0ae79a8f9ba9} + GVFS.Common + + + {1118b427-7063-422f-83b9-5023c8ec5a7a} + GVFS.GVFlt + + + {72701bc3-5da9-4c7a-bf10-9e98c9fc8eac} + GVFS.Tests + + + {bd670d9b-22d8-4ec0-9912-6d0c56c7d7fd} + GVFS + + + + + Always + + + Always + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/GVFlt/DotGit/ExcludeFileTests.cs b/GVFS/GVFS.UnitTests/GVFlt/DotGit/ExcludeFileTests.cs new file mode 100644 index 00000000..a78d1dd8 --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/DotGit/ExcludeFileTests.cs @@ -0,0 +1,97 @@ +using GVFS.GVFlt.DotGit; +using GVFS.Tests.Should; +using GVFS.UnitTests.Virtual; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.UnitTests.GVFlt.DotGit +{ + [TestFixture] + public class ExcludeFileTests : TestsWithCommonRepo + { + [TestCase] + public void HasDefaultEntriesAfterLoad() + { + string excludeFilePath = Path.Combine(this.Repo.GitParentPath, GVFS.Common.GVFSConstants.DotGit.Info.ExcludeName); + ExcludeFile excludeFile = new ExcludeFile(this.Repo.Context, excludeFilePath); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(false); + excludeFile.LoadOrCreate(); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(true); + + List expectedContents = new List() { "*" }; + this.CheckFileContents(excludeFilePath, expectedContents); + } + + [TestCase] + public void WritesOnFolderChange() + { + string excludeFilePath = Path.Combine(this.Repo.GitParentPath, GVFS.Common.GVFSConstants.DotGit.Info.ExcludeName); + ExcludeFile excludeFile = new ExcludeFile(this.Repo.Context, excludeFilePath); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(false); + excludeFile.LoadOrCreate(); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(true); + + excludeFile.FolderChanged("A\\B\\C"); + excludeFile.FolderChanged("A\\D\\E"); + + List expectedContents = new List() { "*", "!/A", "!/A/B", "!/A/B/C", "!/A/B/C/*", "!/A/D", "!/A/D/E", "!/A/D/E/*" }; + this.CheckFileContents(excludeFilePath, expectedContents); + } + + [TestCase] + + public void DoesNotWriteDuplicateFolderEntries() + { + string excludeFilePath = Path.Combine(this.Repo.GitParentPath, GVFS.Common.GVFSConstants.DotGit.Info.ExcludeName); + ExcludeFile excludeFile = new ExcludeFile(this.Repo.Context, excludeFilePath); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(false); + excludeFile.LoadOrCreate(); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(true); + + excludeFile.FolderChanged("A\\B"); + excludeFile.FolderChanged("a\\b"); + excludeFile.FolderChanged("A\\D"); + excludeFile.FolderChanged("A\\d"); + excludeFile.FolderChanged("a\\f"); + excludeFile.FolderChanged("a\\F"); + + List expectedContents = new List() { "*", "!/A", "!/A/B", "!/A/B/*", "!/A/D", "!/A/D/*", "!/a/f", "!/a/f/*" }; + this.CheckFileContents(excludeFilePath, expectedContents); + } + + [TestCase] + public void WritesAfterLoad() + { + string excludeFilePath = Path.Combine(this.Repo.GitParentPath, GVFS.Common.GVFSConstants.DotGit.Info.ExcludeName); + ExcludeFile excludeFile = new ExcludeFile(this.Repo.Context, excludeFilePath); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(false); + excludeFile.LoadOrCreate(); + this.Repo.Context.FileSystem.FileExists(excludeFilePath).ShouldEqual(true); + + excludeFile.FolderChanged("A\\B"); + excludeFile.FolderChanged("A\\D"); + + List expectedContents = new List() { "*", "!/A", "!/A/B", "!/A/B/*", "!/A/D", "!/A/D/*" }; + this.CheckFileContents(excludeFilePath, expectedContents); + + excludeFile = new ExcludeFile(this.Repo.Context, excludeFilePath); + excludeFile.LoadOrCreate(); + excludeFile.FolderChanged("a\\f"); + + expectedContents = new List() { "*", "!/A", "!/A/B", "!/A/B/*", "!/A/D", "!/A/D/*", "!/a/f", "!/a/f/*" }; + this.CheckFileContents(excludeFilePath, expectedContents); + } + + private void CheckFileContents(string sparseCheckoutFilePath, List expectedContents) + { + IEnumerator expectedLines = expectedContents.GetEnumerator(); + expectedLines.MoveNext().ShouldEqual(true); + foreach (string fileLine in this.Repo.Context.FileSystem.ReadLines(sparseCheckoutFilePath)) + { + expectedLines.Current.ShouldEqual(fileLine); + expectedLines.MoveNext(); + } + } + } +} diff --git a/GVFS/GVFS.UnitTests/GVFlt/DotGit/GitConfigFileUtilsTests.cs b/GVFS/GVFS.UnitTests/GVFlt/DotGit/GitConfigFileUtilsTests.cs new file mode 100644 index 00000000..edc51d65 --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/DotGit/GitConfigFileUtilsTests.cs @@ -0,0 +1,77 @@ +using GVFS.GVFlt.DotGit; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.GVFlt.DotGit +{ + [TestFixture] + public class GitConfigFileUtilsTests + { + [TestCase] + public void SanitizeEmptyString() + { + string outputString; + GitConfigFileUtils.TrySanitizeConfigFileLine(string.Empty, out outputString).ShouldEqual(false); + } + + [TestCase] + public void SanitizePureWhiteSpace() + { + string outputString; + GitConfigFileUtils.TrySanitizeConfigFileLine(" ", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine(" \t\t ", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine(" \t\t\n\n ", out outputString).ShouldEqual(false); + } + + [TestCase] + public void SanitizeComment() + { + string outputString; + GitConfigFileUtils.TrySanitizeConfigFileLine("# This is a comment ", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine("# This is a comment #", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine("## This is a comment ##", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine(" ## This is a comment ## ", out outputString).ShouldEqual(false); + GitConfigFileUtils.TrySanitizeConfigFileLine("\t ## This is a comment ## \t ", out outputString).ShouldEqual(false); + } + + [TestCase] + public void TrimWhitspace() + { + string outputString; + GitConfigFileUtils.TrySanitizeConfigFileLine(" // ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("//"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" /* ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/*"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" /A ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigFileUtils.TrySanitizeConfigFileLine("\t /A \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" \t /A \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + } + + [TestCase] + public void TrimTrailingComment() + { + string outputString; + GitConfigFileUtils.TrySanitizeConfigFileLine(" // # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("//"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" /* # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/*"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" /A # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigFileUtils.TrySanitizeConfigFileLine("\t /A \t # Trailing comment! \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigFileUtils.TrySanitizeConfigFileLine(" \t /A \t # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + } + } +} diff --git a/GVFS/GVFS.UnitTests/GVFlt/GVFltActiveEnumerationTests.cs b/GVFS/GVFS.UnitTests/GVFlt/GVFltActiveEnumerationTests.cs new file mode 100644 index 00000000..96be7e5d --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/GVFltActiveEnumerationTests.cs @@ -0,0 +1,440 @@ +using GVFS.GVFlt; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GVFS.UnitTests.GVFlt +{ + [TestFixture] + public class GVFltActiveEnumerationTests + { + [TestCase] + public void EnumerationHandlesEmptyList() + { + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(new List())) + { + activeEnumeration.IsCurrentValid.ShouldEqual(false); + activeEnumeration.MoveNext().ShouldEqual(false); + activeEnumeration.RestartEnumeration(string.Empty); + activeEnumeration.IsCurrentValid.ShouldEqual(false); + } + } + + [TestCase] + public void EnumerateSingleEntryList() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false) + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateMultipleEntries() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateSingleEntryListWithEmptyFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false) + }; + + // Test empty string ("") filter + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + // Test null filter + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString(null).ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateSingleEntryListWithWildcardFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false) + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString("*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString("?").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString("*.*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateSingleEntryListWithMatchingFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false) + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString("a").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.TrySaveFilterString("A").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateSingleEntryListWithNonMatchingFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false) + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + string filter = "b"; + activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true); + activeEnumeration.IsCurrentValid.ShouldEqual(false); + activeEnumeration.MoveNext().ShouldEqual(false); + activeEnumeration.RestartEnumeration(filter); + activeEnumeration.IsCurrentValid.ShouldEqual(false); + } + } + + [TestCase] + public void CannotSetMoreThanOneFilter() + { + string filterString = "*.*"; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(new List())) + { + activeEnumeration.TrySaveFilterString(filterString).ShouldEqual(true); + activeEnumeration.TrySaveFilterString(null).ShouldEqual(false); + activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(false); + activeEnumeration.TrySaveFilterString("?").ShouldEqual(false); + activeEnumeration.GetFilterString().ShouldEqual(filterString); + } + } + + [TestCase] + public void EnumerateMultipleEntryListWithEmptyFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + // Test empty string ("") filter + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString(string.Empty).ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + // Test null filter + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString(null).ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + } + + [TestCase] + public void EnumerateMultipleEntryListWithWildcardFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("*.*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("*.txt").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase))); + } + + // '<' = DOS_STAR, matches 0 or more characters until encountering and matching + // the final . in the name + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("<.txt").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase))); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("?").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 1)); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("?.txt").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase))); + } + + // '>' = DOS_QM, matches any single character, or upon encountering a period or + // end of name string, advances the expression to the end of the + // set of contiguous DOS_QMs. + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString(">.txt").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.EndsWith(".txt", System.StringComparison.OrdinalIgnoreCase))); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("E.???").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase))); + } + + // '"' = DOS_DOT, matches either a . or zero characters beyond name string. + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("E\"*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("E", System.StringComparison.OrdinalIgnoreCase))); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("e\"*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("E", System.StringComparison.OrdinalIgnoreCase))); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("B\"*").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.StartsWith("B.", System.StringComparison.OrdinalIgnoreCase) || entry.Name.Equals("B", System.StringComparison.OrdinalIgnoreCase))); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("e.???").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name.Length == 5 && entry.Name.StartsWith("E.", System.StringComparison.OrdinalIgnoreCase))); + } + } + + [TestCase] + public void EnumerateMultipleEntryListWithMatchingFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("E.bat").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => entry.Name == "E.bat")); + } + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.TrySaveFilterString("e.bat").ShouldEqual(true); + this.ValidateActiveEnumeratorReturnsAllEntries(activeEnumeration, entries.Where(entry => string.Compare(entry.Name, "e.bat", StringComparison.OrdinalIgnoreCase) == 0)); + } + } + + [TestCase] + public void EnumerateMultipleEntryListWithNonMatchingFilter() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + string filter = "g"; + activeEnumeration.TrySaveFilterString(filter).ShouldEqual(true); + activeEnumeration.IsCurrentValid.ShouldEqual(false); + activeEnumeration.MoveNext().ShouldEqual(false); + activeEnumeration.RestartEnumeration(filter); + activeEnumeration.IsCurrentValid.ShouldEqual(false); + } + } + + [TestCase] + public void SettingFilterAdvancesEnumeratorToMatchingEntry() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.ShouldBeSameAs(entries[0]); + activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true); + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.Name.ShouldEqual("D.txt"); + } + } + + [TestCase] + public void RestartingScanWithFilterAdvancesEnumeratorToNewMatchingEntry() + { + List entries = new List() + { + new GVFltFileInfo("a", size: 0, isFolder:false), + new GVFltFileInfo("B", size: 0, isFolder:true), + new GVFltFileInfo("c", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.ShouldBeSameAs(entries[0]); + activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true); + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.Name.ShouldEqual("D.txt"); + + activeEnumeration.RestartEnumeration("c"); + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.Name.ShouldEqual("c"); + } + } + + [TestCase] + public void RestartingScanWithFilterAdvancesEnumeratorToFirstMatchingProjectedEntry() + { + GVFltFileInfo c_foo_file_info = new GVFltFileInfo("c.foo", size: 0, isFolder: false); + List entries = new List() + { + c_foo_file_info, + new GVFltFileInfo("C.TXT", size: 0, isFolder:false), + new GVFltFileInfo("D.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.txt", size: 0, isFolder:false), + new GVFltFileInfo("E.bat", size: 0, isFolder:false), + }; + + using (GVFltActiveEnumeration activeEnumeration = new GVFltActiveEnumeration(entries)) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.ShouldBeSameAs(entries[0]); + activeEnumeration.TrySaveFilterString("D.txt").ShouldEqual(true); + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.Name.ShouldEqual("D.txt"); + + c_foo_file_info.IsProjected = false; + + activeEnumeration.RestartEnumeration("c*"); + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.Name.ShouldEqual("C.TXT"); + } + } + + private void ValidateActiveEnumeratorReturnsAllEntries(GVFltActiveEnumeration activeEnumeration, IEnumerable entries) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + + // activeEnumeration should iterate over each entry in entries + foreach (GVFltFileInfo entry in entries) + { + activeEnumeration.IsCurrentValid.ShouldEqual(true); + activeEnumeration.Current.ShouldBeSameAs(entry); + activeEnumeration.MoveNext(); + } + + // activeEnumeration should no longer be valid after iterating beyond the end of the list + activeEnumeration.IsCurrentValid.ShouldEqual(false); + + // attempts to move beyond the end of the list should fail + activeEnumeration.MoveNext().ShouldEqual(false); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/GVFlt/GVFltCallbacksTests.cs b/GVFS/GVFS.UnitTests/GVFlt/GVFltCallbacksTests.cs new file mode 100644 index 00000000..c313a9b2 --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/GVFltCallbacksTests.cs @@ -0,0 +1,45 @@ +using GVFS.GVFlt; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.GVFlt.DotGit +{ + public class GVFltCallbacksTests + { + [TestCase] + public void CannotDeleteIndexOrPacks() + { + GVFltCallbacks.DoesPathAllowDelete(string.Empty).ShouldEqual(true); + + GVFltCallbacks.DoesPathAllowDelete(@".git\index").ShouldEqual(false); + GVFltCallbacks.DoesPathAllowDelete(@".git\INDEX").ShouldEqual(false); + + GVFltCallbacks.DoesPathAllowDelete(@".git\index.lock").ShouldEqual(true); + GVFltCallbacks.DoesPathAllowDelete(@".git\INDEX.lock").ShouldEqual(true); + GVFltCallbacks.DoesPathAllowDelete(@".git\objects\pack").ShouldEqual(true); + GVFltCallbacks.DoesPathAllowDelete(@".git\objects\pack-temp").ShouldEqual(true); + GVFltCallbacks.DoesPathAllowDelete(@".git\objects\pack\pack-1e88df2a4e234c82858cfe182070645fb96d6131.pack").ShouldEqual(true); + GVFltCallbacks.DoesPathAllowDelete(@".git\objects\pack\pack-1e88df2a4e234c82858cfe182070645fb96d6131.idx").ShouldEqual(true); + } + + [TestCase] + public void IsPathMonitoredForWrites() + { + GVFltCallbacks.IsPathMonitoredForWrites(string.Empty).ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\index").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\INDEX").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\index.lock").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\INDEX.lock").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\head").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\HEAD").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\head.lock").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\HEAD.lock").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\refs\heads\master").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\refs\heads\users\testuser\feature_branch").ShouldEqual(true); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\refs\remotes").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\refs\remotes\master").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\objects\pack").ShouldEqual(false); + GVFltCallbacks.IsPathMonitoredForWrites(@".git\objects").ShouldEqual(false); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/GVFlt/PathUtilTests.cs b/GVFS/GVFS.UnitTests/GVFlt/PathUtilTests.cs new file mode 100644 index 00000000..0f6ce5aa --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/PathUtilTests.cs @@ -0,0 +1,65 @@ +using GVFS.GVFlt; +using GVFS.Tests.Should; +using NUnit.Framework; + +namespace GVFS.UnitTests.GVFlt +{ + [TestFixture] + public class PathUtilTests + { + [TestCase] + public void EmptyStringIsNotInsideDotGitPath() + { + PathUtil.IsPathInsideDotGit(string.Empty).ShouldEqual(false); + } + + [TestCase] + public void IsPathInsideDotGitIsTrueForDotGitPath() + { + PathUtil.IsPathInsideDotGit(@".git\").ShouldEqual(true); + PathUtil.IsPathInsideDotGit(@".GIT\").ShouldEqual(true); + PathUtil.IsPathInsideDotGit(@".git\test_file.txt").ShouldEqual(true); + PathUtil.IsPathInsideDotGit(@".GIT\test_file.txt").ShouldEqual(true); + PathUtil.IsPathInsideDotGit(@".git\test_folder\test_file.txt").ShouldEqual(true); + PathUtil.IsPathInsideDotGit(@".GIT\test_folder\test_file.txt").ShouldEqual(true); + } + + [TestCase] + public void IsPathInsideDotGitIsFalseForNonDotGitPath() + { + PathUtil.IsPathInsideDotGit(@".git").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@".GIT").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@".gitattributes").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@".gitignore").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@".gitsubfolder\").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@".gitsubfolder\test_file.txt").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@"test_file.txt").ShouldEqual(false); + PathUtil.IsPathInsideDotGit(@"test_folder\test_file.txt").ShouldEqual(false); + } + + [TestCase] + public void RemoveTrailingSlashIfPresent() + { + PathUtil.RemoveTrailingSlashIfPresent(string.Empty).ShouldEqual(string.Empty); + PathUtil.RemoveTrailingSlashIfPresent(@"C:\test").ShouldEqual(@"C:\test"); + PathUtil.RemoveTrailingSlashIfPresent(@"C:\test\").ShouldEqual(@"C:\test"); + PathUtil.RemoveTrailingSlashIfPresent(@"C:\test\\").ShouldEqual(@"C:\test"); + PathUtil.RemoveTrailingSlashIfPresent(@"C:\test\\\").ShouldEqual(@"C:\test"); + } + + [TestCase] + public void IsEnumerationFilterSet() + { + PathUtil.IsEnumerationFilterSet(null).ShouldEqual(false); + PathUtil.IsEnumerationFilterSet(string.Empty).ShouldEqual(false); + PathUtil.IsEnumerationFilterSet(" ").ShouldEqual(false); + PathUtil.IsEnumerationFilterSet("*").ShouldEqual(false); + + PathUtil.IsEnumerationFilterSet("*.*").ShouldEqual(true); + PathUtil.IsEnumerationFilterSet("A.*").ShouldEqual(true); + PathUtil.IsEnumerationFilterSet("*.txt").ShouldEqual(true); + PathUtil.IsEnumerationFilterSet("A.txt").ShouldEqual(true); + PathUtil.IsEnumerationFilterSet("A").ShouldEqual(true); + } + } +} diff --git a/GVFS/GVFS.UnitTests/GVFlt/Physical/FileSerializerTests.cs b/GVFS/GVFS.UnitTests/GVFlt/Physical/FileSerializerTests.cs new file mode 100644 index 00000000..7800dc9e --- /dev/null +++ b/GVFS/GVFS.UnitTests/GVFlt/Physical/FileSerializerTests.cs @@ -0,0 +1,75 @@ +using GVFS.GVFlt.DotGit; +using GVFS.Tests.Should; +using GVFS.UnitTests.Virtual; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GVFS.UnitTests.GVFlt.Physical +{ + [TestFixture] + public class FileSerializerTests : TestsWithCommonRepo + { + [TestCase] + public void SerializerCreatesEmptyFileOnRead() + { + string filePath = Path.Combine(this.Repo.GitParentPath, "test-file"); + FileSerializer fileSerializer = new FileSerializer(this.Repo.Context, filePath); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(false); + fileSerializer.ReadAll().ShouldBeEmpty(); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(true); + } + + [TestCase] + public void SerializerCanAppend() + { + string filePath = Path.Combine(this.Repo.GitParentPath, "test-file"); + FileSerializer fileSerializer = new FileSerializer(this.Repo.Context, filePath); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(false); + fileSerializer.ReadAll().ShouldBeEmpty(); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(true); + + List lines = new List() { "test1", "test2", "test3" }; + foreach (string line in lines) + { + fileSerializer.AppendLine(line); + } + + this.Repo.Context.FileSystem.ReadLines(filePath).Count().ShouldEqual(lines.Count); + + IEnumerator expectedLines = lines.GetEnumerator(); + expectedLines.MoveNext().ShouldEqual(true); + foreach (string fileLine in this.Repo.Context.FileSystem.ReadLines(filePath)) + { + expectedLines.Current.ShouldEqual(fileLine); + expectedLines.MoveNext(); + } + } + + [TestCase] + public void SerializerCanLoad() + { + string filePath = Path.Combine(this.Repo.GitParentPath, "test-file"); + FileSerializer fileSerializer = new FileSerializer(this.Repo.Context, filePath); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(false); + fileSerializer.ReadAll().ShouldBeEmpty(); + this.Repo.Context.FileSystem.FileExists(filePath).ShouldEqual(true); + + List lines = new List() { "test1", "test2", "test3" }; + foreach (string line in lines) + { + fileSerializer.AppendLine(line); + } + + fileSerializer.ReadAll().Count().ShouldEqual(lines.Count); + IEnumerator expectedLines = lines.GetEnumerator(); + expectedLines.MoveNext(); + foreach (string fileLine in fileSerializer.ReadAll()) + { + expectedLines.Current.ShouldEqual(fileLine); + expectedLines.MoveNext(); + } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs new file mode 100644 index 00000000..80ae8fd9 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockEnlistment.cs @@ -0,0 +1,15 @@ +using System; +using GVFS.Common; + +namespace GVFS.UnitTests.Mock.Common +{ + public class MockEnlistment : Enlistment + { + public MockEnlistment() + : base("mock:\\path", "mock:\\path", "mock:\\repoUrl", "mock:\\cacheUrl", "mock:\\git", null) + { + } + + public string SmudgedBlobsRoot { get; set; } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockPhysicalGitObjects.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockPhysicalGitObjects.cs new file mode 100644 index 00000000..ce758508 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockPhysicalGitObjects.cs @@ -0,0 +1,43 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; + +namespace GVFS.UnitTests.Mock.Common +{ + public class MockPhysicalGitObjects : GitObjects + { + public MockPhysicalGitObjects(ITracer tracer, Enlistment enlistment, HttpGitObjects httpGitObjects) + : base(tracer, enlistment, httpGitObjects) + { + } + + public override string WriteLooseObject(string repoRoot, Stream responseStream, string sha, byte[] sharedBuf = null) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + // Return "file contents" as "file name". Weird, but proves we got the right thing. + return reader.ReadToEnd(); + } + } + + public override string WriteTempPackFile(HttpGitObjects.GitEndPointResponseData response) + { + Debug.Assert(response.Stream != null, "WriteTempPackFile should not receive a null stream"); + + using (response.Stream) + using (StreamReader reader = new StreamReader(response.Stream)) + { + // Return "file contents" as "file name". Weird, but proves we got the right thing. + return reader.ReadToEnd(); + } + } + + public override GitProcess.Result IndexTempPackFile(string tempPackPath) + { + return new GitProcess.Result(string.Empty, "TestFailure", GitProcess.Result.GenericFailureCode); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockTracer.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockTracer.cs new file mode 100644 index 00000000..b2bf77a8 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockTracer.cs @@ -0,0 +1,65 @@ +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; + +namespace GVFS.UnitTests.Mock.Common +{ + public class MockTracer : ITracer + { + public void Dispose() + { + } + + public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata) + { + } + + public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata, Keywords keyword) + { + } + + public void RelatedError(EventMetadata metadata) + { + } + + public void RelatedError(EventMetadata metadata, Keywords keyword) + { + } + + public void RelatedError(string message) + { + } + + public void RelatedError(string format, params object[] args) + { + } + + public ITracer StartActivity(string activityName, EventLevel level) + { + return new MockTracer(); + } + + public ITracer StartActivity(string activityName, EventLevel level, Keywords keyword) + { + return new MockTracer(); + } + + public ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata) + { + return new MockTracer(); + } + + public ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata, Keywords keyword) + { + return new MockTracer(); + } + + public void Stop() + { + } + + public void Stop(EventMetadata metadata) + { + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MassiveMockFileSystem.cs b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MassiveMockFileSystem.cs new file mode 100644 index 00000000..dc89c7f0 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MassiveMockFileSystem.cs @@ -0,0 +1,129 @@ +using GVFS.Common; +using GVFS.Common.Physical.FileSystem; +using GVFS.Tests.Should; +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace GVFS.UnitTests.Mock.Physical.FileSystem +{ + /// + /// Intentionally stateless mockup of a large physical directory structure. + /// + public class MassiveMockFileSystem : PhysicalFileSystem + { + public const int FoldersPerFolder = 10; + private static Random randy = new Random(0); + private string rootPath; + private int maxDepth; + + public MassiveMockFileSystem(string rootPath, int maxDepth) + { + this.rootPath = rootPath; + this.maxDepth = maxDepth; + } + + public int MaxTreeSize + { + get { return Enumerable.Range(0, this.maxDepth + 1).Sum(i => (int)Math.Pow(FoldersPerFolder, i)); } + } + + public static string RandomPath(int maxDepth) + { + string output = string.Empty; + int depth = randy.Next(1, maxDepth + 1); + for (int i = 0; i < depth; ++i) + { + char letter = (char)randy.Next('a', 'a' + FoldersPerFolder); + output = Path.Combine(output, letter.ToString()); + } + + return output; + } + + public override IDisposable MonitorChanges( + string directory, + NotifyFilters notifyFilter, + Action onCreate, + Action onRename, + Action onDelete) + { + throw new NotImplementedException(); + } + + public override void WriteAllText(string path, string contents) + { + } + + public override string ReadAllText(string path) + { + return string.Empty; + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode) + { + return this.OpenFileStream(path, fileMode, fileAccess, NativeMethods.FileAttributes.FILE_ATTRIBUTE_NORMAL, shareMode); + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, NativeMethods.FileAttributes attributes, FileShare shareMode) + { + return new MemoryStream(); + } + + public override bool FileExists(string path) + { + return false; + } + + public override void DeleteDirectory(string path, bool recursive = false) + { + } + + public override SafeFileHandle LockDirectory(string path) + { + return new SafeFileHandle(IntPtr.Zero, false); + } + + public override IEnumerable ItemsInDirectory(string path) + { + path.StartsWith(this.rootPath).ShouldEqual(true); + + if (path.Count(c => c == '\\') <= this.maxDepth) + { + for (char c = 'a'; c < 'a' + FoldersPerFolder; ++c) + { + yield return new DirectoryItemInfo + { + Name = c.ToString(), + FullName = Path.Combine(path, c.ToString()), + IsDirectory = true, + Length = 0 + }; + } + } + } + + public override FileProperties GetFileProperties(string path) + { + return new FileProperties(FileAttributes.Directory, DateTime.Now, DateTime.Now, DateTime.Now, 0); + } + + public override void DeleteFile(string path) + { + throw new NotImplementedException(); + } + + public override SafeHandle OpenFile(string path, FileMode fileMode, FileAccess fileAccess, FileAttributes attributes, FileShare shareMode) + { + throw new NotImplementedException(); + } + + public override IEnumerable ReadLines(string path) + { + throw new NotImplementedException(); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockDirectory.cs b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockDirectory.cs new file mode 100644 index 00000000..b7f3275b --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockDirectory.cs @@ -0,0 +1,296 @@ +using GVFS.Common; +using GVFS.Common.Physical.FileSystem; +using GVFS.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.UnitTests.Mock.Physical.FileSystem +{ + public class MockDirectory + { + public MockDirectory(string fullName, IEnumerable folders, IEnumerable files) + { + this.FullName = fullName; + this.Name = Path.GetFileName(this.FullName); + + this.Directories = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + if (folders != null) + { + foreach (MockDirectory folder in folders) + { + this.Directories[folder.FullName] = folder; + } + } + + if (files != null) + { + foreach (MockFile file in files) + { + this.Files[file.FullName] = file; + } + } + + this.FileProperties = FileProperties.DefaultDirectory; + } + + public string FullName { get; private set; } + public string Name { get; private set; } + public Dictionary Directories { get; private set; } + public Dictionary Files { get; private set; } + public FileProperties FileProperties { get; set; } + + public MockFile FindFile(string path) + { + MockFile file; + if (this.Files.TryGetValue(path, out file)) + { + return file; + } + + foreach (MockDirectory directory in this.Directories.Values) + { + file = directory.FindFile(path); + if (file != null) + { + return file; + } + } + + return null; + } + + public void AddOrOverwriteFile(MockFile file, string path) + { + string parentPath = path.Substring(0, path.LastIndexOf(GVFSConstants.PathSeparator)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + + if (parentDirectory == null) + { + throw new IOException(); + } + + MockFile existingFileAtPath = parentDirectory.FindFile(path); + + if (existingFileAtPath != null) + { + parentDirectory.Files.Remove(path); + } + + parentDirectory.Files.Add(file.FullName, file); + } + + public void AddFile(MockFile file, string path) + { + string parentPath = path.Substring(0, path.LastIndexOf(GVFSConstants.PathSeparator)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + + if (parentDirectory == null) + { + throw new IOException(); + } + + MockFile existingFileAtPath = parentDirectory.FindFile(path); + existingFileAtPath.ShouldBeNull(); + + parentDirectory.Files.Add(file.FullName, file); + } + + public void RemoveFile(string path) + { + MockFile file; + if (this.Files.TryGetValue(path, out file)) + { + this.Files.Remove(path); + return; + } + + foreach (MockDirectory directory in this.Directories.Values) + { + file = directory.FindFile(path); + if (file != null) + { + directory.RemoveFile(path); + return; + } + } + } + + public MockDirectory FindDirectory(string path) + { + if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) + { + return this; + } + + MockDirectory foundDirectory; + if (this.Directories.TryGetValue(path, out foundDirectory)) + { + return foundDirectory; + } + + foreach (MockDirectory subDirectory in this.Directories.Values) + { + foundDirectory = subDirectory.FindDirectory(path); + if (foundDirectory != null) + { + return foundDirectory; + } + } + + return null; + } + + public MockFile CreateFile(string path) + { + return this.CreateFile(path, string.Empty); + } + + public MockFile CreateFile(string path, string contents, bool createDirectories = false) + { + string parentPath = path.Substring(0, path.LastIndexOf(GVFSConstants.PathSeparator)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + if (createDirectories) + { + if (parentDirectory == null) + { + parentDirectory = this.CreateDirectory(parentPath); + } + } + else + { + parentDirectory.ShouldNotBeNull(); + } + + MockFile newFile = new MockFile(path, contents); + parentDirectory.Files.Add(newFile.FullName, newFile); + + return newFile; + } + + public MockDirectory CreateDirectory(string path) + { + int lastSlashIdx = path.LastIndexOf(GVFSConstants.PathSeparator); + string parentPath = path.Substring(0, lastSlashIdx); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + if (parentDirectory == null) + { + parentDirectory = this.CreateDirectory(parentPath); + } + + MockDirectory newDirectory; + if (!parentDirectory.Directories.TryGetValue(path, out newDirectory)) + { + newDirectory = new MockDirectory(path, null, null); + parentDirectory.Directories.Add(newDirectory.FullName, newDirectory); + } + + return newDirectory; + } + + public void DeleteDirectory(string path) + { + if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) + { + throw new NotSupportedException(); + } + + MockDirectory foundDirectory; + if (this.Directories.TryGetValue(path, out foundDirectory)) + { + this.Directories.Remove(path); + } + else + { + foreach (MockDirectory subDirectory in this.Directories.Values) + { + foundDirectory = subDirectory.FindDirectory(path); + if (foundDirectory != null) + { + subDirectory.DeleteDirectory(path); + return; + } + } + } + } + + public void MoveDirectory(string sourcePath, string targetPath) + { + MockDirectory sourceDirectory; + MockDirectory sourceDirectoryParent; + this.TryGetDirectoryAndParent(sourcePath, out sourceDirectory, out sourceDirectoryParent).ShouldEqual(true); + + int endPathIndex = targetPath.LastIndexOf(GVFSConstants.PathSeparator); + string targetDirectoryPath = targetPath.Substring(0, endPathIndex); + + MockDirectory targetDirectory = this.FindDirectory(targetDirectoryPath); + targetDirectory.ShouldNotBeNull(); + + sourceDirectoryParent.RemoveDirectory(sourceDirectory); + + sourceDirectory.FullName = targetPath; + + targetDirectory.AddDirectory(sourceDirectory); + } + + public void RemoveDirectory(MockDirectory directory) + { + this.Directories.ContainsKey(directory.FullName).ShouldEqual(true); + this.Directories.Remove(directory.FullName); + } + + private void AddDirectory(MockDirectory directory) + { + if (this.Directories.ContainsKey(directory.FullName)) + { + MockDirectory oldDirectory = this.Directories[directory.FullName]; + foreach (MockFile newFile in directory.Files.Values) + { + newFile.FullName = Path.Combine(oldDirectory.FullName, newFile.Name); + oldDirectory.AddOrOverwriteFile(newFile, newFile.FullName); + } + + foreach (MockDirectory newDirectory in directory.Directories.Values) + { + newDirectory.FullName = Path.Combine(oldDirectory.FullName, newDirectory.Name); + this.AddDirectory(newDirectory); + } + } + else + { + this.Directories.Add(directory.FullName, directory); + } + } + + private bool TryGetDirectoryAndParent(string path, out MockDirectory directory, out MockDirectory parentDirectory) + { + if (this.Directories.TryGetValue(path, out directory)) + { + parentDirectory = this; + return true; + } + else + { + string parentPath = path.Substring(0, path.LastIndexOf(GVFSConstants.PathSeparator)); + parentDirectory = this.FindDirectory(parentPath); + if (parentDirectory != null) + { + foreach (MockDirectory subDirectory in this.Directories.Values) + { + directory = subDirectory.FindDirectory(path); + if (directory != null) + { + return true; + } + } + } + } + + directory = null; + parentDirectory = null; + return false; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFile.cs b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFile.cs new file mode 100644 index 00000000..a568df39 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFile.cs @@ -0,0 +1,59 @@ +using GVFS.Common.Physical.FileSystem; +using System; +using System.IO; + +namespace GVFS.UnitTests.Mock.Physical.FileSystem +{ + public class MockFile + { + private ReusableMemoryStream contentStream; + private FileProperties fileProperties; + + public MockFile(string fullName, string contents) + { + this.FullName = fullName; + this.Name = Path.GetFileName(this.FullName); + + this.FileProperties = FileProperties.DefaultFile; + + this.contentStream = new ReusableMemoryStream(contents); + } + + public event Action Changed; + + public string FullName { get; set; } + public string Name { get; set; } + public FileProperties FileProperties + { + get + { + // The underlying content stream is the correct/true source of the file length + // Create a new copy of the properties to make sure the length is set correctly. + FileProperties newProperties = new FileProperties( + this.fileProperties.FileAttributes, + this.fileProperties.CreationTimeUTC, + this.fileProperties.LastAccessTimeUTC, + this.fileProperties.LastWriteTimeUTC, + this.contentStream.Length); + + this.fileProperties = newProperties; + return this.fileProperties; + } + + set + { + this.fileProperties = value; + if (this.Changed != null) + { + this.Changed(); + } + } + } + + public Stream GetContentStream() + { + this.contentStream.Position = 0; + return this.contentStream; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFileSystem.cs b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFileSystem.cs new file mode 100644 index 00000000..4a776df1 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockFileSystem.cs @@ -0,0 +1,176 @@ +using GVFS.Common; +using GVFS.Common.Physical.FileSystem; +using GVFS.Tests.Should; +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace GVFS.UnitTests.Mock.Physical.FileSystem +{ + public class MockFileSystem : PhysicalFileSystem + { + public MockFileSystem(MockDirectory rootDirectory) + { + this.RootDirectory = rootDirectory; + } + + public MockDirectory RootDirectory { get; private set; } + + public override bool FileExists(string path) + { + return this.RootDirectory.FindFile(path) != null; + } + + public override void DeleteFile(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + file.ShouldNotBeNull(); + + this.RootDirectory.RemoveFile(path); + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode) + { + return this.OpenFileStream(path, fileMode, fileAccess, NativeMethods.FileAttributes.FILE_ATTRIBUTE_NORMAL, shareMode); + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, NativeMethods.FileAttributes attributes, FileShare shareMode) + { + MockFile file = this.RootDirectory.FindFile(path); + if (fileMode == FileMode.OpenOrCreate) + { + if (file == null) + { + return this.CreateAndOpenFileStream(path); + } + } + else + { + file.ShouldNotBeNull(); + } + + return file.GetContentStream(); + } + + public override SafeHandle OpenFile(string path, FileMode fileMode, FileAccess fileAccess, FileAttributes attributes, FileShare shareMode) + { + if (fileMode == FileMode.Create) + { + MockFile newFile = this.RootDirectory.CreateFile(path); + FileProperties newProperties = new FileProperties( + attributes, + newFile.FileProperties.CreationTimeUTC, + newFile.FileProperties.LastAccessTimeUTC, + newFile.FileProperties.LastWriteTimeUTC, + newFile.FileProperties.Length); + newFile.FileProperties = newProperties; + } + + return new MockSafeHandle(path, this.OpenFileStream(path, fileMode, fileAccess, shareMode)); + } + + public override void WriteAllText(string path, string contents) + { + MockFile file = new MockFile(path, contents); + this.RootDirectory.AddOrOverwriteFile(file, path); + } + + public override string ReadAllText(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + + using (StreamReader reader = new StreamReader(file.GetContentStream())) + { + return reader.ReadToEnd(); + } + } + + public override IEnumerable ReadLines(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + using (StreamReader reader = new StreamReader(file.GetContentStream())) + { + while (!reader.EndOfStream) + { + yield return reader.ReadLine(); + } + } + } + + public override void DeleteDirectory(string path, bool recursive = false) + { + MockDirectory directory = this.RootDirectory.FindDirectory(path); + directory.ShouldNotBeNull(); + + this.RootDirectory.DeleteDirectory(path); + } + + public override SafeFileHandle LockDirectory(string path) + { + return new SafeFileHandle(IntPtr.Zero, false); + } + + public override IEnumerable ItemsInDirectory(string path) + { + MockDirectory directory = this.RootDirectory.FindDirectory(path); + directory.ShouldNotBeNull(); + + if (directory != null) + { + foreach (MockDirectory subDirectory in directory.Directories.Values) + { + yield return new DirectoryItemInfo() + { + Name = subDirectory.Name, + FullName = subDirectory.FullName, + IsDirectory = true + }; + } + + foreach (MockFile file in directory.Files.Values) + { + yield return new DirectoryItemInfo() + { + FullName = file.FullName, + Name = file.Name, + IsDirectory = false, + Length = file.FileProperties.Length + }; + } + } + } + + public override FileProperties GetFileProperties(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + if (file != null) + { + return file.FileProperties; + } + else + { + return FileProperties.DefaultFile; + } + } + + public override IDisposable MonitorChanges( + string directory, + NotifyFilters notifyFilter, + Action onCreate, + Action onRename, + Action onDelete) + { + throw new NotImplementedException(); + } + + private Stream CreateAndOpenFileStream(string path) + { + MockFile file = this.RootDirectory.CreateFile(path); + file.ShouldNotBeNull(); + + return this.OpenFileStream(path, FileMode.Open, (FileAccess)NativeMethods.FileAccess.FILE_GENERIC_READ, FileShare.Read); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockSafeHandle.cs b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockSafeHandle.cs new file mode 100644 index 00000000..3aa67150 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/FileSystem/MockSafeHandle.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace GVFS.UnitTests.Mock.Physical.FileSystem +{ + /// + /// A "SafeHandle" object to represent fake file contents during native file system calls + /// + public class MockSafeHandle : SafeHandle + { + public MockSafeHandle(string filePath, Stream fileContents) : base(IntPtr.Zero, false) + { + this.FilePath = filePath; + this.FileContents = fileContents; + } + + public string FilePath { get; } + + public Stream FileContents { get; } + + public override bool IsInvalid + { + get { return false; } + } + + protected override bool ReleaseHandle() + { + this.FileContents.Dispose(); + return true; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockBatchHttpGitObjects.cs b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockBatchHttpGitObjects.cs new file mode 100644 index 00000000..bba3b8b3 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockBatchHttpGitObjects.cs @@ -0,0 +1,128 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; + +namespace GVFS.UnitTests.Mock.Physical.Git +{ + public class MockBatchHttpGitObjects : HttpGitObjects + { + private Func objectResolver; + + public MockBatchHttpGitObjects(ITracer tracer, Enlistment enlistment, Func objectResolver) + : base(tracer, enlistment, 1) + { + this.objectResolver = objectResolver; + } + + public override List QueryForFileSizes(IEnumerable objectIds) + { + throw new NotImplementedException(); + } + + public override GitRefs QueryInfoRefs(string branch) + { + throw new NotImplementedException(); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.TryDownloadObjects(objectIdGenerator(), commitDepth, onSuccess, onFailure, preferBatchedLooseObjects); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.StreamObjects(objectIds, onSuccess, onFailure); + } + + public override RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + throw new NotImplementedException(); + } + + private RetryWrapper.InvocationResult StreamObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + for (int i = 0; i < this.MaxRetries; ++i) + { + try + { + using (ReusableMemoryStream mem = new ReusableMemoryStream(string.Empty)) + using (BinaryWriter writer = new BinaryWriter(mem)) + { + writer.Write(new byte[] { (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', 1 }); + + foreach (string objectId in objectIds) + { + string contents = this.objectResolver(objectId); + if (!string.IsNullOrEmpty(contents)) + { + writer.Write(this.SHA1BytesFromString(objectId)); + byte[] bytes = Encoding.UTF8.GetBytes(contents); + writer.Write((long)bytes.Length); + writer.Write(bytes); + } + else + { + writer.Write(new byte[20]); + writer.Write(0L); + } + } + + writer.Write(new byte[20]); + writer.Flush(); + mem.Seek(0, SeekOrigin.Begin); + + RetryWrapper.CallbackResult result = onSuccess( + 1, + new GitEndPointResponseData( + HttpStatusCode.OK, + GVFSConstants.MediaTypes.CustomLooseObjectsMediaType, + mem)); + return new RetryWrapper.InvocationResult(1, true, result.Result); + } + } + catch + { + continue; + } + } + + return new RetryWrapper.InvocationResult(this.MaxRetries, null); + } + + private byte[] SHA1BytesFromString(string s) + { + s.Length.ShouldEqual(40); + + byte[] output = new byte[20]; + for (int x = 0; x < s.Length; x += 2) + { + output[x / 2] = Convert.ToByte(s.Substring(x, 2), 16); + } + + return output; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGVFSGitObjects.cs b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGVFSGitObjects.cs new file mode 100644 index 00000000..7320431c --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGVFSGitObjects.cs @@ -0,0 +1,46 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.Git; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.UnitTests.Mock.Physical.Git +{ + public class MockGVFSGitObjects : GVFSGitObjects + { + private GVFSContext context; + + public MockGVFSGitObjects(GVFSContext context, HttpGitObjects httpGitObjects) + : base(context, httpGitObjects) + { + this.context = context; + } + + public override bool TryDownloadAndSaveCommits(IEnumerable objectShas, int commitDepth) + { + bool output = true; + foreach (string sha in objectShas) + { + RetryWrapper.InvocationResult result = this.GitObjectRequestor.TryDownloadObjects( + new[] { sha }, + commitDepth, + onSuccess: (tryCount, response) => + { + // Add the contents to the mock repo + using (StreamReader reader = new StreamReader(response.Stream)) + { + ((MockGitRepo)this.Context.Repository).AddBlob(sha, "DownloadedFile", reader.ReadToEnd()); + } + + return new RetryWrapper.CallbackResult(new HttpGitObjects.GitObjectTaskResult(true)); + }, + onFailure: null, + preferBatchedLooseObjects: false); + + return result.Succeeded && result.Result.Success; + } + + return output; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitIndex.cs b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitIndex.cs new file mode 100644 index 00000000..a2a87b8b --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitIndex.cs @@ -0,0 +1,20 @@ +using GVFS.Common; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using System; + +namespace GVFS.UnitTests.Mock.Physical.Git +{ + public class MockGitIndex : GitIndex + { + public MockGitIndex(ITracer tracer, Enlistment enlistment, string physicalIndexPath) + : base(tracer, enlistment, physicalIndexPath, physicalIndexPath + ".lock") + { + } + + public override CallbackResult ClearSkipWorktreeAndUpdateEntry(string filePath, DateTime createTimeUtc, DateTime lastWriteTimeUtc, uint fileSize) + { + return CallbackResult.Success; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitRepo.cs b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitRepo.cs new file mode 100644 index 00000000..75317dc9 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockGitRepo.cs @@ -0,0 +1,147 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; + +namespace GVFS.UnitTests.Mock.Physical.Git +{ + public class MockGitRepo : GitRepo + { + private Dictionary objects = new Dictionary(); + private string rootSha; + + public MockGitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem) + : base(tracer, enlistment, fileSystem, new MockGitIndex(tracer, enlistment, "unusedPath")) + { + this.rootSha = Guid.NewGuid().ToString(); + this.AddTree(this.rootSha, "."); + } + + /// + /// Adds an unparented tree to the "repo" + /// + public void AddTree(string sha, string name, params string[] childShas) + { + MockGitObject newObj = new MockGitObject(sha, name, false); + newObj.ChildShas.AddRange(childShas); + this.objects.Add(sha, newObj); + } + + /// + /// Adds an unparented blob to the "repo" + /// + public void AddBlob(string sha, string name, string contents) + { + MockGitObject newObj = new MockGitObject(sha, name, true); + newObj.Content = contents; + this.objects.Add(sha, newObj); + } + + /// + /// Adds a child sha to an existing tree + /// + public void AddChildBySha(string treeSha, string childSha) + { + MockGitObject treeObj = this.GetTree(treeSha); + treeObj.ChildShas.Add(childSha); + } + + /// + /// Adds an parented blob to the "repo" + /// + public string AddChildBlob(string parentSha, string childName, string childContent) + { + string newSha = Guid.NewGuid().ToString(); + this.AddBlob(newSha, childName, childContent); + this.AddChildBySha(parentSha, newSha); + return newSha; + } + + /// + /// Adds an parented tree to the "repo" + /// + public string AddChildTree(string parentSha, string name, params string[] childShas) + { + string newSha = Guid.NewGuid().ToString(); + this.AddTree(newSha, name, childShas); + this.AddChildBySha(parentSha, newSha); + return newSha; + } + + public override string GetHeadTreeSha() + { + return this.rootSha; + } + + public override bool TryCopyBlobContentStream(string blobSha, Action writeAction) + { + if (this.objects.ContainsKey(blobSha)) + { + MockGitObject obj = this.objects[blobSha]; + obj.IsBlob.ShouldEqual(true); + using (Stream contentStream = new ReusableMemoryStream(obj.Content)) + using (StreamReader reader = new StreamReader(contentStream)) + { + writeAction(reader, contentStream.Length); + return true; + } + } + + return false; + } + + public override bool TryGetBlobLength(string blobSha, out long size) + { + MockGitObject obj; + if (this.objects.TryGetValue(blobSha, out obj)) + { + obj.IsBlob.ShouldEqual(true); + size = obj.Content.Length; + return true; + } + + size = 0; + return false; + } + + public override IEnumerable GetTreeEntries(string commitId, string path) + { + throw new NotImplementedException(); + } + + public override IEnumerable GetTreeEntries(string sha) + { + throw new NotImplementedException(); + } + + private MockGitObject GetTree(string treeSha) + { + this.objects.ContainsKey(treeSha).ShouldEqual(true); + MockGitObject obj = this.objects[treeSha]; + obj.IsBlob.ShouldEqual(false); + return obj; + } + + private class MockGitObject + { + public MockGitObject(string sha, string name, bool isBlob) + { + this.Sha = sha; + this.Name = name; + this.IsBlob = isBlob; + this.ChildShas = new List(); + } + + public string Sha { get; private set; } + public string Name { get; set; } + public bool IsBlob { get; set; } + public List ChildShas { get; set; } + public string Content { get; set; } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockHttpGitObjects.cs b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockHttpGitObjects.cs new file mode 100644 index 00000000..39ea8c3b --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/Git/MockHttpGitObjects.cs @@ -0,0 +1,112 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using GVFS.Tests.Should; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace GVFS.UnitTests.Mock.Physical.Git +{ + public class MockHttpGitObjects : HttpGitObjects + { + private Dictionary shaLengths = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary shaContents = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MockHttpGitObjects(ITracer tracer, Enlistment enlistment) + : base(tracer, enlistment, 1) + { + } + + public void AddShaLength(string sha, long length) + { + this.shaLengths.Add(sha, length); + } + + public void AddBlobContent(string sha, string content) + { + this.shaContents.Add(sha, content); + } + + public void AddShaLengths(IEnumerable> shaLengthPairs) + { + foreach (KeyValuePair kvp in shaLengthPairs) + { + this.AddShaLength(kvp.Key, kvp.Value); + } + } + + public override List QueryForFileSizes(IEnumerable objectIds) + { + return objectIds.Select(oid => new GitObjectSize(oid, this.QueryForFileSize(oid))).ToList(); + } + + public override GitRefs QueryInfoRefs(string branch) + { + throw new NotImplementedException(); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.TryDownloadObjects(objectIdGenerator(), commitDepth, onSuccess, onFailure, preferBatchedLooseObjects); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + // When working within the mocks, we do not support multiple objects. + // PhysicalGitObjects should be overridden to serialize the calls. + objectIds.Count().ShouldEqual(1); + string objectId = objectIds.First(); + return this.GetSingleObject(objectId, onSuccess, onFailure); + } + + public override RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + return this.GetSingleObject(objectId, onSuccess, onFailure); + } + + private RetryWrapper.InvocationResult GetSingleObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + if (this.shaContents.ContainsKey(objectId)) + { + RetryWrapper.CallbackResult result = onSuccess( + 1, + new GitEndPointResponseData( + HttpStatusCode.OK, + GVFSConstants.MediaTypes.LooseObjectMediaType, + new ReusableMemoryStream(this.shaContents[objectId]))); + return new RetryWrapper.InvocationResult(1, true, result.Result); + } + + if (onFailure != null) + { + onFailure(new RetryWrapper.ErrorEventArgs(new Exception("Could not find mock object: " + objectId), 1, false)); + } + + return new RetryWrapper.InvocationResult(1, new Exception("Mock failure in TryDownloadObjectsAsync")); + } + + private long QueryForFileSize(string objectId) + { + this.shaLengths.ContainsKey(objectId).ShouldEqual(true); + return this.shaLengths[objectId]; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Mock/Physical/ReusableMemoryStream.cs b/GVFS/GVFS.UnitTests/Mock/Physical/ReusableMemoryStream.cs new file mode 100644 index 00000000..0c4eec3a --- /dev/null +++ b/GVFS/GVFS.UnitTests/Mock/Physical/ReusableMemoryStream.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.Text; + +namespace GVFS.UnitTests.Mock.Physical +{ + public class ReusableMemoryStream : Stream + { + private byte[] contents; + private long length; + private long position; + + public ReusableMemoryStream(string initialContents) + { + this.contents = Encoding.UTF8.GetBytes(initialContents); + this.length = this.contents.Length; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { return this.length; } + } + + public override long Position + { + get { return this.position; } + set { this.position = value; } + } + + public override void Flush() + { + // noop + } + + public override int Read(byte[] buffer, int offset, int count) + { + int actualCount = Math.Min((int)(this.length - this.position), count); + Array.Copy(this.contents, this.Position, buffer, offset, actualCount); + this.Position += actualCount; + + return actualCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + this.position = offset; + } + else if (origin == SeekOrigin.End) + { + this.position = this.length - offset; + } + else + { + this.position += offset; + } + + if (this.position > this.length) + { + this.position = this.length - 1; + } + + return this.position; + } + + public override void SetLength(long value) + { + while (value > this.contents.Length) + { + if (this.contents.Length == 0) + { + this.contents = new byte[1024]; + } + else + { + Array.Resize(ref this.contents, this.contents.Length * 2); + } + } + + this.length = value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (this.position + count > this.contents.Length) + { + this.SetLength(this.position + count); + } + + Array.Copy(buffer, offset, this.contents, this.position, count); + this.position += count; + if (this.position > this.length) + { + this.length = this.position; + } + } + + protected override void Dispose(bool disposing) + { + // This method is a noop besides resetting the position. + // The byte[] in this class is the source of truth for the contents that this + // stream is providing, so we can't dispose it here. + this.position = 0; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Physical/Git/GitCatFileBatchProcessTests.cs b/GVFS/GVFS.UnitTests/Physical/Git/GitCatFileBatchProcessTests.cs new file mode 100644 index 00000000..1dfdcd1e --- /dev/null +++ b/GVFS/GVFS.UnitTests/Physical/Git/GitCatFileBatchProcessTests.cs @@ -0,0 +1,99 @@ +using GVFS.Common.Git; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.UnitTests.Physical.Git +{ + [TestFixture] + public class GitCatFileBatchProcessTests + { + private const string TestTreeCommitId = "HEAD"; + private const string TestTreeSha = "2e866d08e55a796755c1889ae5b30d0e8fad5572"; + private static readonly Encoding GitOutputEncoding = Encoding.GetEncoding(1252); + private Random randy = new Random(0); + + [TestCase] + public void ProcessesNamedEntry() + { + GitTreeEntry[] inputData = new[] + { + new GitTreeEntry("file", this.RandomSha(), false, true), + new GitTreeEntry("Dir", this.RandomSha(), false, true) + }; + + using (MemoryStream testData = new MemoryStream()) + { + // Create test data + WriteTestTreeEntries(inputData, testData); + + testData.Position = 0; + + using (MemoryStream mockStdInStream = new MemoryStream()) + using (StreamWriter mockStdIn = new StreamWriter(mockStdInStream)) + { + GitCatFileBatchProcess dut = new GitCatFileBatchProcess(new StreamReader(testData, GitOutputEncoding), mockStdIn); + GitTreeEntry[] output = dut.GetTreeEntries(TestTreeSha).ToArray(); + + output.Length.ShouldEqual(inputData.Length); + for (int i = 0; i < output.Length; ++i) + { + output[i].Sha.ShouldEqual(inputData[i].Sha); + output[i].Name.ShouldEqual(inputData[i].Name); + output[i].IsBlob.ShouldEqual(inputData[i].IsBlob); + output[i].IsTree.ShouldEqual(inputData[i].IsTree); + } + } + } + } + + private static void WriteTestTreeEntries(GitTreeEntry[] inputData, MemoryStream testData) + { + StreamWriter writer = new StreamWriter(testData); + writer.AutoFlush = true; + + using (MemoryStream rawTree = new MemoryStream()) + using (StreamWriter rawTreeWriter = new StreamWriter(rawTree, GitOutputEncoding)) + { + rawTreeWriter.AutoFlush = true; + foreach (GitTreeEntry entry in inputData) + { + // Tree entry format is ' \0<20 byte sha>' + // End of stream is \n + rawTreeWriter.Write(entry.IsBlob ? "100644 " : "40000 "); + rawTreeWriter.Write(entry.Name + "\0"); + + byte[] bytes = StringToByteArray(entry.Sha); + rawTree.Write(bytes, 0, bytes.Length); + } + + rawTree.Position = 0; + + // cat-file --batch header is ' <# Bytes to follow>\n' + writer.Write(TestTreeSha + " tree " + rawTree.Length + '\n'); + rawTree.CopyTo(testData); + + // Terminate stream with \n + writer.Write('\n'); + } + } + + private static byte[] StringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + private string RandomSha() + { + byte[] bytes = new byte[20]; + this.randy.NextBytes(bytes); + return BitConverter.ToString(bytes).Replace("-", string.Empty); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Physical/Git/PhysicalGitObjectsTests.cs b/GVFS/GVFS.UnitTests/Physical/Git/PhysicalGitObjectsTests.cs new file mode 100644 index 00000000..99f85765 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Physical/Git/PhysicalGitObjectsTests.cs @@ -0,0 +1,174 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.Git; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using GVFS.UnitTests.Mock.Common; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; + +namespace GVFS.UnitTests.Physical.Git +{ + [TestFixture] + public class PhysicalGitObjectsTests + { + private const string ValidTestObjectFileContents = "421dc4df5e1de427e363b8acd9ddb2d41385dbdf"; + private string tempFolder; + + [OneTimeSetUp] + public void Setup() + { + this.tempFolder = Path.Combine(Environment.CurrentDirectory, Path.GetRandomFileName()); + string objectsFolder = Path.Combine( + this.tempFolder, + GVFSConstants.WorkingDirectoryRootName, + GVFSConstants.DotGit.Objects.Pack.Root); + Directory.CreateDirectory(objectsFolder); + } + + [OneTimeTearDown] + public void Teardown() + { + Directory.Delete(this.tempFolder, true); + } + + [TestCase] + public void SucceedsForNormalLookingLooseObjectDownloads() + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(ValidTestObjectFileContents))) + { + httpObjects.MediaType = GVFSConstants.MediaTypes.LooseObjectMediaType; + GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects); + + dut.TryDownloadAndSaveObject(ValidTestObjectFileContents.Substring(0, 2), ValidTestObjectFileContents.Substring(2)) + .ShouldEqual(true); + } + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void FailsZeroByteLooseObjectsDownloads() + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream()) + { + httpObjects.MediaType = GVFSConstants.MediaTypes.LooseObjectMediaType; + GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects); + + Assert.Throws(() => dut.TryDownloadAndSaveObject("aa", "bbcc")); + } + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void FailsNullByteLooseObjectsDownloads() + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream(new byte[256])) + { + httpObjects.MediaType = GVFSConstants.MediaTypes.LooseObjectMediaType; + GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects); + + Assert.Throws(() => dut.TryDownloadAndSaveObject("aa", "bbcc")); + } + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void FailsZeroBytePackDownloads() + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream()) + { + httpObjects.MediaType = GVFSConstants.MediaTypes.PackFileMediaType; + GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects); + + Assert.Throws(() => dut.TryDownloadAndSaveCommits(new[] { "object0", "object1" }, 0)); + } + } + + [TestCase] + [Category(CategoryContants.ExceptionExpected)] + public void FailsNullBytePackDownloads() + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream(new byte[256])) + { + httpObjects.MediaType = GVFSConstants.MediaTypes.PackFileMediaType; + GVFSGitObjects dut = this.CreateTestableGVFSGitObjects(httpObjects); + + Assert.Throws(() => dut.TryDownloadAndSaveCommits(new[] { "object0", "object1" }, 0)); + } + } + + private GVFSGitObjects CreateTestableGVFSGitObjects(MockHttpGitObjects httpObjects) + { + MockTracer tracer = new MockTracer(); + GVFSEnlistment enlistment = new GVFSEnlistment(this.tempFolder, "notused", "notused", "notused", null); + + GVFSContext context = new GVFSContext(tracer, null, null, enlistment); + GVFSGitObjects dut = new GVFSGitObjects(context, httpObjects); + return dut; + } + + private string GetDataPath(string fileName) + { + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(workingDirectory, "Data", fileName); + } + + private class MockHttpGitObjects : HttpGitObjects + { + public MockHttpGitObjects() : base(null, null, 1) + { + } + + public Stream InputStream { get; set; } + public string MediaType { get; set; } + + public static MemoryStream GetRandomStream(int size) + { + Random randy = new Random(0); + MemoryStream stream = new MemoryStream(); + byte[] buffer = new byte[size]; + + randy.NextBytes(buffer); + stream.Write(buffer, 0, buffer.Length); + + stream.Position = 0; + return stream; + } + + public override RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + return this.TryDownloadObjects(new[] { objectId }, 0, onSuccess, onFailure, false); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + int commitDepth, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + onSuccess(0, new GitEndPointResponseData(HttpStatusCode.OK, this.MediaType, this.InputStream)); + + GitObjectTaskResult result = new GitObjectTaskResult(true); + return new RetryWrapper.InvocationResult(0, true, result); + } + + public override List QueryForFileSizes(IEnumerable objectIds) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs b/GVFS/GVFS.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs new file mode 100644 index 00000000..3fc2f949 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs @@ -0,0 +1,181 @@ +using GVFS.Common; +using GVFS.Tests.Should; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GVFS.UnitTests.Prefetch +{ + [TestFixture] + public class PrefetchPacksDeserializerTests + { + private static readonly byte[] PrefetchPackExpectedHeader + = new byte[] + { + (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', + 1 // Version + }; + + [TestCase] + public void PrefetchPacksDeserializer_No_Packs_Succeeds() + { + this.RunPrefetchPacksDeserializerTest(0, false); + } + + [TestCase] + public void PrefetchPacksDeserializer_Single_Pack_With_Index_Receives_Both() + { + this.RunPrefetchPacksDeserializerTest(1, true); + } + + [TestCase] + public void PrefetchPacksDeserializer_Single_Pack_Without_Index_Receives_Only_Pack() + { + this.RunPrefetchPacksDeserializerTest(1, false); + } + + [TestCase] + public void PrefetchPacksDeserializer_Multiple_Packs_With_Indexes() + { + this.RunPrefetchPacksDeserializerTest(10, true); + } + + [TestCase] + public void PrefetchPacksDeserializer_Multiple_Packs_Without_Indexes() + { + this.RunPrefetchPacksDeserializerTest(10, false); + } + + /// + /// A deterministic way to create somewhat unique packs + /// + private static byte[] PackForTimestamp(long timestamp) + { + unchecked + { + Random rand = new Random((int)timestamp); + byte[] data = new byte[100]; + rand.NextBytes(data); + return data; + } + } + + /// + /// A deterministic way to create somewhat unique indexes + /// + private static byte[] IndexForTimestamp(long timestamp) + { + unchecked + { + Random rand = new Random((int)-timestamp); + byte[] data = new byte[50]; + rand.NextBytes(data); + return data; + } + } + + /// + /// Implementation of the PrefetchPack spec to generate data for tests + /// + private void WriteToSpecs(Stream stream, long[] packTimestamps, bool withIndexes) + { + // Header + stream.Write(PrefetchPackExpectedHeader, 0, PrefetchPackExpectedHeader.Length); + + // PackCount + stream.Write(BitConverter.GetBytes((ushort)packTimestamps.Length), 0, 2); + + for (int i = 0; i < packTimestamps.Length; i++) + { + byte[] packContents = PackForTimestamp(packTimestamps[i]); + byte[] indexContents = IndexForTimestamp(packTimestamps[i]); + + // Pack Header + // Timestamp + stream.Write(BitConverter.GetBytes(packTimestamps[i]), 0, 8); + + // Pack length + stream.Write(BitConverter.GetBytes((long)packContents.Length), 0, 8); + + // Pack index length + if (withIndexes) + { + stream.Write(BitConverter.GetBytes((long)indexContents.Length), 0, 8); + } + else + { + stream.Write(BitConverter.GetBytes(-1L), 0, 8); + } + + // Pack data + stream.Write(packContents, 0, packContents.Length); + + if (withIndexes) + { + stream.Write(indexContents, 0, indexContents.Length); + } + } + } + + private void RunPrefetchPacksDeserializerTest(int packCount, bool withIndexes) + { + using (MemoryStream ms = new MemoryStream()) + { + long[] packTimestamps = Enumerable.Range(0, packCount).Select(x => (long)x).ToArray(); + + // Write the data to the memory stream. + this.WriteToSpecs(ms, packTimestamps, withIndexes); + ms.Position = 0; + + Dictionary>> receivedPacksAndIndexes = new Dictionary>>(); + + foreach (PrefetchPacksDeserializer.PackAndIndex pack in new PrefetchPacksDeserializer(ms).EnumeratePacks()) + { + List> packsAndIndexesByUniqueId; + if (!receivedPacksAndIndexes.TryGetValue(pack.UniqueId, out packsAndIndexesByUniqueId)) + { + packsAndIndexesByUniqueId = new List>(); + receivedPacksAndIndexes.Add(pack.UniqueId, packsAndIndexesByUniqueId); + } + + using (MemoryStream packContent = new MemoryStream()) + using (MemoryStream idxContent = new MemoryStream()) + { + pack.PackStream.CopyTo(packContent); + byte[] packData = packContent.ToArray(); + packData.ShouldMatchInOrder(PackForTimestamp(pack.Timestamp)); + packsAndIndexesByUniqueId.Add(Tuple.Create("pack", pack.Timestamp)); + + if (pack.IndexStream != null) + { + pack.IndexStream.CopyTo(idxContent); + byte[] idxData = idxContent.ToArray(); + idxData.ShouldMatchInOrder(IndexForTimestamp(pack.Timestamp)); + packsAndIndexesByUniqueId.Add(Tuple.Create("idx", pack.Timestamp)); + } + } + } + + receivedPacksAndIndexes.Count.ShouldEqual(packCount, "UniqueId count"); + + foreach (List> groupedByUniqueId in receivedPacksAndIndexes.Values) + { + if (withIndexes) + { + groupedByUniqueId.Count.ShouldEqual(2, "Both Pack and Index for UniqueId"); + + // Should only contain 1 index file + groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "idx"); + } + + // should only contain 1 pack file + groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "pack"); + + groupedByUniqueId.Select(x => x.Item2).Distinct().Count().ShouldEqual(1, "Same timestamps for a uniqueId"); + } + } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Program.cs b/GVFS/GVFS.UnitTests/Program.cs new file mode 100644 index 00000000..6a78c6b1 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Program.cs @@ -0,0 +1,22 @@ +using GVFS.Tests; +using GVFS.UnitTests.Category; +using System; +using System.Diagnostics; + +namespace GVFS.UnitTests +{ + public class Program + { + public static void Main(string[] args) + { + NUnitRunner runner = new NUnitRunner(args); + + if (Debugger.IsAttached) + { + runner.ExcludeCategory(CategoryContants.ExceptionExpected); + } + + Environment.ExitCode = runner.RunTests(1); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/Properties/AssemblyInfo.cs b/GVFS/GVFS.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8b802dc9 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS.UnitTests")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8e0d0989-21f6-4dd8-946c-39f992523cc6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GVFS/GVFS.UnitTests/Should/StringShouldExtensions.cs b/GVFS/GVFS.UnitTests/Should/StringShouldExtensions.cs new file mode 100644 index 00000000..f3f1386e --- /dev/null +++ b/GVFS/GVFS.UnitTests/Should/StringShouldExtensions.cs @@ -0,0 +1,17 @@ +using GVFS.Common.Physical.FileSystem; + +namespace GVFS.Tests.Should +{ + public static class StringShouldExtensions + { + public static void ShouldBeAPhysicalFile(this string physicalPath, PhysicalFileSystem fileSystem) + { + fileSystem.FileExists(physicalPath).ShouldEqual(true); + } + + public static void ShouldNotBeAPhysicalFile(this string physicalPath, PhysicalFileSystem fileSystem) + { + fileSystem.FileExists(physicalPath).ShouldEqual(false); + } + } +} diff --git a/GVFS/GVFS.UnitTests/Virtual/CommonRepoSetup.cs b/GVFS/GVFS.UnitTests/Virtual/CommonRepoSetup.cs new file mode 100644 index 00000000..20933d27 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Virtual/CommonRepoSetup.cs @@ -0,0 +1,87 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.Git; +using GVFS.UnitTests.Mock.Common; +using GVFS.UnitTests.Mock.Physical.FileSystem; +using GVFS.UnitTests.Mock.Physical.Git; +using NUnit.Framework; +using System.IO; + +namespace GVFS.UnitTests.Virtual +{ + public class CommonRepoSetup + { + public CommonRepoSetup() + { + MockTracer tracer = new MockTracer(); + + string gitBinPath = GitProcess.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + Assert.Fail("Failed to find git.exe"); + } + + string enlistmentRoot = @"mock:\GVFS\UnitTests\Repo"; + GVFSEnlistment enlistment = new GVFSEnlistment(enlistmentRoot, "fake://repoUrl", "fake://cacheUrl", gitBinPath, null); + + this.GitParentPath = enlistment.WorkingDirectoryRoot; + this.GVFSMetadataPath = enlistment.DotGVFSRoot; + + MockDirectory enlistmentDirectory = new MockDirectory( + enlistmentRoot, + new MockDirectory[] + { + new MockDirectory(this.GitParentPath, folders: null, files: null), + }, + null); + enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git\\config"), ".git config Contents", createDirectories: true); + enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git\\HEAD"), ".git HEAD Contents", createDirectories: true); + enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git\\logs\\HEAD"), "HEAD Contents", createDirectories: true); + enlistmentDirectory.CreateFile(Path.Combine(this.GitParentPath, ".git\\info\\exclude"), "exclude Contents", createDirectories: true); + enlistmentDirectory.CreateDirectory(Path.Combine(this.GitParentPath, GVFSConstants.DotGit.Objects.Pack.Root)); + + MockFileSystem fileSystem = new MockFileSystem(enlistmentDirectory); + this.Repository = new MockGitRepo( + tracer, + enlistment, + fileSystem); + CreateStandardGitTree(this.Repository); + + this.Context = new GVFSContext(tracer, fileSystem, this.Repository, enlistment); + + this.HttpObjects = new MockHttpGitObjects(tracer, enlistment); + this.GitObjects = new MockGVFSGitObjects(this.Context, this.HttpObjects); + } + + public GVFSContext Context { get; private set; } + + public string GitParentPath { get; private set; } + + public string GVFSMetadataPath { get; private set; } + public GVFSGitObjects GitObjects { get; private set; } + + public MockGitRepo Repository { get; private set; } + public MockHttpGitObjects HttpObjects { get; private set; } + + private static void CreateStandardGitTree(MockGitRepo repository) + { + string rootSha = repository.GetHeadTreeSha(); + + string atreeSha = repository.AddChildTree(rootSha, "A"); + repository.AddChildBlob(atreeSha, "A.1.txt", "A.1 in GitTree"); + repository.AddChildBlob(atreeSha, "A.2.txt", "A.2 in GitTree"); + + string btreeSha = repository.AddChildTree(rootSha, "B"); + repository.AddChildBlob(btreeSha, "B.1.txt", "B.1 in GitTree"); + + string dupContentSha = repository.AddChildTree(rootSha, "DupContent"); + repository.AddChildBlob(dupContentSha, "dup1.txt", "This is some duplicate content"); + repository.AddChildBlob(dupContentSha, "dup2.txt", "This is some duplicate content"); + + string dupTreeSha = repository.AddChildTree(rootSha, "DupTree"); + repository.AddChildBlob(dupTreeSha, "B.1.txt", "B.1 in GitTree"); + + repository.AddChildBlob(rootSha, "C.txt", "C in GitTree"); + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.UnitTests/Virtual/DotGit/GitIndexTests.cs b/GVFS/GVFS.UnitTests/Virtual/DotGit/GitIndexTests.cs new file mode 100644 index 00000000..bcd4ebee --- /dev/null +++ b/GVFS/GVFS.UnitTests/Virtual/DotGit/GitIndexTests.cs @@ -0,0 +1,72 @@ +using GVFS.Common; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Physical.Git; +using GVFS.Tests.Should; +using GVFS.UnitTests.Mock.Common; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace GVFS.UnitTests.Virtual.DotGit +{ + [TestFixture] + public class GitIndexTests + { + private readonly List filesInIndex = new List() + { + "anothernewfile.txt", + "test.txt", + "test1.txt", + "test2.txt" + }; + + [TestCase] + public void IndexV2() + { + using (GitIndex index = this.LoadIndex((uint)2)) + { + index.Open(); + index.ClearSkipWorktreeAndUpdateEntry("test.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.ClearSkipWorktreeAndUpdateEntry("test1.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.Close(); + } + } + + [TestCase] + public void IndexV3() + { + using (GitIndex index = this.LoadIndex((uint)3)) + { + index.Open(); + index.ClearSkipWorktreeAndUpdateEntry("test.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.ClearSkipWorktreeAndUpdateEntry("test1.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.Close(); + } + } + + [TestCase] + public void IndexV4() + { + using (GitIndex index = this.LoadIndex((uint)4)) + { + index.Open(); + index.ClearSkipWorktreeAndUpdateEntry("test.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.ClearSkipWorktreeAndUpdateEntry("test1.txt", DateTime.UtcNow, DateTime.UtcNow, 1).ShouldEqual(CallbackResult.Success); + index.Close(); + } + } + + private GitIndex LoadIndex(uint version) + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string path = Path.Combine(workingDirectory, @"Data\index_v" + version); + path.ShouldBeAPhysicalFile(fileSystem); + GitIndex index = new GitIndex(new MockTracer(), new MockEnlistment(), path, path + ".lock"); + index.Initialize(); + return index; + } + } +} diff --git a/GVFS/GVFS.UnitTests/Virtual/TestsWithCommonRepo.cs b/GVFS/GVFS.UnitTests/Virtual/TestsWithCommonRepo.cs new file mode 100644 index 00000000..7894740a --- /dev/null +++ b/GVFS/GVFS.UnitTests/Virtual/TestsWithCommonRepo.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; + +namespace GVFS.UnitTests.Virtual +{ + [TestFixture] + public abstract class TestsWithCommonRepo + { + protected CommonRepoSetup Repo { get; private set; } + + [SetUp] + public virtual void TestSetup() + { + this.Repo = new CommonRepoSetup(); + } + } +} diff --git a/GVFS/GVFS.UnitTests/packages.config b/GVFS/GVFS.UnitTests/packages.config new file mode 100644 index 00000000..00df1c02 --- /dev/null +++ b/GVFS/GVFS.UnitTests/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS/App.config b/GVFS/GVFS/App.config new file mode 100644 index 00000000..b8a7a317 --- /dev/null +++ b/GVFS/GVFS/App.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/GVFS/GVFS/CommandLine/CloneHelper.cs b/GVFS/GVFS/CommandLine/CloneHelper.cs new file mode 100644 index 00000000..3291d3e0 --- /dev/null +++ b/GVFS/GVFS/CommandLine/CloneHelper.cs @@ -0,0 +1,226 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical; +using GVFS.Common.Tracing; +using GVFS.GVFlt; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace GVFS.CommandLine +{ + public class CloneHelper + { + private GVFSEnlistment enlistment; + private HttpGitObjects httpGitObjects; + private ITracer tracer; + + public CloneHelper(ITracer tracer, GVFSEnlistment enlistment, HttpGitObjects httpGitObjects) + { + this.tracer = tracer; + this.enlistment = enlistment; + this.httpGitObjects = httpGitObjects; + } + + public CloneVerb.Result CreateClone(GitRefs refs, string branch) + { + GitObjects gitObjects = new GitObjects(this.tracer, this.enlistment, this.httpGitObjects); + + CloneVerb.Result initRepoResult = this.TryInitRepo(refs, this.enlistment); + if (!initRepoResult.Success) + { + return initRepoResult; + } + + if (!gitObjects.TryDownloadAndSaveCommits(refs.GetTipCommitIds(), commitDepth: 2)) + { + return new CloneVerb.Result("Could not download tip commits from: " + Uri.EscapeUriString(this.enlistment.ObjectsEndpointUrl)); + } + + GitProcess git = new GitProcess(this.enlistment); + if (!this.SetConfigSettings(git)) + { + return new CloneVerb.Result("Unable to configure git repo"); + } + + git.CreateBranchWithUpstream(branch, "origin/" + branch); + + File.WriteAllText( + Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Head), + "ref: refs/heads/" + branch); + + File.AppendAllText( + Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Info.SparseCheckoutPath), + GVFSConstants.GitPathSeparatorString + GVFSConstants.SpecialGitFiles.GitAttributes + "\n"); + + CloneVerb.Result hydrateResult = this.HydrateRootGitAttributes(gitObjects, branch); + if (!hydrateResult.Success) + { + return hydrateResult; + } + + this.CreateGitScript(); + + GitProcess.Result forceCheckoutResult = git.ForceCheckout(branch); + if (forceCheckoutResult.HasErrors) + { + // Ignore errors related to being unable to read objects + string[] errorLines = forceCheckoutResult.Errors.Split('\n'); + StringBuilder cloneErrors = new StringBuilder(); + foreach (string gitError in errorLines) + { + if (IsForceCheckoutErrorCloneFailure(gitError)) + { + cloneErrors.AppendLine(gitError); + } + } + + if (cloneErrors.Length > 0) + { + string error = "Could not complete checkout of branch: " + branch + ", " + cloneErrors.ToString(); + this.tracer.RelatedError(error); + return new CloneVerb.Result(error); + } + } + + GitProcess.Result updateIndexresult = git.UpdateIndexVersion4(); + if (updateIndexresult.HasErrors) + { + string error = "Could not update index, error: " + updateIndexresult.Errors; + this.tracer.RelatedError(error); + return new CloneVerb.Result(error); + } + + string installHooksError; + if (!this.InstallHooks(out installHooksError)) + { + this.tracer.RelatedError(installHooksError); + return new CloneVerb.Result(installHooksError); + } + + using (RepoMetadata repoMetadata = new RepoMetadata(this.enlistment.DotGVFSRoot)) + { + repoMetadata.SaveCurrentDiskLayoutVersion(); + } + + // Prepare the working directory folder for GVFS last to ensure that gvfs mount will fail if gvfs clone has failed + string prepGVFltError; + if (!GVFltCallbacks.TryPrepareFolderForGVFltCallbacks(this.enlistment.WorkingDirectoryRoot, out prepGVFltError)) + { + this.tracer.RelatedError(prepGVFltError); + return new CloneVerb.Result(prepGVFltError); + } + + return new CloneVerb.Result(true); + } + + private static bool IsForceCheckoutErrorCloneFailure(string checkoutError) + { + if (string.IsNullOrWhiteSpace(checkoutError) || + checkoutError.Contains("Already on")) + { + return false; + } + + return true; + } + + private bool SetConfigSettings(GitProcess git) + { + return this.enlistment.TrySetCacheServerUrlConfig() && + GVFSVerb.TrySetGitConfigSettings(git); + } + + private CloneVerb.Result HydrateRootGitAttributes(GitObjects gitObjects, string branch) + { + using (GitCatFileBatchProcess catFile = new GitCatFileBatchProcess(this.enlistment)) + { + string treeSha = catFile.GetTreeSha(branch); + GitTreeEntry gitAttributes = catFile.GetTreeEntries(treeSha).FirstOrDefault(entry => entry.Name.Equals(GVFSConstants.SpecialGitFiles.GitAttributes)); + + if (gitAttributes == null) + { + return new CloneVerb.Result("This branch does not contain a " + GVFSConstants.SpecialGitFiles.GitAttributes + " file in the root folder. This file is required by GVFS clone"); + } + + if (!gitObjects.TryDownloadAndSaveBlobs(new[] { gitAttributes.Sha })) + { + return new CloneVerb.Result("Could not download " + GVFSConstants.SpecialGitFiles.GitAttributes + " file"); + } + } + + return new CloneVerb.Result(true); + } + + private bool InstallHooks(out string error) + { + error = string.Empty; + string installedReadObjectHookPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), GVFSConstants.GVFSReadObjectHookExecutableName); + try + { + File.Copy( + installedReadObjectHookPath, + Path.Combine(this.enlistment.WorkingDirectoryRoot, GVFSConstants.DotGit.Hooks.ReadObjectPath + ".exe")); + } + catch (Exception e) + { + error = "Failed to copy" + installedReadObjectHookPath + ", Exception: " + e.ToString(); + return false; + } + + return true; + } + + private void CreateGitScript() + { + FileInfo gitCmd = new FileInfo(Path.Combine(this.enlistment.EnlistmentRoot, "git.cmd")); + using (FileStream fs = gitCmd.Create()) + using (StreamWriter writer = new StreamWriter(fs)) + { + writer.Write( +@" +@echo OFF +echo . +echo ^ +echo This repo was cloned using GVFS, and the git repo is in the 'src' directory +echo Switching you to the 'src' directory and rerunning your git command +echo  + +@echo ON +cd src +git %* +"); + } + + gitCmd.Attributes = FileAttributes.Hidden; + } + + private CloneVerb.Result TryInitRepo(GitRefs refs, Enlistment enlistmentToInit) + { + string repoPath = enlistmentToInit.WorkingDirectoryRoot; + GitProcess.Result initResult = GitProcess.Init(enlistmentToInit); + if (initResult.HasErrors) + { + string error = string.Format("Could not init repo at to {0}: {1}", repoPath, initResult.Errors); + this.tracer.RelatedError(error); + return new CloneVerb.Result(error); + } + + GitProcess.Result remoteAddResult = new GitProcess(enlistmentToInit).RemoteAdd("origin", enlistmentToInit.RepoUrl); + if (remoteAddResult.HasErrors) + { + string error = string.Format("Could not add remote to {0}: {1}", repoPath, remoteAddResult.Errors); + this.tracer.RelatedError(error); + return new CloneVerb.Result(error); + } + + File.WriteAllText( + Path.Combine(repoPath, GVFSConstants.DotGit.PackedRefs), + refs.ToPackedRefs()); + + return new CloneVerb.Result(true); + } + } +} diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs new file mode 100644 index 00000000..c9de97eb --- /dev/null +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -0,0 +1,290 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.IO; +using System.Linq; + +namespace GVFS.CommandLine +{ + [Verb(CloneVerb.CloneVerbName, HelpText = "Clone a git repo and mount it as a GVFS virtual repo")] + public class CloneVerb : GVFSVerb + { + public const string CloneVerbName = "clone"; + private const string MountVerb = "gvfs mount"; + + [Value( + 0, + Required = true, + MetaName = "Repository URL", + HelpText = "The url of the repo")] + public string RepositoryURL { get; set; } + + [Value( + 1, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the GVFS enlistment root")] + public override string EnlistmentRootPath { get; set; } + + [Option( + "cache-server-url", + Required = false, + Default = "", + HelpText = "Defines the url of the cache server")] + public string CacheServerUrl { get; set; } + + [Option( + 'b', + "branch", + Required = false, + HelpText = "Branch to checkout after clone")] + public string Branch { get; set; } + + [Option( + "single-branch", + Required = false, + Default = false, + HelpText = "Use this option to only download metadata for the branch that will be checked out")] + public bool SingleBranch { get; set; } + + [Option( + "no-mount", + Required = false, + Default = false, + HelpText = "Use this option to only clone, but not mount the repo")] + public bool NoMount { get; set; } + + [Option( + "no-prefetch", + Required = false, + Default = false, + HelpText = "Use this option to not prefetch commits after clone")] + public bool NoPrefetch { get; set; } + + protected override string VerbName + { + get { return CloneVerbName; } + } + + public override void Execute(ITracer tracer = null) + { + if (tracer != null) + { + throw new InvalidOperationException("Clone does not support being called with an existing tracer"); + } + + this.CheckElevated(); + this.CheckGVFltRunning(); + + string fullPath = GVFSEnlistment.ToFullPath(this.EnlistmentRootPath, this.GetDefaultEnlistmentRoot()); + if (fullPath == null) + { + this.ReportErrorAndExit("Unable to write to directory " + this.EnlistmentRootPath); + } + + this.EnlistmentRootPath = fullPath; + + this.Output.WriteLine(); + this.Output.WriteLine("Starting clone of {0} into {1}...", this.RepositoryURL, this.EnlistmentRootPath); + this.Output.WriteLine(); + + this.CacheServerUrl = Enlistment.StripObjectsEndpointSuffix(this.CacheServerUrl); + + try + { + GVFSEnlistment enlistment; + + Result cloneResult = this.TryCreateEnlistment(out enlistment); + if (cloneResult.Success) + { + using (JsonEtwTracer cloneTracer = new JsonEtwTracer(GVFSConstants.GVFSEtwProviderName, "GVFSClone")) + { + cloneTracer.AddLogFileEventListener( + GVFSEnlistment.GetNewGVFSLogFileName( + Path.Combine(this.EnlistmentRootPath, GVFSConstants.DotGVFSPath, GVFSConstants.GVFSLogFolderName), + this.VerbName), + EventLevel.Informational, + Keywords.Any); + cloneTracer.AddConsoleEventListener(EventLevel.Informational, Keywords.Any); + cloneTracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + enlistment.CacheServerUrl, + new EventMetadata + { + { "Branch", this.Branch }, + { "SingleBranch", this.SingleBranch }, + { "NoMount", this.NoMount }, + { "NoPrefetch", this.NoPrefetch } + }); + + cloneResult = this.TryClone(cloneTracer, enlistment); + + if (cloneResult.Success) + { + this.Output.WriteLine("GVFS Enlistment created @ {0}", this.EnlistmentRootPath); + + if (!this.NoPrefetch) + { + PrefetchVerb prefetch = new PrefetchVerb(); + prefetch.EnlistmentRootPath = this.EnlistmentRootPath; + prefetch.Commits = true; + prefetch.Execute(cloneTracer); + } + + if (this.NoMount) + { + this.Output.WriteLine("\r\nIn order to mount, first cd to within your enlistment, then call: "); + this.Output.WriteLine(CloneVerb.MountVerb); + } + else + { + MountVerb mount = new MountVerb(); + mount.EnlistmentRootPath = this.EnlistmentRootPath; + + // Tracer will be disposed in mount.Execute to avoid conflicts with the background process. + mount.Execute(cloneTracer); + } + } + } + } + + // Write to the output after the tracer is disposed so that the error is the last message + // displayed to the user + if (!cloneResult.Success) + { + this.Output.WriteLine("\r\nCannot clone @ {0}", this.EnlistmentRootPath); + this.Output.WriteLine("Error: {0}", cloneResult.ErrorMessage); + Environment.ExitCode = (int)ReturnCode.GenericError; + } + } + catch (AggregateException e) + { + this.Output.WriteLine("Cannot clone @ {0}:", this.EnlistmentRootPath); + foreach (Exception ex in e.Flatten().InnerExceptions) + { + this.Output.WriteLine("Exception: {0}", ex.ToString()); + } + + Environment.ExitCode = (int)ReturnCode.GenericError; + } + catch (VerbAbortedException) + { + throw; + } + catch (Exception e) + { + this.ReportErrorAndExit("Cannot clone @ {0}: {1}", this.EnlistmentRootPath, e.ToString()); + } + } + + private Result TryCreateEnlistment(out GVFSEnlistment enlistment) + { + enlistment = null; + + // Check that EnlistmentRootPath is empty before creating a tracer and LogFileEventListener as + // LogFileEventListener will create a file in EnlistmentRootPath + if (Directory.Exists(this.EnlistmentRootPath) && Directory.EnumerateFileSystemEntries(this.EnlistmentRootPath).Any()) + { + return new Result("Clone directory '" + this.EnlistmentRootPath + "' exists and is not empty"); + } + + string gitBinPath = GitProcess.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + return new Result(GVFSConstants.GitIsNotInstalledError); + } + + string hooksPath = ProcessHelper.WhereDirectory(GVFSConstants.GVFSHooksExecutableName); + if (hooksPath == null) + { + this.ReportErrorAndExit("Could not find " + GVFSConstants.GVFSHooksExecutableName); + } + + enlistment = new GVFSEnlistment( + this.EnlistmentRootPath, + this.RepositoryURL, + this.CacheServerUrl, + gitBinPath, + hooksPath); + + return new Result(true); + } + + private Result TryClone(JsonEtwTracer tracer, GVFSEnlistment enlistment) + { + this.CheckGitVersion(enlistment); + + HttpGitObjects httpGitObjects = new HttpGitObjects(tracer, enlistment, Environment.ProcessorCount); + this.ValidateGVFSVersion(enlistment, httpGitObjects, tracer); + + GitRefs refs = httpGitObjects.QueryInfoRefs(this.SingleBranch ? this.Branch : null); + + if (refs == null) + { + return new Result("Could not query info/refs from: " + Uri.EscapeUriString(enlistment.RepoUrl)); + } + + if (this.Branch == null) + { + this.Branch = refs.GetDefaultBranch(); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Branch", this.Branch); + tracer.RelatedEvent(EventLevel.Informational, "CloneDefaultRemoteBranch", metadata); + } + else + { + if (!refs.HasBranch(this.Branch)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Branch", this.Branch); + tracer.RelatedEvent(EventLevel.Warning, "CloneBranchDoesNotExist", metadata); + + string errorMessage = string.Format("Remote branch {0} not found in upstream origin", this.Branch); + return new Result(errorMessage); + } + } + + if (!enlistment.TryCreateEnlistmentFolders()) + { + string error = "Could not create enlistment directory"; + tracer.RelatedError(error); + return new Result(error); + } + + this.CheckAntiVirusExclusion(enlistment); + + CloneHelper cloneHelper = new CloneHelper(tracer, enlistment, httpGitObjects); + return cloneHelper.CreateClone(refs, this.Branch); + } + + private string GetDefaultEnlistmentRoot() + { + string repoName = this.RepositoryURL.Substring(this.RepositoryURL.LastIndexOf('/') + 1); + return Path.Combine(Directory.GetCurrentDirectory(), repoName); + } + + public class Result + { + public Result(bool success) + { + this.Success = success; + this.ErrorMessage = string.Empty; + } + + public Result(string errorMessage) + { + this.Success = false; + this.ErrorMessage = errorMessage; + } + + public bool Success { get; } + public string ErrorMessage { get; } + } + } +} diff --git a/GVFS/GVFS/CommandLine/DiagnoseVerb.cs b/GVFS/GVFS/CommandLine/DiagnoseVerb.cs new file mode 100644 index 00000000..0c56e997 --- /dev/null +++ b/GVFS/GVFS/CommandLine/DiagnoseVerb.cs @@ -0,0 +1,286 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Physical.FileSystem; +using GVFS.Common.Tracing; +using GVFS.GVFlt; +using Microsoft.Isam.Esent.Collections.Generic; +using System; +using System.IO; +using System.IO.Compression; + +namespace GVFS.CommandLine +{ + [Verb(DiagnoseVerb.DiagnoseVerbName, HelpText = "Diagnose issues with a GVFS repo")] + public class DiagnoseVerb : GVFSVerb.ForExistingEnlistment + { + public const string DiagnoseVerbName = "diagnose"; + + private const string System32LogFilesRoot = @"%SystemRoot%\System32\LogFiles"; + private const string GVFltLogFolderName = "GvFlt"; + + private TextWriter diagnosticLogFileWriter; + + protected override string VerbName + { + get { return DiagnoseVerbName; } + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + string diagnosticsRoot = Path.Combine(enlistment.DotGVFSRoot, "diagnostics"); + + if (!Directory.Exists(diagnosticsRoot)) + { + Directory.CreateDirectory(diagnosticsRoot); + } + + string archiveFolderPath = Path.Combine(diagnosticsRoot, "gvfs_" + DateTime.Now.ToString("yyyyMMdd_HHmmss")); + Directory.CreateDirectory(archiveFolderPath); + + using (FileStream diagnosticLogFile = new FileStream(Path.Combine(archiveFolderPath, "diagnostics.log"), FileMode.CreateNew)) + using (this.diagnosticLogFileWriter = new StreamWriter(diagnosticLogFile)) + { + this.WriteMessage("Collecting diagnostic info into temp folder " + archiveFolderPath); + + this.WriteMessage(string.Empty); + this.WriteMessage("gvfs version " + ProcessHelper.GetCurrentProcessVersion()); + this.WriteMessage(GitProcess.Version(enlistment).Output); + this.WriteMessage(GitProcess.GetInstalledGitBinPath()); + this.WriteMessage(string.Empty); + this.WriteMessage("Enlistment root: " + enlistment.EnlistmentRoot); + this.WriteMessage("Repo URL: " + enlistment.RepoUrl); + this.WriteMessage("Objects URL: " + enlistment.ObjectsEndpointUrl); + this.WriteMessage(string.Empty); + + this.WriteMessage("Copying .gvfs folder..."); + this.CopyAllFiles(enlistment.EnlistmentRoot, archiveFolderPath, GVFSConstants.DotGVFSPath, copySubFolders: false); + + this.WriteMessage("Copying GVFlt logs..."); + string system32LogFilesPath = Environment.ExpandEnvironmentVariables(System32LogFilesRoot); + this.CopyAllFiles(system32LogFilesPath, archiveFolderPath, GVFltLogFolderName, copySubFolders: false); + + this.WriteMessage("Checking on GVFS..."); + this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_log.txt"); + ReturnCode statusResult = this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_status.txt"); + + if (statusResult == ReturnCode.Success) + { + this.WriteMessage("GVFS is mounted. Unmounting so we can read files that GVFS has locked..."); + this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_unmount.txt"); + } + else + { + this.WriteMessage("GVFS was not mounted."); + } + + this.WriteMessage("Copying .git folder..."); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Hooks.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Info.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, GVFSConstants.DotGit.Logs.Root, copySubFolders: true); + + this.CopyEsentDatabase( + enlistment.DotGVFSRoot, + Path.Combine(archiveFolderPath, GVFSConstants.DotGVFSPath), + GVFSConstants.DatabaseNames.BackgroundGitUpdates); + this.CopyEsentDatabase( + enlistment.DotGVFSRoot, + Path.Combine(archiveFolderPath, GVFSConstants.DotGVFSPath), + GVFSConstants.DatabaseNames.DoNotProject); + this.CopyEsentDatabase( + enlistment.DotGVFSRoot, + Path.Combine(archiveFolderPath, GVFSConstants.DotGVFSPath), + GVFSConstants.DatabaseNames.BlobSizes); + this.CopyEsentDatabase( + enlistment.DotGVFSRoot, + Path.Combine(archiveFolderPath, GVFSConstants.DotGVFSPath), + GVFSConstants.DatabaseNames.RepoMetadata); + + this.WriteMessage(string.Empty); + this.WriteMessage("Remounting GVFS..."); + ReturnCode mountResult = this.RunAndRecordGVFSVerb(archiveFolderPath, "gvfs_mount.txt"); + if (mountResult == ReturnCode.Success) + { + this.WriteMessage("Mount succeeded"); + } + else + { + this.WriteMessage("Failed to remount. The reason for failure was captured."); + } + + this.CopyAllFiles(enlistment.DotGVFSRoot, Path.Combine(archiveFolderPath, GVFSConstants.DotGVFSPath), "logs", copySubFolders: false); + } + + string zipFilePath = archiveFolderPath + ".zip"; + ZipFile.CreateFromDirectory(archiveFolderPath, zipFilePath); + PhysicalFileSystem.RecursiveDelete(archiveFolderPath); + + this.Output.WriteLine(); + this.Output.WriteLine("Diagnostics complete. All of the gathered info, as well as all of the output above, is captured in"); + this.Output.WriteLine(zipFilePath); + } + + private void WriteMessage(string message) + { + message = message.TrimEnd('\r', '\n'); + + this.Output.WriteLine(message); + this.diagnosticLogFileWriter.WriteLine(message); + } + + private void CopyAllFiles(string sourceRoot, string targetRoot, string folderName, bool copySubFolders) + { + string sourceFolder = Path.Combine(sourceRoot, folderName); + string targetFolder = Path.Combine(targetRoot, folderName); + + try + { + if (!Directory.Exists(sourceFolder)) + { + this.WriteMessage(string.Format("Skipping {0}, folder does not exist", sourceFolder)); + return; + } + + this.RecursiveFileCopyImpl(sourceFolder, targetFolder, copySubFolders); + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to copy folder {0} in {1} with exception {2}. copySubFolders: {3}", + folderName, + sourceRoot, + e, + copySubFolders)); + } + } + + private void RecursiveFileCopyImpl(string sourcePath, string targetPath, bool copySubFolders) + { + if (!Directory.Exists(targetPath)) + { + Directory.CreateDirectory(targetPath); + } + + foreach (string filePath in Directory.EnumerateFiles(sourcePath)) + { + string fileName = Path.GetFileName(filePath); + try + { + string fileExtension = Path.GetExtension(fileName); + if (!string.Equals(fileExtension, ".exe", StringComparison.OrdinalIgnoreCase)) + { + File.Copy( + Path.Combine(sourcePath, fileName), + Path.Combine(targetPath, fileName)); + } + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to copy '{0}' in {1} with exception {2}", + fileName, + sourcePath, + e)); + } + } + + if (copySubFolders) + { + DirectoryInfo dir = new DirectoryInfo(sourcePath); + foreach (DirectoryInfo subdir in dir.GetDirectories()) + { + string targetFolderPath = Path.Combine(targetPath, subdir.Name); + try + { + this.RecursiveFileCopyImpl(subdir.FullName, targetFolderPath, copySubFolders); + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to copy subfolder '{0}' to '{1}' with exception {2}", + subdir.FullName, + targetFolderPath, + e)); + } + } + } + } + + private ReturnCode RunAndRecordGVFSVerb(string archiveFolderPath, string outputFileName) + where TVerb : GVFSVerb, new() + { + try + { + using (FileStream file = new FileStream(Path.Combine(archiveFolderPath, outputFileName), FileMode.CreateNew)) + using (StreamWriter writer = new StreamWriter(file)) + { + TVerb verb = new TVerb(); + verb.EnlistmentRootPath = this.EnlistmentRootPath; + verb.Output = writer; + + try + { + verb.Execute(); + } + catch (VerbAbortedException) + { + } + + return verb.ReturnCode; + } + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Verb {0} failed with exception {1}", + typeof(TVerb), + e)); + + return ReturnCode.GenericError; + } + } + + private void CopyEsentDatabase(string sourceFolder, string targetFolder, string databaseName) + where TKey : IComparable + { + try + { + if (!Directory.Exists(targetFolder)) + { + Directory.CreateDirectory(targetFolder); + } + + using (FileStream outputFile = new FileStream(Path.Combine(targetFolder, databaseName + ".txt"), FileMode.CreateNew)) + using (StreamWriter writer = new StreamWriter(outputFile)) + { + using (PersistentDictionary dictionary = new PersistentDictionary( + Path.Combine(sourceFolder, databaseName))) + { + this.WriteMessage(string.Format( + "Found {0} entries in {1}", + dictionary.Count, + databaseName)); + + foreach (TKey key in dictionary.Keys) + { + writer.Write(key); + writer.Write(" = "); + writer.WriteLine(dictionary[key].ToString()); + } + } + } + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to copy database {0} with exception {1}", + databaseName, + e)); + } + + // Also copy the database files themselves, in case we failed to read the entries above + this.CopyAllFiles(sourceFolder, targetFolder, databaseName, copySubFolders: false); + } + } +} diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs new file mode 100644 index 00000000..b7b1fd1f --- /dev/null +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -0,0 +1,339 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.ServiceProcess; + +namespace GVFS.CommandLine +{ + public abstract class GVFSVerb + { + public GVFSVerb() + { + this.Output = Console.Out; + this.ReturnCode = ReturnCode.Success; + + this.InitializeDefaultParameterValues(); + } + + public abstract string EnlistmentRootPath { get; set; } + + public TextWriter Output { get; set; } + + public ReturnCode ReturnCode { get; private set; } + + protected abstract string VerbName { get; } + + public static bool TrySetGitConfigSettings(GitProcess git) + { + Dictionary expectedConfigSettings = new Dictionary + { + { "core.autocrlf", "false" }, + { "core.fscache", "true" }, + { "core.gvfs", "true" }, + { "core.preloadIndex", "true" }, + { "core.safecrlf", "false" }, + { "core.sparseCheckout", "true" }, + { GVFSConstants.VirtualizeObjectsGitConfigName, "true" }, + { "credential.validate", "false" }, + { "diff.autoRefreshIndex", "false" }, + { "gc.auto", "0" }, + { "merge.stat", "false" }, + { "receive.autogc", "false" }, + }; + + GitProcess.Result getConfigResult = git.GetAllLocalConfig(); + if (getConfigResult.HasErrors) + { + return false; + } + + Dictionary actualConfigSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string line in getConfigResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + string[] fields = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + if (!actualConfigSettings.ContainsKey(fields[0]) && fields.Length == 2) + { + actualConfigSettings.Add(fields[0], fields[1]); + } + else + { + actualConfigSettings[fields[0]] = null; + } + } + + foreach (string key in expectedConfigSettings.Keys) + { + string actualValue; + if (!actualConfigSettings.TryGetValue(key, out actualValue) || + actualValue != expectedConfigSettings[key]) + { + GitProcess.Result setConfigResult = git.SetInLocalConfig(key, expectedConfigSettings[key]); + if (setConfigResult.HasErrors) + { + return false; + } + } + } + + return true; + } + + public abstract void Execute(ITracer tracer = null); + + public virtual void InitializeDefaultParameterValues() + { + } + + protected void ReportErrorAndExit(string error, params object[] args) + { + if (error != null) + { + this.Output.WriteLine(error, args); + } + + this.ReturnCode = ReturnCode.GenericError; + throw new VerbAbortedException(this); + } + + protected void CheckElevated() + { + if (!ProcessHelper.IsAdminElevated()) + { + this.ReportErrorAndExit("{0} must be run with elevated privileges", this.VerbName); + } + } + + protected void CheckGVFltRunning() + { + bool gvfltServiceRunning = false; + try + { + ServiceController controller = new ServiceController("gvflt"); + gvfltServiceRunning = controller.Status.Equals(ServiceControllerStatus.Running); + } + catch (InvalidOperationException) + { + this.ReportErrorAndExit("Error: GVFlt Service was not found. To resolve, re-install GVFS"); + } + + if (!gvfltServiceRunning) + { + this.ReportErrorAndExit("Error: GVFlt Service is not running. To resolve, run \"sc start gvflt\" from an admin command prompt"); + } + } + + protected void CheckGitVersion(Enlistment enlistment) + { + GitProcess.Result versionResult = GitProcess.Version(enlistment); + if (versionResult.HasErrors) + { + this.ReportErrorAndExit("Error: Unable to retrieve the git version"); + } + + GitVersion gitVersion; + string version = versionResult.Output; + if (version.StartsWith("git version ")) + { + version = version.Substring(12); + } + + if (!GitVersion.TryParse(version, out gitVersion)) + { + this.ReportErrorAndExit("Error: Unable to parse the git version. {0}", version); + } + + if (gitVersion.Platform != GVFSConstants.MinimumGitVersion.Platform) + { + this.ReportErrorAndExit("Error: Invalid version of git {0}. Must use gvfs version.", version); + } + + if (gitVersion.IsLessThan(GVFSConstants.MinimumGitVersion)) + { + this.ReportErrorAndExit( + "Error: Installed git version {0} is less than the minimum version of {1}.", + gitVersion, + GVFSConstants.MinimumGitVersion); + } + } + + protected void CheckAntiVirusExclusion(GVFSEnlistment enlistment) + { + bool isExcluded; + if (AntiVirusExclusions.TryGetIsPathExcluded(enlistment.EnlistmentRoot, out isExcluded)) + { + if (!isExcluded) + { + if (ProcessHelper.IsAdminElevated()) + { + this.Output.WriteLine(); + this.Output.WriteLine("Adding {0} to your antivirus exclusion list", enlistment.EnlistmentRoot); + this.Output.WriteLine(); + + AntiVirusExclusions.AddAntiVirusExclusion(enlistment.EnlistmentRoot); + + if (!AntiVirusExclusions.TryGetIsPathExcluded(enlistment.EnlistmentRoot, out isExcluded) || + !isExcluded) + { + this.ReportErrorAndExit( + "This repo is not excluded from antivirus and we were unable to add it. Add '{0}' to your exclusion list and then run {1} again.", + enlistment.EnlistmentRoot, + this.VerbName); + } + } + else + { + this.ReportErrorAndExit( + "This repo is not excluded from antivirus. Either re-run {1} with elevated privileges, or add '{0}' to your exclusion list and then run {1} again.", + enlistment.EnlistmentRoot, + this.VerbName); + } + } + } + else + { + this.Output.WriteLine(); + this.Output.WriteLine( + "WARNING: Unable to determine if this repo is excluded from antivirus. Please check to ensure that '{0}' is excluded.", + enlistment.EnlistmentRoot); + this.Output.WriteLine(); + } + } + + protected void ValidateGVFSVersion(GVFSEnlistment enlistment, HttpGitObjects httpGitObjects, ITracer tracer) + { + using (ITracer activity = tracer.StartActivity("ValidateGVFSVersion", EventLevel.Informational)) + { + Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion()); + + GVFSConfigResponse config = httpGitObjects.QueryGVFSConfig(); + IEnumerable allowedGvfsClientVersions = + config != null + ? config.AllowedGvfsClientVersions + : null; + + if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) + { + string errorMessage = string.Empty; + if (config == null) + { + errorMessage = "Could not query valid GVFS versions from: " + Uri.EscapeUriString(enlistment.RepoUrl); + } + else + { + errorMessage = "Server not configured to provide supported GVFS versions"; + } + + EventMetadata metadata = new EventMetadata(); + metadata.Add("ErrorMessage", errorMessage); + tracer.RelatedError(metadata, Keywords.Network); + + this.Output.WriteLine(); + this.Output.WriteLine("WARNING: Unable to validate your GVFS version"); + this.Output.WriteLine(); + return; + } + + foreach (GVFSConfigResponse.VersionRange versionRange in config.AllowedGvfsClientVersions) + { + if (currentVersion >= versionRange.Min && + (versionRange.Max == null || currentVersion <= versionRange.Max)) + { + activity.RelatedEvent( + EventLevel.Informational, + "GVFSVersionValidated", + new EventMetadata + { + { "SupportedVersionRange", versionRange }, + }); + return; + } + } + + activity.RelatedError("GVFS version {0} is not supported", currentVersion); + } + + this.ReportErrorAndExit("\r\nERROR: Your GVFS version is no longer supported. Install the latest and try again.\r\n"); + } + + private GVFSEnlistment CreateEnlistment(string enlistmentRootPath) + { + string gitBinPath = GitProcess.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + this.ReportErrorAndExit("Error: " + GVFSConstants.GitIsNotInstalledError); + } + + if (string.IsNullOrWhiteSpace(enlistmentRootPath)) + { + enlistmentRootPath = Environment.CurrentDirectory; + } + + string hooksPath = ProcessHelper.WhereDirectory(GVFSConstants.GVFSHooksExecutableName); + if (hooksPath == null) + { + this.ReportErrorAndExit("Could not find " + GVFSConstants.GVFSHooksExecutableName); + } + + GVFSEnlistment enlistment = null; + try + { + enlistment = GVFSEnlistment.CreateFromDirectory(enlistmentRootPath, null, gitBinPath, hooksPath); + if (enlistment == null) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid GVFS enlistment", + enlistmentRootPath); + } + } + catch (InvalidRepoException e) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid GVFS enlistment. {1}", + enlistmentRootPath, + e.Message); + } + + return enlistment; + } + + public abstract class ForExistingEnlistment : GVFSVerb + { + [Value( + 0, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the GVFS enlistment root")] + public override string EnlistmentRootPath { get; set; } + + public sealed override void Execute(ITracer tracer = null) + { + this.PreExecute(this.EnlistmentRootPath, tracer); + GVFSEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPath); + this.Execute(enlistment, tracer); + } + + protected virtual void PreExecute(string enlistmentRootPath, ITracer tracer = null) + { + } + + protected abstract void Execute(GVFSEnlistment enlistment, ITracer tracer = null); + } + + public class VerbAbortedException : Exception + { + public VerbAbortedException(GVFSVerb verb) + { + this.Verb = verb; + } + + public GVFSVerb Verb { get; } + } + } +} diff --git a/GVFS/GVFS/CommandLine/LogVerb.cs b/GVFS/GVFS/CommandLine/LogVerb.cs new file mode 100644 index 00000000..c8b0cfc3 --- /dev/null +++ b/GVFS/GVFS/CommandLine/LogVerb.cs @@ -0,0 +1,30 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Tracing; + +namespace GVFS.CommandLine +{ + [Verb(LogVerb.LogVerbName, HelpText = "Show the most recent GVFS log")] + public class LogVerb : GVFSVerb.ForExistingEnlistment + { + public const string LogVerbName = "log"; + + protected override string VerbName + { + get { return LogVerbName; } + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + string logFile = enlistment.GetMostRecentGVFSLogFileName(); + if (logFile != null) + { + this.Output.WriteLine("Most recent log file: " + logFile); + } + else + { + this.Output.WriteLine("No log files found at " + enlistment.GVFSLogsRoot); + } + } + } +} diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs new file mode 100644 index 00000000..91b4db9b --- /dev/null +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -0,0 +1,205 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.NamedPipes; +using GVFS.Common.Physical; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; +using System.IO; +using System.Threading; + +namespace GVFS.CommandLine +{ + [Verb(MountVerb.MountVerbName, HelpText = "Mount a GVFS virtual repo")] + public class MountVerb : GVFSVerb.ForExistingEnlistment + { + public const string MountVerbName = "mount"; + private const string MountExeName = "GVFS.Mount.exe"; + + private const int BackgroundProcessConnectTimeoutMS = 15000; + private const int MutexMaxWaitTimeMS = 500; + + [Option( + 'v', + MountParameters.Verbosity, + Default = MountParameters.DefaultVerbosity, + Required = false, + HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] + public string Verbosity { get; set; } + + [Option( + 'k', + MountParameters.Keywords, + Default = MountParameters.DefaultKeywords, + Required = false, + HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] + public string KeywordsCsv { get; set; } + + [Option( + 'd', + MountParameters.DebugWindow, + Default = false, + Required = false, + HelpText = "Show the debug window. By default, all output is written to a log file and no debug window is shown.")] + public bool ShowDebugWindow { get; set; } + + protected override string VerbName + { + get { return MountVerbName; } + } + + public override void InitializeDefaultParameterValues() + { + this.Verbosity = MountParameters.DefaultVerbosity; + this.KeywordsCsv = MountParameters.DefaultKeywords; + } + + protected override void PreExecute(string enlistmentRootPath, ITracer tracer = null) + { + this.Output.WriteLine("Validating repo for mount"); + this.CheckElevated(); + this.CheckGVFltRunning(); + + if (string.IsNullOrWhiteSpace(enlistmentRootPath)) + { + enlistmentRootPath = Environment.CurrentDirectory; + } + + string enlistmentRoot = GVFSEnlistment.GetEnlistmentRoot(enlistmentRootPath); + + if (enlistmentRoot == null) + { + this.ReportErrorAndExit("Error: '{0}' is not a valid GVFS enlistment", enlistmentRootPath); + } + + using (NamedPipeClient pipeClient = new NamedPipeClient(GVFSEnlistment.GetNamedPipeName(enlistmentRoot))) + { + if (pipeClient.Connect(500)) + { + this.ReportErrorAndExit("This repo is already mounted. Try running 'gvfs status'."); + } + } + + string error; + if (!RepoMetadata.CheckDiskLayoutVersion(Path.Combine(enlistmentRoot, GVFSConstants.DotGVFSPath), out error)) + { + this.ReportErrorAndExit("Error: " + error); + } + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + this.CheckGitVersion(enlistment); + this.CheckAntiVirusExclusion(enlistment); + + string mountExeLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), MountExeName); + if (!File.Exists(mountExeLocation)) + { + this.ReportErrorAndExit("Could not find GVFS.Mount.exe. You may need to reinstall GVFS."); + } + + // This tracer is only needed for the HttpGitObjects so we can check the GVFS version. + // If we keep it around longer, it will collide with the background process tracer. + using (ITracer mountTracer = tracer ?? new JsonEtwTracer(GVFSConstants.GVFSEtwProviderName, "Mount")) + { + HttpGitObjects gitObjects = new HttpGitObjects(mountTracer, enlistment, maxConnections: 1); + this.ValidateGVFSVersion(enlistment, gitObjects, mountTracer); + } + + // We have to parse these parameters here to make sure they are valid before + // handing them to the background process which cannot tell the user when they are bad + EventLevel verbosity; + Keywords keywords; + this.ParseEnumArgs(out verbosity, out keywords); + + GitProcess git = new GitProcess(enlistment); + if (!git.IsValidRepo()) + { + this.ReportErrorAndExit("The physical git repo is missing or invalid"); + } + + this.SetGitConfigSettings(git); + + const string ParamPrefix = "--"; + ProcessHelper.StartBackgroundProcess( + mountExeLocation, + string.Join( + " ", + enlistment.EnlistmentRoot, + ParamPrefix + MountParameters.Verbosity, + this.Verbosity, + ParamPrefix + MountParameters.Keywords, + this.KeywordsCsv, + this.ShowDebugWindow ? ParamPrefix + MountParameters.DebugWindow : string.Empty), + createWindow: this.ShowDebugWindow); + + this.Output.WriteLine("Waiting for GVFS to mount"); + + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect(BackgroundProcessConnectTimeoutMS)) + { + this.ReportErrorAndExit("Unable to mount because the background process is not responding."); + } + + bool isMounted = false; + int tryCount = 0; + while (!isMounted) + { + try + { + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(pipeClient.ReadRawResponse()); + + if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.Ready) + { + this.Output.WriteLine("Virtual repo is ready."); + isMounted = true; + } + else if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.MountFailed) + { + this.ReportErrorAndExit("Failed to mount, run 'gvfs log' for details"); + } + else + { + if (tryCount % 10 == 0) + { + this.Output.WriteLine(getStatusResponse.MountStatus + "..."); + } + + Thread.Sleep(500); + tryCount++; + } + } + catch (BrokenPipeException) + { + this.ReportErrorAndExit("Failed to mount, run 'gvfs log' for details"); + } + } + } + } + + private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + { + if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + { + this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); + } + + if (!Enum.TryParse(this.Verbosity, out verbosity)) + { + this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); + } + } + + private void SetGitConfigSettings(GitProcess git) + { + if (!GVFSVerb.TrySetGitConfigSettings(git)) + { + this.ReportErrorAndExit("Unable to configure git repo"); + } + } + } +} \ No newline at end of file diff --git a/GVFS/GVFS/CommandLine/PrefetchHelper.cs b/GVFS/GVFS/CommandLine/PrefetchHelper.cs new file mode 100644 index 00000000..5913b541 --- /dev/null +++ b/GVFS/GVFS/CommandLine/PrefetchHelper.cs @@ -0,0 +1,52 @@ +using GVFS.Common; +using GVFS.Common.Git; +using GVFS.Common.Tracing; +using System.IO; + +namespace GVFS.CommandLine +{ + public class PrefetchHelper + { + private readonly GitObjects gitObjects; + + public PrefetchHelper(ITracer tracer, GVFSEnlistment enlistment, int downloadThreadCount) + { + HttpGitObjects http = new HttpGitObjects(tracer, enlistment, downloadThreadCount); + this.gitObjects = new GitObjects(tracer, enlistment, http); + } + + public void PrefetchCommitsAndTrees() + { + string[] packs = this.gitObjects.ReadPackFileNames(GVFSConstants.PrefetchPackPrefix); + long max = -1; + foreach (string pack in packs) + { + long? timestamp = GetTimestamp(pack); + if (timestamp.HasValue && timestamp > max) + { + max = timestamp.Value; + } + } + + this.gitObjects.DownloadPrefetchPacks(max); + } + + private static long? GetTimestamp(string packName) + { + string filename = Path.GetFileName(packName); + if (!filename.StartsWith(GVFSConstants.PrefetchPackPrefix)) + { + return null; + } + + string[] parts = filename.Split('-'); + long parsed; + if (parts.Length > 1 && long.TryParse(parts[1], out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs new file mode 100644 index 00000000..6c16ef7d --- /dev/null +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -0,0 +1,186 @@ +using CommandLine; +using FastFetch; +using GVFS.Common; +using GVFS.Common.Tracing; +using Microsoft.Diagnostics.Tracing; +using System; + +namespace GVFS.CommandLine +{ + [Verb(PrefetchVerb.PrefetchVerbName, HelpText = "Prefetch remote objects for the current head")] + public class PrefetchVerb : GVFSVerb.ForExistingEnlistment + { + public const string PrefetchVerbName = "prefetch"; + + private const int ChunkSize = 4000; + private static readonly int SearchThreadCount = Environment.ProcessorCount; + private static readonly int DownloadThreadCount = Environment.ProcessorCount; + private static readonly int IndexThreadCount = Environment.ProcessorCount; + + [Option( + 'v', + Parameters.Verbosity, + Default = Parameters.DefaultVerbosity, + Required = false, + HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] + public string Verbosity { get; set; } + + [Option( + 'f', + Parameters.Folders, + Required = false, + Default = Parameters.DefaultPathWhitelist, + HelpText = "A semicolon-delimited list of paths to fetch")] + public string PathWhitelist { get; set; } + + [Option( + Parameters.FoldersList, + Required = false, + Default = Parameters.DefaultPathWhitelistFile, + HelpText = "A file containing line-delimited list of paths to fetch")] + public string PathWhitelistFile { get; set; } + + [Option( + 'c', + Parameters.Commits, + Required = false, + Default = false, + HelpText = "Prefetch the latest set of commit and tree packs")] + public bool Commits { get; set; } + + protected override string VerbName + { + get { return PrefetchVerbName; } + } + + public override void InitializeDefaultParameterValues() + { + this.Verbosity = Parameters.DefaultVerbosity; + this.PathWhitelist = Parameters.DefaultPathWhitelist; + this.PathWhitelistFile = Parameters.DefaultPathWhitelistFile; + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + EventLevel verbosity; + if (!Enum.TryParse(this.Verbosity, out verbosity)) + { + this.ReportErrorAndExit("Error: Invalid verbosity: " + this.Verbosity); + } + + if (tracer != null) + { + this.PerformPrefetch(enlistment, tracer); + } + else + { + using (JsonEtwTracer prefetchTracer = new JsonEtwTracer(GVFSConstants.GVFSEtwProviderName, "Prefetch")) + { + prefetchTracer.AddLogFileEventListener( + GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, this.VerbName), + EventLevel.Informational, + Keywords.Any); + + prefetchTracer.AddConsoleEventListener(verbosity, ~Keywords.Network); + + prefetchTracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + enlistment.CacheServerUrl); + + this.PerformPrefetch(enlistment, prefetchTracer); + } + } + } + + private void PerformPrefetch(GVFSEnlistment enlistment, ITracer tracer) + { + try + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Commits", this.Commits); + metadata.Add("PathWhitelist", this.PathWhitelist); + metadata.Add("PathWhitelistFile", this.PathWhitelistFile); + tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); + + if (this.Commits) + { + if (!string.IsNullOrEmpty(this.PathWhitelistFile) || + !string.IsNullOrWhiteSpace(this.PathWhitelist)) + { + this.ReportErrorAndExit("Cannot supply both --commits (-c) and --folders (-f)"); + } + + PrefetchHelper prefetchHelper = new PrefetchHelper( + tracer, + enlistment, + DownloadThreadCount); + prefetchHelper.PrefetchCommitsAndTrees(); + return; + } + + FetchHelper fetchHelper = new FetchHelper( + tracer, + enlistment, + ChunkSize, + SearchThreadCount, + DownloadThreadCount, + IndexThreadCount); + + if (!FetchHelper.TryLoadPathWhitelist(this.PathWhitelist, this.PathWhitelistFile, tracer, fetchHelper.PathWhitelist)) + { + Environment.ExitCode = (int)ReturnCode.GenericError; + return; + } + + bool gvfsHeadFileExists; + string error; + string projectedCommitId; + + if (!enlistment.TryParseGVFSHeadFile(out gvfsHeadFileExists, out error, out projectedCommitId)) + { + tracer.RelatedError(error); + this.Output.WriteLine(error); + Environment.ExitCode = (int)ReturnCode.GenericError; + return; + } + + fetchHelper.FastFetch(projectedCommitId.Trim(), isBranch: false); + if (fetchHelper.HasFailures) + { + Environment.ExitCode = 1; + } + } + catch (AggregateException e) + { + this.Output.WriteLine("Cannot prefetch @ {0}:", enlistment.EnlistmentRoot); + foreach (Exception ex in e.Flatten().InnerExceptions) + { + this.Output.WriteLine("Exception: {0}", ex.ToString()); + } + + Environment.ExitCode = (int)ReturnCode.GenericError; + } + catch (VerbAbortedException) + { + throw; + } + catch (Exception e) + { + this.ReportErrorAndExit("Cannot prefetch @ {0}: {1}", enlistment.EnlistmentRoot, e.ToString()); + } + } + + private static class Parameters + { + public const string Verbosity = "verbosity"; + public const string Folders = "folders"; + public const string FoldersList = "folders-list"; + public const string Commits = "commits"; + + public const string DefaultVerbosity = "Informational"; + public const string DefaultPathWhitelist = ""; + public const string DefaultPathWhitelistFile = ""; + } + } +} diff --git a/GVFS/GVFS/CommandLine/StatusVerb.cs b/GVFS/GVFS/CommandLine/StatusVerb.cs new file mode 100644 index 00000000..d8b34d79 --- /dev/null +++ b/GVFS/GVFS/CommandLine/StatusVerb.cs @@ -0,0 +1,54 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; + +namespace GVFS.CommandLine +{ + [Verb(StatusVerb.StatusVerbName, HelpText = "Get the status of the GVFS virtual repo")] + public class StatusVerb : GVFSVerb.ForExistingEnlistment + { + public const string StatusVerbName = "status"; + + protected override string VerbName + { + get { return StatusVerbName; } + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + this.CheckAntiVirusExclusion(enlistment); + + this.Output.WriteLine("Attempting to connect to GVFS at {0}...", enlistment.EnlistmentRoot); + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + this.ReportErrorAndExit("Unable to connect to GVFS. Try running 'gvfs mount'"); + } + + this.Output.WriteLine("Connected"); + this.Output.WriteLine(); + + try + { + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(pipeClient.ReadRawResponse()); + + this.Output.WriteLine("Mount status: " + getStatusResponse.MountStatus); + this.Output.WriteLine("GVFS Lock: " + getStatusResponse.LockStatus); + this.Output.WriteLine("Enlistment root: " + getStatusResponse.EnlistmentRoot); + this.Output.WriteLine("Repo URL: " + getStatusResponse.RepoUrl); + this.Output.WriteLine("Objects URL: " + getStatusResponse.ObjectsUrl); + this.Output.WriteLine("Background operations: " + getStatusResponse.BackgroundOperationCount); + this.Output.WriteLine("Disk layout version: " + getStatusResponse.DiskLayoutVersion); + } + catch (BrokenPipeException e) + { + this.ReportErrorAndExit("Unable to communicate with GVFS: " + e.ToString()); + } + } + } + } +} diff --git a/GVFS/GVFS/CommandLine/UnmountVerb.cs b/GVFS/GVFS/CommandLine/UnmountVerb.cs new file mode 100644 index 00000000..d6b90ee3 --- /dev/null +++ b/GVFS/GVFS/CommandLine/UnmountVerb.cs @@ -0,0 +1,97 @@ +using CommandLine; +using GVFS.Common; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; + +namespace GVFS.CommandLine +{ + [Verb(UnmountVerb.UnmountVerbName, HelpText = "Unmount a GVFS virtual repo")] + public class UnmountVerb : GVFSVerb.ForExistingEnlistment + { + public const string UnmountVerbName = "unmount"; + + protected override string VerbName + { + get { return UnmountVerbName; } + } + + protected override void Execute(GVFSEnlistment enlistment, ITracer tracer = null) + { + try + { + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + this.ReportErrorAndExit("Unable to connect to GVFS"); + } + + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + string rawGetStatusResponse = pipeClient.ReadRawResponse(); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(rawGetStatusResponse); + + switch (getStatusResponse.MountStatus) + { + case NamedPipeMessages.GetStatus.Mounting: + this.ReportErrorAndExit("Still mounting, please try again later"); + break; + + case NamedPipeMessages.GetStatus.Unmounting: + this.ReportErrorAndExit("Already unmounting, please wait"); + break; + + case NamedPipeMessages.GetStatus.Ready: + this.Output.WriteLine("Repo is mounted. Starting to unmount..."); + break; + + case NamedPipeMessages.GetStatus.MountFailed: + this.Output.WriteLine("Previous mount attempt failed, run 'gvfs log' for details."); + this.Output.WriteLine("Attempting to unmount anyway..."); + break; + + default: + this.ReportErrorAndExit("Unrecognized response to GetStatus: {0}", rawGetStatusResponse); + break; + } + + pipeClient.SendRequest(NamedPipeMessages.Unmount.Request); + string unmountResponse = pipeClient.ReadRawResponse(); + + switch (unmountResponse) + { + case NamedPipeMessages.Unmount.Acknowledged: + this.Output.WriteLine("Unmount was acknowledged. Waiting for complete unmount..."); + string finalResponse = pipeClient.ReadRawResponse(); + if (finalResponse == NamedPipeMessages.Unmount.Completed) + { + this.Output.WriteLine("Unmount completed"); + } + else + { + this.ReportErrorAndExit("Unrecognized final response to unmount: " + finalResponse); + } + + break; + + case NamedPipeMessages.Unmount.NotMounted: + this.ReportErrorAndExit("Unable to unmount, repo was not mounted"); + break; + + case NamedPipeMessages.Unmount.MountFailed: + this.ReportErrorAndExit("Unable to unmount, previous mount attempt failed"); + break; + + default: + this.ReportErrorAndExit("Unrecognized response to Unmount: " + unmountResponse); + break; + } + } + } + catch (BrokenPipeException e) + { + this.ReportErrorAndExit("Unable to communicate with GVFS: " + e.ToString()); + } + } + } +} diff --git a/GVFS/GVFS/GVFS.csproj b/GVFS/GVFS/GVFS.csproj new file mode 100644 index 00000000..6828203a --- /dev/null +++ b/GVFS/GVFS/GVFS.csproj @@ -0,0 +1,150 @@ + + + + + Debug + AnyCPU + {32220664-594C-4425-B9A0-88E0BE2F3D2A} + Exe + Properties + GVFS + GVFS + v4.5.2 + 512 + true + + + + + true + ..\..\..\BuildOutput\GVFS\bin\x64\Debug\ + ..\..\..\BuildOutput\GVFS\obj\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + ..\..\..\BuildOutput\GVFS\bin\x64\Release\ + ..\..\..\BuildOutput\GVFS\obj\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + true + + + + ..\..\..\packages\CommandLineParser.2.0.275-beta\lib\net45\CommandLine.dll + True + + + False + ..\..\..\packages\Microsoft.Database.Collections.Generic.1.9.4\lib\net40\Esent.Collections.dll + True + + + False + ..\..\..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll + True + + + False + ..\..\..\packages\Microsoft.Database.Isam.1.9.4\lib\net40\Esent.Isam.dll + True + + + ..\..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + + + + + + + + + + + + + + + + CommonAssemblyVersion.cs + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + {07f2a520-2ab7-46dd-97c0-75d8e988d55b} + FastFetch + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + GVFS.Common + + + {1118b427-7063-422f-83b9-5023c8ec5a7a} + GVFS.GVFlt + + + {17498502-aeff-4e70-90cc-1d0b56a8adf5} + GVFS.Mount + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + xcopy /Y $(SolutionDir)..\BuildOutput\GVFS.ReadObjectHook\bin\$(Platform)\$(Configuration)\GVFS.ReadObjectHook.* $(TargetDir) +cmd /c ""c:\Program Files (x86)\Inno Setup 5\ISCC.exe" "/DPlatformAndConfiguration=$(Platform)\$(Configuration)" $(TargetDir)Setup.iss" + + + + + + + + + \ No newline at end of file diff --git a/GVFS/GVFS/GitVirtualFileSystem.ico b/GVFS/GVFS/GitVirtualFileSystem.ico new file mode 100644 index 00000000..bc88ec23 Binary files /dev/null and b/GVFS/GVFS/GitVirtualFileSystem.ico differ diff --git a/GVFS/GVFS/Program.cs b/GVFS/GVFS/Program.cs new file mode 100644 index 00000000..849e9f33 --- /dev/null +++ b/GVFS/GVFS/Program.cs @@ -0,0 +1,45 @@ +using CommandLine; +using GVFS.CommandLine; +using System; + +namespace GVFS +{ + public class Program + { + public static void Main(string[] args) + { + Type[] verbTypes = new Type[] + { + // Verbs that work without an existing enlistment + typeof(CloneVerb), + + // Verbs that require an existing enlistment + typeof(DiagnoseVerb), + typeof(LogVerb), + typeof(MountVerb), + typeof(PrefetchVerb), + typeof(StatusVerb), + typeof(UnmountVerb), + }; + + try + { + new Parser( + settings => + { + settings.CaseSensitive = false; + settings.EnableDashDash = true; + settings.IgnoreUnknownArguments = false; + settings.HelpWriter = Console.Error; + }) + .ParseArguments(args, verbTypes) + .WithParsed(verb => verb.Execute()); + } + catch (GVFSVerb.VerbAbortedException e) + { + // Calling Environment.Exit() is required, to force all background threads to exit as well + Environment.Exit((int)e.Verb.ReturnCode); + } + } + } +} diff --git a/GVFS/GVFS/Properties/AssemblyInfo.cs b/GVFS/GVFS/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b6dab681 --- /dev/null +++ b/GVFS/GVFS/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GVFS")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GVFS")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("32220664-594c-4425-b9a0-88e0be2f3d2a")] diff --git a/GVFS/GVFS/Setup.iss b/GVFS/GVFS/Setup.iss new file mode 100644 index 00000000..91f9b702 --- /dev/null +++ b/GVFS/GVFS/Setup.iss @@ -0,0 +1,270 @@ +; This script requires Inno Setup Compiler 5.5.9 or later to compile +; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php + +; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php + +#define MyAppName "GVFS" +#define MyAppInstallerVersion GetFileVersion("GVFS.exe") +#define MyAppPublisher "Microsoft Corporation" +#define MyAppPublisherURL "http://www.microsoft.com" +#define MyAppURL "https://github.com/Microsoft/gvfs" +#define MyAppExeName "GVFS.exe" +#define EnvironmentKey "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" + +#define GVFltRelative "..\..\..\..\..\packages\Microsoft.GVFS.GVFlt.0.17131.2-preview\filter" +#define HooksRelative "..\..\..\..\GVFS.Hooks\bin" +#define GVFSMountRelative "..\..\..\..\GVFS.Mount\bin" +#define ReadObjectRelative "..\..\..\..\GVFS.ReadObjectHook\bin" + +[Setup] +AppId={{489CA581-F131-4C28-BE04-4FB178933E6D} +AppName={#MyAppName} +AppVersion={#MyAppInstallerVersion} +VersionInfoVersion={#MyAppInstallerVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppPublisherURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +AppCopyright=Copyright © Microsoft 2016 +BackColor=clWhite +BackSolid=yes +DefaultDirName={pf}\{#MyAppName} +OutputBaseFilename=SetupGVFS +OutputDir=Setup +Compression=lzma2 +InternalCompressLevel=ultra64 +SolidCompression=yes +MinVersion=10.0.14374 +DisableDirPage=yes +DisableReadyPage=yes +SetupIconFile=GitVirtualFileSystem.ico +ArchitecturesInstallIn64BitMode=x64 +ArchitecturesAllowed=x64 +;WizardImageFile=Assets\gcmicon128.bmp +;WizardSmallImageFile=Assets\gcmicon64.bmp +WizardImageStretch=no +WindowResizable=no +CloseApplications=yes +ChangesEnvironment=yes +RestartIfNeededByRun=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl"; + +[Types] +Name: "full"; Description: "Full installation"; Flags: iscustom; + +[Components] + +[Files] +; GVFlt Files +DestDir: "{app}\Filter"; Flags: ignoreversion; Source:"{#GVFltRelative}\gvflt.sys" +; gvflt.inf is declared explicitly last within the filter files, so we run the GVFlt install only once after required filter files are present +DestDir: "{app}\Filter"; Flags: ignoreversion; Source: "{#GVFltRelative}\gvflt.inf"; AfterInstall: InstallGVFlt + +; GitHooks Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#HooksRelative}\{#PlatformAndConfiguration}\GVFS.Hooks.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#HooksRelative}\{#PlatformAndConfiguration}\GVFS.Hooks.exe" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#HooksRelative}\{#PlatformAndConfiguration}\GVFS.Hooks.exe.config" + +; GVFS.Mount Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#GVFSMountRelative}\{#PlatformAndConfiguration}\GVFS.Mount.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#GVFSMountRelative}\{#PlatformAndConfiguration}\GVFS.Mount.exe" + +; GVFS.ReadObjectHook files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectRelative}\{#PlatformAndConfiguration}\GVFS.ReadObjectHook.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectRelative}\{#PlatformAndConfiguration}\GVFS.ReadObjectHook.exe" + +; GVFS and FastFetch PDB's +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Collections.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Interop.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Isam.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"FastFetch.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.Common.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.GVFlt.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.GvFltWrapper.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.pdb" + +; FastFetch Files +DestDir: "{app}"; Flags: ignoreversion; Source:"FastFetch.exe" + +; GVFS Files +DestDir: "{app}"; Flags: ignoreversion; Source:"CommandLine.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Collections.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Interop.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"Esent.Isam.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.Common.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.GVFlt.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.GvFltWrapper.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"Microsoft.Diagnostics.Tracing.EventSource.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"Newtonsoft.Json.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.exe.config" +DestDir: "{app}"; Flags: ignoreversion; Source:"GitVirtualFileSystem.ico" +DestDir: "{app}"; Flags: ignoreversion; Source:"GVFS.exe" + +[UninstallDelete] +; Deletes the entire installation directory, including files and subdirectories +Type: filesandordirs; Name: "{app}"; + +[Registry] +Root: HKLM; Subkey: "{#EnvironmentKey}"; \ + ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{app}"; \ + Check: NeedsAddPath(ExpandConstant('{app}')) + +[Code] +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + '{#EnvironmentKey}', + 'PATH', OrigPath) + then begin + Result := True; + exit; + end; + // look for the path with leading and trailing semicolon + // Pos() returns 0 if not found + Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; +end; + +procedure RemovePath(Path: string); +var + Paths: string; + PathMatchIndex: Integer; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then + begin + Log('PATH not found'); + end + else + begin + Log(Format('PATH is [%s]', [Paths])); + + PathMatchIndex := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); + if PathMatchIndex = 0 then + begin + Log(Format('Path [%s] not found in PATH', [Path])); + end + else + begin + Delete(Paths, PathMatchIndex - 1, Length(Path) + 1); + Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths])); + + if RegWriteStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then + begin + Log('PATH written'); + end + else + begin + Log('Error writing PATH'); + end; + end; + end; +end; + +procedure InstallGVFlt(); +var + ResultCode: integer; + StatusText: string; + InstallSuccessful: Boolean; +begin + InstallSuccessful := False; + + StatusText := WizardForm.StatusLabel.Caption; + WizardForm.StatusLabel.Caption := 'Installing GVFlt Driver.'; + WizardForm.ProgressGauge.Style := npbstMarquee; + + try + Exec(ExpandConstant('SC.EXE'), 'stop gvflt', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + + // Note: Programatic install of INF notifies user if the driver being upgraded to is older than the existing, otherwise it works silently... doesn't seem like there is a way to block + if Exec(ExpandConstant('RUNDLL32.EXE'), ExpandConstant('SETUPAPI.DLL,InstallHinfSection DefaultInstall 132 {app}\Filter\gvflt.inf'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + InstallSuccessful := True; + end; + finally + WizardForm.StatusLabel.Caption := StatusText; + WizardForm.ProgressGauge.Style := npbstNormal; + Exec(ExpandConstant('SC.EXE'), 'start gvflt', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; + + if InstallSuccessful = False then + begin + RaiseException('Fatal: An error occured while installing GVFlt drivers.'); + end; +end; + +function IsGVFSRunning(): Boolean; +var + ResultCode: integer; +begin + if Exec('powershell.exe', '-NoProfile "Get-Process gvfs,gvfs.mount | foreach {exit 10}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + if ResultCode = 10 then + begin + Result := True; + end; + if ResultCode = 1 then + begin + Result := False; + end; + end; +end; + +function EnsureGvfsNotRunning(): Boolean; +var + MsgBoxResult: Integer; +begin + MsgBoxResult := IDRETRY; + while (IsGVFSRunning()) Do + begin + if(MsgBoxResult = IDRETRY) then + begin + MsgBoxResult := SuppressibleMsgBox('GVFS is currently running. Please close all instances of GVFS before continuing the installation.', mbError, MB_RETRYCANCEL, IDCANCEL); + end; + if(MsgBoxResult = IDCANCEL) then + begin + Result := False; + Abort(); + end; + end; + Result := True; +end; + +// Below are EVENT FUNCTIONS -> The main entry points of InnoSetup into the code region +// Documentation : http://www.jrsoftware.org/ishelp/index.php?topic=scriptevents + +function InitializeUninstall(): Boolean; +begin + Result := EnsureGvfsNotRunning(); +end; + +// Called just after "install" phase, before "post install" +function NeedRestart(): Boolean; +begin + Result := False; +end; + +function UninstallNeedRestart(): Boolean; +begin + Result := False; +end; + +procedure CurUninstallStepChanged(CurStep: TUninstallStep); +begin + case CurStep of + usUninstall: + begin + RemovePath(ExpandConstant('{app}')); + end; + end; +end; + +procedure InitializeWizard; +begin + if not EnsureGvfsNotRunning() then + begin + Abort(); + end; +end; \ No newline at end of file diff --git a/GVFS/GVFS/packages.config b/GVFS/GVFS/packages.config new file mode 100644 index 00000000..dfaee198 --- /dev/null +++ b/GVFS/GVFS/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/GvFlt_EULA.docx b/GvFlt_EULA.docx new file mode 100644 index 00000000..d8133070 Binary files /dev/null and b/GvFlt_EULA.docx differ diff --git a/License.md b/License.md new file mode 100644 index 00000000..7208551d --- /dev/null +++ b/License.md @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/Protocol.md b/Protocol.md new file mode 100644 index 00000000..1fbd380b --- /dev/null +++ b/Protocol.md @@ -0,0 +1,173 @@ +# The GVFS Protocol (v1) + +The GVFS network protocol consists of four operations on three endpoints. In summary: +* `GET /gvfs/objects/{objectId}` + * Gets a single object in loose-object format +* `POST /gvfs/objects` + * Retrieves one or more objects in packfile or streaming loose object format +* `GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]` + * Retrieves one or more packfiles of non-blobs and optionally packfile indexes in a streaming format +* `POST /gvfs/sizes` + * Retreives the uncompressed, undeltified size of one or more objects + +# `GET /gvfs/objects/{objectId}` +Will return a single object in compressed loose object format, which can be directly +written to `.git/xx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` if desired. The request/response looks +similar to the "Dumb Protocol" as described [here](https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols). + +# `POST /gvfs/objects` +Will return multiple objects, possibly more than the client requested based on request parameters. + +The request consists of a JSON body with the following format: +``` +{ + "objectIds" : [ {JSON array of SHA-1 object IDs, as strings} ], + "commitDepth" : {positive integer} +} +``` + +For example, +``` +{ + "objectIds" : [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "commitDepth" : 1 +} +``` + +## `Accept: application/x-git-packfile` (the default) + +If +* An `Accept` header of `application/x-git-packfile` is specified, or +* No `Accept` header is specified + +A git packfile, indexable via `index-pack`, will be returned to the client. + +If `objectIds` includes a `commit`, then all `tree`s recursively referenced by that commit are also returned. +If any other object type is requested (`tree`, `blob`, or `tag`), then only that object will be returned. + +`commitDepth` - if the requested object is a `commit`, all parents up to `n` levels deep will be returned, along +with all their trees as previously described. Does not include any 'blob's. + +## `Accept: application/x-gvfs-loose-objects` + +**NOTE**: This format is currently only supposed by the cache server, not by VSTS. + +To enable scenarios where multiple objects are required, but less overhead would be incurred by using pre-existing +loose objects (e.g. on a caching proxy), an alternative, packfile-like response format that contains loose objects +is also supported. + +To receive objects in this format, the client **MUST** supply an `Accept` header of `application/x-gvfs-loose-objects` +to the `POST /gvfs/objects` endpoint. Otherwise, the response format will be `application/x-git-packfile`. + +This format will **NOT** perform any `commit` to `tree` expansion, and will return an error if a commit depth +greater than `1` is supplied. Said another way, this `Accept`/return type has no concept of "implicitly-requested" +objects. + +### Version 1 +* Integers are signed and little-endian, unless otherwise specified +* Byte offset 0 is the first byte in the file +* Index offset 0 is the first byte in the first element of an array +* `num_objects` represents the variable number of objects in the file/response + +``` +Count Size (bytes) Chunk Description + +HEADER + ------------------------------------------------------------------------------ +1 | 5 | UTF-8 encoded 'GVFS ' | + | 1 | Unsigned byte version number. Currently, 1. | + ------------------------------------------------------------------------------ + +OBJECT CONTENT + ------------------------------------------------------------------------------ +num_objects | 20 | SHA-1 ID of the object. | + | 8 | Signed-long length of the object. | + | variable | Compressed, raw loose object content. | + ------------------------------------------------------------------------------ + +TRAILER + ------------------------------------------------------------------------------ +1 | 20 | Zero bytes | + ------------------------------------------------------------------------------ +``` + +# `GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]` + +To enable the reuse of already-existing packfiles and indexes, a custom format for transmitting these files +is supported. The `prefetch` endpoint will return one or more packfiles of **non-blob** objects. + +If the optional `lastPackTimestamp` query parameter is supplied, only packs created by the server +after the specific Unix epoch time (approximately, ±10 minutes or so) will be returned. Generally, these packs +will contain only objects introduced to the repository after that UTC-based timestamp, but will not contain +**all** objects introduced after that timestamp. + +A media-type of `application/x-gvfs-timestamped-packfiles-indexes` will be returned from this endpoint. + +## Response format + +* Integers are signed and little-endian, unless otherwise specified +* Byte offset 0 is the first byte in the file +* Index offset 0 is the first byte in the first element of an array +* `num_packs` represents the variable number of packs in the file/response + +### Version 1 + +``` +Count Size (bytes) Chunk Description + +HEADER + ------------------------------------------------------------------------------- +1 | 5 | UTF-8 encoded 'GPRE ' | + | 1 | Unsigned byte version number. Currently, 1. | + ------------------------------------------------------------------------------- + +CONTENT + + ------------------------------------------------------------------------------- +1 | 2 | Unsigned short number of packs. `num_packs`. | + ------------------------------------------------------------------------------- + + ------------------------------------------------------------------------------- +num_packs | 8 | Signed-long pack timestamp in seconds since UTC epoch. | + | 8 | Signed-long length of the pack. | + | 8 | Signed-long length of the pack index. -1 indicates no index. | + | variable | Pack contents. | + | variable | Pack index contents. | + ------------------------------------------------------------------------------- +``` + +Packs **MUST** be sent in increasing `timestamp` order. In the case of a failed connection, this allows the +client to keep the packs it received successfully and "resume" by sending the highest completed timestamp. + +# `POST /gvfs/sizes` +Provides the uncompressed, undeltified length of the requested objects in JSON format. + +The request consists of a JSON body with the following format: +``` +[ {JSON array of SHA-1 object IDs, as strings} ] +``` + +For example, a request of +``` +[ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +] +``` + +Will result in a a response like: +``` +[ + { + "Id" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "Size" : 123 + }, + { + "Id" : "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "Size" : 456 + } +] +``` diff --git a/Readme.md b/Readme.md new file mode 100644 index 00000000..6a2f1875 --- /dev/null +++ b/Readme.md @@ -0,0 +1,47 @@ +# GVFS + +## What is GVFS? + +GVFS stands for Git Virtual File System. GVFS virtualizes the file system beneath your git repo so that git and all tools +see a fully hydrated repo, but GVFS only downloads objects as they are needed. GVFS also manages git's sparse-checkout +to ensure that git operations like status, checkout, etc can be as quick as possible. + +GVFS is still in progress, but it is available here for anyone to try out. Feel free to send us feedback, bug reports, suggestions, and pull requests! + +## Building GVFS + +* Install Visual Studio 2015 Community Edition or higher, and include the C++ language (https://www.visualstudio.com/downloads/) +* Install InnoSetup 5.5.9 or later (http://www.jrsoftware.org/isdl.php) to its default location (or you'll have to change the path in GVFS.csproj post-build step to match) +* Create a folder to clone into, e.g. C:\Repos\GVFS +* Clone this repo into the src subfolder, e.g. C:\Repos\GVFS\src +* Open src\GVFS.sln in Visual Studio +* Build GVFS.sln + +## Testing GVFS + +* GVFS requires Windows 10 Anniversary Update or later +* Enable test signed drivers + * IMPORTANT: do not do this on a production machine. This is for evaluation only. + * First, suspend BitLocker, if it is currently enabled + * Go to Control Panel > System and Security > BitLocker Drive Encryption + * For your OS drive, select "Suspend Protection" (This only suspends BitLocker until the next reboot. It does not disable BitLocker protection.) + * In an elevated command prompt, type `bcdedit -set TESTSIGNING ON` + * Reboot to apply the change, and this will also re-enable BitLocker +* Install GVFS-enabled Git for Windows (2.11.0.gvfs.1.3 or later) from https://github.com/Microsoft/git/releases/tag/gvfs.preview + * This build behaves the same as Git for Windows 2.11.0.windows except if the config value core.gvfs is set to true. +* Install GVFS from your build output + * If you built it as described above, the installer can be found at `c:\Repos\GVFS\BuildOutput\GVFS\bin\x64\[Debug|Release]\Setup\SetupGVFS.exe` +* GVFS will work with any git service that supports the GVFS [protocol](Protocol.md). For now, that means you'll need to create a repo in +Team Services (https://www.visualstudio.com/team-services/), and push some contents to it. There are two constraints: + * Your repo must not enable any clean/smudge filters + * Your repo must have a .gitattributes file in the root that includes the line "* -text" +* gvfs clone +* cd into \src +* Run git commands as you normally would +* gvfs unmount when done + +# Licenses + +The GVFS source code in this repo is available under the MIT license. See [License.md](License.md). + +GVFS relies on the GvFlt filter driver, available as a prerelease NuGet package with its own [license](GvFlt_EULA.docx). \ No newline at end of file diff --git a/Scripts/CreateCommonAssemblyVersion.bat b/Scripts/CreateCommonAssemblyVersion.bat new file mode 100644 index 00000000..b13562c7 --- /dev/null +++ b/Scripts/CreateCommonAssemblyVersion.bat @@ -0,0 +1,2 @@ +mkdir %2\BuildOutput +echo using System.Reflection; [assembly: AssemblyVersion("%1")][assembly: AssemblyFileVersion("%1")] > %2\BuildOutput\CommonAssemblyVersion.cs \ No newline at end of file diff --git a/Scripts/CreateCommonCliAssemblyVersion.bat b/Scripts/CreateCommonCliAssemblyVersion.bat new file mode 100644 index 00000000..48a7db4d --- /dev/null +++ b/Scripts/CreateCommonCliAssemblyVersion.bat @@ -0,0 +1,3 @@ +mkdir %2\BuildOutput +echo #include "stdafx.h" > %2\BuildOutput\CommonAssemblyVersion.h +echo using namespace System::Reflection; [assembly:AssemblyVersion("%1")];[assembly:AssemblyFileVersion("%1")]; >> %2\BuildOutput\CommonAssemblyVersion.h \ No newline at end of file diff --git a/Scripts/CreateCommonVersionHeader.bat b/Scripts/CreateCommonVersionHeader.bat new file mode 100644 index 00000000..0621478b --- /dev/null +++ b/Scripts/CreateCommonVersionHeader.bat @@ -0,0 +1,9 @@ +mkdir %2\BuildOutput + +set comma_version_string=%1 +set comma_version_string=%comma_version_string:.=,% + +echo #define GVFS_FILE_VERSION %comma_version_string% > %2\BuildOutput\CommonVersionHeader.h +echo #define GVFS_FILE_VERSION_STRING "%1" >> %2\BuildOutput\CommonVersionHeader.h +echo #define GVFS_PRODUCT_VERSION %comma_version_string% >> %2\BuildOutput\CommonVersionHeader.h +echo #define GVFS_PRODUCT_VERSION_STRING "%1" >> %2\BuildOutput\CommonVersionHeader.h \ No newline at end of file diff --git a/Scripts/RunFunctionalTests.bat b/Scripts/RunFunctionalTests.bat new file mode 100644 index 00000000..ce4b62b3 --- /dev/null +++ b/Scripts/RunFunctionalTests.bat @@ -0,0 +1,4 @@ +@ECHO OFF +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +%~dp0\..\..\BuildOutput\GVFS.FunctionalTests\bin\x64\%Configuration%\GVFS.FunctionalTests.exe %2 \ No newline at end of file diff --git a/Scripts/RunUnitTests.bat b/Scripts/RunUnitTests.bat new file mode 100644 index 00000000..0112e0dd --- /dev/null +++ b/Scripts/RunUnitTests.bat @@ -0,0 +1,4 @@ +@ECHO OFF +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +%~dp0\..\..\BuildOutput\GVFS.UnitTests\bin\x64\%Configuration%\GVFS.UnitTests.exe \ No newline at end of file diff --git a/Settings.StyleCop b/Settings.StyleCop new file mode 100644 index 00000000..adee05bb --- /dev/null +++ b/Settings.StyleCop @@ -0,0 +1,274 @@ + + + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + + + + + + False + + + + + False + + + + + + + + + + False + + + + + False + + + + + + + + + + True + + + + + False + + + + + + + + + + False + + + + + + + \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..e36cca44 --- /dev/null +++ b/nuget.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +