diff --git a/.editorconfig b/.editorconfig index 1252530c4..cf2bb0aab 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,5 @@ root = true [*.cs] indent_style = space indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3d2be8475..c17e1dc1c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,7 +9,8 @@ Have you read GitHub for Unity's Code of Conduct? By filing an Issue, you are ex - Be sure to run with tracing enabled to capture runtime details in the log file - Include the log file in the PR. - On Windows, the extension log file is at `%LOCALAPPDATA%\GitHubUnity\github-unity.log` - - On macOS, the extension log file is at `~/Library/Application Support/GitHubUnity/github-unity.log` + - On macOS, the extension log file is at `~/Library/Logs/GitHubUnity/github-unity.log` + - On linux, the extension log file is at `~/.local/share/GitHubUnity/github-unity.log` ### Description diff --git a/.gitignore b/.gitignore index bcd8c5dcd..7fc38bf13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .vs/ .idea/ +.vscode/ packages/ _NCrunch_GitHub.Unity *.user .DS_Store build/ TestResult.xml -submodules/ \ No newline at end of file +*.stackdump +*.lastcodeanalysissucceeded \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d9e14a3e9..8ecda521f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "script"] path = script url = git@github.com:github-for-unity/UnityBuildScripts +[submodule "submodules/packaging"] + path = submodules/packaging + url = https://github.com/github-for-unity/packaging diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index db12af918..0707a70fa 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -5,8 +5,12 @@ VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Unity", "src\UnityExtension\Assets\Editor\GitHub.Unity\GitHub.Unity.csproj", "{ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Unity.45", "src\UnityExtension\Assets\Editor\GitHub.Unity\GitHub.Unity.45.csproj", "{ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api", "src\GitHub.Api\GitHub.Api.csproj", "{B389ADAF-62CC-486E-85B4-2D8B078DF763}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api.45", "src\GitHub.Api\GitHub.Api.45.csproj", "{B389ADAF-62CC-486E-85B4-2D8B078DF76B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Logging", "src\GitHub.Logging\GitHub.Logging.csproj", "{BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopyLibrariesToDevelopmentFolder", "src\packaging\CopyLibrariesToDevelopmentFolder\CopyLibrariesToDevelopmentFolder.csproj", "{44257C81-EE4A-4817-9AF4-A26C02AA6DD4}" @@ -25,83 +29,148 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtils", "src\tests\Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskSystem", "src\tests\TaskSystemIntegrationTests\TaskSystem.csproj", "{1A382F40-FD9E-43E1-89C1-320073F35CE9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "src\tests\TestApp\TestApp.csproj", "{08B87D2A-8CF1-4211-B7AA-5209F00F72F8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "src\tests\CommandLine\CommandLine.csproj", "{08B87D2A-8CF1-4211-B7AA-5209F00F72F8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebServer", "src\tests\TestWebServer\TestWebServer.csproj", "{3DD3451C-30FA-4294-A3A9-1E080342F867}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityTests", "src\UnityExtension\Assets\Editor\UnityTests\UnityTests.csproj", "{462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtensionLoader", "src\UnityExtension\Assets\Editor\GitHub.Unity\ExtensionLoader\ExtensionLoader.csproj", "{6B0EAB30-511A-44C1-87FE-D9AB7E34D115}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityShim", "src\UnityShim\UnityShim.csproj", "{F94F8AE1-C171-4A83-89E8-6557CA91A188}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugNoUnity|Any CPU = DebugNoUnity|Any CPU dev|Any CPU = dev|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.dev|Any CPU.ActiveCfg = dev|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.dev|Any CPU.Build.0 = dev|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.Build.0 = Release|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.dev|Any CPU.ActiveCfg = dev|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.dev|Any CPU.Build.0 = dev|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Release|Any CPU.Build.0 = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.dev|Any CPU.ActiveCfg = dev|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.dev|Any CPU.Build.0 = dev|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.ActiveCfg = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.Build.0 = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.dev|Any CPU.ActiveCfg = dev|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.dev|Any CPU.Build.0 = dev|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Release|Any CPU.Build.0 = Release|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.dev|Any CPU.ActiveCfg = dev|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.dev|Any CPU.Build.0 = dev|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Release|Any CPU.Build.0 = Release|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.dev|Any CPU.ActiveCfg = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.dev|Any CPU.Build.0 = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Release|Any CPU.Build.0 = Release|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.dev|Any CPU.ActiveCfg = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.dev|Any CPU.Build.0 = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Release|Any CPU.Build.0 = Release|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.dev|Any CPU.ActiveCfg = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.dev|Any CPU.Build.0 = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Release|Any CPU.Build.0 = Release|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DEF4226-7740-457F-9199-34174C49A978}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.dev|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.dev|Any CPU.Build.0 = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Release|Any CPU.Build.0 = Release|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.dev|Any CPU.ActiveCfg = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.dev|Any CPU.Build.0 = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Release|Any CPU.ActiveCfg = Release|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Release|Any CPU.Build.0 = Release|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A382F40-FD9E-43E1-89C1-320073F35CE9}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {1A382F40-FD9E-43E1-89C1-320073F35CE9}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.dev|Any CPU.ActiveCfg = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.dev|Any CPU.Build.0 = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Release|Any CPU.Build.0 = Release|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.dev|Any CPU.ActiveCfg = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.dev|Any CPU.Build.0 = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Release|Any CPU.Build.0 = Release|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DD3451C-30FA-4294-A3A9-1E080342F867}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {3DD3451C-30FA-4294-A3A9-1E080342F867}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.dev|Any CPU.ActiveCfg = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.dev|Any CPU.Build.0 = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.Build.0 = Release|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.ActiveCfg = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.Build.0 = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.Build.0 = Release|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.dev|Any CPU.ActiveCfg = dev|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.dev|Any CPU.Build.0 = dev|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.Build.0 = Release|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.dev|Any CPU.ActiveCfg = dev|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.dev|Any CPU.Build.0 = dev|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -115,5 +184,9 @@ Global {1A382F40-FD9E-43E1-89C1-320073F35CE9} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} {08B87D2A-8CF1-4211-B7AA-5209F00F72F8} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} {3DD3451C-30FA-4294-A3A9-1E080342F867} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66BD4D50-3779-4912-9596-2C838BF24911} EndGlobalSection EndGlobal diff --git a/GitHub.Unity.sln.DotSettings b/GitHub.Unity.sln.DotSettings index 31c5e56f1..2166c735a 100644 --- a/GitHub.Unity.sln.DotSettings +++ b/GitHub.Unity.sln.DotSettings @@ -22,8 +22,11 @@ END_OF_LINE 1 1 + False + False False True + NEVER False True False @@ -339,8 +342,13 @@ SSH <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + True + True + True + True True True + True True True True diff --git a/LICENSE b/LICENSE index 9f06ebe84..a306528ee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-2018 GitHub +Copyright (c) 2016-2019 GitHub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 280831465..1cd67b951 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,36 @@ # [GitHub for Unity](https://unity.github.com) -## Notices - -From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. - -![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true) +## NOTICE OF DEPRECATION -[![Join the chat at https://gitter.im/github-for-unity/Unity](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github-for-unity/Unity?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Join the chat at https://discord.gg/5zH8hVx](https://img.shields.io/badge/discord-join%20chat-7289DA.svg)](https://discord.gg/5zH8hVx) -[![GitHub for Unity live coding on Twitch](https://img.shields.io/badge/twitch-live%20coding-6441A4.svg)](https://www.twitch.tv/sh4na) +This project is dead y'all! Remove GitHub for Unity from your project, then go to https://github.com/spoiledcat/git-for-unity and install Git for Unity from the instructions there. -## About +# What is it The GitHub for Unity extension brings [Git](https://git-scm.com/) and GitHub into [Unity](https://unity3d.com/), integrating source control into your work with friendly and accessible tools and workflows. -**Please note:** this software is currently alpha quality. Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. - -#### Table Of Contents - -[Installing GitHub for Unity](#installing-github-for-unity) - * [Requirements](#requirements) - * [Git on macOS](#git-on-macos) - * [Git on Windows](#git-on-windows) - * [Installation](#installation) - * [Log files](#log-files) - * [Windows](#windows) - * [macOS](#macos) - -[Building and Contributing](#building-and-contributing) - -[Quick Guide to GitHub for Unity](#quick-guide-to-github-for-unity) - * [Opening the GitHub window](#opening-the-github-window) - * [Initialize Repository](#initialize-repository) - * [Authentication](#authentication) - * [Publish a new repository](#publish-a-new-repository) - * [Commiting your work - Changes tab](#commiting-your-work---changes-tab) - * [Pushing/pulling your work - History tab](#pushingpulling-your-work---history-tab) - * [Branches tab](#branches-tab) - * [Settings tab](#settings-tab) - -[More Resources](#more-resources) - -[License](#license) - -## Installing GitHub for Unity - -### Requirements - -- Unity 5.4 or higher - - There's currently an blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. -- Git and Git LFS 2.x - -#### Git on macOS - -The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the Settings tab on the GitHub window. - -The easiest way of installing git and git lfs is to install [Homebrew](https://brew.sh/) and then do `brew install git git-lfs`. +You can reach the team right here by opening a [new issue](https://github.com/github-for-unity/Unity/issues/new). You can also tweet at [@GitHubUnity](https://twitter.com/GitHubUnity) -Make sure a Git user and email address are set in the `~/.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `~/.gitconfig` file and adding the following section, if it doesn't exist yet: +[![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true)](https://ci.appveyor.com/project/github-windows/unity) -``` -[user] - name = Your Name - email = Your Email -``` - -#### Git on Windows - -The GitHub for Unity extension ships with a bundle of Git and Git LFS, to ensure that you have the correct version. These will be installed into `%LOCALAPPDATA%\GitHubUnity` when the extension runs for the first time. - -Make sure a Git user and email address are set in the `%HOME%\.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `%HOME%\.gitconfig` file and adding the following section, if it doesn't exist yet: - -``` -[user] - name = Your Name - email = Your Email -``` - -Once the extension is installed, you can open a command line with the same Git and Git LFS version that the extension uses by going to `Window` -> `GitHub Command Line` in Unity. - -### Installation - -This extensions needs to be installed (and updated) for each Unity project that you want to version control. -First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); -it will be saved as a file with the extension `.unitypackage`. -To install it, open Unity, then open the project you want to version control, and then double click on the downloaded package. -Alternatively, import the package by clicking Assets, Import Package, Custom Package, then select the downloaded package. - -#### Log files - -##### macOS - -he extension log file can be found at `~/Library/Application Support/GitHubUnity/github-unity.log` +## Notices -##### Windows +Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. -The extension log file can be found at `%LOCALAPPDATA%\GitHubUnity\github-unity.log` +From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. ## Building and Contributing -The [CONTRIBUTING.md](CONTRIBUTING.md) document will help you get setup and familiar with the source. The [documentation](docs/) folder also contains more resources relevant to the project. - Please read the [How to Build](docs/contributing/how-to-build.md) document for information on how to build GitHub for Unity. -If you're looking for something to work on, check out the [up-for-grabs](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) label. - - -## I have a problem with GitHub for Unity - -First, please search the [open issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen) -and [closed issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aclosed) -to see if your issue hasn't already been reported (it may also be fixed). - -If you can't find an issue that matches what you're seeing, open a [new issue](https://github.com/github-for-unity/Unity/issues/new) -and fill out the template to provide us with enough information to investigate -further. - -## Quick Guide to GitHub for Unity - -### Opening the GitHub window - -You can access the GitHub window by going to Windows -> GitHub. The window opens by default next to the Inspector window. - -### Initialize Repository - -![Initialize repository screenshot](https://user-images.githubusercontent.com/10103121/37807041-bb4446a6-2e19-11e8-9fff-a431309b8515.png) - -If the current Unity project is not in a Git repository, the GitHub for Unity extension will offer to initialize the repository for you. This will: - -- Initialize a git repository at the Unity project root via `git init` -- Initialize git-lfs via `git lfs install` -- Set up a `.gitignore` file at the Unity project root. -- Set up a `.gitattributes` file at the Unity project root with a large list of known binary filetypes (images, audio, etc) that should be tracked by LFS -- Configure the project to serialize meta files as text -- Create an initial commit with the `.gitignore` and `.gitattributes` file. - -### Authentication - -To set up credentials in Git so you can push and pull, you can sign in to GitHub by going to `Window` -> `GitHub` -> `Account` -> `Sign in`. You only have to sign in successfully once, your credentials will remain on the system for all Git operations in Unity and outside of it. If you've already signed in once but the Account dropdown still says `Sign in`, ignore it, it's a bug. - -![Authentication screenshot](https://user-images.githubusercontent.com/121322/27644895-8f22f904-5bd9-11e7-8a93-e6bfe0c24a74.png) - -### Publish a new repository - -1. Go to [github.com](https://github.com) and create a new empty repository - do not add a license, readme or other files during the creation process. -2. Copy the **https** URL shown in the creation page -3. In Unity, go to `Windows` -> `GitHub` -> `Settings` and paste the url into the `Remote` textbox. -3. Click `Save repository`. -4. Go to the `History` tab and click `Push`. - -### Commiting your work - Changes tab - -You can see which files have been changed and commit them through the Changes tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` - -![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) - -### Pushing/pulling your work - History tab - -The history tab includes a `Push` button to push your work to the server. Make sure you have a remote url configured in the `Settings` tab so that you can push and pull your work. - -To receive updates from the server by clicking on the `Pull` button. You cannot pull if you have local changes, so commit your changes before pulling. - -![History tab screenshot](https://user-images.githubusercontent.com/121322/27644965-c1109bba-5bd9-11e7-9257-4fa38f5c67d1.png) - -### Branches tab - -![Branches tab screenshot](https://user-images.githubusercontent.com/121322/27644978-cd3c5622-5bd9-11e7-9dcb-6ae5d5c7dc8a.png) - -### Settings tab - -You can configure your user data in the Settings tab, along with the path to the Git installation. +The [CONTRIBUTING.md](CONTRIBUTING.md) document will help you get setup and familiar with the source. The [documentation](docs/) folder also contains more resources relevant to the project. -Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. +If you're looking for something to work on, check out the [up-for-grabs](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) label. -![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) +## How to use -## More Resources +The [quick guide to GitHub for Unity](docs/using/quick-guide.md) -See [unity.github.com](https://unity.github.com) for more product-oriented -information about GitHub for Unity. +More [in-depth information](docs/readme.md) ## License @@ -188,6 +40,6 @@ The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized Invertocat designs that include "logo" in the file title in the following -folder: [IconsAndLogos](https://github.com/github-for-unity/Unity/tree/master/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos). +folder: [IconsAndLogos](src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos). Copyright 2015 - 2018 GitHub, Inc. diff --git a/appveyor.yml b/appveyor.yml index a5d0c4580..bfb3e1b77 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,35 +1,28 @@ -version: '{build}.{branch}' environment: GHFU_KEY: secure: KFcQA1VOCEMGUgy2dxH8G5O7C9DsAtQrnc6LakFpd9BRFtNnt2E8RSadPoJwQ9gztWaLS8vQLdU7cV5Ivt01LOnPI2kU1fQd2SHtKwJFve8ppvK/yZ/luhiXvIdGeEAiXQyuc1WUwuECoJVA6n7/uQKr1Q+eHniMitHuFpyQ7OqnwF6f+4TeBS6D78fKd3QoeP4XDdxCjNWPNmLv7BvWFMm5CuTK9aAWhOy8em/nVIED8qt36uHnncsDn+DH7uunj+VwmhVS+4yhuKHHz5naiUAHHIziZ4wBW6Q8rcf7xYEeISjlfxJ6TXs4Wwp406AO+n3v1DZaxSXwvoxplfopeGyb6imJfbwdTU/MHf2uj9wXobR8UhDarcrugVW+J3bqZyvkg20HfSe80gfQUBlK5OdMAp58dhWkddvJSO5TnzqmLlo/60gZxjheIbjdLaavSKcM4xOALXQlBbJJgVQrB/F6tYf7pRK3BlS8VoakyOGjJRzsSNdssSVrLVW3rwANORbH6Z1ZYvQw2ObP6/EBMceer0+JV4y9zB5q9C68erlr1NtJB0xKUp9/7I5GQj4lJ+pDsaFdsj40SyyD4yazSZf/3VIhZi/rQTJm0Ft0ifTZGSxnNTOVMZ5fsoJmUL0bn75Xt9q59cKYzK041HtEzRSElnBeuTf+Sm/MLLV28P1sonwntMhcYQ5ZPIuGmKa8jAJ0kxPXyT56MPJpwbNrbCOw2t9hXg5QbYv/+0RcoRoJ7P/5OY3M6mj8Emtu8N0SFKD6lfv9KNLFAyBuF6Ml7RyOs5RRogIdEapegTY7jwGH7igibrP0lt6HYshM7hKQrRYm2saokBV9TfgV9qnPyGs5/zyTTUGW4y0LavxiXl/vQIpxwCg4liCf1Dgw4Zrxvh40bziKGc3X06RJcysJ8cskOi6gB8eK8VTbAy6/Ufgv+pyjIns1iJxmdVbl8MrlgPXmOephdRPYZJDiFd4ynw0Slm1mqbzPWHdQ/mtMGxNRcysIOPKzKaKvK7Syor5SNtv4HU097dyjVQyW9krHaX/DSnx++dMDCIZJEDYxFw7LmTvl6AFWU4C3HM7+26cHBuBMBYS1PwcRijG+hwsHiXIomVuVglcxp5HC2eFbtBF9h1g030/tCeIBhZIVdSVO78319CsL1+aVtI5WjeQglH1OcTS42OF1Nb/4EjaN4I/w6yRJU5dmK7Q+rHQ+7NnPT4n5flQV6oe1XNbLen0raDuGos6v+aaoOQ50HlCSMMBJ3liapVXIAQ+Z/XM/cNZZa1TB6/C363Hjrts6Uq3IjKXmomhA33je+Wl6mTZqBucXUJs76p+ZgKubWvfzK/e6tORJggAgFNoa9Y56r3t7J8UdUolt301I1cCVz9CvYMUsWTmjKTR5SpssbbcuRcFUhgkHrhsSq0rBef/dfn5VEP8sateiqbpMne5iWO4Wc0Qx2/fJfx2zf9oqO3MihAyYDwPI7JCrmccJY9cHV89YlrytZuh87aBQ8d+T5ELdeG3bbYGYbKJ/yOGxMo8cRSomWUp2809Ea/lgu6WjyY1SdEjh20fwONOTRd/AA3h3XIMU7NY999YOEy1zj3yeP+awozDWQU8GSojA/ceLU5HT0U/RT2XJgkzUFb/u8wanNG6InPqvZy01bLR8JZmqXZl/4XgNpEsbzwPxjzuZJP6O4f2Usq1ZmHUfwlouXLWuHTv5/DPYJ9kO91wjzAH46IsoadmQkRDomHYCDPRCnYoS2zBkmBNukCgCKWsSwD5Msw/tpgIorMi5AAFhIOeWt/7tcKZ5nslbbnmZFtDkOBriPEiOjrAziRqFNAdBjecMrckRQrlkYjkpJG/pXZXYq219/8Sy1/HdNrWTCdq7nc947Fvq41CfumT4c2TqhVc/oflJ9SaxIl2A1Vtbw5LkmQKL08vOitsyZgRupcbqLcSYGo2dG+ks0gK5o2rvNp1nyN3ADh7JFmtD7/og55zlsAj7wP6rLEZ44h+dk7+Sh96WoCPfzSJnchg2vsydTpK6sG3Cp5qjEk0Tps88nX0SPvEPxwEpBL08+XIlg5OVxiTIYI0NSeEjAxwip2ptgaeI4c0yPB5SahMArEw/8YeEflWhiDyjoG4Bw0O1v9fRSSYiFhcYwrkr6yBK81hx6uH6DzqDqtKOxwJ3kKhapfwZXStmeOt4AwiSUHO8TiX1t0i7Jqwl7mRduz3LfmqGCeEsNxnLuhc6MPeEva8LO8ILCEcVz8bHlwUMWqabdZRm9UtbWtZp/u8ffPSBbgNFna2kKFr/F7dmXiv18CpNHOGxb/rSdmIaov1nXJR7XUyKPRO548PHE6iNxuNjWjcuFw1L0IUCNEeVUs7tvNHUTYOXRvfXNm3DbhjFnGix2JVCB2xz6QhDV4Hh6y0/rJl0b2dW25iM5HZdwCBGwgGM+9HyD7r+OBiRn+rd996c81+JsWL4jsa//16uwcbEpsF3tAB7b0by4qHbeZ+Gs3M06Sje4UVpLgKQVHSd/hfo4M70v3APhyz0WFBhLLZyouz0OdazKZ4W+HGBcunAPw/sYdMYZLe4ZmA6B+wxtSzojNKFaCFWoh3S5vLClZTraj7Mhh02PPsY0fmo15ceHBwKjMfGZ0pXt8uiPL29ECUstxSLVnPv6M4uXPJa7k+0lvj7XdB7aJ/LzexPAa/Z1+hsr2sO9An5qPnKM5Tp5zj9Xq2T7WBiDObYLxYZX5ez32jKfSYgv3cpIo5HnhKB3rZL3Alp6iJ2NFsDiB6pIUc2YQ3UU8wiMU90ifA83ORttzRDdLCuH1lYCHPk8rcVqeydgNrI4pRVrdIah3wm6hHc7YjSSnjIOhcl286iVtYgn10RUKxcs//ElgoGm0IkefKRy2WcDDL+10ZifpSWxRu0yrpwlxd0uHCAhrkOEnvaamn+0TSu/6s9VxoUyn9ZJhY7Jgnb6Z9Qxi4C+u2vXf6lOQvzl4AawnD9DW+w2L6hr2njGhvgjj2VLIHM/GIOV/OaYW97AiW0NBuEGDyBiuj8TxIUL7IuVj+QZVfyUzZHHL0c0Hy4jlQ+sh2nFzOAGWVZwEdAvLl9JCCs46iA9DHtBSrHxit7lytyspp7q8TYfE1lA0pIwkx20E3t+4CNdUQAr/IJaZJxhdfKAyW3UipP4LdRbweyYHZYFkoN0gEDMrzE0yB7XFNw5ddm/+o8KIuSUl44UVFcp2j0KPfuXadx7Pz1aa5HKpVUdc5CfJOjqgPJFn/MQU702YdUaV0qD+EHDOiVv313gUHdy9kpieQ3s2LDSh0qBkPdxLAdYXKLP24Mj3V+A2lyHU1WtLrIEVP37eCAFSYPf6Lz6TW4zrEBpHF4nwlE8M+0jQ/oB4lINxnkCa3YKYLFMiZ3dAmqGzVElesgymmB21xvdfrHgB1Z5OtQqYT8PPAw6llujXv6Pj9CqDGGS4U8UeW5GCFi/qyV6+hdg2IUsWtSzkbLJ5n8cfafEYeRBRgzK/B6qlTmoOrRl+bzmjVCJX29P+38KCpu7srnSQ+T0fR6t0OWyHGfC/39iMzATnhpiIXdnngVV9Cypgod5we44C2Rb4Or/nr5mdEidElIIthDiD7GHPNSeMXrdxs+ow76rh42DiY7x0L0SMRWyUEz0seL1JdBCdNn/7LuSn4CVpggqZD8anf9n+IUjrJtqQ+AvaogfuxM65byhGK4iVIijrogfBHb4nGywXxeEKe03JJ8nOWWN2ndyNhMW1dfNGraHvAt7DWL+/tp4qKCA89VFaZjwsqINANF1VVwh96SB6qT4tlKJjaPD3YpawT6Jfs+cg3pMj36FIPzHoNd/r+LwCBZ0WiA5xZiO0DX6WhwTfJVStsz4i9VXElCmWF2dpf5kTEC0T62Y1VCc++M1cTfwX34mdHPvdsm1Vi1qpqz4HTez8ateFukyj1FIN7++eYWoBJBoclhb3y/VUFwepORi84pz1fXUSSl8Fpg2U7NRyj+gcM5v/VAC1FGR4CJVpODIdROF7mCrLTbPzLn8Fv7EJHgHKNeU/sIT13+5V/UJSZPAxWcaUKhRWWuShSVb/1U13LjiWkHvmuH7SVLHbJDO5C5lA589rz4weTMd1OSymPuNB/xj2d2YrJUwqB3olsaxwm8w/bs2ot4GF4HFAdx3l0ESiR8jkBNAvr6vwRcXv+7nfXRpx2Mo5QU2YaunbqZxibmtNCQZBH8ZpQyUZOek4A5qDh6HW2VyJqKXeE8u1fbtOzB9xDYxgTrlVFhCw== -clone_script: -- ps: >- - if(-not $env:appveyor_pull_request_number) { - git lfs clone -q -n --branch=$env:appveyor_repo_branch https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder - git checkout -qf $env:appveyor_repo_commit - } else { - git lfs clone -q -n https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder - git fetch -q origin +refs/pull/$env:appveyor_pull_request_number/merge: - git lfs fetch origin FETCH_HEAD - git checkout -qf FETCH_HEAD - } - - Set-Location $env:appveyor_build_folder + matrix: + - node_version: '8' install: - ps: >- + $full_build = Test-Path env:GHFU_KEY + + $package = $full_build + git submodule sync git submodule init - $full_build = Test-Path env:GHFU_KEY - if ($full_build) { + $env:BUILD_TYPE="full" $fileContent = "-----BEGIN RSA PRIVATE KEY-----`n" $fileContent += $env:GHFU_KEY.Replace(' ', "`n") $fileContent += "`n-----END RSA PRIVATE KEY-----`n" Set-Content c:\users\appveyor\.ssh\id_rsa $fileContent + Install-Product node $env:node_version } else { + $env:BUILD_TYPE="partial" git submodule deinit script $destdir = Join-Path $env:appveyor_build_folder 'lib' $destfile = Join-Path $destdir 'deps.zip' @@ -43,6 +36,24 @@ install: nuget restore GitHub.Unity.sln + Set-Location $env:appveyor_build_folder + + $version = Get-Content "$($env:appveyor_build_folder)\common\SolutionInfo.cs" | %{ $regex = "const string GitHubForUnityVersion = `"([^`"]*)`""; if ($_ -match $regex) { $matches[1] } } + + $env:package_version="$($version).$($env:APPVEYOR_BUILD_NUMBER)" + + Update-AppveyorBuild -Version $env:package_version + + $message = "Building " + + if ($package) { $message += "and packaging "} + + if ($full_build) { $message += "(full build)" } else { $message += "(partial build)" } + + $message += " version " + $env:package_version + " " + + Write-Host $message + assembly_info: patch: false file: common\SolutionInfo.cs @@ -53,13 +64,44 @@ build: project: GitHub.Unity.sln verbosity: minimal test: + assemblies: + except: + - '**\*.TestRunner.dll' + - '**\*.TestUtils.dll' categories: except: - DoNotRunOnAppVeyor -artifacts: -- path: unity\PackageProject - type: zip - name: github-for-unity-packageproject -- path: build\*.log -on_failure: - - ps: Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } +on_success: +- ps: | + if ($package) { + $rootdir=$env:appveyor_build_folder + Set-Location $rootdir + $sourcedir="$rootdir\unity\PackageProject" + $packagename="github-for-unity-$($env:package_version)" + $packagefile="$rootdir\$($packagename).unitypackage" + $commitfile="$sourcedir\commit" + $zipfile="$rootdir\PackageProject-$($env:package_version).zip" + + # generate mdb files + Write-Output "Generating mdb files" + Get-ChildItem -Recurse "$($sourcedir)\*.pdb" | foreach { $_.fullname.substring(0, $_.fullname.length - $_.extension.length) } | foreach { Write-Output "Generating $($_).mdb"; & 'lib\pdb2mdb.exe' "$($_).dll" } + + # generate unitypackage + Write-Output "Generating $packagefile" + submodules\packaging\unitypackage\run.ps1 -PathToPackage:$sourcedir -OutputFolder:$rootdir -PackageName:$packagename + + # save commit + Add-Content $commitfile $appveyor_repo_commit + + Write-Output "Zipping $sourcedir to $zipfile" + 7z a $zipfile $sourcedir + + Write-Output "Uploading $zipfile" + Push-AppveyorArtifact $zipfile -DeploymentName source + Push-AppveyorArtifact $packagefile -DeploymentName package + Push-AppveyorArtifact "$($packagefile).md5" -DeploymentName package + } +on_finish: +- ps: | + Set-Location $env:appveyor_build_folder + Get-ChildItem $env:appveyor_build_folder\build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name -DeploymentName logs } diff --git a/build.sh b/build.sh index 2ebedc2c0..900c994f7 100755 --- a/build.sh +++ b/build.sh @@ -29,7 +29,7 @@ fi if [ x"$OS" == x"Windows" ]; then common/nuget restore GitHub.Unity.sln else - nuget restore GitHub.Unity.sln + mono common/nuget.exe restore GitHub.Unity.sln fi xbuild GitHub.Unity.sln /verbosity:minimal /property:Configuration=$Configuration /target:$Target || true diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 93fe06d45..eb95db6a0 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -6,12 +6,12 @@ using System.Runtime.InteropServices; [assembly: AssemblyProduct("GitHub for Unity")] -[assembly: AssemblyVersion(System.AssemblyVersionInformation.Version)] -[assembly: AssemblyFileVersion(System.AssemblyVersionInformation.Version)] +[assembly: AssemblyVersion(System.AssemblyVersionInformation.VersionForAssembly)] +[assembly: AssemblyFileVersion(System.AssemblyVersionInformation.VersionForAssembly)] [assembly: AssemblyInformationalVersion(System.AssemblyVersionInformation.Version)] [assembly: ComVisible(false)] [assembly: AssemblyCompany("GitHub, Inc.")] -[assembly: AssemblyCopyright("Copyright GitHub, Inc. 2017-2018")] +[assembly: AssemblyCopyright("Copyright GitHub, Inc. 2016-2019")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -31,6 +31,10 @@ namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "0.31.6"; + private const string GitHubForUnityVersion = "1.4.0"; + internal const string VersionForAssembly = GitHubForUnityVersion; + + // If this is an alpha, beta or other pre-release, mark it as such as shown below + internal const string Version = GitHubForUnityVersion; // GitHubForUnityVersion + "-beta1" } } diff --git a/common/build.targets b/common/build.targets index 450909d08..163a071a8 100644 --- a/common/build.targets +++ b/common/build.targets @@ -16,7 +16,11 @@ - Location of Unity dlls is not set. You'll need to install Unity in a known location (the default installation directory for your system), or copy UnityEngine.dll and UnityEditor.dll to the {0}lib folder + The location of Unity dlls is not set. You'll need to install Unity in a known location (the default installation directory for your system), or: + - Copy UnityEngine.dll and UnityEditor.dll to the {0}lib\Managed folder + - Copy UnityEngine.TestRunner.dll to the {0}lib\UnityExtensions/Unity/TestRunner folder + - Copy UnityEditor.TestRunner.dll to the {0}lib\UnityExtensions/Unity/TestRunner/Editor folder + To build the OctokitDebugging solution you need to have https://github.com/github-for-unity/octokit.net checked out in {0} To build the OctokitDebugging solution you need to have https://github.com/github-for-unity/dotnet-httpclient35 checked out in {0} diff --git a/common/packaging.targets b/common/packaging.targets index 77f866e20..a5ddd41a5 100644 --- a/common/packaging.targets +++ b/common/packaging.targets @@ -7,8 +7,9 @@ @@ -22,16 +23,16 @@ + Condition="!$([System.String]::Copy('%(Filename)').Contains('deleteme')) and !$([System.String]::Copy('%(Extension)').Contains('xml'))" /> - + - + diff --git a/common/properties.props b/common/properties.props index f8b204df8..8316329ed 100644 --- a/common/properties.props +++ b/common/properties.props @@ -3,13 +3,14 @@ Internal - ENABLE_METRICS + ENABLE_METRICS + $(BuildDefs);ENABLE_MONO - $(SolutionDir)\script\lib\ - $(SolutionDir)\lib\ - C:\Program Files\Unity\Editor\Data\Managed\ + $(SolutionDir)script\lib\ + $(SolutionDir)lib\ + C:\Program Files\Unity\Editor\Data\ C:\Program Files (x86)\Unity\Editor\Data\Managed\ - \Applications\Unity\Unity.app\Contents\Managed\ + \Applications\Unity\Unity.app\Contents\ Debug $(Configuration) diff --git a/create-octorun-zip.sh b/create-octorun-zip.sh new file mode 100755 index 000000000..4eb568d33 --- /dev/null +++ b/create-octorun-zip.sh @@ -0,0 +1,3 @@ +#!/bin/sh -eu +DIR=$(pwd) +submodules/packaging/octorun/run.sh --path $DIR/octorun --out $DIR/src/GitHub.Api/Resources --source $DIR/src/GitHub.Api/Installer diff --git a/create-unitypackage.sh b/create-unitypackage.sh new file mode 100755 index 000000000..333f7cb90 --- /dev/null +++ b/create-unitypackage.sh @@ -0,0 +1,9 @@ +#!/bin/sh -eu +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +version=$(sed -En 's,.*GitHubForUnityVersion = "(.*)".*,\1,p' common/SolutionInfo.cs) +commitcount=$(git rev-list --count HEAD) +commit=$(git log -n1 --pretty=format:%h) +version="${version}.${commitcount}-${commit}" + +$DIR/submodules/packaging/unitypackage/run.sh --path $DIR/unity/PackageProject --out $DIR --file github-for-unity-$version diff --git a/CREDITS.txt b/credits.txt similarity index 88% rename from CREDITS.txt rename to credits.txt index e7137d888..b8fb5d7e1 100644 --- a/CREDITS.txt +++ b/credits.txt @@ -1,31 +1,5 @@ =============================================================================== -Port of Mono's System.Net.Http (https://github.com/shana/dotnet-httpclient35) -=============================================================================== - -Copyright (c) 2001, 2002, 2003 Ximian, Inc and the individuals listed -on the ChangeLog entries. - -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. - -=============================================================================== -Port of Axosoft's NSFW (https://github.com/StanleyGoldman/sfw) +Port of Axosoft's NSFW (https://github.com/github-for-unity/sfw) =============================================================================== The MIT License (MIT) @@ -50,29 +24,6 @@ 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. -=============================================================================== -Octokit.NET (https://github.com/editor-tools/octokit.net/tree/net3.5) -=============================================================================== - -Copyright (c) 2012 GitHub, Inc. - -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. - =============================================================================== AsyncBridge (https://github.com/OmerMor/AsyncBridge) =============================================================================== diff --git a/docs/contributing/how-to-build.md b/docs/contributing/how-to-build.md index ed32b7e5d..13ef8c2fa 100644 --- a/docs/contributing/how-to-build.md +++ b/docs/contributing/how-to-build.md @@ -13,7 +13,7 @@ This repository is LFS-enabled. To clone it, you should use a git client that su ### MacOS -- Mono 4.x required. +- [Mono 4.x](https://download.mono-project.com/archive/4.8.1/macos-10-universal/) required. You can install it via brew with `brew tap shana/mono && brew install mono@4.8` - Mono 5.x will not work - `UnityEngine.dll` and `UnityEditor.dll`. - If you've installed Unity in the default location of `/Applications/Unity`, the build will be able to reference these DLLs automatically. Otherwise, you'll need to copy these DLLs from `[Unity installation path]/Unity.app/Contents/Managed` into the `lib` directory in order for the build to work @@ -35,12 +35,25 @@ git submodule deinit script ### Important pre-build steps -To be able to authenticate in GitHub for Unity, you'll need to: +The build needs to reference `UnityEngine.dll` and `UnityEditor.dll`. These DLLs are included with Unity. If you've installed Unity in the default location, the build will be able to find them automatically. If not, copy these DLLs from `[your Unity installation path]\Unity\Editor\Data\Managed` into the `lib` directory in order for the build to work. + +#### Developer OAuth app + +Because GitHub for Unity uses OAuth web application flow to interact with the GitHub API and perform actions on behalf of a user, it needs to be bundled with a Client ID and Secret. + +For external contributors, we have bundled a developer OAuth application in the source so that you can complete the sign in flow locally without needing to configure your own application. + +These are listed in `src/GitHub.Api/Application/ApplicationInfo.cs` + +DO NOT TRUST THIS CLIENT ID AND SECRET! THIS IS ONLY FOR TESTING PURPOSES!! + +The limitation with this developer application is that this will not work with GitHub Enterprise. You will see sign-in will fail on the OAuth callback due to the credentials not being present there. + +To provide your own Client ID and Client Secret: - [Register a new developer application](https://github.com/settings/developers) in your profile. - Copy [common/ApplicationInfo_Local.cs-example](../../common/ApplicationInfo_Local.cs-example) to `common/ApplicationInfo_Local.cs` and fill out the clientId/clientSecret fields for your application. -The build needs to reference `UnityEngine.dll` and `UnityEditor.dll`. These DLLs are included with Unity. If you've installed Unity in the default location, the build will be able to find them automatically. If not, copy these DLLs from `[your Unity installation path]\Unity\Editor\Data\Managed` into the `lib` directory in order for the build to work. ### Visual Studio @@ -56,21 +69,14 @@ Once you've built the solution for the first time, you can open `src/UnityExtens The build also creates a Unity test project called `GitHubExtension` inside a directory called `github-unity-test` next to your local clone. For instance, if the repository is located at `c:\Projects\Unity` the test project will be at `c:\Projects\github-unity-test\GitHubExtension`. You can use this project to test binary builds of the extension in a clean environment (all needed DLLs will be copied to it every time you build). -Note: some files might be locked by Unity if have one of the build output projects open when you compile from VS or the command line. This is expected and shouldn't cause issues with your builds. +Note: some files might be locked by Unity if have one of the build output projects open when you compile from VS or the command line. This is expected and shouldn't cause issues with your builds. ## Solution organization The `GitHub.Unity.sln` solution includes several projects: -- dotnet-httpclient35 and octokit: external dependencies for threading and github api support, respectively. These are the submodules. - packaging: empty projects with build rules that copy DLLs to various locations for testing - Tests: unit and integration test projects - GitHub.Logging: A logging helper library - GitHub.Api: The core of the extension. This project is C#6 and includes async/await threading and other features that Unity cannot currently compile. - GitHub.Unity: Unity-specific code. This project is compilable by Unity - -## Other solutions - -Octokit is normally included as a prebuilt DLL (in the `lib` directory), but when updating Octokit or debugging issues with the 3.5 backport, it can sometimes be handy to reference the project directly. The `GitHub.Unity.OctokitDebugging.sln` solution is a helper solution that includes Octokit and dotnet-httpclient35 as referenced projects instead of prebuilt dlls. - -This solution requires a clone of [https://github.com/github-for-unity/octokit.net](https://github.com/github-for-unity/octokit.net) and [https://github.com/github-for-unity/dotnet-httpclient35](https://github.com/github-for-unity/dotnet-httpclient35) to exist next to your GitHub for Unity checkout. diff --git a/docs/readme.md b/docs/readme.md index a95adb7e9..d73e8e840 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -28,7 +28,10 @@ Details about how the team is organizing and shipping GitHub for Unity: - **[Roadmap](process/roadmap.md)** - how we plan for the future - **[Release](process/release-process.md)** - how we review contributions -## Technical +## Using -These documents contain more details about the internals of GitHub for Unity -and how things work: +[Quick Guide](using/quick-guide.md) + +These documents contain more details on how to use the GitHub for Unity plugin: +- **[Installing and Updating the GitHub for Unity package](using/how-to-install-and-update.md)** +- **[Getting Started with the GitHub for Unity package](using/getting-started.md)** diff --git a/docs/using/authenticating-to-github.md b/docs/using/authenticating-to-github.md new file mode 100644 index 000000000..203a3473d --- /dev/null +++ b/docs/using/authenticating-to-github.md @@ -0,0 +1,37 @@ +# Authenticating to GitHub + +## How to sign in to GitHub + +1. Open the **GitHub** window by going to the top level **Window** menu and selecting **GitHub**, as shown below. + + GitHub menu item in the Window menu + +1. Click the **Sign in** button at the top right of the window. + + GitHub menu item in the Window menu + +1. In the **Authenticate** dialog, enter your username or email and password + + GitHub menu item in the Window menu + + If your account requires Two Factor Authentication, you will be prompted for your auth code. + + GitHub menu item in the Window menu + +You will need to create a GitHub account before you can sign in, if you don't have one already. + +- For more information on creating a GitHub account, see "[Signing up for a new GitHub account](https://help.github.com/articles/signing-up-for-a-new-github-account/)". + +### Personal access tokens + +If the sign in operation above fails, you can manually create a personal access token and use it as your password. + +The scopes for the personal access token are: `user`, `repo`. +- *user* scope: Grants access to the user profile data. We currently use this to display your avatar and check whether your plans lets you publish private repositories. +- *repo* scope: Grants read/write access to code, commit statuses, invitations, collaborators, adding team memberships, and deployment statuses for public and private repositories and organizations. This is needed for all git network operations (push, pull, fetch), and for getting information about the repository you're currently working on. + +***Note:*** *Some older versions of the plugin ask for `gist` and `write:public_key`.* + +For more information on creating personal access tokens, see "[Creating a personal access token for the command line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). + +For more information on authenticating with SAML single sign-on, see "[About authentication with SAML single sign-on](https://help.github.com/articles/about-authentication-with-saml-single-sign-on)." diff --git a/docs/using/getting-started.md b/docs/using/getting-started.md new file mode 100644 index 000000000..4c5892a96 --- /dev/null +++ b/docs/using/getting-started.md @@ -0,0 +1,28 @@ +# Getting Started with GitHub for Unity +There are often three states that you might be in when you are setting up your GitHub for Unity package with your Unity project: +- [Setting up a brand new repository](#setting-up-a-new-repository): You do not currently have your Unity project in a repository and you want to publish it to one. +- [Connecting to an existing respository](#connecting-to-an-existing-repository): Your Unity project is already in a repository, but you do not have the GitHub for Unity package installed within it yet. +- [Opening a Unity project that already has the GitHub for Unity package](#connecting-to-an-existing-repository-that-already-has-the-github-for-unity-package): Your Unity project is already in a repository and it already has the GitHub for Unity package and you are trying to setup a new machine. + +# Setting up a New Repository +If you have a Unity project (new or existing) that is not yet connected to any remote repository, you can use the GitHub for Unity package to quickly intialize the repository and publish to a repository. + +1. If your Unity project doesn't yet have the GitHub plugin installed, follow [these](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md) instructions for installing it. +2. Click on the `Initialize a git repository for this project` button +screen shot 2018-05-18 at 9 39 13 am +And you should see the GitHub spinner: +screen shot 2018-05-18 at 9 39 23 am +3. The GitHub tab should reload with the following buttons: + - Publish: Publish this repository to GitHub (Creating a new GitHub repository) + - Account: Login to your GitHub account | Logout from your GitHub account or visit your profile on GitHub.com + - Changes: Local changes that are not yet committed, with a dialog to commit those changes + - History: A history of commits with title, time stamp, and commit author + - Branches: A list of local and remote branches with the ability to create new branches, switch branches, or checkout remote branches + - Settings: your git configuration (pulled from your local git credentials if they have been previously set), your repository configuration (you can manually put the URL to any remote repository here instead of using the Publish button to publish to GitHub), a list of locked files, your git installation details, and general settings to help us better help you if you get stuck + +# Cloning an Existing Repository +GitHub for Unity does not have the functionality to clone projects (yet!). +1. Clone the repository (either through command line or with GitHub Desktop https://desktop.github.com/). +2. Open the project in Unity. +3. Install GitHub for Unity if it is not already installed. +4. The GitHub plugin should load with all functionality enabled. \ No newline at end of file diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md new file mode 100644 index 000000000..fb9dd93d8 --- /dev/null +++ b/docs/using/how-to-install-and-update.md @@ -0,0 +1,51 @@ +# Installation and Updating Guide +[Installing from a Downloaded Package File](#installing-from-a-package-file) +[Installing from the Unity Asset Store](#installing-from-the-asset-store) +[Updating a Previously Installed Version](#updating-the-github-for-unity-package) + +_Note: If your Unity project already has the GitHub for Unity plugin installed, see our [Getting Started](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md) docs before continuing_ + + +# Installing from a Package File +You can download the latest release from the repository at [https://github.com/github-for-unity/Unity/releases](https://github.com/github-for-unity/Unity/releases) or from the website at [https://unity.github.com/](https://unity.github.com/). + +Once you've downloaded the package file, you can quickly install it within Unity. +1. Open a new Unity Project +screen shot 2018-05-18 at 7 10 07 am +2. Go to `Assets -> Import Package -> Custom Package` +screen shot 2018-05-18 at 7 11 35 am +3. Find and open the GitHub for Unity Asset Package +screen shot 2018-05-18 at 7 12 33 am +4. Click `Import` when the dialog pops up +screen shot 2018-05-18 at 7 12 42 am +5. After the package finishes importing, go to `Window -> GitHub` to open the GitHub window +screen shot 2018-05-18 at 7 13 03 am +6. You should see the GitHub window in your Unity project, with a button to initialize your repository +screen shot 2018-05-18 at 7 13 34 am + +# Installing from the Asset Store +1. Open a new Unity Project +screen shot 2018-05-18 at 7 10 07 am +2. Go to `Window -> Asset Store` +screen shot 2018-05-18 at 7 20 19 am +3. Search for "GitHub" in the search bar +screen shot 2018-05-18 at 7 20 51 am +4. Click on the GitHub for Unity Package +screen shot 2018-05-18 at 7 22 15 am +5. Click `Download` then `Install` +screen shot 2018-05-18 at 7 21 37 am +screen shot 2018-05-18 at 7 21 44 am +6. Click `Import` when the dialog pops up +screen shot 2018-05-18 at 7 12 42 am +7. After the package finishes importing, go to `Window -> GitHub` to open the GitHub window +screen shot 2018-05-18 at 7 13 03 am +8. You should see the GitHub window in your Unity project, with a button to initialize your repository +screen shot 2018-05-18 at 7 13 34 am + +# Updating the GitHub for Unity Package + +- If you are running Unity and wish to update GitHub for Unity (unless explicitly stated), be sure that the files in `x64` and `x86` are not selected. + + ![image](https://user-images.githubusercontent.com/417571/44666907-6e6d5a80-a9e7-11e8-8f97-b3b52250a75d.png) + +- Otherwise, it's best to stop Unity and delete GitHub for Unity from your project. Startup Unity and run the package installer like normal. Allowing it to restore everything. diff --git a/docs/using/images/branches-initial-view.png b/docs/using/images/branches-initial-view.png new file mode 100644 index 000000000..811bcf724 --- /dev/null +++ b/docs/using/images/branches-initial-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:558821e67ea26955434da0485785e9240b6c15c44a3790b642fdcd37382e56a2 +size 59798 diff --git a/docs/using/images/changes-view.png b/docs/using/images/changes-view.png new file mode 100644 index 000000000..23000f8ef --- /dev/null +++ b/docs/using/images/changes-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac4637a222a06e6ab57f23082b8e8b2f5cb9e35b9b99776c112ddcebb923dec +size 61918 diff --git a/docs/using/images/confirm-pull-changes.png b/docs/using/images/confirm-pull-changes.png new file mode 100644 index 000000000..722bbd639 --- /dev/null +++ b/docs/using/images/confirm-pull-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b0e1aa1d8d86cb0264c9ca569a5c8e71b0aba73a2eb6611c06ec47670037d7d +size 21478 diff --git a/docs/using/images/confirm-push-changes.png b/docs/using/images/confirm-push-changes.png new file mode 100644 index 000000000..ff1ca1cd0 --- /dev/null +++ b/docs/using/images/confirm-push-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e2e136027b03609f344bfde0c777d8e915544a5e9920f1268e4cc3abc5b1481 +size 18676 diff --git a/docs/using/images/confirm-revert.png b/docs/using/images/confirm-revert.png new file mode 100644 index 000000000..cbf5d1566 --- /dev/null +++ b/docs/using/images/confirm-revert.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba990ce12403bfb53fb387ca3f3e2c447b21814ed6cd7e6f88d968463a5253a8 +size 21973 diff --git a/docs/using/images/create-new-branch-view.png b/docs/using/images/create-new-branch-view.png new file mode 100644 index 000000000..34c69d1ea --- /dev/null +++ b/docs/using/images/create-new-branch-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0239a51f611501dc63c828de206f35d412493871f65e712983666e67dbfc231 +size 62046 diff --git a/docs/using/images/delete-dialog.png b/docs/using/images/delete-dialog.png new file mode 100644 index 000000000..574489f7c --- /dev/null +++ b/docs/using/images/delete-dialog.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99d7cd8ef4957614cc844a08a632159157295414dc4675c1394f3209e30ffa58 +size 76315 diff --git a/docs/using/images/github-authenticate.png b/docs/using/images/github-authenticate.png new file mode 100644 index 000000000..188121d97 --- /dev/null +++ b/docs/using/images/github-authenticate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31ca0f7c4fc5c737a3db2da6eeb3deaa862d27e2cf3d1aa78fa14555b7cac750 +size 5927 diff --git a/docs/using/images/github-menu-item.png b/docs/using/images/github-menu-item.png new file mode 100644 index 000000000..44f3f9458 --- /dev/null +++ b/docs/using/images/github-menu-item.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c423766a70230c245f4bcdb1e33d9e76f267bf789876ecf9050016f9c8671735 +size 19651 diff --git a/docs/using/images/github-sign-in-button.png b/docs/using/images/github-sign-in-button.png new file mode 100644 index 000000000..cb6132c24 --- /dev/null +++ b/docs/using/images/github-sign-in-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79bea09964bcf1964d4d1e1fb9712fe9fc67dc8ffe11f936534c9f2be63fd777 +size 9318 diff --git a/docs/using/images/github-two-factor.png b/docs/using/images/github-two-factor.png new file mode 100644 index 000000000..7a3286616 --- /dev/null +++ b/docs/using/images/github-two-factor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba2174f780ea52a7cf7bbfd5144527bb7b29c3ce6a3200b6b6f8c69b2d6fb48 +size 9665 diff --git a/docs/using/images/locked-scene.png b/docs/using/images/locked-scene.png new file mode 100644 index 000000000..7a839141d --- /dev/null +++ b/docs/using/images/locked-scene.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbc246b18cbd173c19f392fe7ab85bffb8a97a7616ef99e7e37496e307924e49 +size 51983 diff --git a/docs/using/images/locks-view-right-click.png b/docs/using/images/locks-view-right-click.png new file mode 100644 index 000000000..0ee14312c --- /dev/null +++ b/docs/using/images/locks-view-right-click.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:590dae09b3c029e48c3703d6c85a6774b062ebea46f11c2c062d97fedcb436df +size 27251 diff --git a/docs/using/images/locks-view.png b/docs/using/images/locks-view.png new file mode 100644 index 000000000..e8790d74e --- /dev/null +++ b/docs/using/images/locks-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:118f2b2e1bb82b598107eaa73db9a077374ff4f9601e47677eca7296353b1bc3 +size 23274 diff --git a/docs/using/images/name-branch.png b/docs/using/images/name-branch.png new file mode 100644 index 000000000..9926c06db --- /dev/null +++ b/docs/using/images/name-branch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08232fa5cfd41a41c8091197ac27f433fdfa865c4996388dd0c731ac75176442 +size 59599 diff --git a/docs/using/images/new-branch-created.png b/docs/using/images/new-branch-created.png new file mode 100644 index 000000000..2a37a9aeb --- /dev/null +++ b/docs/using/images/new-branch-created.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8543da247ee23364c15abec87e4e3de3e62b85a470e7d3d090ade18fb321072c +size 64045 diff --git a/docs/using/images/post-commit-view.png b/docs/using/images/post-commit-view.png new file mode 100644 index 000000000..81fc7d135 --- /dev/null +++ b/docs/using/images/post-commit-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91e26123236bdd3e692ca7d30ff0d6477311fe739d8008ba19fd543ee3506176 +size 21345 diff --git a/docs/using/images/post-push-history-view.png b/docs/using/images/post-push-history-view.png new file mode 100644 index 000000000..0d2ed767a --- /dev/null +++ b/docs/using/images/post-push-history-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0c2d5ccaddc88d3268e0d57f02229525240ecc71bded951e23171456560268b +size 22485 diff --git a/docs/using/images/pull-view.png b/docs/using/images/pull-view.png new file mode 100644 index 000000000..842ff9660 --- /dev/null +++ b/docs/using/images/pull-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb83a46ebdd34ff46db3a38e8829f5adec5b10824bf69a8c56c9ba6fbd603ff +size 25054 diff --git a/docs/using/images/push-view.png b/docs/using/images/push-view.png new file mode 100644 index 000000000..42bef651a --- /dev/null +++ b/docs/using/images/push-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab87bae0916af3681b7528891a359c77597fa3ea57f13eb6924f1cbcf2f2ab7d +size 14581 diff --git a/docs/using/images/release-lock.png b/docs/using/images/release-lock.png new file mode 100644 index 000000000..545b81c8a --- /dev/null +++ b/docs/using/images/release-lock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f6e05a714d270016d1f8ad4e018a895ed2b023fe59bd34a28df8d315c065649 +size 140913 diff --git a/docs/using/images/request-lock.png b/docs/using/images/request-lock.png new file mode 100644 index 000000000..b877e1d8f --- /dev/null +++ b/docs/using/images/request-lock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f25877141bed2dcbdc4324edcbd802b6f216771e80de704f20dc99a3fdc718b +size 136078 diff --git a/docs/using/images/revert-commit.png b/docs/using/images/revert-commit.png new file mode 100644 index 000000000..daad75c8c --- /dev/null +++ b/docs/using/images/revert-commit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7711f428f3e04ea056c47b5d43109f3941761f4c286360d3616e0598d2d7b75 +size 26119 diff --git a/docs/using/images/revert.png b/docs/using/images/revert.png new file mode 100644 index 000000000..326071a28 --- /dev/null +++ b/docs/using/images/revert.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dea7b36eec8e2d99e1906b6cbc9cd22f52d2db09cc1a0cf34563585720b6649d +size 25141 diff --git a/docs/using/images/success-pull-changes.png b/docs/using/images/success-pull-changes.png new file mode 100644 index 000000000..a246da405 --- /dev/null +++ b/docs/using/images/success-pull-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0407ab83ca311f319ff7feac0dac84b862935e2b50353ff82c067bdadf80e16 +size 16888 diff --git a/docs/using/images/success-push-changes.png b/docs/using/images/success-push-changes.png new file mode 100644 index 000000000..8fba1f506 --- /dev/null +++ b/docs/using/images/success-push-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfd290cb1e52b0acb5503642a3aaef164bf343735729325484e25d8b99293d7e +size 13262 diff --git a/docs/using/images/switch-confirmation.png b/docs/using/images/switch-confirmation.png new file mode 100644 index 000000000..92d623d6c --- /dev/null +++ b/docs/using/images/switch-confirmation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed80f5cf2fab5fd256dd7cd093ad505c966edb95bdc5a15c3f1ace042bec7f6 +size 73947 diff --git a/docs/using/images/switch-or-delete.png b/docs/using/images/switch-or-delete.png new file mode 100644 index 000000000..deab6badf --- /dev/null +++ b/docs/using/images/switch-or-delete.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0d171b2e8f7e212f2446b416235c6232c57c9b4927cfa41891bffa1940ae9d +size 73828 diff --git a/docs/using/images/switched-branches.png b/docs/using/images/switched-branches.png new file mode 100644 index 000000000..c185d25c5 --- /dev/null +++ b/docs/using/images/switched-branches.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542806a8dcc012cdfadadaf9cdab3cf4871bbbbee2c8d73834946df4ed249aec +size 64281 diff --git a/docs/using/locking-files.md b/docs/using/locking-files.md new file mode 100644 index 000000000..f90c8e7a3 --- /dev/null +++ b/docs/using/locking-files.md @@ -0,0 +1,30 @@ +# Locking files + +## Request locks + +From the Project tab, right-click on a file to open the context menu and select `Request Lock`. + + +An additional way to lock a file is by selecting it and going to `Assets` -> `Request Lock`. + +## View locks + +After requesting a lock, a lock icon appears in the bottom right-hand corner of the file. + + +A list of all locked files will appear in the **Locks** view in the GitHub tab. + + +## Release locks + +There are three ways to release locks: + +1. From the Project tab, right-click on the locked file to open the context menu and select the option to `Release Lock`. + + +2. From the GitHub tab under the **Locks** view, right-click to open the context menu and select to `Release Lock`. + + +3. Select the file to unlock and go to select the menu option `Assets` -> `Release Lock`. + +Note: There are also two options for how to release a lock on a file. Always choose the `Release Lock` option first. The `Release Lock (forced)` option can be used to remove someone else's lock. diff --git a/docs/using/managing-branches.md b/docs/using/managing-branches.md new file mode 100644 index 000000000..13fa9c1bd --- /dev/null +++ b/docs/using/managing-branches.md @@ -0,0 +1,38 @@ +# Managing branches + +Initial **Branches** view + +Post commit view + +## Create branch + +1. From the **Branches** view, click on `master` under local branches to enable the `New Branch` button and be able to create a new branch from master. +2. Click on `New Branch`. +Post commit view + +3. Enter a name for the branch and click `Create`. +Post commit view + +4. The new branch will be created from master. +Post commit view + +## Checkout branch + +1. Right-click on a local branch and select `Switch` or double-click on the branch to switch to it. +Post commit view + +2. A dialog will appear asking `Switch branch to 'branch name'?`. Select `Switch`. +Post commit view + +The branch will be checked out. +Post commit view + +## Delete branches + +1. Click on the branch name to be deleted and the `Delete` button becomes enabled. +2. Right-click on a local branch and select `Delete` or click the `Delete` button above the Local branches list. +Post commit view + +3. A dialog appears asking `Are you sure you want to delete the branch: 'branch name'?`. Select `Delete`. + +The branch will be deleted. diff --git a/docs/using/quick-guide.md b/docs/using/quick-guide.md new file mode 100644 index 000000000..2011ee40f --- /dev/null +++ b/docs/using/quick-guide.md @@ -0,0 +1,164 @@ +# Quick Guide + +## More resources + +These documents contain more details on how to use the GitHub for Unity plugin: +- **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** +- **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** +- **[Authenticating to GitHub](https://github.com/github-for-unity/Unity/blob/master/docs/using/authenticating-to-github.md)** +- **[Managing Branches](https://github.com/github-for-unity/Unity/blob/master/docs/using/managing-branches.md)** +- **[Locking Files](https://github.com/github-for-unity/Unity/blob/master/docs/using/locking-files.md)** +- **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md)** +- **[Using the Api](https://github.com/github-for-unity/Unity/blob/master/docs/using/using-the-api.md)** + +## Table of Contents + +[Installing GitHub for Unity](#installing-github-for-unity) + +- [Requirements](#requirements) + - [Git on macOS](#git-on-macos) + - [Git on Windows](#git-on-windows) +- [Installation](#installation) +- [Log files](#log-files) + - [Windows](#windows) + - [macOS](#macos) + +[Quick Guide to GitHub for Unity](#quick-guide-to-github-for-unity) + +- [Opening the GitHub window](#opening-the-github-window) +- [Initialize Repository](#initialize-repository) +- [Authentication](#authentication) +- [Publish a new repository](#publish-a-new-repository) +- [Commiting your work - Changes tab](#commiting-your-work---changes-tab) +- [Pushing/pulling your work - History tab](#pushingpulling-your-work---history-tab) +- [Branches tab](#branches-tab) +- [Settings tab](#settings-tab) + +## Installing GitHub for Unity + +### Requirements + +- Unity 5.4 or higher + - There's currently a blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. +- Git and Git LFS 2.x + +#### Git on macOS + +The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the `Settings` tab on the GitHub window. + +The easiest way of installing git and git lfs is to install [Homebrew](https://brew.sh/) and then do `brew install git git-lfs`. + +Make sure a Git user and email address are set in the `~/.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `~/.gitconfig` file and adding the following section, if it doesn't exist yet: + +``` +[user] + name = Your Name + email = Your Email +``` + +#### Git on Windows + +The GitHub for Unity extension ships with a bundle of Git and Git LFS, to ensure that you have the correct version. These will be installed into `%LOCALAPPDATA%\GitHubUnity` when the extension runs for the first time. + +Make sure a Git user and email address are set in the `%HOME%\.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `%HOME%\.gitconfig` file and adding the following section, if it doesn't exist yet: + +``` +[user] + name = Your Name + email = Your Email +``` + +Once the extension is installed, you can open a command line with the same Git and Git LFS version that the extension uses by going to `Window` -> `GitHub Command Line` in Unity. + +### Installation + +This extensions needs to be installed (and updated) for each Unity project that you want to version control. +First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); it will be saved as a file with the extension `.unitypackage`. +To install it, open Unity, then open the project you want to version control, and then double click on the downloaded package. +Alternatively, import the package by clicking `Assets`, `Import Package`, `Custom Package`, then select the downloaded package. + +#### Log files + +##### macOS + +The extension log file can be found at `~/Library/Logs/GitHubUnity/github-unity.log` + +##### Windows + +The extension log file can be found at `%LOCALAPPDATA%\GitHubUnity\github-unity.log` + +## I have a problem with GitHub for Unity + +First, please search the [open issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen) +and [closed issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aclosed) +to see if your issue hasn't already been reported (it may also be fixed). + +If you can't find an issue that matches what you're seeing, open a [new issue](https://github.com/github-for-unity/Unity/issues/new) +and fill out the template to provide us with enough information to investigate +further. + +## Quick Guide to GitHub for Unity + +### Opening the GitHub window + +You can access the GitHub window by going to `Windows` -> `GitHub`. The window opens by default next to the Inspector window. + +### Initialize Repository + +![Initialize repository screenshot](https://user-images.githubusercontent.com/10103121/37807041-bb4446a6-2e19-11e8-9fff-a431309b8515.png) + +If the current Unity project is not in a Git repository, the GitHub for Unity extension will offer to initialize the repository for you. This will: + +- Initialize a git repository at the Unity project root via `git init` +- Initialize git-lfs via `git lfs install` +- Set up a `.gitignore` file at the Unity project root. +- Set up a `.gitattributes` file at the Unity project root with a large list of known binary filetypes (images, audio, etc) that should be tracked by LFS +- Configure the project to serialize meta files as text +- Create an initial commit with the `.gitignore` and `.gitattributes` file. + +### Authentication + +To set up credentials in Git so you can push and pull, you can sign in to GitHub by going to `Window` -> `GitHub` -> `Account` -> `Sign in`. You only have to sign in successfully once, your credentials will remain on the system for all Git operations in Unity and outside of it. If you've already signed in once but the Account dropdown still says `Sign in`, ignore it, it's a bug. + +![Authentication screenshot](https://user-images.githubusercontent.com/121322/27644895-8f22f904-5bd9-11e7-8a93-e6bfe0c24a74.png) + +For more information on Authentication: - **[Authenticating to GitHub](https://github.com/github-for-unity/Unity/blob/master/docs/using/authenticating-to-github.md)** + +### Publish a new repository + +1. Go to [github.com](https://github.com) and create a new empty repository - do not add a license, readme or other files during the creation process. +2. Copy the **https** URL shown in the creation page +3. In Unity, go to `Windows` -> `GitHub` -> `Settings` and paste the url into the `Remote` textbox. +4. Click `Save repository`. +5. Go to the `History` tab and click `Push`. + +### Commiting your work - Changes tab + +You can see which files have been changed and commit them through the `Changes` tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` + +![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) + +For more information on working with changes: - **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md#commit-changes)** + +### Pushing/pulling your work - History tab + +The history tab includes a `Push` button to push your work to the server. Make sure you have a remote url configured in the `Settings` tab so that you can push and pull your work. + +To receive updates from the server by clicking on the `Pull` button. You cannot pull if you have local changes, so commit your changes before pulling. + +![History tab screenshot](https://user-images.githubusercontent.com/121322/27644965-c1109bba-5bd9-11e7-9257-4fa38f5c67d1.png) + + +For more information on working with changes: - **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md#pulling-changes)** + +### Branches tab + +![Branches tab screenshot](https://user-images.githubusercontent.com/121322/27644978-cd3c5622-5bd9-11e7-9dcb-6ae5d5c7dc8a.png) + +### Settings tab + +You can configure your user data in the `Settings` tab, along with the path to the Git installation. + +Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. + +![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) diff --git a/docs/using/using-the-api.md b/docs/using/using-the-api.md new file mode 100644 index 000000000..ab75cab55 --- /dev/null +++ b/docs/using/using-the-api.md @@ -0,0 +1,75 @@ +# Using the API + +GitHub for Unity provides access to a git client to help users create their own tools to assist in their workflow. + +Users can separate the user interface from the API by removing `GitHub.Unity.dll`. All other libraries are required by the API. + +## Creating an instance of `GitClient` +```cs +var defaultEnvironment = new DefaultEnvironment(); +defaultEnvironment.Initialize(null, NPath.Default, NPath.Default, NPath.Default, Application.dataPath.ToNPath()); + +var processEnvironment = new ProcessEnvironment(defaultEnvironment); +var processManager = new ProcessManager(defaultEnvironment, processEnvironment, TaskManager.Instance.Token); + +var gitClient = new GitClient(defaultEnvironment, processManager, TaskManager.Instance.Token); +``` + +## Full Example +This example creates a window that has a single button which commits all changes. +```cs +using System; +using System.Globalization; +using GitHub.Unity; +using UnityEditor; +using UnityEngine; + +public class CustomGitEditor : EditorWindow +{ + [MenuItem("Window/Custom Git")] + public static void ShowWindow() + { + EditorWindow.GetWindow(typeof(CustomGitEditor)); + } + + [NonSerialized] private GitClient gitClient; + + public void OnEnable() + { + InitGitClient(); + } + + private void InitGitClient() + { + if (gitClient != null) return; + + Debug.Log("Init GitClient"); + + var defaultEnvironment = new DefaultEnvironment(); + defaultEnvironment.Initialize(null, NPath.Default, NPath.Default, + NPath.Default, Application.dataPath.ToNPath()); + + var processEnvironment = new ProcessEnvironment(defaultEnvironment); + var processManager = new ProcessManager(defaultEnvironment, processEnvironment, TaskManager.Instance.Token); + + gitClient = new GitClient(defaultEnvironment, processManager, TaskManager.Instance.Token); + } + + void OnGUI() + { + GUILayout.Label("Custom Git Window", EditorStyles.boldLabel); + + if (GUILayout.Button("Commit Stuff")) + { + var message = DateTime.Now.ToString(CultureInfo.InvariantCulture); + var body = string.Empty; + + gitClient.AddAll() + .Then(gitClient.Commit(message, body)) + .Start(); + } + } +} +``` + + diff --git a/docs/using/working-with-changes.md b/docs/using/working-with-changes.md new file mode 100644 index 000000000..228e4bd3f --- /dev/null +++ b/docs/using/working-with-changes.md @@ -0,0 +1,53 @@ +# Working with changes + +## Commit changes + +All changes made to a repository will show up under the **Changes** view. + +1. Select the changes to be committed. Can choose the All/None options, or select directories or files individually. +2. Enter a Commit summary which describes the purpose of the commit. An optional Commit description can also be entered. +3. Click the button `Commit to [branch name]`. +Changes view + +The commit will not be shown under the **History** view. On the top bar the button `Push (1)` indicates that there is 1 commit to push. + +Post commit view + +## Push changes to GitHub + +1. Click `Push` once ready to push a commit to GitHub. +Push view + +2. A dialog will appear asking `Would you like to push changes to remote 'branch name'?` Select `Push`. +Confirm push dialog + +3. Another dialog will appear when the push to GitHub is complete saying `Branch pushed`. Select `ok`. +Branch pushed + +## Revert changes + +1. From the **History** view, right-click on a commit in the commit list. A `Revert` option will appear. +2. Click `Revert`. +Revert + +3. A dialog will appear asking `Are you sure you want to revert the following commit: "commit message"?`. Select `Revert`. +Confirm revert dialog + +4. A new commit appears titled `Revert "commit summary"` and the view indicates that there is 1 commit to push. +Revert commit + +5. Follow the steps to push the reverted commit to GitHub. + +## Pulling changes + +1. Click the `Fetch` button to get all the latest branches and tags for the repository. The `Pull` button will then show the number of commits to pull from GitHub. +2. Click `Pull`. +Pull changes + +3. A dialog will appear asking `Would you like to pull changes from remote 'branch name'?`. Select `Pull`. +Confirm pull changes dialog + +4. Another dialog appears saying `Local branch is up to date with 'branch name'`. Select `ok`. +Changes pulled + + diff --git a/EULA.txt b/eula.txt similarity index 100% rename from EULA.txt rename to eula.txt diff --git a/generate-package.sh b/generate-package.sh new file mode 100755 index 000000000..e9ef73a4a --- /dev/null +++ b/generate-package.sh @@ -0,0 +1,91 @@ +#!/bin/sh -eu +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +URL="http://ghfvs-installer.github.com" +GIT=0 +LFS=0 +GHU=0 +FILE="" +VERSION="" +RN="" +MSG="" +OS="" + +while [[ $# -gt 0 ]] +do +key="$1" + +case $key in + -h) + URL="$2" + shift + shift + ;; + -p) + FILE="--path $2" + shift + shift + ;; + -v) + VERSION="--version $2" + shift + shift + ;; + -r) + RN="--rn $2" + shift + shift + ;; + -m) + MSG="--msg $2" + shift + shift + ;; + -git) + GIT=1 + shift # past value + ;; + -lfs) + LFS=1 + shift # past value + ;; + -ghu) + GHU=1 + shift # past value + ;; + -windows) + OS="windows" + shift # past value + ;; + -mac) + OS="mac" + shift # past value + ;; + -linux) + OS="linux" + shift # past value + ;; +esac +done + +if [ x"$GIT" = "x0" -a x"$LFS" = "x0" -a x"$GHU" = "x0" ]; then + echo "Usage: generate-package.sh [-git|-lfs|-ghu] [-windows|-mac|-linux only if -git or -lfs] [-v version] [-p path to file] [-h host url (optional)] [-r release notes file (optional)] [-m message file (optional)]" + exit 1 +fi + +if [ x"$GHU" = x"1" ]; then + URL="--url $URL/unity/releases" +else + URL="--url $URL/unity/git/$OS" +fi + +EXEC= +if [ ! -e "/c/" ]; then + EXEC=mono +fi + +if [ ! -e "build/CommandLine/CommandLine.exe" ]; then + >&2 xbuild /target:CommandLine "$DIR/GitHub.Unity.sln" /verbosity:minimal +fi + +$EXEC build/CommandLine/CommandLine.exe --gen-package $VERSION $FILE $URL $RN $MSG diff --git a/lib/.gitignore b/lib/.gitignore index 1ea843a1c..9432a8bee 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1 +1,2 @@ -Unity* \ No newline at end of file +Managed +UnityExtensions \ No newline at end of file diff --git a/lib/ICSharpCode.NRefactory.dll b/lib/ICSharpCode.NRefactory.dll deleted file mode 100644 index f11688c6c..000000000 --- a/lib/ICSharpCode.NRefactory.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e28e35172498877c7e4a33253b0c5df172bce5655b2ab933a922d6777ae5e6d -size 528384 diff --git a/lib/ICSharpCode.SharpZipLib.dll b/lib/ICSharpCode.SharpZipLib.dll deleted file mode 100644 index 108abfd2a..000000000 --- a/lib/ICSharpCode.SharpZipLib.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fbace48694fbfff69ff99ff7aefbde67965f7723092df9c13ff537d2a319f410 -size 192000 diff --git a/src/tests/IntegrationTests/PlatformResources/linux/.placeholder b/lib/Managed/.gitignore similarity index 100% rename from src/tests/IntegrationTests/PlatformResources/linux/.placeholder rename to lib/Managed/.gitignore diff --git a/lib/Mono.Security.dll b/lib/Mono.Security.dll deleted file mode 100644 index a7a97c141..000000000 --- a/lib/Mono.Security.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71ba4944619044e35abf7970e23e9c6b87acd2700dcae8d6655f39c53bcf40e1 -size 294912 diff --git a/lib/UnityExtensions/Unity/TestRunner/Editor/.gitignore b/lib/UnityExtensions/Unity/TestRunner/Editor/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pdb2mdb.exe b/lib/pdb2mdb.exe new file mode 100644 index 000000000..72547bf3c --- /dev/null +++ b/lib/pdb2mdb.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a72425f98bdb5923704946c4010485d471ce9e35611264f394637fb185424619 +size 369664 diff --git a/nuget.config b/nuget.config index 1a5bd9cf3..bad252e01 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,7 @@ - + diff --git a/octorun/bin/octorun-meta b/octorun/bin/octorun-meta new file mode 100644 index 000000000..1d2d3a7c9 --- /dev/null +++ b/octorun/bin/octorun-meta @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../src/bin/app-meta.js'); diff --git a/octorun/bin/octorun-token b/octorun/bin/octorun-token new file mode 100644 index 000000000..3ea7b1500 --- /dev/null +++ b/octorun/bin/octorun-token @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../src/bin/app-token.js'); diff --git a/octorun/src/api.js b/octorun/src/api.js index fcaf96c50..f7a672e39 100644 --- a/octorun/src/api.js +++ b/octorun/src/api.js @@ -1,17 +1,17 @@ var config = require("./configuration"); var octokitWrapper = require("./octokit"); -function ApiWrapper() { - this.octokit = octokitWrapper.createOctokit(); - - if (!config.user || !config.token) { - throw "user and/or token missing"; - } - +function ApiWrapper(host) { if (!config.appName) { throw "appName missing"; } + if (!config.token) { + throw "token missing"; + } + + this.octokit = octokitWrapper.createOctokit(config.appName, host); + this.octokit.authenticate({ type: "oauth", token: config.token diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index 43a2de5bb..4147522cd 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -1,12 +1,11 @@ -var endOfLine = require('os').EOL; var config = require("./configuration"); var octokitWrapper = require("./octokit"); var twoFactorRegex = new RegExp("must specify two-factor authentication otp code", "gi"); -var scopes = ["user", "repo", "gist", "write:public_key"]; +var scopes = ["user", "repo"]; -var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor) { +var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor, host) { if (!config.clientId || !config.clientSecret) { throw "clientId and/or clientSecret missing"; } @@ -15,7 +14,7 @@ var handleAuthentication = function (username, password, onSuccess, onFailure, t throw "appName missing"; } - var octokit = octokitWrapper.createOctokit(); + var octokit = octokitWrapper.createOctokit(config.appName, host); octokit.authenticate({ type: "basic", @@ -27,7 +26,6 @@ var handleAuthentication = function (username, password, onSuccess, onFailure, t if (twoFactor) { headers = { "X-GitHub-OTP": twoFactor, - "user-agent": config.appName }; } diff --git a/octorun/src/bin/app-login.js b/octorun/src/bin/app-login.js index f3484c31b..4577bac81 100644 --- a/octorun/src/bin/app-login.js +++ b/octorun/src/bin/app-login.js @@ -1,12 +1,12 @@ var commander = require("commander"); var package = require('../../package.json'); var authentication = require('../authentication'); -var endOfLine = require('os').EOL; var output = require('../output'); commander .version(package.version) .option('-t, --twoFactor') + .option('-h, --host ') .parse(process.argv); var handleAuthentication = function (username, password, twoFactor) { @@ -19,7 +19,7 @@ var handleAuthentication = function (username, password, twoFactor) { } }, function (error) { output.error(error); - }, twoFactor); + }, twoFactor, commander.host); } var encoding = 'utf-8'; diff --git a/octorun/src/bin/app-meta.js b/octorun/src/bin/app-meta.js new file mode 100644 index 000000000..ecbc36e66 --- /dev/null +++ b/octorun/src/bin/app-meta.js @@ -0,0 +1,52 @@ +var commander = require('commander'); +var package = require('../../package.json'); +var output = require('../output'); + +commander + .version(package.version) + .option('-h, --host ') + .parse(process.argv); + +var host = commander.host; +var port = 443; +var scheme = 'https'; + +if (host) { + var https = require(scheme); + var options = { + protocol: scheme + ':', + hostname: host, + port: port, + path: '/api/v3/meta', + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }; + + var req = https.request(options, function (res) { + var success = res.statusCode == 200; + + if(!success) { + output.error(res.statusCode); + } else { + res.on('data', function (d) { + output.custom("success", d, true); + }); + + res.on('end', function (d) { + process.exit(); + }); + } + }); + + req.on('error', function (error) { + output.error(error); + }); + + req.end(); +} +else { + commander.help(); + process.exit(-1); +} \ No newline at end of file diff --git a/octorun/src/bin/app-organizations.js b/octorun/src/bin/app-organizations.js index 480289aa1..1a67e181f 100644 --- a/octorun/src/bin/app-organizations.js +++ b/octorun/src/bin/app-organizations.js @@ -1,16 +1,15 @@ var commander = require("commander"); var package = require('../../package.json'); var ApiWrapper = require('../api'); -var endOfLine = require('os').EOL; var output = require('../output'); commander .version(package.version) + .option('-h, --host ') .parse(process.argv); try { - - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.getOrgs(function (error, result) { if (error) { output.error(error); diff --git a/octorun/src/bin/app-publish.js b/octorun/src/bin/app-publish.js index 5fe602390..62305763e 100644 --- a/octorun/src/bin/app-publish.js +++ b/octorun/src/bin/app-publish.js @@ -1,7 +1,6 @@ var commander = require("commander"); var package = require('../../package.json') var ApiWrapper = require('../api') -var endOfLine = require('os').EOL; var output = require('../output'); commander @@ -10,6 +9,7 @@ commander .option('-d, --description ') .option('-o, --organization ') .option('-p, --private') + .option('-h, --host ') .parse(process.argv); if(!commander.repository) @@ -24,7 +24,7 @@ if (commander.private) { } try { - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.publish(commander.repository, commander.description, private, commander.organization, function (error, result) { diff --git a/octorun/src/bin/app-token.js b/octorun/src/bin/app-token.js new file mode 100644 index 000000000..5811be43e --- /dev/null +++ b/octorun/src/bin/app-token.js @@ -0,0 +1,66 @@ + +var commander = require('commander'); +var package = require('../../package.json'); +var output = require('../output'); +var config = require("../configuration"); +var querystring = require('querystring'); + +commander + .version(package.version) + .option('-h, --host ') + .parse(process.argv); + +var host = commander.host; +var port = 443; +var scheme = 'https'; + +var valid = host && config.clientId && config.clientSecret && config.token; +if (valid) { + var https = require(scheme); + + var postData = querystring.stringify({ + client_id: config.clientId, + client_secret: config.clientSecret, + code: config.token + }); + + var options = { + protocol: scheme + ':', + hostname: host, + port: port, + path: '/login/oauth/access_token', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }; + + var req = https.request(options, function (res) { + var success = res.statusCode == 200; + + if(!success) { + output.error(res.statusCode); + } else { + res.on('data', function (d) { + output.custom("success", d, true); + }); + + res.on('end', function (d) { + process.exit(); + }); + } + }); + + req.on('error', function (error) { + output.error(error); + }); + + req.write(postData); + + req.end(); +} +else { + commander.help(); + process.exit(-1); +} \ No newline at end of file diff --git a/octorun/src/bin/app-usage.js b/octorun/src/bin/app-usage.js index 07bd3b91c..0fb32b691 100644 --- a/octorun/src/bin/app-usage.js +++ b/octorun/src/bin/app-usage.js @@ -1,8 +1,7 @@ -var commander = require("commander"); -var package = require('../../package.json') -var endOfLine = require('os').EOL; +var commander = require('commander'); +var package = require('../../package.json'); +var config = require('../configuration'); var fs = require('fs'); -var util = require('util'); var output = require('../output'); commander diff --git a/octorun/src/bin/app-validate.js b/octorun/src/bin/app-validate.js index 8ba643021..294fbcbdc 100644 --- a/octorun/src/bin/app-validate.js +++ b/octorun/src/bin/app-validate.js @@ -1,15 +1,15 @@ var commander = require("commander"); var package = require('../../package.json'); -var endOfLine = require('os').EOL; var ApiWrapper = require('../api'); var output = require('../output'); commander .version(package.version) + .option('-h, --host ') .parse(process.argv); try { - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.verifyUser(function (error, result) { if (error) { diff --git a/octorun/src/bin/app.js b/octorun/src/bin/app.js index e40d738b2..095292965 100644 --- a/octorun/src/bin/app.js +++ b/octorun/src/bin/app.js @@ -9,4 +9,6 @@ commander .command('organizations', 'Get Organizations') .command('publish', 'Publish') .command('usage', 'Usage') + .command('token', 'Create OAuth Token') + .command('meta', 'Get Server Meta Data') .parse(process.argv); \ No newline at end of file diff --git a/octorun/src/configuration.js b/octorun/src/configuration.js index f9462acde..0fe3906fd 100644 --- a/octorun/src/configuration.js +++ b/octorun/src/configuration.js @@ -3,13 +3,11 @@ require("dotenv").config({silent: true}); var clientId = process.env.OCTOKIT_CLIENT_ID; var clientSecret = process.env.OCTOKIT_CLIENT_SECRET; var appName = process.env.OCTOKIT_USER_AGENT; -var user = process.env.OCTORUN_USER; var token = process.env.OCTORUN_TOKEN; module.exports = { clientId: clientId, clientSecret: clientSecret, appName: appName, - user: user, token: token }; \ No newline at end of file diff --git a/octorun/src/octokit.js b/octorun/src/octokit.js index 1cf90b1ac..b0ab0a42f 100644 --- a/octorun/src/octokit.js +++ b/octorun/src/octokit.js @@ -1,19 +1,20 @@ var Octokit = require('octokit-rest-for-node-v0.12'); -var createOctokit = function () { - return Octokit({ +var createOctokit = function (appName, host) { + var octokitConfiguration = { timeout: 0, requestMedia: 'application/vnd.github.v3+json', headers: { - 'user-agent': 'octokit/rest.js v1.2.3' + 'user-agent': appName } + }; - // change for custom GitHub Enterprise URL - //host: 'api.github.com', - //pathPrefix: '', - //protocol: 'https', - //port: 443 - }); + if (host) { + octokitConfiguration.host = host; + octokitConfiguration.pathPrefix = 'api/v3'; + } + + return Octokit(octokitConfiguration); }; module.exports = { createOctokit: createOctokit }; \ No newline at end of file diff --git a/octorun/version b/octorun/version index de08fdd40..998379c47 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -9fcd9faa \ No newline at end of file +902910f48 \ No newline at end of file diff --git a/package.cmd b/package.cmd index e25ddbd41..a47264d8f 100644 --- a/package.cmd +++ b/package.cmd @@ -47,7 +47,7 @@ if not exist "%Unity%" ( del /Q unity\PackageProject\Assets\Plugins\GitHub\Editor\*.pdb.meta del /Q unity\PackageProject\Assets\Plugins\GitHub\Editor\*.xml - for /f tokens^=^2^ usebackq^ delims^=^" %%G in (`find "const string Version" common\SolutionInfo.cs`) do call :Package %%G + for /f tokens^=^2^ usebackq^ delims^=^" %%G in (`find "const string GitHubForUnityVersion" common\SolutionInfo.cs`) do call :Package %%G goto End diff --git a/package.sh b/package.sh index ca09bd5e5..8dc3c93dc 100755 --- a/package.sh +++ b/package.sh @@ -58,7 +58,7 @@ rm -f unity/PackageProject/Assets/Plugins/GitHub/Editor/*.pdb rm -f unity/PackageProject/Assets/Plugins/GitHub/Editor/*.pdb.meta rm -f unity/PackageProject/Assets/Plugins/GitHub/Editor/*.xml -Version=`sed -En 's,.*Version = "(.*)".*,\1,p' common/SolutionInfo.cs` +Version=`sed -En 's,.*GitHubForUnityVersion = "(.*)".*,\1,p' common/SolutionInfo.cs` commitcount=`git rev-list --count HEAD` commit=`git log -n1 --pretty=format:%h` Version="${Version}.${commitcount}-${commit}" diff --git a/run-test-webserver.sh b/run-test-webserver.sh new file mode 100755 index 000000000..8ab846630 --- /dev/null +++ b/run-test-webserver.sh @@ -0,0 +1,16 @@ +#!/bin/sh -eu +PORT="50000" +if [ $# -eq 1 ]; then + PORT="$1" +fi + +EXEC="mono " +if [ -e "/c/" ]; then + EXEC="" +fi + +if [ ! -e build/CommandLine/CommandLine.exe ]; then + >&2 xbuild /target:CommandLine GitHub.Unity.sln /verbosity:minimal +fi + +$EXEC build/CommandLine/CommandLine.exe --web --port $PORT diff --git a/script b/script index 48d975141..d373977da 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 48d975141aae81c47fe64981518edc726ee520f0 +Subproject commit d373977da73bdf7f9170e778638c80e5b49ca3b3 diff --git a/src/.gitignore b/src/.gitignore index 59f76e79c..a7abc4e4a 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -311,4 +311,6 @@ sysinfo.txt # Builds *.apk -*.unitypackage \ No newline at end of file +*.unitypackage +UnityExtension/**/manifest.json +tests/IntegrationTests/IOTestsRepo/ \ No newline at end of file diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index e6a35e10d..97a26797a 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -1,215 +1,74 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using System.Net; using GitHub.Logging; using System.Runtime.Serialization; using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using GitHub.Unity.Json; namespace GitHub.Unity { - class ApiClient : IApiClient + public class ApiClient : IApiClient { private static readonly ILogging logger = LogHelper.GetLogger(); + private static readonly Regex httpStatusErrorRegex = new Regex("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static readonly Regex accessTokenRegex = new Regex("access_token=(.*?)&", RegexOptions.Compiled); + public HostAddress HostAddress { get; } - public UriString OriginalUrl { get; } private readonly IKeychain keychain; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; - private readonly NPath nodeJsExecutablePath; - private readonly NPath octorunScriptPath; private readonly ILoginManager loginManager; + private readonly IEnvironment environment; + private IKeychainAdapter keychainAdapter; + private Connection connection; - public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, NPath nodeJsExecutablePath, NPath octorunScriptPath) + public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, + IEnvironment environment, UriString host = null) { - Guard.ArgumentNotNull(hostUrl, nameof(hostUrl)); Guard.ArgumentNotNull(keychain, nameof(keychain)); - HostAddress = HostAddress.Create(hostUrl); - OriginalUrl = hostUrl; + host = host == null + ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) + : new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + + HostAddress = HostAddress.Create(host); + this.keychain = keychain; this.processManager = processManager; this.taskManager = taskManager; - this.nodeJsExecutablePath = nodeJsExecutablePath; - this.octorunScriptPath = octorunScriptPath; - loginManager = new LoginManager(keychain, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret, - processManager: processManager, - taskManager: taskManager, - nodeJsExecutablePath: nodeJsExecutablePath, - octorunScript: octorunScriptPath); + this.environment = environment; + loginManager = new LoginManager(keychain, processManager, taskManager, environment); } - public async Task Logout(UriString host) + public ITask Logout(UriString host) { - await LogoutInternal(host); + return loginManager.Logout(host); } - private async Task LogoutInternal(UriString host) - { - await loginManager.Logout(host); - } - - public async Task CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null) + public void CreateRepository(string name, string description, bool isPrivate, + Action callback, string organization = null) { Guard.ArgumentNotNull(callback, "callback"); - try - { - var repository = await CreateRepositoryInternal(name, organization, description, isPrivate); - callback(repository, null); - } - catch (Exception e) - { - callback(null, e); - } - } - - public async Task GetOrganizations(Action onSuccess, Action onError = null) - { - Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); - await GetOrganizationInternal(onSuccess, onError); - } - - public async Task GetCurrentUser(Action onSuccess, Action onError = null) - { - Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); - try - { - var user = await GetCurrentUser(); - onSuccess(user); - } - catch (Exception e) - { - onError?.Invoke(e); - } - } - - public async Task Login(string username, string password, Action need2faCode, Action result) - { - Guard.ArgumentNotNull(need2faCode, "need2faCode"); - Guard.ArgumentNotNull(result, "result"); - - LoginResultData res = null; - try - { - res = await loginManager.Login(OriginalUrl, username, password); - } - catch (Exception ex) - { - logger.Warning(ex); - result(false, ex.Message); - return; - } - - if (res.Code == LoginResultCodes.CodeRequired) - { - var resultCache = new LoginResult(res, result, need2faCode); - need2faCode(resultCache); - } - else - { - result(res.Code == LoginResultCodes.Success, res.Message); - } - } - public async Task ContinueLogin(LoginResult loginResult, string code) - { - LoginResultData result = null; - try - { - result = await loginManager.ContinueLogin(loginResult.Data, code); - } - catch (Exception ex) - { - loginResult.Callback(false, ex.Message); - return; - } - if (result.Code == LoginResultCodes.CodeFailed) + new FuncTask(taskManager.Token, () => { - loginResult.TwoFACallback(new LoginResult(result, loginResult.Callback, loginResult.TwoFACallback)); - } - loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message); - } + EnsureValidCredentials(); - public async Task LoginAsync(string username, string password, Func need2faCode) - { - Guard.ArgumentNotNull(need2faCode, "need2faCode"); + var command = new StringBuilder("publish"); - LoginResultData res = null; - try - { - res = await loginManager.Login(OriginalUrl, username, password); - } - catch (Exception) - { - return false; - } - - if (res.Code == LoginResultCodes.CodeRequired) - { - var resultCache = new LoginResult(res, null, null); - var code = need2faCode(resultCache); - return await ContinueLoginAsync(resultCache, need2faCode, code); - } - else - { - return res.Code == LoginResultCodes.Success; - } - } - - public async Task ContinueLoginAsync(LoginResult loginResult, Func need2faCode, string code) - { - LoginResultData result = null; - try - { - result = await loginManager.ContinueLogin(loginResult.Data, code); - } - catch (Exception) - { - return false; - } - - if (result.Code == LoginResultCodes.CodeFailed) - { - var resultCache = new LoginResult(result, null, null); - code = need2faCode(resultCache); - if (String.IsNullOrEmpty(code)) - return false; - return await ContinueLoginAsync(resultCache, need2faCode, code); - } - return result.Code == LoginResultCodes.Success; - } - - private async Task GetCurrentUser() - { - //TODO: ONE_USER_LOGIN This assumes we only support one login - var keychainConnection = keychain.Connections.FirstOrDefault(); - if (keychainConnection == null) - throw new KeychainEmptyException(); - - var keychainAdapter = await GetValidatedKeychainAdapter(keychainConnection); - - // we can't trust that the system keychain has the username filled out correctly. - // if it doesn't, we need to grab the username from the server and check it - // unfortunately this means that things will be slower when the keychain doesn't have all the info - if (keychainConnection.User == null || keychainAdapter.Credential.Username != keychainConnection.Username) - { - keychainConnection.User = await GetValidatedGitHubUser(keychainConnection, keychainAdapter); - } - return keychainConnection.User; - } - - private async Task CreateRepositoryInternal(string repositoryName, string organization, string description, bool isPrivate) - { - try - { - logger.Trace("Creating repository"); - - var user = await GetCurrentUser(); - var keychainAdapter = keychain.Connect(OriginalUrl); + if (!HostAddress.IsGitHubDotCom()) + { + command.Append(" -h "); + command.Append(HostAddress.ApiUri.Host); + } - var command = new StringBuilder("publish -r \""); - command.Append(repositoryName); + command.Append(" -r \""); + command.Append(name); command.Append("\""); if (!string.IsNullOrEmpty(description)) @@ -231,11 +90,12 @@ private async Task CreateRepositoryInternal(string repositoryN command.Append(" -p"); } - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, command.ToString(), - user: user.Login, userToken: keychainAdapter.Credential.Token) + var adapter = EnsureKeychainAdapter(); + + var octorunTask = new OctorunTask(taskManager.Token, environment, command.ToString(), adapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAwait(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess && ret.Output.Length == 2) { return new GitHubRepository @@ -246,28 +106,102 @@ private async Task CreateRepositoryInternal(string repositoryN } throw new ApiClientException(ret.GetApiErrorMessage() ?? "Publish failed"); - } - catch (Exception ex) + }) + .FinallyInUI((success, ex, repository) => { - logger.Error(ex, "Error Creating Repository"); - throw; - } + if (success) + callback(repository, null); + else + { + logger.Error(ex, "Error creating repository"); + callback(null, ex); + } + }) + .Start(); } - private async Task GetOrganizationInternal(Action onSuccess, Action onError = null) + public void GetEnterpriseServerMeta(Action onSuccess, Action onError = null) { - try + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, () => + { + var octorunTask = new OctorunTask(taskManager.Token, environment, "meta -h " + HostAddress.ApiUri.Host) + .Configure(processManager); + + var ret = octorunTask.RunSynchronously(); + if (ret.IsSuccess) + { + var deserializeObject = SimpleJson.DeserializeObject>(ret.Output[0]); + + return new GitHubHostMeta + { + InstalledVersion = (string)deserializeObject["installed_version"], + GithubServicesSha = (string)deserializeObject["github_services_sha"], + VerifiablePasswordAuthentication = (bool)deserializeObject["verifiable_password_authentication"] + }; + } + + var message = ret.GetApiErrorMessage(); + + logger.Trace("Message: {0}", message); + + if (message != null) + { + if (message.Contains("ETIMEDOUT", StringComparison.InvariantCulture)) + { + message = "Connection timed out."; + } + else if (message.Contains("ECONNREFUSED", StringComparison.InvariantCulture)) + { + message = "Connection refused."; + } + else if (message.Contains("ENOTFOUND", StringComparison.InvariantCulture)) + { + message = "Address not found."; + } + else + { + int httpStatusCode; + if (int.TryParse(message, out httpStatusCode)) + { + var httpStatus = ((HttpStatusCode)httpStatusCode).ToString(); + message = httpStatusErrorRegex.Replace(httpStatus, " $1"); + } + } + } + else + { + message = "Error getting server meta"; + } + + throw new ApiClientException(message); + }) + .FinallyInUI((success, ex, meta) => { - logger.Trace("Getting Organizations"); + if (success) + onSuccess(meta); + else + { + logger.Error(ex, "Error getting server meta"); + onError?.Invoke(ex); + } + }) + .Start(); + } - var user = await GetCurrentUser(); - var keychainAdapter = keychain.Connect(OriginalUrl); + public void GetOrganizations(Action onSuccess, Action onError = null) + { + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, () => + { + var adapter = EnsureKeychainAdapter(); - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, "organizations", - user: user.Login, userToken: keychainAdapter.Credential.Token) + var command = HostAddress.IsGitHubDotCom() ? "organizations" : "organizations -h " + HostAddress.ApiUri.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, + command, adapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAsAsync(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess) { var organizations = new List(); @@ -279,57 +213,238 @@ private async Task GetOrganizationInternal(Action onSuccess, Act Login = ret.Output[i + 1] }); } - - onSuccess(organizations.ToArray()); - return; + return organizations.ToArray(); } throw new ApiClientException(ret.GetApiErrorMessage() ?? "Error getting organizations"); - } - catch (Exception ex) + }) + .FinallyInUI((success, ex, orgs) => + { + if (success) + onSuccess(orgs); + else + { + logger.Error(ex, "Error Getting Organizations"); + onError?.Invoke(ex); + } + }) + .Start(); + } + + private IKeychainAdapter EnsureKeychainAdapter() + { + var adapter = KeychainAdapter; + if (adapter.Credential == null) { - logger.Error(ex, "Error Getting Organizations"); - onError?.Invoke(ex); + throw new ApiClientException("No Credentials found"); } + + return adapter; + } + + public void GetCurrentUser(Action onSuccess, Action onError = null) + { + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, GetCurrentUser) + .FinallyInUI((success, ex, user) => + { + if (success) + onSuccess(user); + else + onError?.Invoke(ex); + }) + .Start(); + } + + public void LoginWithToken(string token, Action result) + { + Guard.ArgumentNotNull(token, "token"); + Guard.ArgumentNotNull(result, "result"); + + new FuncTask(taskManager.Token, + () => loginManager.LoginWithToken(HostAddress.WebUri.Host, token)) + .FinallyInUI((success, ex, res) => + { + if (!success) + { + logger.Warning(ex); + result(false); + return; + } + + result(res); + }) + .Start(); } - private async Task GetValidatedKeychainAdapter(Connection keychainConnection) + public void CreateOAuthToken(string code, Action result) { - var keychainAdapter = await keychain.Load(keychainConnection.Host); - if (keychainAdapter == null) - throw new KeychainEmptyException(); + var command = "token -h " + HostAddress.WebUri.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, code) + .Configure(processManager); + + octorunTask + .Then((b, octorunResult) => + { + if (b && octorunResult.IsSuccess) + { + var first = octorunResult.Output.FirstOrDefault(); + if (first == null) + { + result(false, "Error validating token."); + return; + } - if (string.IsNullOrEmpty(keychainAdapter.Credential?.Username)) + var match = accessTokenRegex.Match(first); + if (match.Success) + { + var token = match.Groups[1].Value; + LoginWithToken(token, b1 => result(b1, "Error validating token.")); + } + else + { + result(false, octorunResult.Output.FirstOrDefault()); + } + } + else + { + result(false, octorunResult.Output.FirstOrDefault()); + } + }) + .Catch(exception => result(false, exception.ToString())) + .Start(); + } + + public void Login(string username, string password, Action need2faCode, Action result) + { + Guard.ArgumentNotNull(need2faCode, "need2faCode"); + Guard.ArgumentNotNull(result, "result"); + + new FuncTask(taskManager.Token, + () => loginManager.Login(HostAddress.WebUri.Host, username, password)) + .FinallyInUI((success, ex, res) => + { + if (!success) + { + logger.Warning(ex); + result(false, ex.Message); + return; + } + + if (res.Code == LoginResultCodes.CodeRequired) + { + var resultCache = new LoginResult(res, result, need2faCode); + need2faCode(resultCache); + } + else + { + result(res.Code == LoginResultCodes.Success, res.Message); + } + }) + .Start(); + } + + public void ContinueLogin(LoginResult loginResult, string code) + { + new FuncTask(taskManager.Token, + () => loginManager.ContinueLogin(loginResult.Data, code)) + .FinallyInUI((success, ex, result) => + { + if (!success) + { + loginResult.Callback(false, ex.Message); + return; + } + if (result.Code == LoginResultCodes.CodeFailed) + { + loginResult.TwoFACallback(new LoginResult(result, loginResult.Callback, loginResult.TwoFACallback)); + } + loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message); + }) + .Start(); + } + + public void EnsureValidCredentials() + { + GetCurrentUser(); + } + + public GitHubUser GetCurrentUser() + { + // we can't trust that the system keychain has the username filled out correctly. + // if it doesn't, we need to grab the username from the server and check it + // unfortunately this means that things will be slower when the keychain doesn't have all the info + if (Connection.User == null || KeychainAdapter.Credential.Username != Connection.Username) { - logger.Warning("LoadKeychainInternal: Username is empty"); - throw new TokenUsernameMismatchException(keychainConnection.Username); + Connection.User = GetValidatedGitHubUser(); } - if (keychainAdapter.Credential.Username != keychainConnection.Username) + return Connection.User; + } + + private Connection Connection + { + get { - logger.Warning("LoadKeychainInternal: Token username does not match"); + if (connection == null) + { + connection = keychain.Connections.FirstOrDefault(x => x.Host.ToUriString().Host == HostAddress.WebUri.Host); + } + + return connection; } + } + + private IKeychainAdapter KeychainAdapter + { + get + { + if (keychainAdapter == null) + { + if (Connection == null) + throw new KeychainEmptyException(); + + var loadedKeychainAdapter = keychain.LoadFromSystem(Connection.Host); + if (loadedKeychainAdapter == null) + throw new KeychainEmptyException(); - return keychainAdapter; + if (string.IsNullOrEmpty(loadedKeychainAdapter.Credential?.Username)) + { + logger.Warning("LoadKeychainInternal: Username is empty"); + throw new TokenUsernameMismatchException(connection.Username); + } + + if (loadedKeychainAdapter.Credential.Username != connection.Username) + { + logger.Warning("LoadKeychainInternal: Token username does not match"); + } + + keychainAdapter = loadedKeychainAdapter; + } + + return keychainAdapter; + } } - private async Task GetValidatedGitHubUser(Connection keychainConnection, IKeychainAdapter keychainAdapter) + private GitHubUser GetValidatedGitHubUser() { try { - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, "validate", - user: keychainConnection.Username, userToken: keychainAdapter.Credential.Token) + var adapter = EnsureKeychainAdapter(); + + var command = HostAddress.IsGitHubDotCom() ? "validate" : "validate -h " + HostAddress.ApiUri.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, adapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAsAsync(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess) { var login = ret.Output[1]; - if (login != keychainConnection.Username) + if (!string.Equals(login, Connection.Username, StringComparison.InvariantCultureIgnoreCase)) { logger.Trace("LoadKeychainInternal: Api username does not match"); - throw new TokenUsernameMismatchException(keychainConnection.Username, login); + throw new TokenUsernameMismatchException(Connection.Username, login); } return new GitHubUser @@ -354,13 +469,20 @@ private async Task GetValidatedGitHubUser(Connection keychainConnect } } - class GitHubUser + public class GitHubHostMeta + { + public bool VerifiablePasswordAuthentication { get; set; } + public string GithubServicesSha { get; set; } + public string InstalledVersion { get; set; } + } + + public class GitHubUser { public string Name { get; set; } public string Login { get; set; } } - class GitHubRepository + public class GitHubRepository { public string Name { get; set; } public string CloneUrl { get; set; } @@ -383,7 +505,7 @@ protected ApiClientException(SerializationInfo info, StreamingContext context) : } [Serializable] - class TokenUsernameMismatchException : ApiClientException + public class TokenUsernameMismatchException : ApiClientException { public string CachedUsername { get; } public string CurrentUsername { get; } @@ -398,7 +520,7 @@ protected TokenUsernameMismatchException(SerializationInfo info, StreamingContex } [Serializable] - class KeychainEmptyException : ApiClientException + public class KeychainEmptyException : ApiClientException { public KeychainEmptyException() { diff --git a/src/GitHub.Api/Application/ApplicationConfiguration.cs b/src/GitHub.Api/Application/ApplicationConfiguration.cs index 40d4e7933..5c8358303 100644 --- a/src/GitHub.Api/Application/ApplicationConfiguration.cs +++ b/src/GitHub.Api/Application/ApplicationConfiguration.cs @@ -5,18 +5,8 @@ namespace GitHub.Unity public static class ApplicationConfiguration { public const int DefaultWebTimeout = 3000; - - static ApplicationConfiguration() - { - var executingAssembly = typeof(ApplicationConfiguration).Assembly; - AssemblyName = executingAssembly.GetName(); - } - - /// - /// The currently executing assembly. - /// - public static AssemblyName AssemblyName { get; } - + public const int DefaultGitTimeout = 5000; public static int WebTimeout { get; set; } = DefaultWebTimeout; + public static int GitTimeout { get; set; } = DefaultGitTimeout; } } diff --git a/src/GitHub.Api/Application/ApplicationInfo.cs b/src/GitHub.Api/Application/ApplicationInfo.cs index f70bf4241..f15f7d482 100644 --- a/src/GitHub.Api/Application/ApplicationInfo.cs +++ b/src/GitHub.Api/Application/ApplicationInfo.cs @@ -6,15 +6,32 @@ static partial class ApplicationInfo #if DEBUG public const string ApplicationName = "GitHub for Unity Debug"; public const string ApplicationProvider = "GitHub"; + public const string ApplicationSafeName = "GitHubUnity-dev"; #else public const string ApplicationName = "GitHubUnity"; public const string ApplicationProvider = "GitHub"; -#endif public const string ApplicationSafeName = "GitHubUnity"; +#endif public const string ApplicationDescription = "GitHub for Unity"; +#if DEBUG +/* + For external contributors, we have bundled a developer OAuth application + called `GitHub for Unity (dev)` so that you can complete the sign in flow + locally without needing to configure your own application. + This is for testing only and it is (obviously) public, proceed with caution. + + For a release build, you should create a new oauth application on github.com, + copy the `common/ApplicationInfo_Local.cs-example` + template to `common/ApplicationInfo_Local.cs` and fill out the `myClientId` and + `myClientSecret` fields for your oauth app. + */ + internal static string ClientId { get; private set; } = "924a97f36926f535e72c"; + internal static string ClientSecret { get; private set; } = "b4fa550b7f8e38034c6b1339084fa125eebb6155"; +#else internal static string ClientId { get; private set; } = ""; internal static string ClientSecret { get; private set; } = ""; +#endif public static string Version { get { return System.AssemblyVersionInformation.Version; } } diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 45ac09712..5d258087e 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -3,282 +3,376 @@ using System.Threading.Tasks; using System.Collections.Generic; using GitHub.Logging; +using static GitHub.Unity.GitInstaller; namespace GitHub.Unity { - abstract class ApplicationManagerBase : IApplicationManager + public class ApplicationManagerBase : IApplicationManager { protected static ILogging Logger { get; } = LogHelper.GetLogger(); private RepositoryManager repositoryManager; - private Progress progressReporter; + private ProgressReporter progressReporter = new ProgressReporter(); + private Progress progress = new Progress(TaskBase.Default); protected bool isBusy; + private bool firstRun; + protected bool FirstRun { get { return firstRun; } set { firstRun = value; } } + private Guid instanceId; + protected Guid InstanceId { get { return instanceId; } set { instanceId = value; } } + public event Action OnProgress { add { progressReporter.OnProgress += value; } remove { progressReporter.OnProgress -= value; } } - public ApplicationManagerBase(SynchronizationContext synchronizationContext) + public ApplicationManagerBase(SynchronizationContext synchronizationContext, IEnvironment environment) { - progressReporter = new Progress(); + UIScheduler = ThreadingHelper.GetUIScheduler(synchronizationContext); + SynchronizationContext = synchronizationContext; - SynchronizationContext.SetSynchronizationContext(SynchronizationContext); ThreadingHelper.SetUIThread(); - UIScheduler = TaskScheduler.FromCurrentSynchronizationContext(); ThreadingHelper.MainThreadScheduler = UIScheduler; + + Environment = environment; TaskManager = new TaskManager(UIScheduler); + Platform = new Platform(Environment); + ProcessManager = new ProcessManager(Environment, Platform.GitEnvironment, TaskManager.Token); + GitClient = new GitClient(Environment, ProcessManager, TaskManager.Token); } protected void Initialize() { - // accessing Environment triggers environment initialization if it hasn't happened yet - Platform = new Platform(Environment); - - UserSettings = new UserSettings(Environment); - LocalSettings = new LocalSettings(Environment); - SystemSettings = new SystemSettings(Environment); - - UserSettings.Initialize(); - LocalSettings.Initialize(); - SystemSettings.Initialize(); - LogHelper.TracingEnabled = UserSettings.Get(Constants.TraceLoggingKey, false); - ProcessManager = new ProcessManager(Environment, Platform.GitEnvironment, CancellationToken); + ApplicationConfiguration.WebTimeout = UserSettings.Get(Constants.WebTimeoutKey, ApplicationConfiguration.WebTimeout); + ApplicationConfiguration.GitTimeout = UserSettings.Get(Constants.GitTimeoutKey, ApplicationConfiguration.GitTimeout); Platform.Initialize(ProcessManager, TaskManager); - GitClient = new GitClient(Environment, ProcessManager, TaskManager.Token); + progress.OnProgress += progressReporter.UpdateProgress; + UsageTracker = new UsageTracker(TaskManager, GitClient, ProcessManager, UserSettings, Environment, Platform.Keychain, InstanceId.ToString()); + +#if ENABLE_METRICS + var metricsService = new MetricsService(ProcessManager, + TaskManager, + Platform.Keychain, + Environment); + UsageTracker.MetricsService = metricsService; +#endif } - public void Run(bool firstRun) + public void Run() { - Logger.Trace("Run - CurrentDirectory {0}", NPath.CurrentDirectory); isBusy = true; + progress.UpdateProgress(0, 100, "Initializing..."); - var endTask = new ActionTask(CancellationToken, - (_, state) => InitializeEnvironment(state)) - { Affinity = TaskAffinity.UI }; - - ITask setExistingEnvironmentPath; - if (Environment.IsMac) - { - setExistingEnvironmentPath = new SimpleProcessTask(CancellationToken, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") - .Configure(ProcessManager, dontSetupGit: true) - .Catch(e => true) // make sure this doesn't throw if the task fails - .Then((success, path) => success ? path?.Split(new[] { "\"" }, StringSplitOptions.None)[1] : null); - } - else + if (firstRun) { - setExistingEnvironmentPath = new FuncTask(CancellationToken, () => null); + UsageTracker.IncrementNumberOfStartups(); } - setExistingEnvironmentPath.OnEnd += (t, path, success, ex) => + var thread = new Thread(() => { - if (path != null) + GitInstallationState state = new GitInstallationState(); + try { - Logger.Trace("Existing Environment Path Original:{0} Updated:{1}", Environment.Path, path); - Environment.Path = path; - } - }; - - var setupOctorun = new OctorunInstaller(Environment, TaskManager).SetupOctorunIfNeeded(); - var setOctorunEnvironment = new ActionTask(CancellationToken, - (s, octorunPath) => Environment.OctorunScriptPath = octorunPath); + if (Environment.IsMac) + { + var getEnvPath = new SimpleProcessTask(TaskManager.Token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") + .Configure(ProcessManager, dontSetupGit: true) + .Catch(e => true); // make sure this doesn't throw if the task fails + var path = getEnvPath.RunSynchronously(); + if (getEnvPath.Successful) + { + Logger.Trace("Existing Environment Path Original:{0} Updated:{1}", Environment.Path, path); + Environment.Path = path?.Split(new[] { "\"" }, StringSplitOptions.None)[1]; + } + } - var getGitFromSettings = new FuncTask(CancellationToken, () => - { - var gitExecutablePath = SystemSettings.Get(Constants.GitInstallPathKey)?.ToNPath(); - if (gitExecutablePath.HasValue && gitExecutablePath.Value.FileExists()) // we have a git path + progress.UpdateProgress(20, 100, "Setting up octorun..."); + + Environment.OctorunScriptPath = new OctorunInstaller(Environment, TaskManager) + .SetupOctorunIfNeeded(); + + progress.UpdateProgress(50, 100, "Setting up git..."); + + state = Environment.GitInstallationState; + if (!state.GitIsValid && !state.GitLfsIsValid && FirstRun) + { + // importing old settings + NPath gitExecutablePath = Environment.SystemSettings.Get(Constants.GitInstallPathKey, NPath.Default); + if (gitExecutablePath.IsInitialized) + { + Environment.SystemSettings.Unset(Constants.GitInstallPathKey); + state.GitExecutablePath = gitExecutablePath; + state.GitInstallationPath = gitExecutablePath.Parent.Parent; + Environment.GitInstallationState = state; + } + } + + + var installer = new GitInstaller(Environment, ProcessManager, TaskManager.Token); + installer.Progress.OnProgress += progressReporter.UpdateProgress; + if (state.GitIsValid && state.GitLfsIsValid) + { + if (firstRun) + { + installer.ValidateGitVersion(state); + if (state.GitIsValid) + { + installer.ValidateGitLfsVersion(state); + } + } + } + + if (!state.GitIsValid || !state.GitLfsIsValid) + { + state = installer.SetupGitIfNeeded(); + } + + SetupGit(state); + + progress.UpdateProgress(80, 100, "Initializing repository..."); + + if (state.GitIsValid && state.GitLfsIsValid) + { + RestartRepository(); + } + + progress.UpdateProgress(100, 100, "Initialized"); + } + catch (Exception ex) { - Logger.Trace("Using git install path from settings: {0}", gitExecutablePath); - return gitExecutablePath.Value; + Logger.Error(ex, "A problem ocurred setting up Git"); + progress.UpdateProgress(90, 100, "Initialization failed"); } - return NPath.Default; + + new ActionTask(TaskManager.Token, (s, gitIsValid) => + { + InitializationComplete(); + if (gitIsValid) + { + InitializeUI(); + } + }, + () => state.GitIsValid && state.GitLfsIsValid) + { Affinity = TaskAffinity.UI } + .Start(); }); + thread.Start(); + } - getGitFromSettings.OnEnd += (t, path, _, __) => + public void SetupGit(GitInstaller.GitInstallationState state) + { + if (!state.GitIsValid || !state.GitLfsIsValid) { - if (path.IsInitialized) + if (!state.GitExecutablePath.IsInitialized) { - var state = new GitInstaller.GitInstallationState { - GitExecutablePath = path, - GitIsValid = true - }; - endTask.PreviousResult = state; - endTask.Start(); - return; + Logger.Warning(Localization.GitNotFound); } - Logger.Trace("Using portable git"); - - var setupGit = new GitInstaller(Environment, ProcessManager, TaskManager).SetupGitIfNeeded(); - t.Then(setupGit); - setupGit.Finally((s, state) => + else if (!state.GitLfsExecutablePath.IsInitialized) + { + Logger.Warning(Localization.GitLFSNotFound); + } + else if (state.GitVersion < Constants.MinimumGitVersion) + { + Logger.Warning(String.Format(Localization.GitVersionTooLow, state.GitExecutablePath, state.GitVersion, Constants.MinimumGitVersion)); + } + else if (state.GitLfsVersion < Constants.MinimumGitLfsVersion) { - endTask.PreviousResult = state; - endTask.Start(); - }); - setupGit.Progress(progressReporter.UpdateProgress); - // append installer task to top chain - }; - - var setupChain = setExistingEnvironmentPath.Then(setupOctorun); - setupChain.OnEnd += (t, path, _, __) => + Logger.Warning(String.Format(Localization.GitLfsVersionTooLow, state.GitLfsExecutablePath, state.GitLfsVersion, Constants.MinimumGitLfsVersion)); + } + return; + } + + Environment.GitInstallationState = state; + Environment.User.Initialize(GitClient); + + if (firstRun) { - t.GetEndOfChain().Then(setOctorunEnvironment).Then(getGitFromSettings); - }; + if (Environment.RepositoryPath.IsInitialized) + { + UpdateMergeSettings(); - setupChain.Start(); + GitClient.LfsInstall() + .Catch(e => + { + Logger.Error(e, "Error running lfs install"); + return true; + }) + .RunSynchronously(); + } + + if (Environment.IsWindows) + { + var credentialHelper = GitClient.GetConfig("credential.helper", GitConfigSource.Global) + .Catch(e => + { + Logger.Error(e, "Error getting the credential helper"); + return true; + }).RunSynchronously(); + + if (string.IsNullOrEmpty(credentialHelper)) + { + Logger.Warning("No Windows CredentialHelper found: Setting to wincred"); + GitClient.SetConfig("credential.helper", "wincred", GitConfigSource.Global) + .Catch(e => + { + Logger.Error(e, "Error setting the credential helper"); + return true; + }) + .RunSynchronously(); + } + } + } } - public ITask InitializeRepository() + public void InitializeRepository() { - //Logger.Trace("Running Repository Initialize"); + isBusy = true; + progress.UpdateProgress(0, 100, "Initializing..."); + var thread = new Thread(() => + { + var success = true; + try + { + var targetPath = NPath.CurrentDirectory; - var targetPath = NPath.CurrentDirectory; + var gitignore = targetPath.Combine(".gitignore"); + var gitAttrs = targetPath.Combine(".gitattributes"); + var assetsGitignore = targetPath.Combine("Assets", ".gitignore"); - var unityYamlMergeExec = Environment.UnityApplicationContents.Combine("Tools", "UnityYAMLMerge" + Environment.ExecutableExtension); + var filesForInitialCommit = new List { gitignore, gitAttrs, assetsGitignore }; - var yamlMergeCommand = Environment.IsWindows - ? $@"'{unityYamlMergeExec}' merge -p ""$BASE"" ""$REMOTE"" ""$LOCAL"" ""$MERGED""" - : $@"'{unityYamlMergeExec}' merge -p '$BASE' '$REMOTE' '$LOCAL' '$MERGED'"; + GitClient.Init().RunSynchronously(); + progress.UpdateProgress(10, 100, "Initializing..."); - var gitignore = targetPath.Combine(".gitignore"); - var gitAttrs = targetPath.Combine(".gitattributes"); - var assetsGitignore = targetPath.Combine("Assets", ".gitignore"); + ConfigureMergeSettings(); + progress.UpdateProgress(20, 100, "Initializing..."); - var filesForInitialCommit = new List { gitignore, gitAttrs, assetsGitignore }; + GitClient.LfsInstall().RunSynchronously(); + progress.UpdateProgress(30, 100, "Initializing..."); - var task = - GitClient.Init() - .Then(GitClient.SetConfig("merge.unityyamlmerge.cmd", yamlMergeCommand, GitConfigSource.Local)) - .Then(GitClient.SetConfig("merge.unityyamlmerge.trustExitCode", "false", GitConfigSource.Local)) - .Then(GitClient.LfsInstall()) - .ThenInUI(SetProjectToTextSerialization) - .Then(new ActionTask(CancellationToken, _ => { AssemblyResources.ToFile(ResourceType.Generic, ".gitignore", targetPath, Environment); AssemblyResources.ToFile(ResourceType.Generic, ".gitattributes", targetPath, Environment); - assetsGitignore.CreateFile(); - })) - .Then(GitClient.Add(filesForInitialCommit)) - .Then(GitClient.Commit("Initial commit", null)) - .Then(_ => - { + GitClient.Add(filesForInitialCommit).RunSynchronously(); + progress.UpdateProgress(60, 100, "Initializing..."); + GitClient.Commit("Initial commit", null).RunSynchronously(); + progress.UpdateProgress(70, 100, "Initializing..."); Environment.InitializeRepository(); - RestartRepository(); - }) - .ThenInUI(InitializeUI); - return task; - } + } + catch (Exception ex) + { + Logger.Error(ex, "A problem ocurred initializing the repository"); + progress.UpdateProgress(90, 100, "Failed to initialize repository"); + success = false; + } - public void RestartRepository() - { - if (Environment.RepositoryPath.IsInitialized) - { - repositoryManager = Unity.RepositoryManager.CreateInstance(Platform, TaskManager, GitClient, Environment.FileSystem, Environment.RepositoryPath); - repositoryManager.Initialize(); - Environment.Repository.Initialize(repositoryManager, TaskManager); - repositoryManager.Start(); - Environment.Repository.Start(); - Logger.Trace($"Got a repository? {(Environment.Repository != null ? Environment.Repository.LocalPath : "null")}"); - } + if (success) + { + progress.UpdateProgress(90, 100, "Initializing..."); + RestartRepository(); + TaskManager.RunInUI(InitializeUI); + UsageTracker.IncrementProjectsInitialized(); + progress.UpdateProgress(100, 100, "Initialized"); + } + isBusy = false; + }); + thread.Start(); } - protected void SetupMetrics(string unityVersion, bool firstRun) + private void ConfigureMergeSettings(string keyName = null) { - //Logger.Trace("Setup metrics"); + var unityYamlMergeExec = + Environment.UnityApplicationContents.Combine("Tools", "UnityYAMLMerge" + Environment.ExecutableExtension); - var usagePath = Environment.UserCachePath.Combine(Constants.UsageFile); - - string id = null; - if (UserSettings.Exists(Constants.GuidKey)) - { - id = UserSettings.Get(Constants.GuidKey); - } + var yamlMergeCommand = $"'{unityYamlMergeExec}' merge -h -p --force %O %B %A %A"; - if (String.IsNullOrEmpty(id)) - { - id = Guid.NewGuid().ToString(); - UserSettings.Set(Constants.GuidKey, id); - } + keyName = keyName ?? "unityyamlmerge"; -#if ENABLE_METRICS - var metricsService = new MetricsService(ProcessManager, - TaskManager, - Environment.FileSystem, - Environment.NodeJsExecutablePath, - Environment.OctorunScriptPath); + GitClient.SetConfig($"merge.{keyName}.name", "Unity SmartMerge (UnityYamlMerge)", GitConfigSource.Local).Catch(e => { + Logger.Error(e, "Error setting merge." + keyName + ".name"); + return true; + }).RunSynchronously(); - UsageTracker = new UsageTracker(metricsService, UserSettings, usagePath, id, unityVersion); + GitClient.SetConfig($"merge.{keyName}.driver", yamlMergeCommand, GitConfigSource.Local).Catch(e => { + Logger.Error(e, "Error setting merge." + keyName + ".driver"); + return true; + }).RunSynchronously(); - if (firstRun) - { - UsageTracker.IncrementLaunchCount(); - } -#endif + GitClient.SetConfig($"merge.{keyName}.recursive", "binary", GitConfigSource.Local).Catch(e => { + Logger.Error(e, "Error setting merge." + keyName + ".recursive"); + return true; + }).RunSynchronously(); } - protected abstract void SetupMetrics(); - protected abstract void InitializeUI(); - protected abstract void SetProjectToTextSerialization(); - - /// - /// Initialize environment after finding where git is. This needs to run on the main thread - /// - /// - /// - private void InitializeEnvironment(GitInstaller.GitInstallationState installationState) + private void UpdateMergeSettings() { - isBusy = false; - SetupMetrics(); - - if (!installationState.GitIsValid) + var gitAttributesPath = Environment.RepositoryPath.Combine(".gitattributes"); + if (gitAttributesPath.FileExists()) { - return; + var readAllText = gitAttributesPath.ReadAllText(); + var containsLegacyUnityYamlMergeError = readAllText.Contains("unityamlmerge"); + + if (containsLegacyUnityYamlMergeError) + { + ConfigureMergeSettings("unityamlmerge"); + } } - var gitInstallDetails = new GitInstaller.GitInstallDetails(Environment.UserCachePath, Environment.IsWindows); - var isCustomGitExec = installationState.GitExecutablePath != gitInstallDetails.GitExecutablePath; + GitClient.UnSetConfig("merge.unityyamlmerge.cmd", GitConfigSource.Local).Catch(e => { + Logger.Error(e, "Error removing merge.unityyamlmerge.cmd"); + return true; + }).RunSynchronously(); - Environment.GitExecutablePath = installationState.GitExecutablePath; - Environment.GitLfsExecutablePath = installationState.GitLfsExecutablePath; + GitClient.UnSetConfig("merge.unityyamlmerge.trustExitCode", GitConfigSource.Local).Catch(e => { + Logger.Error(e, "Error removing merge.unityyamlmerge.trustExitCode"); + return true; + }).RunSynchronously(); - Environment.IsCustomGitExecutable = isCustomGitExec; - Environment.User.Initialize(GitClient); + ConfigureMergeSettings(); + } + + public void RestartRepository() + { + if (!Environment.RepositoryPath.IsInitialized) + return; - var afterGitSetup = new ActionTask(CancellationToken, RestartRepository) - .ThenInUI(InitializeUI); + repositoryManager?.Dispose(); - ITask task = afterGitSetup; - if (Environment.IsWindows) - { - var credHelperTask = GitClient.GetConfig("credential.helper", GitConfigSource.Global); - credHelperTask.OnEnd += (thisTask, credentialHelper, success, exception) => - { - if (!success || string.IsNullOrEmpty(credentialHelper)) - { - Logger.Warning("No Windows CredentialHelper found: Setting to wincred"); - thisTask - .Then(GitClient.SetConfig("credential.helper", "wincred", GitConfigSource.Global)) - .Then(afterGitSetup); - } - else - thisTask.Then(afterGitSetup); - }; - task = credHelperTask; - } - task.Start(); + repositoryManager = Unity.RepositoryManager.CreateInstance(Platform, TaskManager, GitClient, Environment.RepositoryPath); + repositoryManager.Initialize(); + Environment.Repository.Initialize(repositoryManager, TaskManager); + repositoryManager.Start(); + Environment.Repository.Start(); + Logger.Trace($"Got a repository? {(Environment.Repository != null ? Environment.Repository.LocalPath : "null")}"); } + protected virtual void InitializeUI() {} + protected virtual void InitializationComplete() {} private bool disposed = false; + private IOAuthCallbackManager oAuthCallbackManager; + protected virtual void Dispose(bool disposing) { if (disposing) { if (disposed) return; disposed = true; - if (TaskManager != null) TaskManager.Dispose(); - if (repositoryManager != null) repositoryManager.Dispose(); + if (ProcessManager != null) + { + ProcessManager.Stop(); + } + if (TaskManager != null) + { + TaskManager.Dispose(); + TaskManager = null; + } + if (repositoryManager != null) + { + repositoryManager.Dispose(); + repositoryManager = null; + } } } @@ -287,18 +381,30 @@ public void Dispose() Dispose(true); } - public abstract IEnvironment Environment { get; } - + public IEnvironment Environment { get; private set; } public IPlatform Platform { get; protected set; } public virtual IProcessEnvironment GitEnvironment { get; set; } public IProcessManager ProcessManager { get; protected set; } - public CancellationToken CancellationToken { get { return TaskManager.Token; } } public ITaskManager TaskManager { get; protected set; } public IGitClient GitClient { get; protected set; } - public ISettings LocalSettings { get; protected set; } - public ISettings SystemSettings { get; protected set; } - public ISettings UserSettings { get; protected set; } + public ISettings LocalSettings { get { return Environment.LocalSettings; } } + public ISettings SystemSettings { get { return Environment.SystemSettings; } } + public ISettings UserSettings { get { return Environment.UserSettings; } } public IUsageTracker UsageTracker { get; protected set; } + + public IOAuthCallbackManager OAuthCallbackManager + { + get + { + if (oAuthCallbackManager == null) + { + oAuthCallbackManager = new OAuthCallbackManager(); + } + + return oAuthCallbackManager; + } + } + public bool IsBusy { get { return isBusy; } } protected TaskScheduler UIScheduler { get; private set; } protected SynchronizationContext SynchronizationContext { get; private set; } diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 0ab28ceaf..09c79611f 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -1,19 +1,19 @@ -using System.Threading.Tasks; -using System; +using System; namespace GitHub.Unity { - interface IApiClient + public interface IApiClient { HostAddress HostAddress { get; } - UriString OriginalUrl { get; } - Task CreateRepository(string name, string description, bool isPrivate, + void CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null); - Task GetOrganizations(Action onSuccess, Action onError = null); - Task Login(string username, string password, Action need2faCode, Action result); - Task ContinueLogin(LoginResult loginResult, string code); - Task LoginAsync(string username, string password, Func need2faCode); - Task Logout(UriString host); - Task GetCurrentUser(Action onSuccess, Action onError = null); + void GetOrganizations(Action onSuccess, Action onError = null); + void Login(string username, string password, Action need2faCode, Action result); + void ContinueLogin(LoginResult loginResult, string code); + void LoginWithToken(string token, Action result); + ITask Logout(UriString host); + void GetCurrentUser(Action onSuccess, Action onError = null); + void GetEnterpriseServerMeta(Action onSuccess, Action onError = null); + void CreateOAuthToken(string code, Action result); } } diff --git a/src/GitHub.Api/Application/IApplicationManager.cs b/src/GitHub.Api/Application/IApplicationManager.cs index 4642101ed..ab82a27d3 100644 --- a/src/GitHub.Api/Application/IApplicationManager.cs +++ b/src/GitHub.Api/Application/IApplicationManager.cs @@ -6,7 +6,6 @@ namespace GitHub.Unity { public interface IApplicationManager : IDisposable { - CancellationToken CancellationToken { get; } IEnvironment Environment { get; } IPlatform Platform { get; } IProcessEnvironment GitEnvironment { get; } @@ -17,10 +16,12 @@ public interface IApplicationManager : IDisposable ITaskManager TaskManager { get; } IGitClient GitClient { get; } IUsageTracker UsageTracker { get; } + IOAuthCallbackManager OAuthCallbackManager { get; } bool IsBusy { get; } - void Run(bool firstRun); - void RestartRepository(); - ITask InitializeRepository(); + void Run(); + void InitializeRepository(); event Action OnProgress; + void SetupGit(GitInstaller.GitInstallationState state); + void RestartRepository(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Application/Organization.cs b/src/GitHub.Api/Application/Organization.cs index e78849dd6..8deea7d99 100644 --- a/src/GitHub.Api/Application/Organization.cs +++ b/src/GitHub.Api/Application/Organization.cs @@ -1,8 +1,8 @@ namespace GitHub.Unity { - class Organization + public class Organization { public string Name { get; set; } public string Login { get; set; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/Credential.cs b/src/GitHub.Api/Authentication/Credential.cs index f7c74d3a5..86e76c458 100644 --- a/src/GitHub.Api/Authentication/Credential.cs +++ b/src/GitHub.Api/Authentication/Credential.cs @@ -16,9 +16,10 @@ public Credential(UriString host, string username, string token) this.Token = token; } - public void UpdateToken(string token) + public void Update(string token, string username) { this.Token = token; + this.Username = username; } public UriString Host { get; private set; } diff --git a/src/GitHub.Api/Authentication/ICredentialManager.cs b/src/GitHub.Api/Authentication/ICredentialManager.cs index 28742a6c7..94aef5a97 100644 --- a/src/GitHub.Api/Authentication/ICredentialManager.cs +++ b/src/GitHub.Api/Authentication/ICredentialManager.cs @@ -8,15 +8,14 @@ public interface ICredential : IDisposable UriString Host { get; } string Username { get; } string Token { get; } - void UpdateToken(string token); + void Update(string token, string username); } public interface ICredentialManager { - Task Load(UriString host); - Task Save(ICredential cred); - Task Delete(UriString host); + ICredential Load(UriString host); + void Save(ICredential cred); + void Delete(UriString host); bool HasCredentials(); - ICredential CachedCredentials { get; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/IKeychain.cs b/src/GitHub.Api/Authentication/IKeychain.cs index 4aa1bcb97..4a14e1e2f 100644 --- a/src/GitHub.Api/Authentication/IKeychain.cs +++ b/src/GitHub.Api/Authentication/IKeychain.cs @@ -1,23 +1,19 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace GitHub.Unity { public interface IKeychain { IKeychainAdapter Connect(UriString host); - Task Load(UriString host); - Task Clear(UriString host, bool deleteFromCredentialManager); - Task Save(UriString host); - void UpdateToken(UriString host, string token); - void SetCredentials(ICredential credential); + IKeychainAdapter LoadFromSystem(UriString host); + void Clear(UriString host, bool deleteFromCredentialManager); + void SaveToSystem(UriString host); void Initialize(); Connection[] Connections { get; } IList Hosts { get; } bool HasKeys { get; } - void SetToken(UriString host, string token); event Action ConnectionsChanged; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/ILoginManager.cs b/src/GitHub.Api/Authentication/ILoginManager.cs index 32defff79..c78112c0e 100644 --- a/src/GitHub.Api/Authentication/ILoginManager.cs +++ b/src/GitHub.Api/Authentication/ILoginManager.cs @@ -8,23 +8,32 @@ namespace GitHub.Unity interface ILoginManager { /// - /// Attempts to log into a GitHub server. + /// Attempts to log into a GitHub server with a username and password. /// - /// + /// The host. /// The username. /// The password. /// The logged in user. /// /// The login authorization failed. /// - Task Login(UriString host, string username, string password); - Task ContinueLogin(LoginResultData loginResultData, string twofacode); + LoginResultData Login(UriString host, string username, string password); + + LoginResultData ContinueLogin(LoginResultData loginResultData, string twofacode); /// /// Logs out of GitHub server. /// /// The address of the server. /// - Task Logout(UriString hostAddress); + ITask Logout(UriString hostAddress); + + /// + /// Attempts to log into a GitHub server with a token. + /// + /// The host. + /// The token. + /// + bool LoginWithToken(UriString host, string token); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 2b3c933e2..c71d45cbe 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using GitHub.Logging; namespace GitHub.Unity @@ -68,7 +67,7 @@ public bool Equals(Connection other) } } - class Keychain : IKeychain + public class Keychain : IKeychain { const string ConnectionFile = "connections.json"; @@ -95,32 +94,36 @@ public Keychain(IEnvironment environment, ICredentialManager credentialManager) public IKeychainAdapter Connect(UriString host) { + Guard.ArgumentNotNull(host, nameof(host)); return FindOrCreateAdapter(host); } - public async Task Load(UriString host) + public IKeychainAdapter LoadFromSystem(UriString host) { - var keychainAdapter = FindOrCreateAdapter(host); - var connection = GetConnection(host); + Guard.ArgumentNotNull(host, nameof(host)); - //logger.Trace($@"Loading KeychainAdapter Host:""{host}"" Cached Username:""{cachedConnection.Username}"""); - - var keychainItem = await credentialManager.Load(host); - if (keychainItem == null) + var keychainAdapter = Connect(host) as KeychainAdapter; + var credential = credentialManager.Load(host); + if (credential == null) { logger.Warning("Cannot load host from Credential Manager; removing from cache"); - await Clear(host, false); + Clear(host, false); keychainAdapter = null; } else { - if (keychainItem.Username != connection.Username) + keychainAdapter.Set(credential); + var connection = GetConnection(host); + if (connection.Username == null) { - logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", keychainItem.Username, connection.Username); + connection.Username = credential.Username; + SaveConnectionsToDisk(); } - //logger.Trace("Loaded from Credential Manager Host:\"{0}\" Username:\"{1}\"", keychainItem.Host, keychainItem.Username); - keychainAdapter.Set(keychainItem); + if (credential.Username != connection.Username) + { + logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", credential.Username, connection.Username); + } } return keychainAdapter; } @@ -138,56 +141,35 @@ private KeychainAdapter FindOrCreateAdapter(UriString host) public void Initialize() { - //logger.Trace("Initialize"); LoadConnectionsFromDisk(); } - public async Task Clear(UriString host, bool deleteFromCredentialManager) + public void Clear(UriString host, bool deleteFromCredentialManager) { - //logger.Trace("Clear Host:{0}", host); - //clear octokit credentials - await RemoveCredential(host, deleteFromCredentialManager); - RemoveConnection(host); - } + Guard.ArgumentNotNull(host, nameof(host)); - public async Task Save(UriString host) - { - //logger.Trace("Save: {0}", host); - var keychainAdapter = await AddCredential(host); - AddConnection(new Connection(host, keychainAdapter.Credential.Username)); - } + RemoveConnection(host); - public void SetCredentials(ICredential credential) - { - //logger.Trace("SetCredentials Host:{0}", credential.Host); - var keychainAdapter = GetKeychainAdapter(credential.Host); - keychainAdapter.Set(credential); + //clear octokit credentials + RemoveCredential(host, deleteFromCredentialManager); } - public void SetToken(UriString host, string token) + public void SaveToSystem(UriString host) { - //logger.Trace("SetToken Host:{0}", host); - var keychainAdapter = GetKeychainAdapter(host); - keychainAdapter.UpdateToken(token); - } + Guard.ArgumentNotNull(host, nameof(host)); - public void UpdateToken(UriString host, string token) - { - //logger.Trace("UpdateToken Host:{0}", host); - var keychainAdapter = GetKeychainAdapter(host); - var keychainItem = keychainAdapter.Credential; - keychainItem.UpdateToken(token); + var keychainAdapter = AddCredential(host); + AddConnection(new Connection(host, keychainAdapter.Credential.Username)); } private void LoadConnectionsFromDisk() { - //logger.Trace("ReadCacheFromDisk Path:{0}", cachePath.ToString()); if (cachePath.FileExists()) { var json = cachePath.ReadAllText(); try { - var conns = SimpleJson.DeserializeObject(json); + var conns = json.FromJson(); UpdateConnections(conns); } catch (IOException ex) @@ -205,10 +187,9 @@ private void LoadConnectionsFromDisk() private void SaveConnectionsToDisk(bool raiseChangedEvent = true) { - //logger.Trace("WriteCacheToDisk Count:{0} Path:{1}", connectionCache.Count, cachePath.ToString()); try { - var json = SimpleJson.SerializeObject(connections.Values.ToArray()); + var json = connections.Values.ToJson(); cachePath.WriteAllText(json); } catch (IOException ex) @@ -234,7 +215,7 @@ private KeychainAdapter GetKeychainAdapter(UriString host) return credentialAdapter; } - private async Task AddCredential(UriString host) + private KeychainAdapter AddCredential(UriString host) { var keychainAdapter = GetKeychainAdapter(host); if (string.IsNullOrEmpty(keychainAdapter.Credential.Token)) @@ -243,12 +224,12 @@ private async Task AddCredential(UriString host) } // saves credential in git credential manager (host, username, token) - await credentialManager.Delete(host); - await credentialManager.Save(keychainAdapter.Credential); + credentialManager.Delete(host); + credentialManager.Save(keychainAdapter.Credential); return keychainAdapter; } - private async Task RemoveCredential(UriString host, bool deleteFromCredentialManager) + private void RemoveCredential(UriString host, bool deleteFromCredentialManager) { KeychainAdapter k; if (keychainAdapters.TryGetValue(host, out k)) @@ -259,18 +240,18 @@ private async Task RemoveCredential(UriString host, bool deleteFromCredentialMan if (deleteFromCredentialManager) { - await credentialManager.Delete(host); + credentialManager.Delete(host); } } private Connection GetConnection(UriString host) { if (!connections.ContainsKey(host)) - throw new ArgumentException($"{host} is not found", nameof(host)); + return AddConnection(new Connection(host, null)); return connections[host]; } - private void AddConnection(Connection connection) + private Connection AddConnection(Connection connection) { // create new connection in the connection cache for this host if (connections.ContainsKey(connection.Host)) @@ -278,6 +259,7 @@ private void AddConnection(Connection connection) else connections.Add(connection.Host, connection); SaveConnectionsToDisk(); + return connection; } private void RemoveConnection(UriString host) @@ -290,15 +272,6 @@ private void RemoveConnection(UriString host) } } - private void RemoveAllConnections() - { - if (connections.Count > 0) - { - connections.Clear(); - SaveConnectionsToDisk(); - } - } - private void UpdateConnections(Connection[] conns) { var updated = false; @@ -326,4 +299,4 @@ private void UpdateConnections(Connection[] conns) public IList Hosts => connections.Keys.ToArray(); public bool HasKeys => connections.Any(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/KeychainAdapter.cs b/src/GitHub.Api/Authentication/KeychainAdapter.cs index 3fdde60b3..7a0c5d3aa 100644 --- a/src/GitHub.Api/Authentication/KeychainAdapter.cs +++ b/src/GitHub.Api/Authentication/KeychainAdapter.cs @@ -1,6 +1,6 @@ namespace GitHub.Unity { - class KeychainAdapter : IKeychainAdapter + public class KeychainAdapter : IKeychainAdapter { public ICredential Credential { get; private set; } @@ -9,9 +9,9 @@ public void Set(ICredential credential) Credential = credential; } - public void UpdateToken(string token) + public void Update(string token, string username) { - Credential.UpdateToken(token); + Credential.Update(token, username); } public void Clear() @@ -23,5 +23,8 @@ public void Clear() public interface IKeychainAdapter { ICredential Credential { get; } + void Set(ICredential credential); + void Update(string token, string username); + void Clear(); } } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index ce0a85ad2..6f892b659 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; +using System.Text; using GitHub.Logging; namespace GitHub.Unity @@ -18,49 +16,62 @@ public enum LoginResultCodes /// /// Provides services for logging into a GitHub server. /// - class LoginManager : ILoginManager + public class LoginManager : ILoginManager { private readonly ILogging logger = LogHelper.GetLogger(); private readonly IKeychain keychain; - private readonly string clientId; - private readonly string clientSecret; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; - private readonly NPath? nodeJsExecutablePath; - private readonly NPath? octorunScript; + private readonly IEnvironment environment; /// /// Initializes a new instance of the class. /// /// - /// The application's client API ID. - /// The application's client API secret. /// /// /// /// public LoginManager( - IKeychain keychain, - string clientId, - string clientSecret, - IProcessManager processManager = null, ITaskManager taskManager = null, NPath? nodeJsExecutablePath = null, NPath? octorunScript = null) + IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, + IEnvironment environment) { Guard.ArgumentNotNull(keychain, nameof(keychain)); - Guard.ArgumentNotNullOrWhiteSpace(clientId, nameof(clientId)); - Guard.ArgumentNotNullOrWhiteSpace(clientSecret, nameof(clientSecret)); this.keychain = keychain; - this.clientId = clientId; - this.clientSecret = clientSecret; this.processManager = processManager; this.taskManager = taskManager; - this.nodeJsExecutablePath = nodeJsExecutablePath; - this.octorunScript = octorunScript; + this.environment = environment; + } + + public bool LoginWithToken(UriString host, string token) + { + Guard.ArgumentNotNull(host, nameof(host)); + Guard.ArgumentNotNullOrWhiteSpace(token, nameof(token)); + + var keychainAdapter = keychain.Connect(host); + keychainAdapter.Set(new Credential(host, "[token]", token)); + + try + { + var username = RetrieveUsername(token, host); + keychainAdapter.Update(token, username); + keychain.SaveToSystem(host); + + return true; + } + catch (Exception e) + { + logger.Warning(e, "Login Exception"); + + keychain.Clear(host, false); + return false; + } } /// - public async Task Login( + public LoginResultData Login( UriString host, string username, string password) @@ -71,12 +82,12 @@ public async Task Login( // Start by saving the username and password, these will be used by the `IGitHubClient` // until an authorization token has been created and acquired: - keychain.Connect(host); - keychain.SetCredentials(new Credential(host, username, password)); + var keychainAdapter = keychain.Connect(host); + keychainAdapter.Set(new Credential(host, username, password)); try { - var loginResultData = await TryLogin(host, username, password); + var loginResultData = TryLogin(host, username, password); if (loginResultData.Code == LoginResultCodes.Success || loginResultData.Code == LoginResultCodes.CodeRequired) { if (string.IsNullOrEmpty(loginResultData.Token)) @@ -84,8 +95,14 @@ public async Task Login( throw new InvalidOperationException("Returned token is null or empty"); } - keychain.SetToken(host, loginResultData.Token); - await keychain.Save(host); + keychainAdapter.Update(loginResultData.Token, username); + + if (loginResultData.Code == LoginResultCodes.Success) + { + username = RetrieveUsername(loginResultData.Token, host); + keychainAdapter.Update(loginResultData.Token, username); + keychain.SaveToSystem(host); + } return loginResultData; } @@ -96,21 +113,23 @@ public async Task Login( { logger.Warning(e, "Login Exception"); - await keychain.Clear(host, false); + keychain.Clear(host, false); return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } } - public async Task ContinueLogin(LoginResultData loginResultData, string twofacode) + public LoginResultData ContinueLogin(LoginResultData loginResultData, string twofacode) { var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); + if (keychainAdapter.Credential == null) { + return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); + } var username = keychainAdapter.Credential.Username; var password = keychainAdapter.Credential.Token; try { - logger.Trace("2FA Continue"); - loginResultData = await TryLogin(host, username, password, twofacode); + loginResultData = TryLogin(host, username, password, twofacode); if (loginResultData.Code == LoginResultCodes.Success) { @@ -119,8 +138,10 @@ public async Task ContinueLogin(LoginResultData loginResultData throw new InvalidOperationException("Returned token is null or empty"); } - keychain.SetToken(host, loginResultData.Token); - await keychain.Save(host); + keychainAdapter.Update(loginResultData.Token, username); + username = RetrieveUsername(loginResultData.Token, host); + keychainAdapter.Update(loginResultData.Token, username); + keychain.SaveToSystem(host); return loginResultData; } @@ -131,42 +152,42 @@ public async Task ContinueLogin(LoginResultData loginResultData { logger.Warning(e, "Login Exception"); - await keychain.Clear(host, false); + keychain.Clear(host, false); return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } } /// - public async Task Logout(UriString hostAddress) + public ITask Logout(UriString hostAddress) { Guard.ArgumentNotNull(hostAddress, nameof(hostAddress)); - - await new ActionTask(keychain.Clear(hostAddress, true)).StartAwait(); + return taskManager.Run(() => keychain.Clear(hostAddress, true), "Signing out"); } - private async Task TryLogin( + private LoginResultData TryLogin( UriString host, string username, string password, string code = null ) { - if (!nodeJsExecutablePath.HasValue) + var hasTwoFactorCode = code != null; + + var command = new StringBuilder("login"); + + if (hasTwoFactorCode) { - throw new InvalidOperationException("nodeJsExecutablePath must be set"); + command.Append(" --twoFactor"); } - if (!octorunScript.HasValue) + if (!HostAddress.IsGitHubDotCom(host)) { - throw new InvalidOperationException("octorunScript must be set"); + command.Append(" -h "); + command.Append(host.Host); } - var hasTwoFactorCode = code != null; - - var arguments = hasTwoFactorCode ? "login --twoFactor" : "login"; - var loginTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, - arguments, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret); - loginTask.Configure(processManager, workingDirectory: octorunScript.Value.Parent.Parent, withInput: true); + var loginTask = new OctorunTask(taskManager.Token, environment, command.ToString()); + loginTask.Configure(processManager, withInput: true); loginTask.OnStartProcess += proc => { proc.StandardInput.WriteLine(username); @@ -178,7 +199,7 @@ private async Task TryLogin( proc.StandardInput.Close(); }; - var ret = await loginTask.StartAwait(); + var ret = loginTask.RunSynchronously(); if (ret.IsSuccess) { @@ -195,9 +216,24 @@ private async Task TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } + + private string RetrieveUsername(string token, UriString host) + { + var command = HostAddress.IsGitHubDotCom(host) ? "validate" : "validate -h " + host.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, token) + .Configure(processManager); + + var validateResult = octorunTask.RunSynchronously(); + if (!validateResult.IsSuccess) + { + throw new InvalidOperationException("Authentication validation failed"); + } + + return validateResult.Output[1]; + } } - class LoginResultData + public class LoginResultData { public LoginResultCodes Code; public string Message; diff --git a/src/GitHub.Api/Authentication/OAuthCallbackManager.cs b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs new file mode 100644 index 000000000..b5b75b094 --- /dev/null +++ b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using GitHub.Logging; + +namespace GitHub.Unity +{ + public interface IOAuthCallbackManager + { + event Action OnCallback; + bool IsRunning { get; } + void Start(); + void Stop(); + } + + public class OAuthCallbackManager : IOAuthCallbackManager + { + const int CallbackPort = 42424; + public static readonly Uri CallbackUrl = new Uri($"http://localhost:{CallbackPort}/callback"); + + private static readonly ILogging logger = LogHelper.GetLogger(); + private static readonly object _lock = new object(); + + + private readonly CancellationTokenSource cancelSource; + + private HttpListener httpListener; + public bool IsRunning { get; private set; } + + public event Action OnCallback; + + public OAuthCallbackManager() + { + cancelSource = new CancellationTokenSource(); + } + + public void Start() + { + if (!IsRunning) + { + lock(_lock) + { + if (!IsRunning) + { + logger.Trace("Starting"); + + httpListener = new HttpListener(); + httpListener.Prefixes.Add(CallbackUrl.AbsoluteUri + "/"); + httpListener.Start(); + Task.Factory.StartNew(Listen, cancelSource.Token); + IsRunning = true; + } + } + } + } + + public void Stop() + { + logger.Trace("Stopping"); + cancelSource.Cancel(); + } + + private void Listen() + { + try + { + using (httpListener) + { + using (cancelSource.Token.Register(httpListener.Stop)) + { + while (true) + { + var context = httpListener.GetContext(); + var queryParts = HttpUtility.ParseQueryString(context.Request.Url.Query); + + var state = queryParts["state"]; + var code = queryParts["code"]; + + logger.Trace("OnCallback: {0}", state); + if (OnCallback != null) + { + OnCallback(state, code); + } + + context.Response.StatusCode = 200; + context.Response.Close(); + } + } + } + } + catch (Exception ex) + { + logger.Trace(ex.Message); + } + finally + { + IsRunning = false; + httpListener = null; + } + } + } +} diff --git a/src/GitHub.Api/Cache/CacheContainer.cs b/src/GitHub.Api/Cache/CacheContainer.cs index f8de8c283..2c07ff867 100644 --- a/src/GitHub.Api/Cache/CacheContainer.cs +++ b/src/GitHub.Api/Cache/CacheContainer.cs @@ -30,9 +30,7 @@ public void InvalidateAll() { foreach (var cache in caches.Values) { - // force an invalidation if the cache is valid, otherwise it will do it on its own - if (cache.Value.ValidateData()) - cache.Value.InvalidateData(); + cache.Value.InvalidateData(); } } @@ -92,6 +90,7 @@ public void Dispose() public IBranchCache BranchCache { get { return (IBranchCache)caches[CacheType.Branches].Value; } } public IGitLogCache GitLogCache { get { return (IGitLogCache)caches[CacheType.GitLog].Value; } } + public IGitFileLogCache GitFileLogCache { get { return (IGitFileLogCache)caches[CacheType.GitFileLog].Value; } } public IGitAheadBehindCache GitTrackingStatusCache { get { return (IGitAheadBehindCache)caches[CacheType.GitAheadBehind].Value; } } public IGitStatusCache GitStatusEntriesCache { get { return (IGitStatusCache)caches[CacheType.GitStatus].Value; } } public IGitLocksCache GitLocksCache { get { return (IGitLocksCache)caches[CacheType.GitLocks].Value; } } @@ -164,7 +163,7 @@ public DateTimeOffset UpdatedTime if (!updatedTimeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(updatedTimeString, Constants.Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(updatedTimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { updatedTimeValue = result; } diff --git a/src/GitHub.Api/Cache/CacheInterfaces.cs b/src/GitHub.Api/Cache/CacheInterfaces.cs index 810db7774..ae815beac 100644 --- a/src/GitHub.Api/Cache/CacheInterfaces.cs +++ b/src/GitHub.Api/Cache/CacheInterfaces.cs @@ -9,6 +9,7 @@ public enum CacheType RepositoryInfo, Branches, GitLog, + GitFileLog, GitAheadBehind, GitStatus, GitLocks, @@ -22,6 +23,7 @@ public interface ICacheContainer : IDisposable IBranchCache BranchCache { get; } IGitLogCache GitLogCache { get; } + IGitFileLogCache GitFileLogCache { get; } IGitAheadBehindCache GitTrackingStatusCache { get; } IGitStatusCache GitStatusEntriesCache { get; } IGitLocksCache GitLocksCache { get; } @@ -40,6 +42,7 @@ public interface IManagedCache bool ValidateData(); void InvalidateData(); + void ResetInvalidation(); DateTimeOffset LastUpdatedAt { get; } CacheType CacheType { get; } @@ -91,7 +94,7 @@ public interface IBranchCache : IManagedCache ILocalConfigBranchDictionary LocalConfigBranches { get; } IRemoteConfigBranchDictionary RemoteConfigBranches { get; } IConfigRemoteDictionary ConfigRemotes { get; } - + void SetRemotes(Dictionary remoteConfigs, Dictionary> configBranches, GitRemote[] gitRemotes, GitBranch[] gitBranches); void SetLocals(Dictionary configBranches, GitBranch[] gitBranches); } @@ -102,6 +105,7 @@ public interface IRepositoryInfoCacheData GitBranch? CurrentGitBranch { get; } ConfigRemote? CurrentConfigRemote { get; } ConfigBranch? CurrentConfigBranch { get; } + string CurrentHead { get; } } public interface IRepositoryInfoCache : IManagedCache, IRepositoryInfoCacheData, ICanUpdate @@ -113,6 +117,11 @@ public interface IGitLogCache : IManagedCache List Log { get; set; } } + public interface IGitFileLogCache : IManagedCache + { + GitFileLog FileLog { get; set; } + } + public interface ICanUpdate { void UpdateData(T data); diff --git a/src/GitHub.Api/Cache/CachingClasses.cs b/src/GitHub.Api/Cache/CachingClasses.cs index 0f02368f6..a93f8c586 100644 --- a/src/GitHub.Api/Cache/CachingClasses.cs +++ b/src/GitHub.Api/Cache/CachingClasses.cs @@ -14,5 +14,6 @@ sealed class RepositoryInfoCacheData : IRepositoryInfoCacheData public GitBranch? CurrentGitBranch { get; set; } public ConfigRemote? CurrentConfigRemote { get; set; } public ConfigBranch? CurrentConfigBranch { get; set; } + public string CurrentHead { get; set; } } } diff --git a/src/GitHub.Api/Events/RepositoryWatcher.cs b/src/GitHub.Api/Events/RepositoryWatcher.cs index 3ac349cd1..a9c852fc6 100644 --- a/src/GitHub.Api/Events/RepositoryWatcher.cs +++ b/src/GitHub.Api/Events/RepositoryWatcher.cs @@ -8,7 +8,7 @@ namespace GitHub.Unity { - interface IRepositoryWatcher : IDisposable + public interface IRepositoryWatcher : IDisposable { void Start(); void Stop(); @@ -23,13 +23,14 @@ interface IRepositoryWatcher : IDisposable int CheckAndProcessEvents(); } - class RepositoryWatcher : IRepositoryWatcher + public class RepositoryWatcher : IRepositoryWatcher { private readonly RepositoryPathConfiguration paths; private readonly CancellationToken cancellationToken; private readonly NPath[] ignoredPaths; private readonly ManualResetEventSlim pauseEvent; private NativeInterface nativeInterface; + private NativeInterface worktreeNativeInterface; private bool running; private int lastCountOfProcessedEvents = 0; private bool processingEvents; @@ -64,6 +65,11 @@ public void Initialize() try { nativeInterface = new NativeInterface(pathsRepositoryPath); + + if (paths.IsWorktree) + { + worktreeNativeInterface = new NativeInterface(paths.WorktreeDotGitPath); + } } catch (Exception ex) { @@ -81,6 +87,17 @@ public void Start() Logger.Trace("Watching Path: \"{0}\"", paths.RepositoryPath.ToString()); + if (paths.IsWorktree) + { + if (worktreeNativeInterface == null) + { + Logger.Warning("Worktree NativeInterface is null"); + throw new InvalidOperationException("Worktree NativeInterface is null"); + } + + Logger.Trace("Watching Additional Path for Worktree: \"{0}\"", paths.WorktreeDotGitPath); + } + running = true; pauseEvent.Reset(); Task.Factory.StartNew(WatcherLoop, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); @@ -89,12 +106,7 @@ public void Start() public void Stop() { if (!running) - { - //Logger.Warning("Watcher already stopped"); return; - } - - //Logger.Trace("Stopping watcher"); running = false; pauseEvent.Set(); @@ -102,8 +114,6 @@ public void Stop() private void WatcherLoop() { - //Logger.Trace("Starting watcher"); - while (running) { if (cancellationToken.IsCancellationRequested) @@ -136,9 +146,16 @@ public int CheckAndProcessEvents() var fileEvents = nativeInterface.GetEvents(); if (fileEvents.Length > 0) { - //Logger.Trace("Handling {0} Events", fileEvents.Length); processedEventCount = ProcessEvents(fileEvents); - //Logger.Trace("Processed {0} Events", processedEventCount); + } + + if (worktreeNativeInterface != null) + { + fileEvents = worktreeNativeInterface.GetEvents(); + if (fileEvents.Length > 0) + { + processedEventCount = processedEventCount + ProcessEvents(fileEvents); + } } lastCountOfProcessedEvents = processedEventCount; @@ -164,13 +181,11 @@ private int ProcessEvents(Event[] fileEvents) break; } - //Logger.Trace(fileEvent.Describe()); - var eventDirectory = new NPath(fileEvent.Directory); var fileA = eventDirectory.Combine(fileEvent.FileA); // handling events in .git/* - if (fileA.IsChildOf(paths.DotGitPath)) + if (fileA.IsChildOf(paths.DotGitPath) || (paths.WorktreeDotGitPath.IsInitialized && fileA.IsChildOf(paths.WorktreeDotGitPath))) { if (!events.Contains(EventType.ConfigChanged) && fileA.Equals(paths.DotGitConfig)) { @@ -215,28 +230,24 @@ private int FireEvents(HashSet events) int eventsProcessed = 0; if (events.Contains(EventType.ConfigChanged)) { - //Logger.Trace("ConfigChanged"); ConfigChanged?.Invoke(); eventsProcessed++; } if (events.Contains(EventType.HeadChanged)) { - //Logger.Trace("HeadChanged"); HeadChanged?.Invoke(); eventsProcessed++; } if (events.Contains(EventType.LocalBranchesChanged)) { - //Logger.Trace("LocalBranchesChanged"); LocalBranchesChanged?.Invoke(); eventsProcessed++; } if (events.Contains(EventType.RemoteBranchesChanged)) { - //Logger.Trace("RemoteBranchesChanged"); RemoteBranchesChanged?.Invoke(); eventsProcessed++; } @@ -245,7 +256,6 @@ private int FireEvents(HashSet events) { if (!events.Contains(EventType.RepositoryChanged)) { - //Logger.Trace("IndexChanged"); IndexChanged?.Invoke(); eventsProcessed++; } @@ -253,14 +263,12 @@ private int FireEvents(HashSet events) if (events.Contains(EventType.RepositoryChanged)) { - //Logger.Trace("RepositoryChanged"); RepositoryChanged?.Invoke(); eventsProcessed++; } if (events.Contains(EventType.RepositoryCommitted)) { - //Logger.Trace("RepositoryCommitted"); RepositoryCommitted?.Invoke(); eventsProcessed++; } diff --git a/src/GitHub.Api/Extensions/EnvironmentExtensions.cs b/src/GitHub.Api/Extensions/EnvironmentExtensions.cs index 6baff476c..ca69c7d81 100644 --- a/src/GitHub.Api/Extensions/EnvironmentExtensions.cs +++ b/src/GitHub.Api/Extensions/EnvironmentExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace GitHub.Unity { @@ -41,5 +44,15 @@ public static NPath GetAssetPath(this IEnvironment environment, NPath path) return repositoryPath.Combine(path).MakeAbsolute().RelativeTo(projectPath); } + + public static IEnumerable ToNPathList(this string envPath, IEnvironment environment) + { + return envPath + .Split(Path.PathSeparator) + .Where(x => x != null) + .Select(x => environment.ExpandEnvironmentVariables(x.Trim('"', '\''))) + .Where(x => !String.IsNullOrEmpty(x)) + .Select(x => x.ToNPath()); + } } } diff --git a/src/GitHub.Api/Extensions/ListExtensions.cs b/src/GitHub.Api/Extensions/ListExtensions.cs index 839f58d78..bc20ed376 100644 --- a/src/GitHub.Api/Extensions/ListExtensions.cs +++ b/src/GitHub.Api/Extensions/ListExtensions.cs @@ -12,7 +12,7 @@ public static string Join(this IEnumerable list, string separator) return null; return String.Join(separator, list.Select(x => x?.ToString()).ToArray()); } - + public static IEnumerable> Spool(this IEnumerable items, int spoolLength) { var currentSpoolLength = 0; @@ -40,5 +40,15 @@ public static IEnumerable> Spool(this IEnumerable items, int yield return currentList; } } + + public static T[] Append(this T[] array, T item) + { + if (array == null) + return new T[] { item }; + var ret = new T[array.Length]; + array.CopyTo(ret, 0); + ret[ret.Length - 1] = item; + return ret; + } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Extensions/StringExtensions.cs b/src/GitHub.Api/Extensions/StringExtensions.cs index 0f6eae53d..ebb2c4351 100644 --- a/src/GitHub.Api/Extensions/StringExtensions.cs +++ b/src/GitHub.Api/Extensions/StringExtensions.cs @@ -7,6 +7,22 @@ namespace GitHub.Unity { + static class DateTimeOffsetExtensions + { + private const string Today = "Today"; + private const string Yesterday = "Yesterday"; + + public static string CreateRelativeTime(this DateTimeOffset @from, DateTimeOffset to) + { + return String.Format("{0}, {1:HH}:{1:mm}", + @from.DayOfYear == to.DayOfYear + ? Today + : @from.DayOfYear == to.DayOfYear - 1 + ? Yesterday + : @from.ToString("d MMM yyyy"), @from); + } + } + static class StringExtensions { public static bool Contains(this string s, string expectedSubstring, StringComparison comparison) @@ -24,6 +40,11 @@ public static string ToNullIfEmpty(this string s) return String.IsNullOrEmpty(s) ? null : s; } + public static string ToEmptyIfNull(this string s) + { + return String.IsNullOrEmpty(s) ? String.Empty : s; + } + public static bool StartsWith(this string s, char c) { if (String.IsNullOrEmpty(s)) return false; @@ -82,24 +103,6 @@ public static string JoinForAppending(string separator, IEnumerable valu : string.Empty; } - public static string RemoveSurroundingQuotes(this string s) - { - Guard.ArgumentNotNull(s, "string"); - - if (s.Length < 2) - return s; - - var quoteCharacters = new[] { '"', '\'' }; - char firstCharacter = s[0]; - if (!quoteCharacters.Contains(firstCharacter)) - return s; - - if (firstCharacter != s[s.Length - 1]) - return s; - - return s.Substring(1, s.Length - 2); - } - public static string RightAfter(this string s, char search) { if (s == null) return null; diff --git a/src/GitHub.Api/Git/FailureSeverity.cs b/src/GitHub.Api/Git/FailureSeverity.cs index fdaa58345..3d34f95ef 100644 --- a/src/GitHub.Api/Git/FailureSeverity.cs +++ b/src/GitHub.Api/Git/FailureSeverity.cs @@ -1,8 +1,8 @@ namespace GitHub.Unity { - enum FailureSeverity + public enum FailureSeverity { Moderate, Critical }; -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitBranch.cs b/src/GitHub.Api/Git/GitBranch.cs index 857212810..ecb6fede9 100644 --- a/src/GitHub.Api/Git/GitBranch.cs +++ b/src/GitHub.Api/Git/GitBranch.cs @@ -10,12 +10,12 @@ public struct GitBranch public string name; public string tracking; - public GitBranch(string name, string tracking) + public GitBranch(string name, string tracking = null) { Guard.ArgumentNotNullOrWhiteSpace(name, "name"); this.name = name; - this.tracking = tracking; + this.tracking = tracking ?? string.Empty; } public override int GetHashCode() @@ -64,7 +64,7 @@ public bool Equals(GitBranch other) public override string ToString() { - return $"{Name} Tracking? {Tracking}"; + return $"{Name} Tracking? {Tracking ?? "[NULL]"}"; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index ebc337aaa..6aca3146b 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -1,101 +1,311 @@ using GitHub.Logging; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; -using System.Threading.Tasks; +using GitHub.Unity.Git.Tasks; +using static GitHub.Unity.GitInstaller; namespace GitHub.Unity { + /// + /// Client that provides access to git functionality + /// public interface IGitClient { - ITask ValidateGitInstall(NPath path, bool isCustomGit); - - ITask Init(IOutputProcessor processor = null); - - ITask LfsInstall(IOutputProcessor processor = null); - - ITask AheadBehindStatus(string gitRef, string otherRef, - IOutputProcessor processor = null); - + /// + /// Executes `git init` to initialize a git repo. + /// + /// A custom output processor instance + /// String output of git command + ITask Init(IOutputProcessor processor = null); + + /// + /// Executes `git lfs install` to install LFS hooks. + /// + /// A custom output processor instance + /// String output of git command + ITask LfsInstall(IOutputProcessor processor = null); + + /// + /// Executes `git rev-list` to determine the ahead/behind status between two refs. + /// + /// Ref to compare + /// Ref to compare against + /// A custom output processor instance + /// output + ITask AheadBehindStatus(string gitRef, string otherRef, IOutputProcessor processor = null); + + /// + /// Executes `git status` to determine the working directory status. + /// + /// A custom output processor instance + /// output ITask Status(IOutputProcessor processor = null); - ITask GetConfig(string key, GitConfigSource configSource, - IOutputProcessor processor = null); - - ITask SetConfig(string key, string value, GitConfigSource configSource, - IOutputProcessor processor = null); - + /// + /// Executes `git config get` to get a configuration value. + /// + /// The configuration key to get + /// The config source (unspecified, local,user,global) to use + /// A custom output processor instance + /// String output of git command + ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null); + + /// + /// Executes `git config set` to set a configuration value. + /// + /// The configuration key to set + /// The value to set + /// The config source (unspecified, local,user,global) to use + /// A custom output processor instance + /// String output of git command + ITask SetConfig(string key, string value, GitConfigSource configSource, IOutputProcessor processor = null); + + /// + /// Executes `git config --unset` to remove a configuration value. + /// + /// The configuration key to remove + /// The config source (unspecified, local,user,global) to use + /// A custom output processor instance + /// String output of git command + ITask UnSetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null); + + /// + /// Executes two `git config get` commands to get the git user and email. + /// + /// output ITask GetConfigUserAndEmail(); - ITask> ListLocks(bool local, - BaseOutputListProcessor processor = null); - - ITask Pull(string remote, string branch, - IOutputProcessor processor = null); - - ITask Push(string remote, string branch, - IOutputProcessor processor = null); - - ITask Revert(string changeset, - IOutputProcessor processor = null); - - ITask Fetch(string remote, - IOutputProcessor processor = null); - - ITask SwitchBranch(string branch, - IOutputProcessor processor = null); - - ITask DeleteBranch(string branch, bool deleteUnmerged = false, - IOutputProcessor processor = null); - - ITask CreateBranch(string branch, string baseBranch, - IOutputProcessor processor = null); - - ITask RemoteAdd(string remote, string url, - IOutputProcessor processor = null); - - ITask RemoteRemove(string remote, - IOutputProcessor processor = null); - - ITask RemoteChange(string remote, string url, - IOutputProcessor processor = null); - - ITask Commit(string message, string body, - IOutputProcessor processor = null); - - ITask Add(IList files, - IOutputProcessor processor = null); - + /// + /// Executes `git lfs locks` to get a list of lfs locks from the git lfs server. + /// + /// + /// A custom output processor instance + /// of output + ITask> ListLocks(bool local, BaseOutputListProcessor processor = null); + + /// + /// Executes `git pull` to perform a pull operation. + /// + /// The remote to pull from + /// The branch to pull + /// A custom output processor instance + /// String output of git command + ITask Pull(string remote, string branch, IOutputProcessor processor = null); + + /// + /// Executes `git push` to perform a push operation. + /// + /// The remote to push to + /// The branch to push + /// A custom output processor instance + /// String output of git command + ITask Push(string remote, string branch, IOutputProcessor processor = null); + + /// + /// Executes `git revert` to perform a revert operation. + /// + /// The changeset to revert + /// A custom output processor instance + /// String output of git command + ITask Revert(string changeset, IOutputProcessor processor = null); + + /// + /// Executes `git fetch` to perform a fetch operation. + /// + /// The remote to fetch from + /// A custom output processor instance + /// String output of git command + ITask Fetch(string remote, IOutputProcessor processor = null); + + /// + /// Executes `git checkout` to switch branches. + /// + /// The branch to checkout + /// A custom output processor instance + /// String output of git command + ITask SwitchBranch(string branch, IOutputProcessor processor = null); + + /// + /// Executes `git branch -d` to delete a branch. + /// + /// The branch to delete + /// The flag to indicate the branch should be deleted even if not merged + /// A custom output processor instance + /// String output of git command + ITask DeleteBranch(string branch, bool deleteUnmerged = false, IOutputProcessor processor = null); + + /// + /// Executes `git branch` to create a branch. + /// + /// The name of branch to create + /// The name of branch to create from + /// A custom output processor instance + /// String output of git command + ITask CreateBranch(string branch, string baseBranch, IOutputProcessor processor = null); + + /// + /// Executes `git remote add` to add a git remote. + /// + /// The remote to add + /// The url of the remote + /// A custom output processor instance + /// String output of git command + ITask RemoteAdd(string remote, string url, IOutputProcessor processor = null); + + /// + /// Executes `git remote rm` to remove a git remote. + /// + /// The remote to remove + /// A custom output processor instance + /// String output of git command + ITask RemoteRemove(string remote, IOutputProcessor processor = null); + + /// + /// Executes `git remote set-url` to change the url of a git remote. + /// + /// The remote to change + /// The url to change to + /// A custom output processor instance + /// String output of git command + ITask RemoteChange(string remote, string url, IOutputProcessor processor = null); + + /// + /// Executes `git commit` to perform a commit operation. + /// + /// The commit message summary + /// The commit message body + /// A custom output processor instance + /// String output of git command + ITask Commit(string message, string body, IOutputProcessor processor = null); + + /// + /// Executes at least one `git add` command to add the list of files to the git index. + /// + /// The file to add + /// A custom output processor instance + /// String output of git command + ITask Add(IList files, IOutputProcessor processor = null); + + /// + /// Executes `git add -A` to add all files to the git index. + /// + /// A custom output processor instance + /// String output of git command ITask AddAll(IOutputProcessor processor = null); - ITask Discard(IList files, - IOutputProcessor processor = null); - + /// + /// Executes at least one `git checkout` command to discard changes to the list of files. + /// + /// The files to discard + /// A custom output processor instance + /// String output of git command + ITask Discard(IList files, IOutputProcessor processor = null); + + /// + /// Executes `git checkout -- .` to discard all changes in the working directory. + /// + /// A custom output processor instance + /// String output of git command ITask DiscardAll(IOutputProcessor processor = null); - ITask Remove(IList files, - IOutputProcessor processor = null); - - ITask AddAndCommit(IList files, string message, string body, - IOutputProcessor processor = null); - - ITask Lock(string file, - IOutputProcessor processor = null); - - ITask Unlock(string file, bool force, - IOutputProcessor processor = null); - + /// + /// Executes at least one `git checkout` command to checkout files at the given changeset + /// + /// The md5 of the changeset + /// The files to check out + /// A custom output processor instance + /// String output of git command + ITask CheckoutVersion(string changeset, IList files, IOutputProcessor processor = null); + + /// + /// Executes at least one `git reset HEAD` command to remove files from the git index. + /// + /// The files to remove + /// A custom output processor instance + /// String output of git command + ITask Remove(IList files, IOutputProcessor processor = null); + + /// + /// Executes at least one `git add` command to add the list of files to the git index. Followed by a `git commit` command to commit the changes. + /// + /// The files to add and commit + /// The commit message summary + /// The commit message body + /// A custom output processor instance + /// String output of git command + ITask AddAndCommit(IList files, string message, string body, IOutputProcessor processor = null); + + /// + /// Executes `git lfs lock` to lock a file. + /// + /// The file to lock + /// A custom output processor instance + /// String output of git command + ITask Lock(NPath file, IOutputProcessor processor = null); + + /// + /// Executes `git lfs unlock` to unlock a file. + /// + /// The file to unlock + /// If force should be used + /// A custom output processor instance + /// String output of git command + ITask Unlock(NPath file, bool force, IOutputProcessor processor = null); + + /// + /// Executes `git log` to get the history of the current branch. + /// + /// A custom output processor instance + /// of output ITask> Log(BaseOutputListProcessor processor = null); - ITask Version(IOutputProcessor processor = null); - - ITask LfsVersion(IOutputProcessor processor = null); - + /// + /// Executes `git log -- ` to get the history of a specific file. + /// + /// + /// A custom output processor instance + /// of output + ITask> LogFile(string file, BaseOutputListProcessor processor = null); + + /// + /// Executes `git --version` to get the git version. + /// + /// A custom output processor instance + /// output + ITask Version(IOutputProcessor processor = null); + + /// + /// Executes `git lfs version` to get the git lfs version. + /// + /// A custom output processor instance + /// output + ITask LfsVersion(IOutputProcessor processor = null); + + /// + /// Executes `git count-objects` to get the size of the git repo in kilobytes. + /// + /// A custom output processor instance + /// output + ITask CountObjects(IOutputProcessor processor = null); + + /// + /// Executes two `git set config` commands to set the git name and email. + /// + /// The username to set + /// The email to set + /// output ITask SetConfigNameAndEmail(string username, string email); + + /// + /// Executes `git rev-parse --short HEAD` to get the current commit sha of the current branch. + /// + /// A custom output processor instance + /// String output of git command + ITask GetHead(IOutputProcessor processor = null); } - class GitClient : IGitClient + public class GitClient : IGitClient { private const string UserNameConfigKey = "user.name"; private const string UserEmailConfigKey = "user.email"; @@ -110,103 +320,104 @@ public GitClient(IEnvironment environment, IProcessManager processManager, Cance this.cancellationToken = cancellationToken; } - public ITask ValidateGitInstall(NPath path, bool isCustomGit) - { - Version gitVersion = null; - Version gitLfsVersion = null; - - var endTask = new FuncTask(cancellationToken, - () => new ValidateGitInstallResult( - gitVersion?.CompareTo(Constants.MinimumGitVersion) >= 0 && - gitLfsVersion?.CompareTo(Constants.MinimumGitLfsVersion) >= 0, - gitVersion, gitLfsVersion)); - - if (path.FileExists()) - { - var gitLfsVersionTask = new GitLfsVersionTask(cancellationToken) - .Configure(processManager, path, dontSetupGit: isCustomGit); - gitLfsVersionTask.OnEnd += (t, v, _, __) => gitLfsVersion = v; - var gitVersionTask = new GitVersionTask(cancellationToken) - .Configure(processManager, path, dontSetupGit: isCustomGit); - gitVersionTask.OnEnd += (t, v, _, __) => gitVersion = v; - - gitVersionTask - .Then(gitLfsVersionTask) - .Finally(endTask); - } - return endTask; - } - - public ITask Init(IOutputProcessor processor = null) + /// + public ITask Init(IOutputProcessor processor = null) { return new GitInitTask(cancellationToken, processor) .Configure(processManager); } - public ITask LfsInstall(IOutputProcessor processor = null) + /// + public ITask LfsInstall(IOutputProcessor processor = null) { - //Logger.Trace("LfsInstall"); - return new GitLfsInstallTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask Status(IOutputProcessor processor = null) { - //Logger.Trace("Status"); - return new GitStatusTask(new GitObjectFactory(environment), cancellationToken, processor) .Configure(processManager); } + /// public ITask AheadBehindStatus(string gitRef, string otherRef, IOutputProcessor processor = null) { - //Logger.Trace("AheadBehindStatus"); - return new GitAheadBehindStatusTask(gitRef, otherRef, cancellationToken, processor) .Configure(processManager); } + /// public ITask> Log(BaseOutputListProcessor processor = null) { - //Logger.Trace("Log"); - return new GitLogTask(new GitObjectFactory(environment), cancellationToken, processor) - .Configure(processManager); + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, list) => success ? list : new List()); } - public ITask Version(IOutputProcessor processor = null) + /// + public ITask> LogFile(string file, BaseOutputListProcessor processor = null) { - //Logger.Trace("Version"); + if (file == NPath.Default) + { + return new FuncTask>(cancellationToken, () => new List(0)); + } + + return new GitLogTask(file, new GitObjectFactory(environment), cancellationToken, processor) + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, list) => success ? list : new List()); + } + /// + public ITask Version(IOutputProcessor processor = null) + { return new GitVersionTask(cancellationToken, processor) .Configure(processManager); } - public ITask LfsVersion(IOutputProcessor processor = null) + /// + public ITask LfsVersion(IOutputProcessor processor = null) { - //Logger.Trace("LfsVersion"); - return new GitLfsVersionTask(cancellationToken, processor) .Configure(processManager); } - public ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null) + /// + public ITask CountObjects(IOutputProcessor processor = null) { - //Logger.Trace("GetConfig: {0}", key); + return new GitCountObjectsTask(cancellationToken, processor) + .Configure(processManager); + } + /// + public ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null) + { return new GitConfigGetTask(key, configSource, cancellationToken, processor) .Configure(processManager); } + /// public ITask SetConfig(string key, string value, GitConfigSource configSource, IOutputProcessor processor = null) { - //Logger.Trace("SetConfig"); - return new GitConfigSetTask(key, value, configSource, cancellationToken, processor) .Configure(processManager); } + /// + public ITask UnSetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null) + { + return new GitConfigUnSetTask(key, configSource, cancellationToken, processor) + .Configure(processManager); + } + + /// public ITask GetConfigUserAndEmail() { string username = null; @@ -226,11 +437,11 @@ public ITask GetConfigUserAndEmail() email = value; } })).Then(success => { - //Logger.Trace("{0}:{1} {2}:{3}", UserNameConfigKey, username, UserEmailConfigKey, email); return new GitUser(username, email); }); } + /// public ITask SetConfigNameAndEmail(string username, string email) { return SetConfig(UserNameConfigKey, username, GitConfigSource.User) @@ -238,123 +449,109 @@ public ITask SetConfigNameAndEmail(string username, string email) .Then(b => new GitUser(username, email)); } + /// public ITask> ListLocks(bool local, BaseOutputListProcessor processor = null) { - //Logger.Trace("ListLocks"); - - return new GitListLocksTask(new GitObjectFactory(environment), local, cancellationToken, processor) - .Configure(processManager); + return new GitListLocksTask(local, cancellationToken, processor) + .Configure(processManager, environment.GitLfsExecutablePath); } + /// public ITask Pull(string remote, string branch, IOutputProcessor processor = null) { - //Logger.Trace("Pull"); - return new GitPullTask(remote, branch, cancellationToken, processor) .Configure(processManager); } + /// public ITask Push(string remote, string branch, IOutputProcessor processor = null) { - //Logger.Trace("Push"); - return new GitPushTask(remote, branch, true, cancellationToken, processor) .Configure(processManager); } + /// public ITask Revert(string changeset, IOutputProcessor processor = null) { - //Logger.Trace("Revert"); - return new GitRevertTask(changeset, cancellationToken, processor) .Configure(processManager); } + /// public ITask Fetch(string remote, IOutputProcessor processor = null) { - //Logger.Trace("Fetch"); - - return new GitFetchTask(remote, cancellationToken, true, processor) + return new GitFetchTask(remote, cancellationToken, processor: processor) .Configure(processManager); } + /// public ITask SwitchBranch(string branch, IOutputProcessor processor = null) { - //Logger.Trace("SwitchBranch"); - return new GitSwitchBranchesTask(branch, cancellationToken, processor) .Configure(processManager); } + /// public ITask DeleteBranch(string branch, bool deleteUnmerged = false, IOutputProcessor processor = null) { - //Logger.Trace("DeleteBranch"); - return new GitBranchDeleteTask(branch, deleteUnmerged, cancellationToken, processor) .Configure(processManager); } + /// public ITask CreateBranch(string branch, string baseBranch, IOutputProcessor processor = null) { - //Logger.Trace("CreateBranch"); - return new GitBranchCreateTask(branch, baseBranch, cancellationToken, processor) .Configure(processManager); } + /// public ITask RemoteAdd(string remote, string url, IOutputProcessor processor = null) { - //Logger.Trace("RemoteAdd"); - return new GitRemoteAddTask(remote, url, cancellationToken, processor) .Configure(processManager); } + /// public ITask RemoteRemove(string remote, IOutputProcessor processor = null) { - //Logger.Trace("RemoteRemove"); - return new GitRemoteRemoveTask(remote, cancellationToken, processor) .Configure(processManager); } + /// public ITask RemoteChange(string remote, string url, IOutputProcessor processor = null) { - //Logger.Trace("RemoteChange"); - return new GitRemoteChangeTask(remote, url, cancellationToken, processor) .Configure(processManager); } + /// public ITask Commit(string message, string body, IOutputProcessor processor = null) { - //Logger.Trace("Commit"); - return new GitCommitTask(message, body, cancellationToken, processor) .Configure(processManager); } + /// public ITask AddAll(IOutputProcessor processor = null) { - //Logger.Trace("Add all files"); - return new GitAddTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask Add(IList files, IOutputProcessor processor = null) { - //Logger.Trace("Add Files"); - GitAddTask last = null; foreach (var batch in files.Spool(5000)) { @@ -373,11 +570,10 @@ public ITask Add(IList files, return last; } + /// public ITask Discard( IList files, IOutputProcessor processor = null) { - //Logger.Trace("Checkout Files"); - GitCheckoutTask last = null; foreach (var batch in files.Spool(5000)) { @@ -396,54 +592,82 @@ public ITask Discard( IList files, return last; } + /// public ITask DiscardAll(IOutputProcessor processor = null) { - //Logger.Trace("Checkout all files"); - return new GitCheckoutTask(cancellationToken, processor) .Configure(processManager); } + /// + public ITask CheckoutVersion(string changeset, IList files, IOutputProcessor processor = null) + { + return new GitCheckoutTask(changeset, files, cancellationToken, processor) + .Configure(processManager); + } + + /// public ITask Remove(IList files, IOutputProcessor processor = null) { - //Logger.Trace("Remove"); + GitRemoveFromIndexTask last = null; + foreach (var batch in files.Spool(5000)) + { + var current = new GitRemoveFromIndexTask(batch, cancellationToken, processor).Configure(processManager); + if (last == null) + { + last = current; + } + else + { + last.Then(current); + last = current; + } + } - return new GitRemoveFromIndexTask(files, cancellationToken, processor) - .Configure(processManager); + return last; } + /// public ITask AddAndCommit(IList files, string message, string body, IOutputProcessor processor = null) { - //Logger.Trace("AddAndCommit"); - return Add(files) .Then(new GitCommitTask(message, body, cancellationToken) .Configure(processManager)); } - public ITask Lock(string file, + /// + public ITask Lock(NPath file, IOutputProcessor processor = null) { - //Logger.Trace("Lock"); - return new GitLockTask(file, cancellationToken, processor) - .Configure(processManager); + .Configure(processManager, environment.GitLfsExecutablePath); } - public ITask Unlock(string file, bool force, + /// + public ITask Unlock(NPath file, bool force, IOutputProcessor processor = null) { - //Logger.Trace("Unlock"); - return new GitUnlockTask(file, force, cancellationToken, processor) - .Configure(processManager); + .Configure(processManager, environment.GitLfsExecutablePath); + } + + /// + public ITask GetHead(IOutputProcessor processor = null) + { + return new FirstNonNullLineProcessTask(cancellationToken, "rev-parse --short HEAD") { Name = "Getting current head..." } + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, head) => success ? head : null); } protected static ILogging Logger { get; } = LogHelper.GetLogger(); } + [Serializable] public struct GitUser { public static GitUser Default = new GitUser(); @@ -451,8 +675,8 @@ public struct GitUser public string name; public string email; - public string Name { get { return name; } } - public string Email { get { return email; } } + public string Name => name; + public string Email { get { return String.IsNullOrEmpty(email) ? String.Empty : email; } } public GitUser(string name, string email) { @@ -464,7 +688,7 @@ public override int GetHashCode() { int hash = 17; hash = hash * 23 + (name?.GetHashCode() ?? 0); - hash = hash * 23 + (email?.GetHashCode() ?? 0); + hash = hash * 23 + Email.GetHashCode(); return hash; } @@ -479,8 +703,7 @@ public bool Equals(GitUser other) { return String.Equals(name, other.name) && - String.Equals(email, other.email) - ; + Email.Equals(other.Email); } public static bool operator ==(GitUser lhs, GitUser rhs) diff --git a/src/GitHub.Api/Git/GitConfig.cs b/src/GitHub.Api/Git/GitConfig.cs index b2277295c..882538af3 100644 --- a/src/GitHub.Api/Git/GitConfig.cs +++ b/src/GitHub.Api/Git/GitConfig.cs @@ -167,7 +167,7 @@ public interface IGitConfig void SetInt(string section, string key, int value); } - class GitConfig : IGitConfig + public class GitConfig : IGitConfig { private readonly ConfigFileManager manager; private SectionParser sectionParser; @@ -233,9 +233,9 @@ public bool TryGet(string section, string key, out T value) if (ret) { if (value is float) - value = (T)(object)sect.GetFloat(key); + value = (T)(object)sect.TryGetFloat(key); else if (value is int) - value = (T)(object)sect.GetInt(key); + value = (T)(object)sect.TryGetInt(key); else value = (T)(object)sect.GetString(key); } @@ -249,12 +249,12 @@ public string GetString(string section, string key) public float GetFloat(string section, string key) { - return sections[section].GetFloat(key); + return sections[section].TryGetFloat(key); } public int GetInt(string section, string key) { - return sections[section].GetInt(key); + return sections[section].TryGetInt(key); } public void Set(string section, string key, T value) @@ -296,7 +296,7 @@ private void SetAndWrite(string section, string key, string value) manager.Save(sb.ToString()); } - class Section : Dictionary + public class Section : Dictionary> { public Section(string name, string description = null) { @@ -306,27 +306,28 @@ public Section(string name, string description = null) public string TryGetString(string key) { - if (ContainsKey(key)) - return this[key]; + List val; + if (TryGetValue(key, out val)) + return val.First(); return null; } public string GetString(string key) { - return this[key]; + return this[key].First(); } - public int GetInt(string key) + public int TryGetInt(string key) { - var value = this[key]; + var value = TryGetString(key); int result = 0; int.TryParse(value, out result); return result; } - public float GetFloat(string key) + public float TryGetFloat(string key) { - var value = this[key]; + var value = TryGetString(key); float result = 0F; float.TryParse(value, out result); return result; @@ -334,7 +335,9 @@ public float GetFloat(string key) public void SetString(string key, string value) { - this[key] = value; + if (!ContainsKey(key)) + this[key] = new List(); + this[key].Add(value); } public void SetInt(string key, int value) @@ -361,7 +364,7 @@ public override string ToString() public string Description { get; private set; } } - class SectionParser + public class SectionParser { private static readonly Regex CommentPattern = new Regex(@"^[;#].*", RegexOptions.Compiled); private static readonly Regex SectionPattern = new Regex(@"^\[(.*)\]$", RegexOptions.Compiled); @@ -446,8 +449,7 @@ private void AddKeyValuePairToLoadedSectionFromLine(string line) var match = PairPattern.Match(line); var key = match.Groups[1].Value.Trim(); var value = match.Groups[2].Value; - - loadedSection.Add(key, value); + loadedSection.SetString(key, value); } private void EnsureFileBeginsWithSection() @@ -461,7 +463,7 @@ private void EnsureFileBeginsWithSection() public Dictionary> GroupSections { get; private set; } } - class ConfigFileManager + public class ConfigFileManager { private static readonly string[] emptyContents = new string[0]; @@ -510,4 +512,4 @@ public bool Save(string contents) public string[] Lines { get; private set; } } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitCredentialManager.cs b/src/GitHub.Api/Git/GitCredentialManager.cs index 4379dd5c0..fe090d0c7 100644 --- a/src/GitHub.Api/Git/GitCredentialManager.cs +++ b/src/GitHub.Api/Git/GitCredentialManager.cs @@ -1,19 +1,20 @@ using GitHub.Logging; using System; using System.Collections.Generic; -using System.Threading.Tasks; +using System.Linq; +using GitHub.Unity.Git.Tasks; namespace GitHub.Unity { - class GitCredentialManager : ICredentialManager + public class GitCredentialManager : ICredentialManager { private static ILogging Logger { get; } = LogHelper.GetLogger(); - private ICredential credential; private string credHelper = null; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; + private readonly Dictionary credentials = new Dictionary(); public GitCredentialManager(IProcessManager processManager, ITaskManager taskManager) @@ -24,48 +25,43 @@ public GitCredentialManager(IProcessManager processManager, public bool HasCredentials() { - return credential != null; + return credentials != null && credentials.Any(); } - public ICredential CachedCredentials { get { return credential; } } - - public async Task Delete(UriString host) + public void Delete(UriString host) { - //Logger.Trace("Delete: {0}", host); - - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return; - await RunCredentialHelper( + RunCredentialHelper( "erase", new string[] { String.Format("protocol={0}", host.Protocol), String.Format("host={0}", host.Host) - }).StartAwait(); - credential = null; + }).RunSynchronously(); + credentials.Remove(host); } - public async Task Load(UriString host) + public ICredential Load(UriString host) { - //Logger.Trace("Load: {0}", host); - - if (credential == null) + ICredential credential; + if (!credentials.TryGetValue(host, out credential)) { - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return null; string kvpCreds = null; - kvpCreds = await RunCredentialHelper( + kvpCreds = RunCredentialHelper( "get", new string[] { String.Format("protocol={0}", host.Protocol), String.Format("host={0}", host.Host) - }).StartAwait(); + }).RunSynchronously(); if (String.IsNullOrEmpty(kvpCreds)) { - Logger.Error("No credentials are stored"); + // we didn't find credentials, stop here return null; } @@ -92,45 +88,43 @@ public async Task Load(UriString host) } credential = new Credential(host, user, password); + credentials.Add(host, credential); } + return credential; } - public async Task Save(ICredential cred) + public void Save(ICredential cred) { - this.credential = cred; + this.credentials.Add(cred.Host, cred); - //Logger.Trace("Save: {0}", credential.Host); - - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return; var data = new List { - String.Format("protocol={0}", credential.Host.Protocol), - String.Format("host={0}", credential.Host.Host), - String.Format("username={0}", credential.Username), - String.Format("password={0}", credential.Token) + String.Format("protocol={0}", cred.Host.Protocol), + String.Format("host={0}", cred.Host.Host), + String.Format("username={0}", cred.Username), + String.Format("password={0}", cred.Token) }; var task = RunCredentialHelper("store", data.ToArray()); - await task.StartAwait(); + task.RunSynchronously(); if (!task.Successful) { Logger.Error("Failed to save credentials"); } } - private async Task LoadCredentialHelper() + private bool LoadCredentialHelper() { if (credHelper != null) return true; - //Logger.Trace("Loading Credential Helper"); - - credHelper = await new GitConfigGetTask("credential.helper", GitConfigSource.NonSpecified, taskManager.Token) + credHelper = new GitConfigGetTask("credential.helper", GitConfigSource.NonSpecified, taskManager.Token) .Configure(processManager) - .StartAwait(); + .RunSynchronously(); //Logger.Trace("Loaded Credential Helper: {0}", credHelper); @@ -145,8 +139,6 @@ private async Task LoadCredentialHelper() private ITask RunCredentialHelper(string action, string[] lines) { - //Logger.Trace("RunCredentialHelper helper:\"{0}\" action:\"{1}\"", credHelper, action); - SimpleProcessTask task; if (credHelper.StartsWith('!')) { @@ -173,4 +165,4 @@ private ITask RunCredentialHelper(string action, string[] lines) return task; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitFileLog.cs b/src/GitHub.Api/Git/GitFileLog.cs new file mode 100644 index 000000000..6795d6c4d --- /dev/null +++ b/src/GitHub.Api/Git/GitFileLog.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Unity +{ + [Serializable] + public struct GitFileLog + { + public static GitFileLog Default = new GitFileLog(null, new List(0)); + + public string path; + public List logEntries; + + public GitFileLog(string path, List logEntries) + { + this.path = path; + this.logEntries = logEntries; + } + + public string Path + { + get { return path; } + set { path = value; } + } + + public List LogEntries + { + get { return logEntries; } + set { logEntries = value; } + } + } +} diff --git a/src/GitHub.Api/Git/GitLock.cs b/src/GitHub.Api/Git/GitLock.cs index f604a0fdf..e7a961ae7 100644 --- a/src/GitHub.Api/Git/GitLock.cs +++ b/src/GitHub.Api/Git/GitLock.cs @@ -1,4 +1,6 @@ using System; +using System.Globalization; +using GitHub.Logging; namespace GitHub.Unity { @@ -7,50 +9,76 @@ public struct GitLock { public static GitLock Default = new GitLock(); - public int ID; - public string Path; - public string FullPath; - public string User; + public string id; + public string path; + public GitUser owner; + [NotSerialized] public string lockedAtString; + private string LockedAtString { get { return lockedAtString != null ? lockedAtString : String.Empty; } } + public DateTimeOffset locked_at + { + get + { + DateTimeOffset dt; + if (!DateTimeOffset.TryParseExact(LockedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, + CultureInfo.InvariantCulture, Constants.DateTimeStyle, out dt)) + { + locked_at = DateTimeOffset.MinValue; + return DateTimeOffset.MinValue; + } + return dt; + } + set + { + lockedAtString = value.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); + } + } + [NotSerialized] public string ID => id ?? String.Empty; + [NotSerialized] public NPath Path => path?.ToNPath() ?? NPath.Default; + [NotSerialized] public GitUser Owner => owner; + [NotSerialized] public DateTimeOffset LockedAt => locked_at; - public GitLock(string path, string fullPath, string user, int id) + public GitLock(string id, NPath path, GitUser owner, DateTimeOffset locked_at) { - Path = path; - FullPath = fullPath; - User = user; - ID = id; + this.id = id; + this.path = path.IsInitialized ? path.ToString() : null; + this.owner = owner; + this.lockedAtString = locked_at.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); } public override bool Equals(object other) { if (other is GitLock) - { - return this.Equals((GitLock)other); - } + return Equals((GitLock)other); return false; } - public bool Equals(GitLock p) + public bool Equals(GitLock other) { - return ID == p.ID; + return this == other; } public override int GetHashCode() { - return 17 * 23 + ID.GetHashCode(); + int hash = 17; + hash = hash * 23 + ID.GetHashCode(); + hash = hash * 23 + Path.GetHashCode(); + hash = hash * 23 + owner.GetHashCode(); + hash = hash * 23 + locked_at.GetHashCode(); + return hash; } public static bool operator ==(GitLock lhs, GitLock rhs) { - return lhs.Equals(rhs); + return lhs.ID == rhs.ID && lhs.Path == rhs.Path && lhs.owner == rhs.owner && lhs.locked_at == rhs.locked_at; } public static bool operator !=(GitLock lhs, GitLock rhs) { - return !(lhs.Equals(rhs)); + return !(lhs == rhs); } public override string ToString() { - return $"{{GitLock ({User}) '{Path}'}}"; + return $"{{ID:{ID}, path:{Path}, owner:{{{owner}}}, locked_at:'{locked_at}'}}"; } } } diff --git a/src/GitHub.Api/Git/GitLogEntry.cs b/src/GitHub.Api/Git/GitLogEntry.cs index a36b1fe73..a4656fdef 100644 --- a/src/GitHub.Api/Git/GitLogEntry.cs +++ b/src/GitHub.Api/Git/GitLogEntry.cs @@ -8,9 +8,6 @@ namespace GitHub.Unity [Serializable] public struct GitLogEntry { - private const string Today = "Today"; - private const string Yesterday = "Yesterday"; - public static GitLogEntry Default = new GitLogEntry(String.Empty, String.Empty, String.Empty, String.Empty, String.Empty, String.Empty, String.Empty, DateTimeOffset.MinValue, DateTimeOffset.MinValue, new List(), String.Empty, String.Empty); public string commitID; @@ -61,18 +58,7 @@ public GitLogEntry(string commitID, this.mergeB = mergeB ?? string.Empty; } - public string PrettyTimeString - { - get - { - DateTimeOffset now = DateTimeOffset.Now, relative = Time.ToLocalTime(); - - return String.Format("{0}, {1:HH}:{1:mm}", - relative.DayOfYear == now.DayOfYear - ? Today - : relative.DayOfYear == now.DayOfYear - 1 ? Yesterday : relative.ToString("d MMM yyyy"), relative); - } - } + public string PrettyTimeString => Time.ToLocalTime().CreateRelativeTime(DateTimeOffset.Now); [NonSerialized] private DateTimeOffset? timeValue; public DateTimeOffset Time @@ -82,7 +68,7 @@ public DateTimeOffset Time if (!timeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(TimeString, Constants.Iso8601Format, CultureInfo.InvariantCulture,DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(TimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture,DateTimeStyles.None, out result)) { timeValue = result; } @@ -109,7 +95,7 @@ public DateTimeOffset CommitTime if (!commitTimeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(CommitTimeString, Constants.Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(CommitTimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { commitTimeValue = result; } @@ -230,4 +216,4 @@ public override string ToString() return sb.ToString(); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitObjectFactory.cs b/src/GitHub.Api/Git/GitObjectFactory.cs index 489c0475a..d0fe3b640 100644 --- a/src/GitHub.Api/Git/GitObjectFactory.cs +++ b/src/GitHub.Api/Git/GitObjectFactory.cs @@ -3,7 +3,7 @@ namespace GitHub.Unity { - class GitObjectFactory : IGitObjectFactory + public class GitObjectFactory : IGitObjectFactory { private readonly IEnvironment environment; @@ -12,21 +12,13 @@ public GitObjectFactory(IEnvironment environment) this.environment = environment; } - public GitStatusEntry CreateGitStatusEntry(string path, GitFileStatus status, string originalPath = null, bool staged = false) + public GitStatusEntry CreateGitStatusEntry(string path, GitFileStatus indexStatus, GitFileStatus workTreeStatus = GitFileStatus.None, string originalPath = null) { var absolutePath = new NPath(path).MakeAbsolute(); var relativePath = absolutePath.RelativeTo(environment.RepositoryPath); var projectPath = absolutePath.RelativeTo(environment.UnityProjectPath); - return new GitStatusEntry(relativePath, absolutePath, projectPath, status, originalPath?.ToNPath(), staged); - } - - public GitLock CreateGitLock(string path, string user, int id) - { - var npath = new NPath(path).MakeAbsolute(); - var fullPath = npath.RelativeTo(environment.RepositoryPath); - - return new GitLock(path, fullPath, user, id); + return new GitStatusEntry(relativePath, absolutePath, projectPath, indexStatus, workTreeStatus, originalPath?.ToNPath()); } } } diff --git a/src/GitHub.Api/Git/GitStatusEntry.cs b/src/GitHub.Api/Git/GitStatusEntry.cs index 1421c5554..8ba7c6d58 100644 --- a/src/GitHub.Api/Git/GitStatusEntry.cs +++ b/src/GitHub.Api/Git/GitStatusEntry.cs @@ -11,22 +11,22 @@ public struct GitStatusEntry public string fullPath; public string projectPath; public string originalPath; - public GitFileStatus status; - public bool staged; + public GitFileStatus indexStatus; + public GitFileStatus workTreeStatus; public GitStatusEntry(string path, string fullPath, string projectPath, - GitFileStatus status, - string originalPath = null, bool staged = false) + GitFileStatus indexStatus, GitFileStatus workTreeStatus, + string originalPath = null) { Guard.ArgumentNotNullOrWhiteSpace(path, "path"); Guard.ArgumentNotNullOrWhiteSpace(fullPath, "fullPath"); this.path = path; - this.status = status; + this.indexStatus = indexStatus; + this.workTreeStatus = workTreeStatus; this.fullPath = fullPath; this.projectPath = projectPath; this.originalPath = originalPath; - this.staged = staged; } public override int GetHashCode() @@ -36,8 +36,8 @@ public override int GetHashCode() hash = hash * 23 + (fullPath?.GetHashCode() ?? 0); hash = hash * 23 + (projectPath?.GetHashCode() ?? 0); hash = hash * 23 + (originalPath?.GetHashCode() ?? 0); - hash = hash * 23 + status.GetHashCode(); - hash = hash * 23 + staged.GetHashCode(); + hash = hash * 23 + indexStatus.GetHashCode(); + hash = hash * 23 + workTreeStatus.GetHashCode(); return hash; } @@ -55,8 +55,8 @@ public bool Equals(GitStatusEntry other) String.Equals(fullPath, other.fullPath) && String.Equals(projectPath, other.projectPath) && String.Equals(originalPath, other.originalPath) && - status == other.status && - staged == other.staged + indexStatus == other.indexStatus && + workTreeStatus == other.workTreeStatus ; } @@ -79,6 +79,49 @@ public bool Equals(GitStatusEntry other) return !(lhs == rhs); } + public static GitFileStatus ParseStatusMarker(char changeFlag) + { + GitFileStatus status = GitFileStatus.None; + switch (changeFlag) + { + case 'M': + status = GitFileStatus.Modified; + break; + case 'A': + status = GitFileStatus.Added; + break; + case 'D': + status = GitFileStatus.Deleted; + break; + case 'R': + status = GitFileStatus.Renamed; + break; + case 'C': + status = GitFileStatus.Copied; + break; + case 'U': + status = GitFileStatus.Unmerged; + break; + case 'T': + status = GitFileStatus.TypeChange; + break; + case 'X': + status = GitFileStatus.Unknown; + break; + case 'B': + status = GitFileStatus.Broken; + break; + case '?': + status = GitFileStatus.Untracked; + break; + case '!': + status = GitFileStatus.Ignored; + break; + default: break; + } + return status; + } + public string Path => path; public string FullPath => fullPath; @@ -87,13 +130,21 @@ public bool Equals(GitStatusEntry other) public string OriginalPath => originalPath; - public GitFileStatus Status => status; + public GitFileStatus Status => workTreeStatus != GitFileStatus.None ? workTreeStatus : indexStatus; + public GitFileStatus IndexStatus => indexStatus; + public GitFileStatus WorkTreeStatus => workTreeStatus; + + public bool Staged => indexStatus != GitFileStatus.None && !Unmerged && !Untracked && !Ignored; + + public bool Unmerged => (indexStatus == workTreeStatus && (indexStatus == GitFileStatus.Added || indexStatus == GitFileStatus.Deleted)) || + indexStatus == GitFileStatus.Unmerged || workTreeStatus == GitFileStatus.Unmerged; - public bool Staged => staged; + public bool Untracked => workTreeStatus == GitFileStatus.Untracked; + public bool Ignored => workTreeStatus == GitFileStatus.Ignored; public override string ToString() { - return $"Path:'{Path}' Status:'{Status}' FullPath:'{FullPath}' ProjectPath:'{ProjectPath}' OriginalPath:'{OriginalPath}' Staged:'{Staged}'"; + return $"Path:'{Path}' Status:'{Status}' FullPath:'{FullPath}' ProjectPath:'{ProjectPath}' OriginalPath:'{OriginalPath}' Staged:'{Staged}' Unmerged:'{Unmerged}' Status:'{IndexStatus}' Status:'{WorkTreeStatus}' "; } } } diff --git a/src/GitHub.Api/Git/IGitObjectFactory.cs b/src/GitHub.Api/Git/IGitObjectFactory.cs index 4e1ec6f08..7458c9403 100644 --- a/src/GitHub.Api/Git/IGitObjectFactory.cs +++ b/src/GitHub.Api/Git/IGitObjectFactory.cs @@ -1,8 +1,7 @@ namespace GitHub.Unity { - interface IGitObjectFactory + public interface IGitObjectFactory { - GitStatusEntry CreateGitStatusEntry(string path, GitFileStatus status, string originalPath = null, bool staged = false); - GitLock CreateGitLock(string path, string user, int id); + GitStatusEntry CreateGitStatusEntry(string path, GitFileStatus indexStatus, GitFileStatus workTreeStatus, string originalPath = null); } } diff --git a/src/GitHub.Api/Git/IRepository.cs b/src/GitHub.Api/Git/IRepository.cs index 9e4031ac7..7bd579d37 100644 --- a/src/GitHub.Api/Git/IRepository.cs +++ b/src/GitHub.Api/Git/IRepository.cs @@ -6,7 +6,7 @@ namespace GitHub.Unity /// /// Represents a repository, either local or retrieved via the GitHub API. /// - public interface IRepository : IEquatable, IDisposable + public interface IRepository : IEquatable, IDisposable, IBackedByCache { void Initialize(IRepositoryManager theRepositoryManager, ITaskManager theTaskManager); void Start(); @@ -18,10 +18,10 @@ public interface IRepository : IEquatable, IDisposable ITask Push(); ITask Fetch(); ITask Revert(string changeset); - ITask RequestLock(string file); - ITask ReleaseLock(string file, bool force); + ITask RequestLock(NPath file); + ITask ReleaseLock(NPath file, bool force); ITask DiscardChanges(GitStatusEntry[] discardEntries); - void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent); + ITask CheckoutVersion(string changeset, IList files); /// /// Gets the name of the repository. @@ -61,8 +61,11 @@ public interface IRepository : IEquatable, IDisposable string CurrentBranchName { get; } List CurrentLog { get; } bool IsBusy { get; } + string CurrentHead { get; } + GitFileLog CurrentFileLog { get; } event Action LogChanged; + event Action FileLogChanged; event Action TrackingStatusChanged; event Action StatusEntriesChanged; event Action CurrentBranchChanged; @@ -75,5 +78,11 @@ public interface IRepository : IEquatable, IDisposable ITask RemoteAdd(string remote, string url); ITask RemoteRemove(string remote); ITask Push(string remote); + ITask DeleteBranch(string branch, bool force); + ITask CreateBranch(string branch, string baseBranch); + ITask SwitchBranch(string branch); + ITask UpdateFileLog(string path); + void Refresh(CacheType cacheType); + event Action OnProgress; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 4101e5caa..84cd58853 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -4,12 +4,16 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Threading; namespace GitHub.Unity { + public interface IBackedByCache + { + void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent); + } + [DebuggerDisplay("{DebuggerDisplay,nq}")] - sealed class Repository : IEquatable, IRepository + public class Repository : IEquatable, IRepository { private static ILogging Logger = LogHelper.GetLogger(); @@ -20,8 +24,11 @@ sealed class Repository : IEquatable, IRepository private string name; private HashSet cacheInvalidationRequests = new HashSet(); private Dictionary> cacheUpdateEvents; + private ProgressReporter progressReporter = new ProgressReporter(); + private string lastFileLog; public event Action LogChanged; + public event Action FileLogChanged; public event Action TrackingStatusChanged; public event Action StatusEntriesChanged; public event Action CurrentBranchChanged; @@ -31,6 +38,11 @@ sealed class Repository : IEquatable, IRepository public event Action LocksChanged; public event Action RemoteBranchListChanged; public event Action LocalAndRemoteBranchListChanged; + public event Action OnProgress + { + add { progressReporter.OnProgress += value; } + remove { progressReporter.OnProgress -= value; } + } /// /// Initializes a new instance of the class. @@ -53,6 +65,7 @@ public Repository(NPath localPath, ICacheContainer container) { CacheType.GitAheadBehind, c => TrackingStatusChanged?.Invoke(c) }, { CacheType.GitLocks, c => LocksChanged?.Invoke(c) }, { CacheType.GitLog, c => LogChanged?.Invoke(c) }, + { CacheType.GitFileLog, c => FileLogChanged?.Invoke(c) }, { CacheType.GitStatus, c => StatusEntriesChanged?.Invoke(c) }, { CacheType.GitUser, cacheUpdateEvent => { } }, { CacheType.RepositoryInfo, cacheUpdateEvent => { @@ -72,7 +85,6 @@ public Repository(NPath localPath, ICacheContainer container) public void Initialize(IRepositoryManager theRepositoryManager, ITaskManager theTaskManager) { - //Logger.Trace("Initialize"); Guard.ArgumentNotNull(theRepositoryManager, nameof(theRepositoryManager)); Guard.ArgumentNotNull(theTaskManager, nameof(theTaskManager)); @@ -82,17 +94,27 @@ public void Initialize(IRepositoryManager theRepositoryManager, ITaskManager the this.repositoryManager.GitStatusUpdated += RepositoryManagerOnGitStatusUpdated; this.repositoryManager.GitAheadBehindStatusUpdated += RepositoryManagerOnGitAheadBehindStatusUpdated; this.repositoryManager.GitLogUpdated += RepositoryManagerOnGitLogUpdated; + this.repositoryManager.GitFileLogUpdated += RepositoryManagerOnGitFileLogUpdated; this.repositoryManager.GitLocksUpdated += RepositoryManagerOnGitLocksUpdated; this.repositoryManager.LocalBranchesUpdated += RepositoryManagerOnLocalBranchesUpdated; this.repositoryManager.RemoteBranchesUpdated += RepositoryManagerOnRemoteBranchesUpdated; this.repositoryManager.DataNeedsRefreshing += RefreshCache; + try + { + this.taskManager.OnProgress += progressReporter.UpdateProgress; + } + catch (Exception ex) + { + LogHelper.Error(ex); + } + } public void Start() { foreach (var cacheType in cacheInvalidationRequests) { - RefreshCache(cacheType); + CacheHasBeenInvalidated(cacheType); } } @@ -117,11 +139,20 @@ public ITask SetupRemote(string remote, string remoteUrl) public ITask Push() => repositoryManager.Push(CurrentRemote.Value.Name, CurrentBranch?.Name); public ITask Fetch() => repositoryManager.Fetch(CurrentRemote.Value.Name); public ITask Revert(string changeset) => repositoryManager.Revert(changeset); - public ITask RequestLock(string file) => repositoryManager.LockFile(file); - public ITask ReleaseLock(string file, bool force) => repositoryManager.UnlockFile(file, force); + public ITask RequestLock(NPath file) => repositoryManager.LockFile(file); + public ITask ReleaseLock(NPath file, bool force) => repositoryManager.UnlockFile(file, force); public ITask DiscardChanges(GitStatusEntry[] gitStatusEntry) => repositoryManager.DiscardChanges(gitStatusEntry); + public ITask CheckoutVersion(string changeset, IList files) => repositoryManager.CheckoutVersion(changeset, files); public ITask RemoteAdd(string remote, string url) => repositoryManager.RemoteAdd(remote, url); public ITask RemoteRemove(string remote) => repositoryManager.RemoteRemove(remote); + public ITask DeleteBranch(string branch, bool force) => repositoryManager.DeleteBranch(branch, force); + public ITask CreateBranch(string branch, string baseBranch) => repositoryManager.CreateBranch(branch, baseBranch); + public ITask SwitchBranch(string branch) => repositoryManager.SwitchBranch(branch); + public ITask UpdateFileLog(string path) + { + lastFileLog = path; + return repositoryManager.UpdateFileLog(path); + } public void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(cacheType, cacheUpdateEvent); @@ -160,14 +191,19 @@ public bool Equals(IRepository other) private void RefreshCache(CacheType cacheType) { - taskManager.RunInUI(() => + taskManager.RunInUI(() => Refresh(cacheType)); + } + + public void Refresh(CacheType cacheType) + { + var cache = cacheContainer.GetCache(cacheType); + cache.InvalidateData(); + + // take the opportunity to possibly refresh the locks cache, if it has timed out + if (cacheType != CacheType.GitLocks) { - var cache = cacheContainer.GetCache(cacheType); - // if the cache has valid data, we need to force an invalidation to refresh it - // if it doesn't have valid data, it will trigger an invalidation automatically - if (cache.ValidateData()) - cache.InvalidateData(); - }); + cacheContainer.GetCache(CacheType.GitLocks).ValidateData(); + } } private void CacheHasBeenInvalidated(CacheType cacheType) @@ -182,20 +218,26 @@ private void CacheHasBeenInvalidated(CacheType cacheType) switch (cacheType) { case CacheType.Branches: - repositoryManager?.UpdateBranches(); + repositoryManager?.UpdateBranches().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); break; case CacheType.GitLog: - repositoryManager?.UpdateGitLog(); + repositoryManager?.UpdateGitLog().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); + break; + + case CacheType.GitFileLog: + repositoryManager?.UpdateFileLog(lastFileLog).Catch(ex => InvalidationFailed(ex, cacheType)).Start(); break; case CacheType.GitAheadBehind: - repositoryManager?.UpdateGitAheadBehindStatus(); + repositoryManager?.UpdateGitAheadBehindStatus().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); break; case CacheType.GitLocks: if (CurrentRemote != null) - repositoryManager?.UpdateLocks(); + { + repositoryManager?.UpdateLocks().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); + } break; case CacheType.GitUser: @@ -203,11 +245,11 @@ private void CacheHasBeenInvalidated(CacheType cacheType) break; case CacheType.RepositoryInfo: - repositoryManager?.UpdateRepositoryInfo(); + repositoryManager?.UpdateRepositoryInfo().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); break; case CacheType.GitStatus: - repositoryManager?.UpdateGitStatus(); + repositoryManager?.UpdateGitStatus().Catch(ex => InvalidationFailed(ex, cacheType)).Start(); break; default: @@ -215,19 +257,29 @@ private void CacheHasBeenInvalidated(CacheType cacheType) } } - private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, ConfigRemote? remote) + private bool InvalidationFailed(Exception ex, CacheType cacheType) + { + Logger.Warning(ex, "Error invalidating {0}", cacheType); + var managedCache = cacheContainer.GetCache(cacheType); + managedCache.ResetInvalidation(); + return false; + } + + + private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, ConfigRemote? remote, string head) { taskManager.RunInUI(() => { var data = new RepositoryInfoCacheData(); data.CurrentConfigBranch = branch; - data.CurrentGitBranch = branch.HasValue ? (GitBranch?)GetLocalGitBranch(branch.Value.name, branch.Value) : null; + data.CurrentGitBranch = branch.HasValue ? (GitBranch?)GetLocalGitBranch(branch.Value) : null; data.CurrentConfigRemote = remote; data.CurrentGitRemote = remote.HasValue ? (GitRemote?)GetGitRemote(remote.Value) : null; + data.CurrentHead = head; name = null; cloneUrl = null; cacheContainer.RepositoryInfoCache.UpdateData(data); - + // force refresh of the Name and CloneUrl propertys var n = Name; }); @@ -257,6 +309,11 @@ private void RepositoryManagerOnGitLogUpdated(List gitLogEntries) taskManager.RunInUI(() => cacheContainer.GitLogCache.Log = gitLogEntries); } + private void RepositoryManagerOnGitFileLogUpdated(GitFileLog gitFileLog) + { + taskManager.RunInUI(() => cacheContainer.GitFileLogCache.FileLog = gitFileLog); + } + private void RepositoryManagerOnGitLocksUpdated(List gitLocks) { taskManager.RunInUI(() => cacheContainer.GitLocksCache.GitLocks = gitLocks); @@ -276,16 +333,15 @@ private void RepositoryManagerOnRemoteBranchesUpdated(Dictionary localConfigBranchDictionary) { taskManager.RunInUI(() => { - var gitLocalBranches = localConfigBranchDictionary.Values.Select(x => GetLocalGitBranch(CurrentBranchName, x)).ToArray(); + var gitLocalBranches = localConfigBranchDictionary.Values.Select(x => GetLocalGitBranch(x)).ToArray(); cacheContainer.BranchCache.SetLocals(localConfigBranchDictionary, gitLocalBranches); }); } - private static GitBranch GetLocalGitBranch(string currentBranchName, ConfigBranch x) + private static GitBranch GetLocalGitBranch(ConfigBranch x) { var branchName = x.Name; - var trackingName = x.IsTracking ? x.Remote.Value.Name + "/" + branchName : "[None]"; - var isActive = branchName == currentBranchName; + var trackingName = x.IsTracking ? x.Remote.Value.Name + "/" + branchName : null; return new GitBranch(branchName, trackingName); } @@ -323,7 +379,9 @@ public void Dispose() public string CurrentBranchName => CurrentConfigBranch?.Name; public GitRemote? CurrentRemote => cacheContainer.RepositoryInfoCache.CurrentGitRemote; public List CurrentLog => cacheContainer.GitLogCache.Log; + public GitFileLog CurrentFileLog => cacheContainer.GitFileLogCache.FileLog; public List CurrentLocks => cacheContainer.GitLocksCache.GitLocks; + public string CurrentHead => cacheContainer.RepositoryInfoCache.CurrentHead; public UriString CloneUrl { @@ -376,12 +434,11 @@ public string Name CloneUrl, LocalPath, CurrentBranch, CurrentRemote); } - public interface IUser + public interface IUser : IBackedByCache { string Name { get; } string Email { get; } event Action Changed; - void CheckUserChangedEvent(CacheUpdateEvent cacheUpdateEvent); void Initialize(IGitClient client); void SetNameAndEmail(string name, string email); } @@ -397,19 +454,25 @@ public class User : IUser public User(ICacheContainer cacheContainer) { - this.cacheContainer = cacheContainer; - cacheContainer.CacheInvalidated += (type) => { if (type == CacheType.GitUser) GitUserCacheOnCacheInvalidated(); }; - cacheContainer.CacheUpdated += (type, dt) => { if (type == CacheType.GitUser) CacheHasBeenUpdated(dt); }; + if (cacheContainer != null) + { + this.cacheContainer = cacheContainer; + cacheContainer.CacheInvalidated += (type) => { if (type == CacheType.GitUser) GitUserCacheOnCacheInvalidated(); }; + cacheContainer.CacheUpdated += (type, dt) => { if (type == CacheType.GitUser) CacheHasBeenUpdated(dt); }; + } } - public void CheckUserChangedEvent(CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, cacheUpdateEvent); + public void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, cacheUpdateEvent); public void Initialize(IGitClient client) { Guard.ArgumentNotNull(client, nameof(client)); gitClient = client; if (needsRefresh) - cacheContainer.GitUserCache.InvalidateData(); + { + needsRefresh = false; + GitUserCacheOnCacheInvalidated(); + } } public void SetNameAndEmail(string name, string email) @@ -436,19 +499,16 @@ private void CacheHasBeenUpdated(DateTimeOffset timeOffset) private void GitUserCacheOnCacheInvalidated() { - //Logger.Trace("GitUserCache Invalidated"); UpdateUserAndEmail(); } private void HandleUserCacheUpdatedEvent(CacheUpdateEvent cacheUpdateEvent) { - //Logger.Trace("GitUserCache Updated {0}", cacheUpdateEvent.UpdatedTime); Changed?.Invoke(cacheUpdateEvent); } private void UpdateUserAndEmail() { - //Logger.Trace("UpdateUserAndEmail"); if (gitClient == null) { needsRefresh = true; @@ -456,6 +516,7 @@ private void UpdateUserAndEmail() } gitClient.GetConfigUserAndEmail() + .Catch(InvalidationFailed) .ThenInUI((success, value) => { if (success) @@ -465,7 +526,15 @@ private void UpdateUserAndEmail() } }).Start(); } - + + private bool InvalidationFailed(Exception ex) + { + Logger.Warning(ex, "Error invalidating user cache"); + var managedCache = cacheContainer.GetCache(CacheType.GitUser); + managedCache.ResetInvalidation(); + return false; + } + public string Name { get { return cacheContainer.GitUserCache.Name; } @@ -480,4 +549,4 @@ public string Email protected static ILogging Logger { get; } = LogHelper.GetLogger(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index a2768154e..7b2045b43 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -9,10 +9,11 @@ namespace GitHub.Unity public interface IRepositoryManager : IDisposable { event Action IsBusyChanged; - event Action CurrentBranchUpdated; + event Action CurrentBranchUpdated; event Action GitStatusUpdated; event Action> GitLocksUpdated; event Action> GitLogUpdated; + event Action GitFileLogUpdated; event Action> LocalBranchesUpdated; event Action, Dictionary>> RemoteBranchesUpdated; event Action GitAheadBehindStatusUpdated; @@ -34,23 +35,27 @@ public interface IRepositoryManager : IDisposable ITask SwitchBranch(string branch); ITask DeleteBranch(string branch, bool deleteUnmerged = false); ITask CreateBranch(string branch, string baseBranch); - ITask LockFile(string file); - ITask UnlockFile(string file, bool force); + ITask LockFile(NPath file); + ITask UnlockFile(NPath file, bool force); ITask DiscardChanges(GitStatusEntry[] gitStatusEntries); - void UpdateGitLog(); - void UpdateGitStatus(); - void UpdateGitAheadBehindStatus(); - void UpdateLocks(); + ITask CheckoutVersion(string changeset, IList files); + ITask UpdateGitLog(); + ITask UpdateGitStatus(); + ITask UpdateGitAheadBehindStatus(); + ITask UpdateLocks(); + ITask UpdateRepositoryInfo(); + ITask UpdateBranches(); + ITask UpdateFileLog(string path); + + int WaitForEvents(); - void UpdateRepositoryInfo(); IGitConfig Config { get; } IGitClient GitClient { get; } bool IsBusy { get; } - void UpdateBranches(); } - interface IRepositoryPathConfiguration + public interface IRepositoryPathConfiguration { NPath RepositoryPath { get; } NPath DotGitPath { get; } @@ -59,15 +64,19 @@ interface IRepositoryPathConfiguration NPath DotGitIndex { get; } NPath DotGitHead { get; } NPath DotGitConfig { get; } + NPath WorktreeDotGitPath { get; } + bool IsWorktree { get; } } - class RepositoryPathConfiguration : IRepositoryPathConfiguration + public class RepositoryPathConfiguration : IRepositoryPathConfiguration { public RepositoryPathConfiguration(NPath repositoryPath) { RepositoryPath = repositoryPath; + WorktreeDotGitPath = NPath.Default; DotGitPath = repositoryPath.Combine(".git"); + NPath commonPath; if (DotGitPath.FileExists()) { DotGitPath = @@ -75,17 +84,37 @@ public RepositoryPathConfiguration(NPath repositoryPath) .Where(x => x.StartsWith("gitdir:")) .Select(x => x.Substring(7).Trim().ToNPath()) .First(); + if (DotGitPath.Combine("commondir").FileExists()) + { + commonPath = DotGitPath.Combine("commondir").ReadAllLines() + .Select(x => x.Trim().ToNPath()) + .First(); + commonPath = DotGitPath.Combine(commonPath); + + IsWorktree = true; + WorktreeDotGitPath = commonPath; + } + else + { + commonPath = DotGitPath; + } + } + else + { + commonPath = DotGitPath; } - BranchesPath = DotGitPath.Combine("refs", "heads"); - RemotesPath = DotGitPath.Combine("refs", "remotes"); + BranchesPath = commonPath.Combine("refs", "heads"); + RemotesPath = commonPath.Combine("refs", "remotes"); DotGitIndex = DotGitPath.Combine("index"); DotGitHead = DotGitPath.Combine("HEAD"); - DotGitConfig = DotGitPath.Combine("config"); + DotGitConfig = commonPath.Combine("config"); DotGitCommitEditMsg = DotGitPath.Combine("COMMIT_EDITMSG"); } + public bool IsWorktree { get; } public NPath RepositoryPath { get; } + public NPath WorktreeDotGitPath { get; } public NPath DotGitPath { get; } public NPath BranchesPath { get; } public NPath RemotesPath { get; } @@ -95,36 +124,35 @@ public RepositoryPathConfiguration(NPath repositoryPath) public NPath DotGitCommitEditMsg { get; } } - class RepositoryManager : IRepositoryManager + public class RepositoryManager : IRepositoryManager { private readonly IGitConfig config; private readonly IGitClient gitClient; private readonly IRepositoryPathConfiguration repositoryPaths; - private readonly IFileSystem fileSystem; private readonly CancellationToken token; private readonly IRepositoryWatcher watcher; private bool isBusy; - public event Action CurrentBranchUpdated; + public event Action CurrentBranchUpdated; public event Action IsBusyChanged; public event Action GitStatusUpdated; public event Action GitAheadBehindStatusUpdated; public event Action> GitLocksUpdated; public event Action> GitLogUpdated; + public event Action GitFileLogUpdated; public event Action> LocalBranchesUpdated; public event Action, Dictionary>> RemoteBranchesUpdated; public event Action DataNeedsRefreshing; public RepositoryManager(IGitConfig gitConfig, - IRepositoryWatcher repositoryWatcher, IGitClient gitClient, - IFileSystem fileSystem, + IRepositoryWatcher repositoryWatcher, + IGitClient gitClient, CancellationToken token, IRepositoryPathConfiguration repositoryPaths) { this.repositoryPaths = repositoryPaths; - this.fileSystem = fileSystem; this.token = token; this.gitClient = gitClient; this.watcher = repositoryWatcher; @@ -140,7 +168,7 @@ public RepositoryManager(IGitConfig gitConfig, } public static RepositoryManager CreateInstance(IPlatform platform, ITaskManager taskManager, IGitClient gitClient, - IFileSystem fileSystem, NPath repositoryRoot) + NPath repositoryRoot) { var repositoryPathConfiguration = new RepositoryPathConfiguration(repositoryRoot); string filePath = repositoryPathConfiguration.DotGitConfig; @@ -149,25 +177,22 @@ public static RepositoryManager CreateInstance(IPlatform platform, ITaskManager var repositoryWatcher = new RepositoryWatcher(platform, repositoryPathConfiguration, taskManager.Token); return new RepositoryManager(gitConfig, repositoryWatcher, - gitClient, fileSystem, + gitClient, taskManager.Token, repositoryPathConfiguration); } public void Initialize() { - //Logger.Trace("Initialize"); watcher.Initialize(); } public void Start() { - //Logger.Trace("Start"); watcher.Start(); } public void Stop() { - //Logger.Trace("Stop"); watcher.Stop(); } @@ -186,7 +211,6 @@ public ITask CommitAllFiles(string message, string body) { var task = GitClient.AddAll() .Then(GitClient.Commit(message, body)); - return HookupHandlers(task, true); } @@ -194,13 +218,17 @@ public ITask CommitFiles(List files, string message, string body) { var task = GitClient.Add(files) .Then(GitClient.Commit(message, body)); - return HookupHandlers(task, true); } public ITask Fetch(string remote) { var task = GitClient.Fetch(remote); + task.OnEnd += (_, __, success, ___) => + { + if (success) + UpdateGitAheadBehindStatus().Start(); + }; return HookupHandlers(task, false); } @@ -213,6 +241,11 @@ public ITask Pull(string remote, string branch) public ITask Push(string remote, string branch) { var task = GitClient.Push(remote, branch); + task.OnEnd += (_, __, success, ___) => + { + if (success) + UpdateGitAheadBehindStatus().Start(); + }; return HookupHandlers(task, false); } @@ -258,44 +291,18 @@ public ITask CreateBranch(string branch, string baseBranch) return HookupHandlers(task, false); } - public ITask LockFile(string file) + public ITask LockFile(NPath file) { - var task = GitClient.Lock(file); - return HookupHandlers(task, false).Then(UpdateLocks); - } - - public ITask UnlockFile(string file, bool force) - { - var task = GitClient.Unlock(file, force); - return HookupHandlers(task, false).Then(UpdateLocks); - } - - public void UpdateGitLog() - { - var task = GitClient.Log() - .Then((success, logEntries) => - { - if (success) - { - GitLogUpdated?.Invoke(logEntries); - } - }); - task = HookupHandlers(task, false); - task.Start(); + var task = GitClient.Lock(file) + .Then(() => DataNeedsRefreshing?.Invoke(CacheType.GitLocks)); + return HookupHandlers(task, false); } - public void UpdateGitStatus() + public ITask UnlockFile(NPath file, bool force) { - var task = GitClient.Status() - .Then((success, status) => - { - if (success) - { - GitStatusUpdated?.Invoke(status); - } - }); - task = HookupHandlers(task, false); - task.Start(); + var task = GitClient.Unlock(file, force) + .Then(() => DataNeedsRefreshing?.Invoke(CacheType.GitLocks)); + return HookupHandlers(task, false); } public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) @@ -305,14 +312,14 @@ public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) ActionTask task = null; task = new ActionTask(token, (_, entries) => { - var itemsToDelete = new List(); + var itemsToDelete = new List(); var itemsToRevert = new List(); foreach (var gitStatusEntry in gitStatusEntries) { - if (gitStatusEntry.status == GitFileStatus.Added || gitStatusEntry.status == GitFileStatus.Untracked) + if (gitStatusEntry.WorkTreeStatus == GitFileStatus.Added || gitStatusEntry.WorkTreeStatus == GitFileStatus.Untracked) { - itemsToDelete.Add(gitStatusEntry.path); + itemsToDelete.Add(gitStatusEntry.path.ToNPath().MakeAbsolute()); } else { @@ -324,7 +331,7 @@ public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) { foreach (var itemToDelete in itemsToDelete) { - fileSystem.FileDelete(itemToDelete); + itemToDelete.DeleteIfExists(); } } @@ -333,68 +340,125 @@ public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) task.Then(GitClient.Discard(itemsToRevert)); } } - , () => gitStatusEntries); - + , () => gitStatusEntries) + { Message = "Discarding changes..." }; return HookupHandlers(task, true); } - public void UpdateGitAheadBehindStatus() + public ITask CheckoutVersion(string changeset, IList files) + { + var task = GitClient.CheckoutVersion(changeset, files) + .Then(() => DataNeedsRefreshing?.Invoke(CacheType.GitStatus)); + return HookupHandlers(task, false); + } + + public ITask UpdateGitLog() + { + var task = GitClient.Log() + .Then((success, logEntries) => + { + if (success) + { + GitLogUpdated?.Invoke(logEntries); + } + }); + return HookupHandlers(task, false); + } + + public ITask UpdateFileLog(string path) + { + var task = GitClient.LogFile(path) + .Then((success, logEntries) => + { + if (success) + { + var gitFileLog = new GitFileLog(path, logEntries); + GitFileLogUpdated?.Invoke(gitFileLog); + } + }); + return HookupHandlers(task, false); + } + + public ITask UpdateGitStatus() + { + var task = GitClient.Status() + .Then((success, status) => + { + if (success) + { + GitStatusUpdated?.Invoke(status); + } + }); + return HookupHandlers(task, false); + } + + public ITask UpdateGitAheadBehindStatus() { ConfigBranch? configBranch; ConfigRemote? configRemote; GetCurrentBranchAndRemote(out configBranch, out configRemote); + var updateTask = new ActionTask(token, (success, status) => + { + if (success) + { + GitAheadBehindStatusUpdated?.Invoke(status); + } + }); if (configBranch.HasValue && configBranch.Value.Remote.HasValue) { var name = configBranch.Value.Name; var trackingName = configBranch.Value.IsTracking ? configBranch.Value.Remote.Value.Name + "/" + configBranch.Value.TrackingBranch : "[None]"; var task = GitClient.AheadBehindStatus(name, trackingName) - .Then((success, status) => - { - if (success) - { - GitAheadBehindStatusUpdated?.Invoke(status); - } - }); - task = HookupHandlers(task, false); - task.Start(); + .Then(updateTask); + return HookupHandlers(task, false); } else { - GitAheadBehindStatusUpdated?.Invoke(GitAheadBehindStatus.Default); + updateTask.PreviousResult = GitAheadBehindStatus.Default; + return updateTask; } } - public void UpdateLocks() + public ITask UpdateLocks() { - GitClient.ListLocks(false) + var task = GitClient.ListLocks(false) .Then((success, locks) => { if (success) { GitLocksUpdated?.Invoke(locks); } - }) - .Start(); + }); + return HookupHandlers(task, false); + } - public void UpdateBranches() + public ITask UpdateBranches() { - UpdateLocalBranches(); - UpdateRemoteBranches(); + var task = new ActionTask(token, () => + { + UpdateLocalBranches(); + UpdateRemoteBranches(); + }) + { Message = "Updating branches..." }; + return HookupHandlers(task, false); } - public void UpdateRepositoryInfo() + public ITask UpdateRepositoryInfo() { - ConfigBranch? branch; - ConfigRemote? remote; - GetCurrentBranchAndRemote(out branch, out remote); - - Logger.Trace("CurrentBranch: {0}", branch.HasValue ? branch.Value.ToString() : "[NULL]"); - Logger.Trace("CurrentRemote: {0}", remote.HasValue ? remote.Value.ToString() : "[NULL]"); - CurrentBranchUpdated?.Invoke(branch, remote); + var task = new ActionTask(token, () => + { + ConfigBranch? branch; + ConfigRemote? remote; + GetCurrentBranchAndRemote(out branch, out remote); + var currentHead = GitClient.GetHead().RunSynchronously(); + CurrentBranchUpdated?.Invoke(branch, remote, currentHead); + }) + { Message = "Updating repository info..." }; + return HookupHandlers(task, false); } private void GetCurrentBranchAndRemote(out ConfigBranch? branch, out ConfigRemote? remote) @@ -448,18 +512,30 @@ private ITask HookupHandlers(ITask task, bool filesystemChangesExpected) { if (isExclusive) { - //Logger.Trace("Starting Operation - Setting Busy Flag"); IsBusy = true; } if (filesystemChangesExpected) { - //Logger.Trace("Starting Operation - Disable Watcher"); watcher.Stop(); } }; - task.Finally(success => + task.OnEnd += (_, __, ___) => + { + if (filesystemChangesExpected) + { + //Logger.Trace("Ended Operation - Enable Watcher"); + watcher.Start(); + } + + if (isExclusive) + { + //Logger.Trace("Ended Operation - Clearing Busy Flag"); + IsBusy = false; + } + }; + task.Catch(_ => { if (filesystemChangesExpected) { @@ -472,6 +548,7 @@ private ITask HookupHandlers(ITask task, bool filesystemChangesExpected) //Logger.Trace("Ended Operation - Clearing Busy Flag"); IsBusy = false; } + }); return task; } @@ -483,13 +560,11 @@ private string GetCurrentHead() private void WatcherOnRemoteBranchesChanged() { - Logger.Trace("WatcherOnRemoteBranchesChanged"); DataNeedsRefreshing?.Invoke(CacheType.Branches); } private void WatcherOnLocalBranchesChanged() { - Logger.Trace("WatcherOnLocalBranchesChanged"); DataNeedsRefreshing?.Invoke(CacheType.Branches); // the watcher should tell us what branch has changed so we can fire this only // when the active branch has changed @@ -499,20 +574,17 @@ private void WatcherOnLocalBranchesChanged() private void WatcherOnRepositoryCommitted() { - Logger.Trace("WatcherOnRepositoryCommitted"); DataNeedsRefreshing?.Invoke(CacheType.GitLog); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void WatcherOnRepositoryChanged() { - Logger.Trace("WatcherOnRepositoryChanged"); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void WatcherOnConfigChanged() { - Logger.Trace("WatcherOnConfigChanged"); config.Reset(); DataNeedsRefreshing?.Invoke(CacheType.Branches); DataNeedsRefreshing?.Invoke(CacheType.RepositoryInfo); @@ -521,7 +593,6 @@ private void WatcherOnConfigChanged() private void WatcherOnHeadChanged() { - Logger.Trace("WatcherOnHeadChanged"); DataNeedsRefreshing?.Invoke(CacheType.RepositoryInfo); DataNeedsRefreshing?.Invoke(CacheType.GitLog); DataNeedsRefreshing?.Invoke(CacheType.GitAheadBehind); @@ -529,18 +600,13 @@ private void WatcherOnHeadChanged() private void WatcherOnIndexChanged() { - Logger.Trace("WatcherOnIndexChanged"); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void UpdateLocalBranches() { - Logger.Trace("UpdateLocalBranches"); - var branches = new Dictionary(); UpdateLocalBranches(branches, repositoryPaths.BranchesPath, config.GetBranches().Where(x => x.IsTracking), ""); - - Logger.Trace("OnLocalBranchListUpdated {0} branches", branches.Count); LocalBranchesUpdated?.Invoke(branches); } @@ -566,8 +632,6 @@ private void UpdateLocalBranches(Dictionary branches, NPat private void UpdateRemoteBranches() { - Logger.Trace("UpdateRemoteBranches"); - var remotes = config.GetRemotes().ToArray().ToDictionary(x => x.Name, x => x); var remoteBranches = new Dictionary>(); @@ -589,9 +653,7 @@ private void UpdateRemoteBranches() } } - Logger.Trace("OnRemoteBranchListUpdated {0} remotes", remotes.Count); RemoteBranchesUpdated?.Invoke(remotes, remoteBranches); - UpdateGitAheadBehindStatus(); } @@ -608,6 +670,7 @@ private void Dispose(bool disposing) GitStatusUpdated = null; GitAheadBehindStatusUpdated = null; GitLogUpdated = null; + GitFileLogUpdated = null; GitLocksUpdated = null; LocalBranchesUpdated = null; RemoteBranchesUpdated = null; @@ -634,7 +697,6 @@ private set { if (isBusy != value) { - Logger.Trace("IsBusyChanged Value:{0}", value); isBusy = value; IsBusyChanged?.Invoke(isBusy); } diff --git a/src/GitHub.Api/Git/Tasks/GitAddTask.cs b/src/GitHub.Api/Git/Tasks/GitAddTask.cs index e61fa5eda..a9c277ad3 100644 --- a/src/GitHub.Api/Git/Tasks/GitAddTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitAddTask.cs @@ -1,10 +1,9 @@ -using System; using System.Collections.Generic; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitAddTask : ProcessTask + public class GitAddTask : ProcessTask { private const string TaskName = "git add"; private readonly string arguments; @@ -32,5 +31,6 @@ public GitAddTask(CancellationToken token, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Staging files..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs b/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs index 12592b022..2a18cccb1 100644 --- a/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitAheadBehindStatusTask : ProcessTask + public class GitAheadBehindStatusTask : ProcessTask { private const string TaskName = "git rev-list"; private readonly string arguments; @@ -17,5 +17,6 @@ public GitAheadBehindStatusTask(string gitRef, string otherRef, public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Querying status..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs b/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs index 673979f69..1ef33ddbd 100644 --- a/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitBranchCreateTask : ProcessTask + public class GitBranchCreateTask : ProcessTask { private const string TaskName = "git branch"; private readonly string arguments; @@ -21,5 +21,6 @@ public GitBranchCreateTask(string newBranch, string baseBranch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Creating branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs b/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs index 983e40944..70bc9b596 100644 --- a/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitBranchDeleteTask : ProcessTask + public class GitBranchDeleteTask : ProcessTask { private const string TaskName = "git branch -d"; private readonly string arguments; @@ -18,5 +18,6 @@ public GitBranchDeleteTask(string branch, bool deleteUnmerged, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Deleting branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs index f64c89130..582b8458d 100644 --- a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitCheckoutTask : ProcessTask + public class GitCheckoutTask : ProcessTask { private const string TaskName = "git checkout"; private readonly string arguments; @@ -30,7 +29,29 @@ public GitCheckoutTask(CancellationToken token, arguments = "checkout -- ."; } + public GitCheckoutTask( + string changeset, + IEnumerable files, + CancellationToken token, + IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) + { + Guard.ArgumentNotNull(files, "files"); + Name = TaskName; + + arguments = "checkout "; + arguments += changeset; + arguments += " -- "; + + foreach (var file in files) + { + arguments += " \"" + file.ToNPath().ToString(SlashMode.Forward) + "\""; + } + + Message = "Checking out files at rev " + changeset.Substring(0, 7); + } + public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Checking out files..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs index b6d17809b..e36e4c208 100644 --- a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs @@ -1,27 +1,46 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitCommitTask : ProcessTask + public class GitCommitTask : ProcessTask { private const string TaskName = "git commit"; + + private readonly string message; + private readonly string body; private readonly string arguments; + private NPath tempFile; + public GitCommitTask(string message, string body, CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) { Guard.ArgumentNotNullOrWhiteSpace(message, "message"); + this.message = message; + this.body = body ?? string.Empty; + Name = TaskName; - arguments = "-c i18n.commitencoding=utf8 commit "; - arguments += String.Format(" -m \"{0}\"", message); - if (!String.IsNullOrEmpty(body)) - arguments += String.Format(" -m \"{0}\"", body); + tempFile = NPath.GetTempFilename("GitCommitTask"); + arguments = $"-c i18n.commitencoding=utf8 commit --file \"{tempFile}\""; + } + + protected override void RaiseOnStart() + { + base.RaiseOnStart(); + tempFile.WriteAllLines(new [] { message, Environment.NewLine, body }); + } + + protected override void RaiseOnEnd() + { + tempFile.DeleteIfExists(); + base.RaiseOnEnd(); } public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Committing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs index a33a418f5..3833d465a 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitConfigGetAllTask : ProcessTaskWithListOutput + public class GitConfigGetAllTask : ProcessTaskWithListOutput { private const string TaskName = "git config get"; private readonly string arguments; @@ -48,5 +48,6 @@ public GitConfigGetTask(string key, GitConfigSource configSource, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading configuration..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs index 8d0fd3d92..4935b5864 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitConfigListTask : ProcessTaskWithListOutput> + public class GitConfigListTask : ProcessTaskWithListOutput> { private const string TaskName = "git config list"; private readonly string arguments; @@ -28,5 +28,6 @@ public GitConfigListTask(GitConfigSource configSource, CancellationToken token, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Reading configuration..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs index 4f4604236..369f5150c 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitConfigSetTask : ProcessTask + public class GitConfigSetTask : ProcessTask { private readonly string arguments; @@ -23,5 +23,6 @@ public GitConfigSetTask(string key, string value, GitConfigSource configSource, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Writing configuration..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitConfigUnSetTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigUnSetTask.cs new file mode 100644 index 000000000..808e97193 --- /dev/null +++ b/src/GitHub.Api/Git/Tasks/GitConfigUnSetTask.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; + +namespace GitHub.Unity.Git.Tasks +{ + public class GitConfigUnSetTask : ProcessTask + { + private readonly string arguments; + + public GitConfigUnSetTask(string key, GitConfigSource configSource, + CancellationToken token, IOutputProcessor processor = null) + : base(token, processor ?? new SimpleOutputProcessor()) + { + var source = ""; + source += + configSource == GitConfigSource.NonSpecified ? "--unset" : + configSource == GitConfigSource.Local ? "--local --unset" : + configSource == GitConfigSource.User ? "--global --unset" : + "--system --unset"; + arguments = String.Format("config {0} {1}", source, key); + Name = String.Format("config {0} {1}", source, key); + } + + public override string ProcessArguments { get { return arguments; } } + public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Writing configuration..."; + } +} diff --git a/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs new file mode 100644 index 000000000..c8e8b4ae8 --- /dev/null +++ b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs @@ -0,0 +1,22 @@ +using System.Threading; + +namespace GitHub.Unity.Git.Tasks +{ + public class GitCountObjectsTask : ProcessTask + { + private const string TaskName = "git count-objects"; + + public GitCountObjectsTask(CancellationToken token, IOutputProcessor processor = null) + : base(token, processor ?? new GitCountObjectsProcessor()) + { + Name = TaskName; + } + + public override string ProcessArguments + { + get { return "count-objects"; } + } + public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Counting git objects..."; + } +} diff --git a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs index c5a006b8b..445758422 100644 --- a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs @@ -1,37 +1,41 @@ using System; -using System.Text; +using System.Collections.Generic; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitFetchTask : ProcessTask + public class GitFetchTask : ProcessTask { private const string TaskName = "git fetch"; private readonly string arguments; public GitFetchTask(string remote, - CancellationToken token, bool prune = false, IOutputProcessor processor = null) + CancellationToken token, bool prune = true, bool tags = true, IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) { Name = TaskName; - var stringBuilder = new StringBuilder(); - stringBuilder.Append("fetch"); - + var args = new List { "fetch" }; + if (prune) { - stringBuilder.Append(" -p"); + args.Add("--prune"); + } + + if (tags) + { + args.Add("--tags"); } if (!String.IsNullOrEmpty(remote)) { - stringBuilder.Append(" "); - stringBuilder.Append(remote); + args.Add(remote); } - arguments = stringBuilder.ToString(); + arguments = args.Join(" "); } public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Fetching..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitInitTask.cs b/src/GitHub.Api/Git/Tasks/GitInitTask.cs index f669db44d..e47f7b0ab 100644 --- a/src/GitHub.Api/Git/Tasks/GitInitTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitInitTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitInitTask : ProcessTask + public class GitInitTask : ProcessTask { private const string TaskName = "git init"; @@ -14,5 +14,6 @@ public GitInitTask(CancellationToken token, IOutputProcessor processor = public override string ProcessArguments { get { return "init"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Initializing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs b/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs index 0aaef1dfc..6cf4f89c6 100644 --- a/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitLfsInstallTask : ProcessTask + public class GitLfsInstallTask : ProcessTask { private const string TaskName = "git lsf install"; @@ -14,5 +14,6 @@ public GitLfsInstallTask(CancellationToken token, IOutputProcessor proce public override string ProcessArguments { get { return "lfs install"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Initializing LFS..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs b/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs index e50428d30..dad8f566c 100644 --- a/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs @@ -1,13 +1,12 @@ -using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitLfsVersionTask : ProcessTask + public class GitLfsVersionTask : ProcessTask { private const string TaskName = "git lfs version"; - public GitLfsVersionTask(CancellationToken token, IOutputProcessor processor = null) + public GitLfsVersionTask(CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new LfsVersionOutputProcessor()) { Name = TaskName; @@ -15,5 +14,6 @@ public GitLfsVersionTask(CancellationToken token, IOutputProcessor proc public override string ProcessArguments { get { return "lfs version"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading LFS version..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs b/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs index 97938f95a..58c1aae56 100644 --- a/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitListLocalBranchesTask : ProcessTaskWithListOutput + public class GitListLocalBranchesTask : ProcessTaskWithListOutput { private const string TaskName = "git list local branches"; private const string Arguments = "branch -vv"; @@ -13,10 +13,8 @@ public GitListLocalBranchesTask(CancellationToken token, BaseOutputListProcessor Name = TaskName; } - public override string ProcessArguments - { - get { return Arguments ; } - } + public override string ProcessArguments => Arguments; + public override string Message { get; set; } = "Listing local branches..."; } @@ -31,9 +29,7 @@ public GitListRemoteBranchesTask(CancellationToken token) Name = TaskName; } - public override string ProcessArguments - { - get { return Arguments; } - } + public override string ProcessArguments => Arguments; + public override string Message { get; set; } = "Listing remote branches..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs index d73197348..6aadd5198 100644 --- a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs @@ -1,18 +1,18 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitListLocksTask : ProcessTaskWithListOutput + public class GitListLocksTask : ProcessTaskWithListOutput { private const string TaskName = "git lfs locks"; private readonly string args; - public GitListLocksTask(IGitObjectFactory gitObjectFactory, bool local, + public GitListLocksTask(bool local, CancellationToken token, BaseOutputListProcessor processor = null) - : base(token, processor ?? new LockOutputProcessor(gitObjectFactory)) + : base(token, processor ?? new LocksOutputProcessor()) { Name = TaskName; - args = "lfs locks"; + args = "locks --json"; if (local) { args += " --local"; @@ -20,5 +20,6 @@ public GitListLocksTask(IGitObjectFactory gitObjectFactory, bool local, } public override string ProcessArguments => args; + public override string Message { get; set; } = "Reading locks..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLockTask.cs b/src/GitHub.Api/Git/Tasks/GitLockTask.cs index 4903f8341..c51004553 100644 --- a/src/GitHub.Api/Git/Tasks/GitLockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLockTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitLockTask : ProcessTask + public class GitLockTask : ProcessTask { private const string TaskName = "git lfs lock"; private readonly string arguments; @@ -14,10 +14,11 @@ public GitLockTask(string path, { Name = TaskName; Guard.ArgumentNotNullOrWhiteSpace(path, "path"); - arguments = String.Format("lfs lock \"{0}\"", path); + arguments = String.Format("lock \"{0}\"", path); } public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Locking file..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLogTask.cs b/src/GitHub.Api/Git/Tasks/GitLogTask.cs index d416ae660..ace25072a 100644 --- a/src/GitHub.Api/Git/Tasks/GitLogTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLogTask.cs @@ -1,21 +1,55 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitLogTask : ProcessTaskWithListOutput + public class GitLogTask : ProcessTaskWithListOutput { private const string TaskName = "git log"; + private const string baseArguments = @"-c i18n.logoutputencoding=utf8 -c core.quotepath=false log --pretty=format:""%H%n%P%n%aN%n%aE%n%aI%n%cN%n%cE%n%cI%n%B---GHUBODYEND---"" --name-status"; + private readonly string arguments; public GitLogTask(IGitObjectFactory gitObjectFactory, + CancellationToken token, + BaseOutputListProcessor processor = null) + : this(0, gitObjectFactory, token, processor) + { + } + + public GitLogTask(string file, + IGitObjectFactory gitObjectFactory, + CancellationToken token, BaseOutputListProcessor processor = null) + : this(file, 0, gitObjectFactory, token, processor) + { + } + + public GitLogTask(int numberOfCommits, IGitObjectFactory gitObjectFactory, + CancellationToken token, + BaseOutputListProcessor processor = null) + : base(token, processor ?? new LogEntryOutputProcessor(gitObjectFactory)) + { + Name = TaskName; + arguments = baseArguments; + if (numberOfCommits > 0) + arguments += " -n " + numberOfCommits; + } + + public GitLogTask(string file, int numberOfCommits, + IGitObjectFactory gitObjectFactory, CancellationToken token, BaseOutputListProcessor processor = null) : base(token, processor ?? new LogEntryOutputProcessor(gitObjectFactory)) { Name = TaskName; + arguments = baseArguments; + if (numberOfCommits > 0) + arguments += " -n " + numberOfCommits; + arguments += " -- "; + arguments += " \"" + file + "\""; } public override string ProcessArguments { - get { return @"-c i18n.logoutputencoding=utf8 -c core.quotepath=false log --pretty=format:""%H%n%P%n%aN%n%aE%n%aI%n%cN%n%cE%n%cI%n%B---GHUBODYEND---"" --name-status"; } + get { return arguments; } } + public override string Message { get; set; } = "Loading the history..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitPullTask.cs b/src/GitHub.Api/Git/Tasks/GitPullTask.cs index fe51032bf..768c4fc27 100644 --- a/src/GitHub.Api/Git/Tasks/GitPullTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitPullTask.cs @@ -2,9 +2,9 @@ using System.Text; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitPullTask : ProcessTask + public class GitPullTask : ProcessTask { private const string TaskName = "git pull"; private readonly string arguments; @@ -34,5 +34,6 @@ public GitPullTask(string remote, string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Pulling..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitPushTask.cs b/src/GitHub.Api/Git/Tasks/GitPushTask.cs index 0e4c74cfd..4eb533b61 100644 --- a/src/GitHub.Api/Git/Tasks/GitPushTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitPushTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitPushTask : ProcessTask + public class GitPushTask : ProcessTask { private const string TaskName = "git push"; private readonly string arguments; @@ -30,5 +30,6 @@ public GitPushTask(string remote, string branch, bool setUpstream, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Pushing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs index f6e948009..375793699 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRemoteAddTask : ProcessTask + public class GitRemoteAddTask : ProcessTask { private const string TaskName = "git remote add"; private readonly string arguments; @@ -21,5 +21,6 @@ public GitRemoteAddTask(string remote, string url, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Adding remote..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs index 6be6ffbef..88899a928 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRemoteBranchDeleteTask : ProcessTask + public class GitRemoteBranchDeleteTask : ProcessTask { private const string TaskName = "git push --delete"; private readonly string arguments; @@ -21,5 +21,6 @@ public GitRemoteBranchDeleteTask(string remote, string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Deleting remote branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs index 02eb69a48..3b2ff7a2a 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRemoteChangeTask : ProcessTask + public class GitRemoteChangeTask : ProcessTask { private const string TaskName = "git remote set-url"; private readonly string arguments; @@ -21,5 +21,6 @@ public GitRemoteChangeTask(string remote, string url, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Switching remotes..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs index c1538cb63..f8eb75234 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRemoteListTask : ProcessTaskWithListOutput + public class GitRemoteListTask : ProcessTaskWithListOutput { private const string TaskName = "git remote"; @@ -13,5 +13,6 @@ public GitRemoteListTask(CancellationToken token, BaseOutputListProcessor + public class GitRemoteRemoveTask : ProcessTask { private const string TaskName = "git remote rm"; private readonly string arguments; @@ -19,5 +19,6 @@ public GitRemoteRemoveTask(string remote, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Deleting remote..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoveFromIndexTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoveFromIndexTask.cs index ab83c01ee..391d884b0 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoveFromIndexTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoveFromIndexTask.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRemoveFromIndexTask : ProcessTask + public class GitRemoveFromIndexTask : ProcessTask { private const string TaskName = "git reset HEAD"; private readonly string arguments; @@ -26,5 +26,6 @@ public GitRemoveFromIndexTask(IEnumerable files, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Unstaging files..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitRevertTask.cs b/src/GitHub.Api/Git/Tasks/GitRevertTask.cs index 862b78c4d..b677546b5 100644 --- a/src/GitHub.Api/Git/Tasks/GitRevertTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRevertTask.cs @@ -1,8 +1,8 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitRevertTask : ProcessTask + public class GitRevertTask : ProcessTask { private const string TaskName = "git revert"; private readonly string arguments; @@ -18,5 +18,6 @@ public GitRevertTask(string changeset, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Reverting commit..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs index da66e2c98..2acf8d26a 100644 --- a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs @@ -1,22 +1,23 @@ using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitStatusTask : ProcessTask + public class GitStatusTask : ProcessTask { private const string TaskName = "git status"; public GitStatusTask(IGitObjectFactory gitObjectFactory, CancellationToken token, IOutputProcessor processor = null) - : base(token, processor ?? new StatusOutputProcessor(gitObjectFactory)) + : base(token, processor ?? new GitStatusOutputProcessor(gitObjectFactory)) { Name = TaskName; } public override string ProcessArguments { - get { return "-c i18n.logoutputencoding=utf8 -c core.quotepath=false status -b -u --porcelain"; } + get { return "-c i18n.logoutputencoding=utf8 -c core.quotepath=false --no-optional-locks status -b -u --porcelain"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Listing changed files..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs b/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs index 80efe5e40..7e4f4e283 100644 --- a/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitSwitchBranchesTask : ProcessTask + public class GitSwitchBranchesTask : ProcessTask { private const string TaskName = "git checkout"; private readonly string arguments; @@ -19,5 +19,6 @@ public GitSwitchBranchesTask(string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Switching branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs index ad2c80eb2..ff534cd76 100644 --- a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs @@ -1,21 +1,21 @@ using System.Text; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitUnlockTask : ProcessTask + public class GitUnlockTask : ProcessTask { private const string TaskName = "git lfs unlock"; private readonly string arguments; - public GitUnlockTask(string path, bool force, + public GitUnlockTask(NPath path, bool force, CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) { Guard.ArgumentNotNullOrWhiteSpace(path, "path"); Name = TaskName; - var stringBuilder = new StringBuilder("lfs unlock "); + var stringBuilder = new StringBuilder("unlock "); if (force) { @@ -23,7 +23,7 @@ public GitUnlockTask(string path, bool force, } stringBuilder.Append("\""); - stringBuilder.Append(path); + stringBuilder.Append(path.ToString(SlashMode.Forward)); stringBuilder.Append("\""); arguments = stringBuilder.ToString(); @@ -31,6 +31,7 @@ public GitUnlockTask(string path, bool force, public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Unlocking file..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitVersionTask.cs b/src/GitHub.Api/Git/Tasks/GitVersionTask.cs index d02936829..d4d01493a 100644 --- a/src/GitHub.Api/Git/Tasks/GitVersionTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitVersionTask.cs @@ -1,13 +1,12 @@ -using System; using System.Threading; -namespace GitHub.Unity +namespace GitHub.Unity.Git.Tasks { - class GitVersionTask : ProcessTask + public class GitVersionTask : ProcessTask { private const string TaskName = "git --version"; - public GitVersionTask(CancellationToken token, IOutputProcessor processor = null) + public GitVersionTask(CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new VersionOutputProcessor()) { Name = TaskName; @@ -15,5 +14,6 @@ public GitVersionTask(CancellationToken token, IOutputProcessor process public override string ProcessArguments { get { return "--version"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading git version..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/TreeData.cs b/src/GitHub.Api/Git/TreeData.cs index ac9e67b2c..997cbffed 100644 --- a/src/GitHub.Api/Git/TreeData.cs +++ b/src/GitHub.Api/Git/TreeData.cs @@ -7,6 +7,7 @@ public interface ITreeData { string Path { get; } bool IsActive { get; } + bool IsChecked { get; } } [Serializable] @@ -64,6 +65,7 @@ public bool Equals(GitBranchTreeData other) public string Path => GitBranch.Name; public bool IsActive => isActive; + public bool IsChecked => false; } [Serializable] @@ -73,11 +75,13 @@ public struct GitStatusEntryTreeData : ITreeData public GitStatusEntry gitStatusEntry; public bool isLocked; + public bool isChecked; public GitStatusEntryTreeData(GitStatusEntry gitStatusEntry, bool isLocked = false) { this.isLocked = isLocked; this.gitStatusEntry = gitStatusEntry; + isChecked = gitStatusEntry.Staged; } public override int GetHashCode() @@ -127,5 +131,6 @@ public bool Equals(GitStatusEntryTreeData other) public GitStatusEntry GitStatusEntry => gitStatusEntry; public GitFileStatus FileStatus => gitStatusEntry.Status; public bool IsLocked => isLocked; + public bool IsChecked => isChecked; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/ValidateGitInstallResult.cs b/src/GitHub.Api/Git/ValidateGitInstallResult.cs deleted file mode 100644 index 978931eb3..000000000 --- a/src/GitHub.Api/Git/ValidateGitInstallResult.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; - -namespace GitHub.Unity -{ - public struct ValidateGitInstallResult - { - public bool IsValid; - public Version GitVersion; - public Version GitLfsVersion; - - public ValidateGitInstallResult(bool isValid, Version gitVersion, Version gitLfsVersion) - { - IsValid = isValid; - GitVersion = gitVersion; - GitLfsVersion = gitLfsVersion; - } - - public override int GetHashCode() - { - int hash = 17; - hash = hash * 23 + IsValid.GetHashCode(); - hash = hash * 23 + (GitVersion?.GetHashCode() ?? 0); - hash = hash * 23 + (GitLfsVersion?.GetHashCode() ?? 0); - return hash; - } - - public override bool Equals(object other) - { - if (other is ValidateGitInstallResult) - return Equals((ValidateGitInstallResult)other); - return false; - } - - public bool Equals(ValidateGitInstallResult other) - { - return IsValid == other.IsValid && - object.Equals(GitVersion, other.GitVersion) && - object.Equals(GitLfsVersion, other.GitLfsVersion) - ; - } - - public static bool operator ==(ValidateGitInstallResult lhs, ValidateGitInstallResult rhs) - { - // If both are null, or both are same instance, return true. - if (ReferenceEquals(lhs, rhs)) - return true; - - // If one is null, but not both, return false. - if (((object)lhs == null) || ((object)rhs == null)) - return false; - - // Return true if the fields match: - return lhs.Equals(rhs); - } - - public static bool operator !=(ValidateGitInstallResult lhs, ValidateGitInstallResult rhs) - { - return !(lhs == rhs); - } - } -} \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj new file mode 100644 index 000000000..9833d34f1 --- /dev/null +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -0,0 +1,327 @@ + + + + + Debug + AnyCPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B} + Library + Properties + GitHub.Unity + GitHub.Api.45 + v4.5 + 512 + + 6 + + + ..\UnityExtension\Assets\Editor\build\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs);NET_4_6 + prompt + 4 + false + false + true + ..\..\common\codeanalysis-debug.ruleset + + + Debug + + + + $(SolutionDir)lib\Mono.Posix.dll + + + $(SolutionDir)\lib\sfw\sfw.net.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Localization.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metrics\MetricsService.cs + + + Properties\ApplicationInfo_Local.cs + + + + + + + Properties\ApplicationInfo_Local.cs-example + + + Properties\ApplicationInfo_Local.cs + + + + + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + Localization.Designer.cs + Designer + + + + + + \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject b/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject new file mode 100644 index 000000000..ebf9681fd --- /dev/null +++ b/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + + ..\..\script\lib\Managed\UnityEditor.dll + ..\..\script\lib\Managed\UnityEngine.dll + + + \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 5842dcc0d..4b2c8d7e5 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -25,7 +25,7 @@ DEBUG;TRACE;$(BuildDefs) prompt 4 - true + false false true @@ -35,8 +35,8 @@ TRACE;$(BuildDefs) prompt 4 - Release - true + Release + false false true @@ -60,12 +60,6 @@ $(SolutionDir)\packages\AsyncBridge.Net35.0.2.3333.0\lib\net35-Client\AsyncBridge.Net35.dll True - - $(SolutionDir)lib\ICSharpCode.SharpZipLib.dll - - - $(SolutionDir)lib\Mono.Security.dll - $(SolutionDir)lib\Mono.Posix.dll @@ -83,28 +77,28 @@ $(SolutionDir).\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll True - - - - + + + + + - @@ -114,15 +108,19 @@ + - + + + + @@ -136,6 +134,25 @@ + + + + + + + + + + + + + + + + + + + @@ -163,15 +180,17 @@ + + + + - + - - @@ -181,8 +200,6 @@ True Localization.resx - - @@ -236,6 +253,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -266,41 +309,21 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + + + + + + + + + + @@ -315,8 +338,11 @@ Other similar extension points exist, see Microsoft.Common.targets. + --> + + + - --> \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.v3.ncrunchproject b/src/GitHub.Api/GitHub.Api.v3.ncrunchproject index 2f25da2c4..8fa6360df 100644 --- a/src/GitHub.Api/GitHub.Api.v3.ncrunchproject +++ b/src/GitHub.Api/GitHub.Api.v3.ncrunchproject @@ -1,8 +1,8 @@  - ..\..\script\lib\UnityEditor.dll - ..\..\script\lib\UnityEngine.dll + ..\..\script\lib\Managed\UnityEngine.dll + ..\..\script\lib\Managed\UnityEditor.dll True diff --git a/src/GitHub.Api/Helpers/AssemblyResources.cs b/src/GitHub.Api/Helpers/AssemblyResources.cs index dd359ff6f..d189eae1e 100644 --- a/src/GitHub.Api/Helpers/AssemblyResources.cs +++ b/src/GitHub.Api/Helpers/AssemblyResources.cs @@ -14,6 +14,21 @@ class AssemblyResources { public static NPath ToFile(ResourceType resourceType, string resource, NPath destinationPath, IEnvironment environment) { + /* + This function attempts to get files embedded in the callers assembly. + GitHub.Unity which tends to contain logos + GitHub.Api which tends to contain application resources + + Each file's name is their physical path in the project. + + When running tests, we assume the tests are looking for application resources, and default to returning GitHub.Api + + First check for the resource in the calling assembly. + If the resource cannot be found, fallback to looking in GitHub.Api's assembly. + If the resource is still not found, it attempts to find it in the file system + */ + + var target = destinationPath.Combine(resource); var os = ""; if (resourceType == ResourceType.Platform) { @@ -25,15 +40,28 @@ public static NPath ToFile(ResourceType resourceType, string resource, NPath des : resourceType == ResourceType.Platform ? "PlatformResources" : "Resources"; + // all the resources are embedded in GitHub.Api var asm = Assembly.GetCallingAssembly(); - if (!asm.FullName.StartsWith("IntegrationTests")) + if (resourceType != ResourceType.Icon) asm = typeof(AssemblyResources).Assembly; var stream = asm.GetManifestResourceStream( String.Format("GitHub.Unity.{0}{1}.{2}", type, !string.IsNullOrEmpty(os) ? "." + os : os, resource)); if (stream != null) - return destinationPath.Combine(resource).WriteAllBytes(stream.ToByteArray()); + { + target.DeleteIfExists(); + return target.WriteAllBytes(stream.ToByteArray()); + } - return environment.ExtensionInstallPath.Combine(type, os, resource).Copy(destinationPath.Combine(resource)); + // if we're not in the test runner, we might be running in a Unity-compiled GitHub.Unity assembly, which doesn't + // embed the resources in the assembly + // check the filesystem + NPath possiblePath = environment.ExtensionInstallPath.Combine(type, os, resource); + if (possiblePath.FileExists()) + { + target.DeleteIfExists(); + return possiblePath.Copy(target); + } + return NPath.Default; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index 7f064b382..1d0da6a86 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -1,4 +1,4 @@ -using System; +using System.Globalization; namespace GitHub.Unity { @@ -6,13 +6,38 @@ static class Constants { public const string GuidKey = "Guid"; public const string MetricsKey = "MetricsEnabled"; - public const string UsageFile = "usage.json"; + public const string UsageFile = "metrics.json"; public const string GitInstallPathKey = "GitInstallPath"; public const string TraceLoggingKey = "EnableTraceLogging"; public const string WebTimeoutKey = "WebTimeout"; - public const string Iso8601Format = "yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz"; + public const string GitTimeoutKey = "GitTimeout"; + public const string Iso8601Format = @"yyyy-MM-dd\THH\:mm\:ss.fffzzz"; + public const string Iso8601FormatZ = @"yyyy-MM-dd\THH\:mm\:ss\Z"; + public static readonly string[] Iso8601Formats = { + Iso8601Format, + Iso8601FormatZ, + @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.ffffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.fffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.ffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.ffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.fzzz", + @"yyyy-MM-dd\THH\:mm\:sszzz", + @"yyyy-MM-dd\THH\:mm\:ss.fffffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ffffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.fffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.fff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.f\Z", + }; + public const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; + public const string SkipVersionKey = "SkipVersion"; + public const string GitInstallationState = "GitInstallationState"; - public static readonly Version MinimumGitVersion = new Version(2, 11, 0); - public static readonly Version MinimumGitLfsVersion = new Version(2, 3, 4); + public static readonly TheVersion MinimumGitVersion = TheVersion.Parse("2.0"); + public static readonly TheVersion MinimumGitLfsVersion = TheVersion.Parse("2.0"); + public static readonly TheVersion DesiredGitVersion = TheVersion.Parse("2.11"); + public static readonly TheVersion DesiredGitLfsVersion = TheVersion.Parse("2.4"); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Helpers/Progress.cs b/src/GitHub.Api/Helpers/Progress.cs index 4dc90a4a9..312cd43da 100644 --- a/src/GitHub.Api/Helpers/Progress.cs +++ b/src/GitHub.Api/Helpers/Progress.cs @@ -1,10 +1,13 @@ using GitHub.Logging; using System; +using System.Collections.Generic; +using System.Linq; namespace GitHub.Unity { public interface IProgress { + void UpdateProgress(long value, long total, string message = null); ITask Task { get; } /// /// From 0 to 1 @@ -16,33 +19,62 @@ public interface IProgress event Action OnProgress; } + public class ProgressReporter + { + public event Action OnProgress; + private Dictionary tasks = new Dictionary(); + private Progress progress = new Progress(TaskBase.Default); + + public void UpdateProgress(IProgress prog) + { + long total = 0; + long value = 0; + lock (tasks) + { + if (!tasks.ContainsKey(prog.Task)) + tasks.Add(prog.Task, prog); + else + tasks[prog.Task] = prog; + + total = tasks.Values.Select(x => x.Total).Sum(); + value = tasks.Values.Select(x => x.Value).Sum(); + + if (prog.Percentage == 1f) + tasks.Remove(prog.Task); + } + progress.UpdateProgress(value, total, prog.Message); + OnProgress?.Invoke(progress); + } + } + public class Progress : IProgress { private static ILogging Logger = LogHelper.GetLogger(); - public ITask Task { get; internal set; } - public float Percentage { get { return Total > 0 ? (float)(double)Value / Total : 0f; } } - public long Value { get; internal set; } - public long Total { get; internal set; } - public string Message { get; internal set; } - - private long previousValue; - private float averageSpeed = -1f; - private float lastSpeed = 0f; - private float smoothing = 0.005f; + public ITask Task { get; } + public float Percentage { get; private set; } + public long Value { get; private set; } + public long Total { get; private set; } + public string Message { get; private set; } + + private long previousValue = -1; public event Action OnProgress; - public void UpdateProgress(IProgress progress) + public Progress(ITask task) { - Task = progress.Task; - UpdateProgress(progress.Value, progress.Total, progress.Message); + this.Task = task; } public void UpdateProgress(long value, long total, string message = null) { - Total = total; - Value = value; - Message = message ?? Message; - if (Total == 0 || ((float)(double)Value / Total) - ((float)(double)previousValue / Total) > 1f / 100f) + Total = total == 0 ? 100 : total; + Value = value > Total ? Total : value; + Message = String.IsNullOrEmpty(message) ? Message : message; + float fTotal = Total; + float fValue = Value; + Percentage = fValue / fTotal; + var delta = (fValue / fTotal - previousValue / fTotal) * 100f; + + if (Value != previousValue && (fValue == 0f || delta > 1f || fValue == fTotal)) { // signal progress in 1% increments or if we don't know what the total is previousValue = Value; OnProgress?.Invoke(this); diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index dd2cf8483..1ae5e3ba7 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -36,7 +36,7 @@ // NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). // define if you are using .net framework <= 3.0 or < WP7.5 -//#define SIMPLE_JSON_NO_LINQ_EXPRESSION +#define SIMPLE_JSON_NO_LINQ_EXPRESSION // NOTE: uncomment the following line if you are compiling under Window Metro style application/library. // usually already defined in properties @@ -66,12 +66,14 @@ using System.Reflection; using System.Runtime.Serialization; using System.Text; -using GitHub.Reflection; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using GitHub.Unity.Json; // ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable RedundantExplicitArrayCreation // ReSharper disable SuggestUseVarKeywordEvident -namespace GitHub +namespace GitHub.Unity.Json { /// /// Represents the json array. @@ -482,10 +484,7 @@ public override IEnumerable GetDynamicMemberNames() } #endif } -} -namespace GitHub -{ /// /// This class encodes and decodes JSON strings. /// Spec. details, see http://www.json.org/ @@ -517,6 +516,7 @@ static class SimpleJson private static readonly char[] EscapeTable; private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' }; + private static readonly string EscapeCharactersString = new string(EscapeCharacters); static SimpleJson() { @@ -620,7 +620,7 @@ public static string EscapeToJavascriptString(string jsonString) StringBuilder sb = new StringBuilder(); char c; - for (int i = 0; i < jsonString.Length; ) + for (int i = 0; i < jsonString.Length;) { c = jsonString[i++]; @@ -1250,12 +1250,7 @@ class PocoJsonSerializerStrategy : IJsonSerializerStrategy internal static readonly Type[] EmptyTypes = new Type[0]; internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - private static readonly string[] Iso8601Format = new string[] - { - @"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z", - @"yyyy-MM-dd\THH:mm:ss\Z", - @"yyyy-MM-dd\THH:mm:ssK" - }; + private static readonly string[] Iso8601Format = Constants.Iso8601Formats; public PocoJsonSerializerStrategy() { @@ -1271,7 +1266,7 @@ protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName) internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(Type key) { - return ReflectionUtils.GetContructor(key, key.IsArray ? ArrayConstructorParameterTypes : EmptyTypes); + return ReflectionUtils.GetContructor(key, (key.IsArray || ReflectionUtils.IsAssignableFrom(typeof(IList), key))? ArrayConstructorParameterTypes : EmptyTypes); } internal virtual IDictionary GetterValueFactory(Type type) @@ -1282,14 +1277,14 @@ internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(T if (propertyInfo.CanRead) { MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (getMethod.IsStatic || !getMethod.IsPublic) + if (!CanAddProperty(propertyInfo, getMethod)) continue; result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); } } foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) { - if (fieldInfo.IsStatic || !fieldInfo.IsPublic) + if (!CanAddField(fieldInfo)) continue; result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); } @@ -1304,20 +1299,40 @@ internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(T if (propertyInfo.CanWrite) { MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (setMethod.IsStatic || !setMethod.IsPublic) + if (!CanAddProperty(propertyInfo, setMethod)) continue; result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); } } foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) { - if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) + if (fieldInfo.IsInitOnly || !CanAddField(fieldInfo)) continue; result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); } return result; } + protected virtual bool CanAddField(FieldInfo field) + { + if (field.IsStatic) + return false; + if (ReflectionUtils.GetAttribute(field, typeof(NotSerializedAttribute)) != null) + return false; + if (ReflectionUtils.GetAttribute(field, typeof(CompilerGeneratedAttribute)) != null) + return false; + return true; + } + + protected virtual bool CanAddProperty(PropertyInfo property, MethodInfo method) + { + if (method.IsStatic) + return false; + if (ReflectionUtils.GetAttribute(property, typeof(NotSerializedAttribute)) != null) + return false; + return true; + } + public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) { return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); @@ -1329,7 +1344,7 @@ public virtual object DeserializeObject(object value, Type type) if (type == null) throw new ArgumentNullException("type"); string str = value as string; - if (type == typeof (Guid) && string.IsNullOrEmpty(str)) + if (type == typeof(Guid) && string.IsNullOrEmpty(str)) return default(Guid); if (value == null) @@ -1341,27 +1356,31 @@ public virtual object DeserializeObject(object value, Type type) { if (str.Length != 0) // We know it can't be null now. { + if (type == typeof(NPath) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(NPath))) + return new NPath(str); if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) return DateTime.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) return new Guid(str); + if (type == typeof(UriString) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(UriString))) + return new UriString(str); if (type == typeof(Uri)) { - bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); + bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); Uri result; if (isValid && Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out result)) return result; - return null; + return null; } - if (type == typeof(string)) - return str; + if (type == typeof(string)) + return str; - return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); + return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); } else { @@ -1472,7 +1491,9 @@ protected virtual object SerializeEnum(Enum p) protected virtual bool TrySerializeKnownTypes(object input, out object output) { bool returnValue = true; - if (input is DateTime) + if (input is NPath || input is UriString) + output = input.ToString(); + else if (input is DateTime) output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); else if (input is DateTimeOffset) output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); @@ -1592,8 +1613,6 @@ private static bool CanAdd(MemberInfo info, out string jsonKey) #endif - namespace Reflection - { // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules // that might be in place in the target project. [GeneratedCode("reflection-utils", "1.0.0")] @@ -1648,7 +1667,7 @@ public static Type GetGenericListElementType(Type type) foreach (Type implementedInterface in interfaces) { if (IsTypeGeneric(implementedInterface) && - implementedInterface.GetGenericTypeDefinition() == typeof (IList<>)) + implementedInterface.GetGenericTypeDefinition() == typeof(IList<>)) { return GetGenericTypeArguments(implementedInterface)[0]; } @@ -1837,7 +1856,13 @@ public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo con public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) { ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); + // if it's a value type (i.e., struct), it won't have a default constructor, so use Activator instead + return constructorInfo == null ? (type.IsValueType ? GetConstructorForValueType(type) : null) : GetConstructorByReflection(constructorInfo); + } + + static ConstructorDelegate GetConstructorForValueType(Type type) + { + return delegate(object[] args) { return Activator.CreateInstance(type); }; } #if !SIMPLE_JSON_NO_LINQ_EXPRESSION @@ -1864,7 +1889,8 @@ public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo con public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) { ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); + // if it's a value type (i.e., struct), it won't have a default constructor, so use Activator instead + return constructorInfo == null ? (type.IsValueType ? GetConstructorForValueType(type) : null) : GetConstructorByExpression(constructorInfo); } #endif @@ -1924,6 +1950,9 @@ public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) #if SIMPLE_JSON_NO_LINQ_EXPRESSION return GetSetMethodByReflection(propertyInfo); #else + // if it's a struct, we want to use reflection, as linq expressions modify copies of the object and not the real thing + if (propertyInfo.DeclaringType.IsValueType) + return GetSetMethodByReflection(propertyInfo); return GetSetMethodByExpression(propertyInfo); #endif } @@ -1933,6 +1962,9 @@ public static SetDelegate GetSetMethod(FieldInfo fieldInfo) #if SIMPLE_JSON_NO_LINQ_EXPRESSION return GetSetMethodByReflection(fieldInfo); #else + // if it's a struct, we want to use reflection, as linq expressions modify copies of the object and not the real thing + if (fieldInfo.DeclaringType.IsValueType) + return GetSetMethodByReflection(fieldInfo); return GetSetMethodByExpression(fieldInfo); #endif } @@ -2119,6 +2151,110 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() } } + +} + +namespace GitHub.Unity +{ + using GitHub.Unity.Json; + + [System.AttributeUsage(System.AttributeTargets.Property | + System.AttributeTargets.Field)] + public sealed class NotSerializedAttribute : Attribute + { + } + + public static class JsonSerializerExtensions + { + static JsonSerializationStrategy publicLowerCaseStrategy = new JsonSerializationStrategy(true, true); + static JsonSerializationStrategy publicUpperCaseStrategy = new JsonSerializationStrategy(false, true); + static JsonSerializationStrategy privateLowerCaseStrategy = new JsonSerializationStrategy(true, false); + static JsonSerializationStrategy privateUpperCaseStrategy = new JsonSerializationStrategy(false, false); + + public static string ToJson(this T model, bool lowerCase = false, bool onlyPublic = true) + { + return SimpleJson.SerializeObject(model, GetStrategy(lowerCase, onlyPublic)); + } + + public static T FromJson(this string json, bool lowerCase = false, bool onlyPublic = true) + { + return SimpleJson.DeserializeObject(json, GetStrategy(lowerCase, onlyPublic)); + } + + public static T FromObject(this object obj, bool lowerCase = false, bool onlyPublic = true) + { + if (obj == null) + return default(T); + var ret = GetStrategy(lowerCase, onlyPublic).DeserializeObject(obj, typeof(T)); + if (ret is T) + return (T)ret; + return default(T); + } + + private static JsonSerializationStrategy GetStrategy(bool lowerCase, bool onlyPublic) + { + if (lowerCase && onlyPublic) + return publicLowerCaseStrategy; + if (lowerCase && !onlyPublic) + return privateLowerCaseStrategy; + if (!lowerCase && onlyPublic) + return publicUpperCaseStrategy; + return privateUpperCaseStrategy; + } + + /// + /// Convert from PascalCase to camelCase. + /// + private static string ToJsonPropertyName(string propertyName) + { + Guard.ArgumentNotNullOrWhiteSpace(propertyName, "propertyName"); + int i = 0; + while (i < propertyName.Length && char.IsUpper(propertyName[i])) + i++; + return propertyName.Substring(0, i).ToLowerInvariant() + propertyName.Substring(i); + } + + public class JsonSerializationStrategy : PocoJsonSerializerStrategy + { + private bool toLowerCase = false; + private bool onlyPublic = true; + + public JsonSerializationStrategy(bool toLowerCase, bool onlyPublic) + { + this.toLowerCase = toLowerCase; + this.onlyPublic = onlyPublic; + } + + protected override bool CanAddField(FieldInfo field) + { + var canAdd = base.CanAddField(field); + return canAdd && ((onlyPublic && field.IsPublic) || !onlyPublic); + } + + protected override bool CanAddProperty(PropertyInfo property, MethodInfo method) + { + var canAdd = base.CanAddProperty(property, method); + if (!canAdd) + return false; + + // we always serialize public things + if (method.IsPublic) + return true; + + // if the getter is private and we're only serializing public things, skip this property + if (onlyPublic && method.Name.StartsWith("get_")) + return false; + + return true; + } + + protected override string MapClrMemberNameToJsonFieldName(string clrPropertyName) + { + if (!toLowerCase) + return base.MapClrMemberNameToJsonFieldName(clrPropertyName); + return ToJsonPropertyName(clrPropertyName); + } + } } } // ReSharper restore LoopCanBeConvertedToQuery diff --git a/src/GitHub.Api/Helpers/TaskHelpers.cs b/src/GitHub.Api/Helpers/TaskHelpers.cs index 33481c029..fb5695329 100644 --- a/src/GitHub.Api/Helpers/TaskHelpers.cs +++ b/src/GitHub.Api/Helpers/TaskHelpers.cs @@ -8,14 +8,18 @@ static class TaskHelpers { public static Task GetCompletedTask(T result) { +#if NET_4_6 + return Task.FromResult(result); +#else return TaskEx.FromResult(result); +#endif } public static Task ToTask(this Exception exception) { - TaskCompletionSource completionSource = new TaskCompletionSource(); - completionSource.TrySetException(exception); - return completionSource.Task; + TaskCompletionSource completionSource = new TaskCompletionSource(); + completionSource.TrySetException(exception); + return completionSource.Task; } } diff --git a/src/GitHub.Api/IO/FileSystem.cs b/src/GitHub.Api/IO/FileSystem.cs index 6592f30a1..e217f9eff 100644 --- a/src/GitHub.Api/IO/FileSystem.cs +++ b/src/GitHub.Api/IO/FileSystem.cs @@ -23,13 +23,11 @@ public interface IFileSystem IEnumerable GetDirectories(string path); IEnumerable GetDirectories(string path, string pattern); IEnumerable GetDirectories(string path, string pattern, SearchOption searchOption); - string GetDirectoryName(string path); string GetFileNameWithoutExtension(string fileName); IEnumerable GetFiles(string path); IEnumerable GetFiles(string path, string pattern); IEnumerable GetFiles(string path, string pattern, SearchOption searchOption); string GetFullPath(string path); - string GetParentDirectory(string path); string GetRandomFileName(); string GetTempPath(); Stream OpenRead(string path); @@ -46,6 +44,7 @@ public interface IFileSystem void WriteLines(string path, string[] contents); char DirectorySeparatorChar { get; } + string GetProcessDirectory(); } @@ -102,11 +101,6 @@ public string GetFullPath(string path) return Path.GetFullPath(path); } - public string GetDirectoryName(string path) - { - return Path.GetDirectoryName(path); - } - public bool DirectoryExists(string path) { return Directory.Exists(path); @@ -118,11 +112,6 @@ public bool ExistingPathIsDirectory(string path) return (attr & FileAttributes.Directory) == FileAttributes.Directory; } - public string GetParentDirectory(string path) - { - return Directory.GetParent(path).FullName; - } - public IEnumerable GetDirectories(string path, string pattern) { return Directory.GetDirectories(path, pattern); @@ -143,9 +132,10 @@ public string GetFileNameWithoutExtension(string fileName) return Path.GetFileNameWithoutExtension(fileName); } + public IEnumerable GetFiles(string path) { - return Directory.GetFiles(path); + return GetFiles(path, "*"); } public IEnumerable GetFiles(string path, string pattern) @@ -155,7 +145,43 @@ public IEnumerable GetFiles(string path, string pattern) public IEnumerable GetFiles(string path, string pattern, SearchOption searchOption) { - return Directory.GetFiles(path, pattern, searchOption); + foreach (var file in GetFiles(path, pattern)) + yield return file; + + if (searchOption != SearchOption.AllDirectories) + yield break; + +#if ENABLE_MONO + if (NPath.IsUnix) + { + try + { + path = Mono.Unix.UnixPath.GetCompleteRealPath(path); + } + catch + {} + } +#endif + foreach (var dir in GetDirectories(path)) + { + var realdir = dir; +#if ENABLE_MONO + if (NPath.IsUnix) + { + try + { + realdir = Mono.Unix.UnixPath.GetCompleteRealPath(dir); + } + catch + {} + } +#endif + if (path != realdir) + { + foreach (var file in GetFiles(dir, pattern, searchOption)) + yield return file; + } + } } public byte[] ReadAllBytes(string path) @@ -205,6 +231,11 @@ public string GetCurrentDirectory() return Directory.GetCurrentDirectory(); } + public string GetProcessDirectory() + { + return Directory.GetCurrentDirectory(); + } + public void WriteAllText(string path, string contents) { File.WriteAllText(path, contents); @@ -264,4 +295,4 @@ public char DirectorySeparatorChar get { return Path.DirectorySeparatorChar; } } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index 3a9ff272a..c8605de6a 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -27,6 +27,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -34,6 +36,7 @@ namespace GitHub.Unity { [Serializable] + [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct NPath : IEquatable, IComparable { public static NPath Default; @@ -110,7 +113,7 @@ private static bool HasNonDotDotLastElement(List stack) private static string ParseDriveLetter(string path, out string driveLetter) { - if (path.Length >= 2 && path[1] == ':') + if (path.Length >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) { driveLetter = path[0].ToString(); return path.Substring(2); @@ -224,6 +227,7 @@ public NPath ChangeExtension(string extension) newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.'); return new NPath(newElements, _isRelative, _driveLetter); } + #endregion construction #region inspection @@ -304,7 +308,7 @@ public bool Exists(NPath append) public bool DirectoryExists() { ThrowIfNotInitialized(); - return FileSystem.DirectoryExists(ToString()); + return FSWrapper.DirectoryExists(this); } public bool DirectoryExists(string append) @@ -320,13 +324,13 @@ public bool DirectoryExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return DirectoryExists(); - return FileSystem.DirectoryExists(Combine(append).ToString()); + return FSWrapper.DirectoryExists(Combine(append)); } public bool FileExists() { ThrowIfNotInitialized(); - return FileSystem.FileExists(ToString()); + return FSWrapper.FileExists(this); } public bool FileExists(string append) @@ -342,7 +346,7 @@ public bool FileExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return FileExists(); - return FileSystem.FileExists(Combine(append).ToString()); + return FSWrapper.FileExists(Combine(append)); } public string ExtensionWithDot @@ -471,11 +475,13 @@ public override int GetHashCode() int hash = 17; // Suitable nullity checks etc, of course :) hash = hash * 23 + _isInitialized.GetHashCode(); + if (!_isInitialized) + return hash; hash = hash * 23 + _isRelative.GetHashCode(); foreach (var element in _elements) - hash = hash * 23 + (IsLinux ? element : element.ToUpperInvariant()).GetHashCode(); + hash = hash * 23 + (IsUnix ? element : element.ToUpperInvariant()).GetHashCode(); if (_driveLetter != null) - hash = hash * 23 + (IsLinux ? _driveLetter : _driveLetter.ToUpperInvariant()).GetHashCode(); + hash = hash * 23 + (IsUnix ? _driveLetter : _driveLetter.ToUpperInvariant()).GetHashCode(); return hash; } } @@ -529,7 +535,7 @@ public bool IsRoot public IEnumerable Files(string filter, bool recurse = false) { - return FileSystem.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FSWrapper.GetFiles(this, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Files(bool recurse = false) @@ -549,7 +555,7 @@ public IEnumerable Contents(bool recurse = false) public IEnumerable Directories(string filter, bool recurse = false) { - return FileSystem.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FSWrapper.GetDirectories(this, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Directories(bool recurse = false) @@ -566,7 +572,7 @@ public NPath CreateFile() ThrowIfRelative(); ThrowIfRoot(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToString(), new byte[0]); + FSWrapper.WriteAllBytes(this, new byte[0]); return this; } @@ -591,7 +597,7 @@ public NPath CreateDirectory() if (IsRoot) throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString()); - FileSystem.DirectoryCreate(ToString()); + FSWrapper.DirectoryCreate(this); return this; } @@ -627,7 +633,6 @@ public NPath Copy(NPath dest) public NPath Copy(NPath dest, Func fileFilter) { ThrowIfNotInitialized(); - ThrowIfRelative(); ThrowIfNotInitialized(dest); if (dest.IsRelative) @@ -661,7 +666,7 @@ NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func absoluteDestination.EnsureParentDirectoryExists(); - FileSystem.FileCopy(ToString(), absoluteDestination.ToString(), true); + FSWrapper.FileCopy(this, absoluteDestination, true); return absoluteDestination; } @@ -684,20 +689,27 @@ public void Delete(DeleteMode deleteMode = DeleteMode.Normal) if (IsRoot) throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString()); - if (FileExists()) - FileSystem.FileDelete(ToString()); - else if (DirectoryExists()) - try + var isFile = FileExists(); + var isDir = DirectoryExists(); + if (!isFile && !isDir) + throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); + + try + { + if (isFile) { - FileSystem.DirectoryDelete(ToString(), true); + FSWrapper.FileDelete(this); } - catch (IOException) + else { - if (deleteMode == DeleteMode.Normal) - throw; + FSWrapper.DirectoryDelete(this, true); } - else - throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); + } + catch (IOException) + { + if (deleteMode == DeleteMode.Normal) + throw; + } } public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal) @@ -771,7 +783,6 @@ public NPath Move(NPath dest) { ThrowIfNotInitialized(); ThrowIfNotInitialized(dest); - ThrowIfRelative(); if (IsRoot) throw new NotSupportedException("Move is not supported on a root level directory because it would be dangerous:" + ToString()); @@ -784,91 +795,83 @@ public NPath Move(NPath dest) if (FileExists()) { + dest.DeleteIfExists(); dest.EnsureParentDirectoryExists(); - FileSystem.FileMove(ToString(), dest.ToString()); + FSWrapper.FileMove(this, dest); return dest; } if (DirectoryExists()) { - FileSystem.DirectoryMove(ToString(), dest.ToString()); + FSWrapper.DirectoryMove(this, dest); return dest; } - throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString()); + throw new ArgumentException("Move() called on a path that doesn't exist: " + ToProcessDirectory().ToString()); } public NPath WriteAllText(string contents) { ThrowIfNotInitialized(); - ThrowIfRelative(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToString(), contents); + FSWrapper.WriteAllText(this, contents); return this; } public string ReadAllText() { ThrowIfNotInitialized(); - ThrowIfRelative(); - return FileSystem.ReadAllText(ToString()); + return FSWrapper.ReadAllText(this); } public NPath WriteAllText(string contents, Encoding encoding) { ThrowIfNotInitialized(); - ThrowIfRelative(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToString(), contents, encoding); + FSWrapper.WriteAllText(this, contents, encoding); return this; } public string ReadAllText(Encoding encoding) { ThrowIfNotInitialized(); - ThrowIfRelative(); - return FileSystem.ReadAllText(ToString(), encoding); + return FSWrapper.ReadAllText(this, encoding); } public NPath WriteLines(string[] contents) { ThrowIfNotInitialized(); - ThrowIfRelative(); EnsureParentDirectoryExists(); - FileSystem.WriteLines(ToString(), contents); + FSWrapper.WriteLines(this, contents); return this; } public NPath WriteAllLines(string[] contents) { ThrowIfNotInitialized(); - ThrowIfRelative(); EnsureParentDirectoryExists(); - FileSystem.WriteAllLines(ToString(), contents); + FSWrapper.WriteAllLines(this, contents); return this; } public string[] ReadAllLines() { ThrowIfNotInitialized(); - ThrowIfRelative(); - return FileSystem.ReadAllLines(ToString()); + return FSWrapper.ReadAllLines(this); } public NPath WriteAllBytes(byte[] contents) { ThrowIfNotInitialized(); - ThrowIfRelative(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToString(), contents); + FSWrapper.WriteAllBytes(this, contents); return this; } public byte[] ReadAllBytes() { ThrowIfNotInitialized(); - ThrowIfRelative(); - return FileSystem.ReadAllBytes(ToString()); + return FSWrapper.ReadAllBytes(this); } @@ -906,6 +909,14 @@ public static NPath CurrentDirectory } } + public static NPath ProcessDirectory + { + get + { + return new NPath(FileSystem.GetProcessDirectory()); + } + } + public static NPath HomeDirectory { get @@ -952,6 +963,13 @@ private static void ThrowIfNotInitialized(NPath path) path.ThrowIfNotInitialized(); } + public NPath ToProcessDirectory() + { + if (!IsRelative) + return this; + return MakeAbsolute().RelativeTo(NPath.ProcessDirectory); + } + public NPath EnsureDirectoryExists(string append = "") { ThrowIfNotInitialized(); @@ -1082,26 +1100,45 @@ public static IFileSystem FileSystem { if (_fileSystem == null) #if UNITY_4 || UNITY_5 || UNITY_5_3_OR_NEWER - _fileSystem = new FileSystem(UnityEngine.Application.dataPath); + FileSystem = new FileSystem(UnityEngine.Application.dataPath); #else - _fileSystem = new FileSystem(Directory.GetCurrentDirectory()); + FileSystem = new FileSystem(Directory.GetCurrentDirectory()); #endif return _fileSystem; } set { _fileSystem = value; + FSWrapper = new FSWrapper(value); + } + } + + private static FSWrapper _fsWrapper; + private static FSWrapper FSWrapper + { + get + { + if (_fsWrapper == null) + { + // this will initialize both FileSystem and FSWrapper + var fs = FileSystem; + } + return _fsWrapper; + } + set + { + _fsWrapper = value; } } - private static bool? _isLinux; - internal static bool IsLinux + private static bool? _isUnix; + internal static bool IsUnix { get { - if (!_isLinux.HasValue) - _isLinux = FileSystem.DirectoryExists("/proc"); - return _isLinux.Value; + if (!_isUnix.HasValue) + _isUnix = Environment.OSVersion.Platform == PlatformID.MacOSX || Environment.OSVersion.Platform == PlatformID.Unix; + return _isUnix.Value; } } @@ -1110,13 +1147,15 @@ private static StringComparison PathStringComparison { get { - // this is lazily evaluated because IsLinux uses the FileSystem object and that can be set + // this is lazily evaluated because IsUnix uses the FileSystem object and that can be set // after static constructors happen here if (!_pathStringComparison.HasValue) - _pathStringComparison = IsLinux ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + _pathStringComparison = IsUnix ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; return _pathStringComparison.Value; } } + + internal string DebuggerDisplay => ToString(); } public static class Extensions @@ -1170,7 +1209,7 @@ public static NPath Resolve(this NPath path) { // Add a reference to Mono.Posix with an .rsp file in the Assets folder with the line "-r:Mono.Posix.dll" for this to work #if ENABLE_MONO - if (!path.IsInitialized || !NPath.IsLinux /* nothing to resolve on windows */ || path.IsRelative || !path.FileExists()) + if (!path.IsInitialized || !NPath.IsUnix /* nothing to resolve on windows */ || path.IsRelative || !path.FileExists()) return path; return new NPath(Mono.Unix.UnixPath.GetCompleteRealPath(path.ToString())); #else @@ -1180,8 +1219,20 @@ public static NPath Resolve(this NPath path) public static string CalculateMD5(this NPath path) { - return NPath.FileSystem.CalculateFileMD5(path); + return NPath.FileSystem.CalculateFileMD5(path.ToProcessDirectory()); } + + public static NPath CreateTempDirectory(this NPath baseDir, string myprefix = "") + { + var random = new Random(); + while (true) + { + var candidate = baseDir.Combine(myprefix + "_" + random.Next()); + if (!candidate.Exists()) + return candidate.CreateDirectory(); + } + } + } public enum SlashMode @@ -1196,4 +1247,122 @@ public enum DeleteMode Normal, Soft } + + + class FSWrapper + { + private readonly IFileSystem fileSystem; + + public FSWrapper(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + public void DirectoryCreate(NPath path) + { + fileSystem.DirectoryCreate(path.ToProcessDirectory().ToString()); + } + + public void DirectoryDelete(NPath path, bool recursive) + { + fileSystem.DirectoryDelete(path.ToProcessDirectory().ToString(), recursive); + } + + public bool DirectoryExists(NPath path) + { + return fileSystem.DirectoryExists(path.ToProcessDirectory().ToString()); + } + public void DirectoryMove(NPath from, NPath to) + { + fileSystem.DirectoryMove(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString()); + } + public bool ExistingPathIsDirectory(NPath path) + { + return fileSystem.ExistingPathIsDirectory(path.ToProcessDirectory().ToString()); + } + public void FileCopy(NPath from, NPath to, bool overwrite) + { + fileSystem.FileCopy(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString(), overwrite); + } + public void FileDelete(NPath path) + { + fileSystem.FileDelete(path.ToProcessDirectory().ToString()); + } + public bool FileExists(NPath path) + { + return fileSystem.FileExists(path.ToProcessDirectory().ToString()); + } + public void FileMove(NPath from, NPath to) + { + fileSystem.FileMove(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString()); + } + public IEnumerable GetDirectories(NPath path) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString()); + } + public IEnumerable GetDirectories(NPath path, string pattern) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString(), pattern); + } + public IEnumerable GetDirectories(NPath path, string pattern, SearchOption searchOption) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString(), pattern, searchOption); + } + public IEnumerable GetFiles(NPath path) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString()); + } + public IEnumerable GetFiles(NPath path, string pattern) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString(), pattern); + } + public IEnumerable GetFiles(NPath path, string pattern, SearchOption searchOption) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString(), pattern, searchOption); + } + public Stream OpenRead(NPath path) + { + return fileSystem.OpenRead(path.ToProcessDirectory().ToString()); + } + public Stream OpenWrite(NPath path, FileMode mode) + { + return fileSystem.OpenWrite(path.ToProcessDirectory().ToString(), mode); + } + public byte[] ReadAllBytes(NPath path) + { + return fileSystem.ReadAllBytes(path.ToProcessDirectory().ToString()); + } + public string[] ReadAllLines(NPath path) + { + return fileSystem.ReadAllLines(path.ToProcessDirectory().ToString()); + } + public string ReadAllText(NPath path) + { + return fileSystem.ReadAllText(path.ToProcessDirectory().ToString()); + } + public string ReadAllText(NPath path, Encoding encoding) + { + return fileSystem.ReadAllText(path.ToProcessDirectory().ToString(), encoding); + } + public void WriteAllBytes(NPath path, byte[] bytes) + { + fileSystem.WriteAllBytes(path.ToProcessDirectory().ToString(), bytes); + } + public void WriteAllLines(NPath path, string[] contents) + { + fileSystem.WriteAllLines(path.ToProcessDirectory().ToString(), contents); + } + public void WriteAllText(NPath path, string contents) + { + fileSystem.WriteAllText(path.ToProcessDirectory().ToString(), contents); + } + public void WriteAllText(NPath path, string contents, Encoding encoding) + { + fileSystem.WriteAllText(path.ToProcessDirectory().ToString(), contents, encoding); + } + public void WriteLines(NPath path, string[] contents) + { + fileSystem.WriteLines(path.ToProcessDirectory().ToString(), contents); + } + } } diff --git a/src/GitHub.Api/IO/Utils.cs b/src/GitHub.Api/IO/Utils.cs index fb4358dd9..62b54be25 100644 --- a/src/GitHub.Api/IO/Utils.cs +++ b/src/GitHub.Api/IO/Utils.cs @@ -80,11 +80,12 @@ public static bool Copy(Stream source, Stream destination, return success; } - public static bool VerifyFileIntegrity(NPath file, NPath md5file) + public static bool VerifyFileIntegrity(NPath file, string md5) { - var expected = md5file.ReadAllText(); + if (!file.IsInitialized || !file.FileExists()) + return false; var actual = file.CalculateMD5(); - return expected.Equals(actual, StringComparison.InvariantCultureIgnoreCase); + return md5.Equals(actual, StringComparison.InvariantCultureIgnoreCase); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs new file mode 100644 index 000000000..98faecb62 --- /dev/null +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using GitHub.Logging; + +namespace GitHub.Unity +{ + public static class CopyHelper + { + private static readonly ILogging Logger = LogHelper.GetLogger(typeof(CopyHelper)); + + public static void Copy(NPath fromPath, NPath toPath) + { + Logger.Trace("Copying from {0} to {1}", fromPath, toPath); + + try + { + CopyFolder(fromPath, toPath); + } + catch (Exception ex1) + { + Logger.Warning(ex1, "Error copying."); + + try + { + CopyFolderContents(fromPath, toPath); + } + catch (Exception ex2) + { + Logger.Error(ex2, "Error copying contents."); + throw; + } + } + finally + { + fromPath.DeleteIfExists(); + } + } + public static void CopyFolder(NPath fromPath, NPath toPath) + { + Logger.Trace("CopyFolder from {0} to {1}", fromPath, toPath); + toPath.DeleteIfExists(); + toPath.EnsureParentDirectoryExists(); + fromPath.Move(toPath); + } + + public static void CopyFolderContents(NPath fromPath, NPath toPath) + { + Logger.Trace("CopyFolderContents from {0} to {1}", fromPath, toPath); + toPath.DeleteContents(); + fromPath.MoveFiles(toPath, true); + } + } +} diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 772878462..d8dd7594b 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -1,10 +1,11 @@ using System; using System.Threading; using GitHub.Logging; +using GitHub.Unity.Git.Tasks; namespace GitHub.Unity { - class GitInstaller + public class GitInstaller { private static readonly ILogging Logger = LogHelper.GetLogger(); private readonly CancellationToken cancellationToken; @@ -14,203 +15,279 @@ class GitInstaller private readonly GitInstallDetails installDetails; private readonly IZipHelper sharpZipLibHelper; - ITask installationTask; + public IProgress Progress { get; } = new Progress(TaskBase.Default); public GitInstaller(IEnvironment environment, IProcessManager processManager, - ITaskManager taskManager, + CancellationToken token, GitInstallDetails installDetails = null) { this.environment = environment; this.processManager = processManager; this.sharpZipLibHelper = ZipHelper.Instance; - this.cancellationToken = taskManager.Token; + this.cancellationToken = token; this.installDetails = installDetails ?? new GitInstallDetails(environment.UserCachePath, environment.IsWindows); } - public ITask SetupGitIfNeeded() + public GitInstallationState SetupGitIfNeeded(GitInstallationState state = null) { - //Logger.Trace("SetupGitIfNeeded"); - GitInstallationState installationState = new GitInstallationState(); - installationTask = new FuncTask(cancellationToken, (success, path) => path) - { Name = "Git Installation - Complete" }; - installationTask.OnStart += thisTask => thisTask.UpdateProgress(0, 100); - installationTask.OnEnd += (thisTask, result, success, exception) => thisTask.UpdateProgress(100, 100); - - ITask startTask = null; - if (!environment.IsWindows) + var skipSystemProbing = state != null; + + state = VerifyGitSettings(state); + if (state.GitIsValid && state.GitLfsIsValid) { - var findTask = new FindExecTask("git", cancellationToken) - .Configure(processManager, dontSetupGit: true) - .Catch(e => true); - findTask.OnEnd += (thisTask, path, success, exception) => - { - // we should doublecheck that system git is usable here - installationState.GitIsValid = success; - if (success) - { - installationState.GitExecutablePath = path; - installationState.GitInstallationPath = path.Resolve().Parent.Parent; - } - }; - findTask.Then(new FindExecTask("git-lfs", cancellationToken) - .Configure(processManager, dontSetupGit: true)) - .Catch(e => true); - findTask.OnEnd += (thisTask, path, success, exception) => - { - installationState.GitLfsIsValid = success; - if (success) - { - // we should doublecheck that system git is usable here - installationState.GitLfsExecutablePath = path; - installationState.GitLfsInstallationPath = path.Resolve().Parent.Parent; - } - }; - startTask = findTask.Then(s => installationState); + Logger.Trace("Using git install path from settings: {0}", state.GitExecutablePath); + state.GitLastCheckTime = DateTimeOffset.Now; + return state; } - else + + if (!skipSystemProbing) { - startTask = new FuncTask(cancellationToken, () => - { - return VerifyPortableGitInstallation(); - }) - { Name = "Git Installation - Verify" }; + if (environment.IsMac) + state = FindGit(state); } - startTask = startTask.Then(new FuncTask(cancellationToken, (success, installState) => - { - if (installState.GitIsValid && installState.GitLfsIsValid) - { - return installState; - } + state = SetDefaultPaths(state); + state = CheckForGitUpdates(state); - installState = VerifyZipFiles(installState); - installState = GrabZipFromResourcesIfNeeded(installState); - return installState; - }) - { Name = "Git Installation - Validate" } - ); + if (state.GitIsValid && state.GitLfsIsValid) + { + state.GitLastCheckTime = DateTimeOffset.Now; + return state; + } - startTask.OnEnd += (thisTask, installState, success, exception) => + state = VerifyZipFiles(state); + // on developer builds, prefer local zips over downloading +#if DEVELOPER_BUILD + state = GrabZipFromResourcesIfNeeded(state); + state = GetZipsIfNeeded(state); +#else + state = GetZipsIfNeeded(state); + state = GrabZipFromResourcesIfNeeded(state); +#endif + state = ExtractGit(state); + + // if installing from zip failed (internet down maybe?), try to find a usable system git + if (!state.GitIsValid && state.GitInstallationPath == installDetails.GitInstallationPath) + state = FindGit(state); + if (!state.GitLfsIsValid && state.GitLfsInstallationPath == installDetails.GitLfsInstallationPath) + state = FindGitLfs(state); + state.GitLastCheckTime = DateTimeOffset.Now; + return state; + } + + public GitInstallationState VerifyGitSettings(GitInstallationState state = null) + { + state = state ?? environment.GitInstallationState; + if (!state.GitExecutablePath.IsInitialized && !state.GitLfsExecutablePath.IsInitialized) + return state; + + state = ValidateGitVersion(state); + if (state.GitIsValid) + state.GitInstallationPath = state.GitExecutablePath.Parent.Parent; + + if (!state.GitLfsExecutablePath.IsInitialized) { - if (installState.GitIsValid && installState.GitLfsIsValid) - { - Logger.Trace("Skipping git installation"); - thisTask.Then(installationTask); - return; - } + // look for it in the directory where we would install it from the bundle + state.GitLfsExecutablePath = installDetails.GitLfsExecutablePath; + } + + state = ValidateGitLfsVersion(state); - var downloadZipTask = DownloadZipsIfNeeded(installState); - downloadZipTask.OnEnd += ExtractPortableGit; - thisTask.Then(downloadZipTask); - }; + if (state.GitLfsIsValid) + state.GitLfsInstallationPath = state.GitLfsExecutablePath.Parent; - return startTask; + return state; } - private GitInstallationState VerifyPortableGitInstallation() + public GitInstallationState FindSystemGit(GitInstallationState state) { - var state = new GitInstallationState(); - var gitExists = installDetails.GitExecutablePath.IsInitialized && installDetails.GitExecutablePath.FileExists(); - var gitLfsExists = installDetails.GitLfsExecutablePath.IsInitialized && installDetails.GitLfsExecutablePath.FileExists(); + state = FindGit(state); + state = FindGitLfs(state); + return state; + } - if (gitExists) + private GitInstallationState FindGit(GitInstallationState state) + { + if (!state.GitIsValid) { - var actualmd5 = installDetails.GitExecutablePath.CalculateMD5(); - var expectedmd5 = environment.IsWindows ? GitInstallDetails.WindowsGitExecutableMD5 : GitInstallDetails.MacGitExecutableMD5; - state.GitIsValid = expectedmd5.Equals(actualmd5, StringComparison.InvariantCultureIgnoreCase); + var gitPath = new FindExecTask("git", cancellationToken) + .Configure(processManager, dontSetupGit: true) + .Catch(e => true) + .RunSynchronously(); + state.GitExecutablePath = gitPath; + state = ValidateGitVersion(state); if (state.GitIsValid) - { - state.GitInstallationPath = installDetails.GitInstallationPath; - state.GitExecutablePath = installDetails.GitExecutablePath; - } - else - { - Logger.Trace($"Path {installDetails.GitExecutablePath} has MD5 {actualmd5} expected {expectedmd5}"); - } + state.GitInstallationPath = gitPath.Parent.Parent; } - else - Logger.Trace($"{installDetails.GitExecutablePath} does not exist"); + return state; + } - if (gitLfsExists) + private GitInstallationState FindGitLfs(GitInstallationState state) + { + if (!state.GitLfsIsValid) { - var actualmd5 = installDetails.GitLfsExecutablePath.CalculateMD5(); - var expectedmd5 = environment.IsWindows ? GitInstallDetails.WindowsGitLfsExecutableMD5 : GitInstallDetails.MacGitLfsExecutableMD5; - state.GitLfsIsValid = expectedmd5.Equals(actualmd5, StringComparison.InvariantCultureIgnoreCase); + var gitLfsPath = new FindExecTask("git-lfs", cancellationToken) + .Configure(processManager, dontSetupGit: true) + .Catch(e => true) + .RunSynchronously(); + state.GitLfsExecutablePath = gitLfsPath; + state = ValidateGitLfsVersion(state); if (state.GitLfsIsValid) - { - state.GitLfsInstallationPath = installDetails.GitInstallationPath; - state.GitLfsExecutablePath = installDetails.GitLfsExecutablePath; - } - else - { - Logger.Trace($"Path {installDetails.GitLfsExecutablePath} has MD5 {actualmd5} expected {expectedmd5}"); - } + state.GitLfsInstallationPath = state.GitLfsExecutablePath.Parent; } - else - Logger.Trace($"{installDetails.GitLfsExecutablePath} does not exist"); + return state; + } - installationTask.UpdateProgress(10, 100); + public GitInstallationState SetDefaultPaths(GitInstallationState state) + { + if (!state.GitIsValid && environment.IsWindows) + { + state.GitInstallationPath = installDetails.GitInstallationPath; + state.GitExecutablePath = installDetails.GitExecutablePath; + state = ValidateGitVersion(state); + } + + if (!state.GitLfsIsValid) + { + state.GitLfsExecutablePath = installDetails.GitLfsExecutablePath; + state.GitLfsInstallationPath = state.GitLfsExecutablePath.Parent; + state = ValidateGitLfsVersion(state); + } return state; } - private GitInstallationState VerifyZipFiles(GitInstallationState state) + public GitInstallationState ValidateGitVersion(GitInstallationState state) { - var md5 = AssemblyResources.ToFile(ResourceType.Platform, "git.zip.md5", installDetails.ZipPath, environment); - if (!md5.FileExists() || (installDetails.GitZipPath.FileExists() && !Utils.VerifyFileIntegrity(installDetails.GitZipPath, md5))) + if (!state.GitExecutablePath.IsInitialized || !state.GitExecutablePath.FileExists()) { - installDetails.GitZipPath.DeleteIfExists(); + state.GitIsValid = false; + return state; } - state.GitZipExists = installDetails.GitZipPath.FileExists(); + var version = new GitVersionTask(cancellationToken) + .Configure(processManager, state.GitExecutablePath, dontSetupGit: true) + .Catch(e => true) + .RunSynchronously(); + state.GitIsValid = version >= Constants.MinimumGitVersion; + state.GitVersion = version; + return state; + } - md5 = AssemblyResources.ToFile(ResourceType.Platform, "git-lfs.zip.md5", installDetails.ZipPath, environment); - // check whether the git-lfs zip file exists and is valid - if (!md5.FileExists() || (installDetails.GitLfsZipPath.FileExists() && !Utils.VerifyFileIntegrity(installDetails.GitLfsZipPath, md5))) + public GitInstallationState ValidateGitLfsVersion(GitInstallationState state) + { + if (!state.GitLfsExecutablePath.IsInitialized || !state.GitLfsExecutablePath.FileExists()) { - installDetails.GitLfsZipPath.DeleteIfExists(); + state.GitLfsIsValid = false; + return state; } - state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); - installationTask.UpdateProgress(20, 100); + var version = new ProcessTask(cancellationToken, "version", new LfsVersionOutputProcessor()) + .Configure(processManager, state.GitLfsExecutablePath, dontSetupGit: true) + .Catch(e => true) + .RunSynchronously(); + state.GitLfsIsValid = version >= Constants.MinimumGitLfsVersion; + state.GitLfsVersion = version; return state; } - private GitInstallationState GrabZipFromResourcesIfNeeded(GitInstallationState state) + private GitInstallationState CheckForGitUpdates(GitInstallationState state) { - if (!state.GitZipExists) + if (state.GitInstallationPath == installDetails.GitInstallationPath) { - AssemblyResources.ToFile(ResourceType.Platform, "git.zip", installDetails.ZipPath, environment); + state.GitPackage = Package.Load(environment, installDetails.GitPackageFeed); + if (state.GitPackage != null) + { + state.GitIsValid = state.GitVersion >= state.GitPackage.Version; + if (state.GitIsValid) + { + state.IsCustomGitPath = state.GitExecutablePath != installDetails.GitExecutablePath; + } + else + { + Logger.Trace($"{installDetails.GitExecutablePath} is out of date"); + } + } } - state.GitZipExists = installDetails.GitZipPath.FileExists(); - if (!state.GitLfsZipExists) + if (state.GitLfsInstallationPath == installDetails.GitLfsInstallationPath) { - AssemblyResources.ToFile(ResourceType.Platform, "git-lfs.zip", installDetails.ZipPath, environment); + state.GitLfsPackage = Package.Load(environment, installDetails.GitLfsPackageFeed); + if (state.GitLfsPackage != null) + { + state.GitLfsIsValid = state.GitLfsVersion >= state.GitLfsPackage.Version; + if (!state.GitLfsIsValid) + { + Logger.Trace($"{installDetails.GitLfsExecutablePath} is out of date"); + } + } } - state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); - installationTask.UpdateProgress(30, 100); return state; } - private ITask DownloadZipsIfNeeded(GitInstallationState state) + private GitInstallationState VerifyZipFiles(GitInstallationState state) { - var downloader = new Downloader(); - downloader.Catch(e => true); - if (!state.GitIsValid) - downloader.QueueDownload(installDetails.GitZipUrl, installDetails.GitZipMd5Url, installDetails.ZipPath); - if (!state.GitLfsIsValid) - downloader.QueueDownload(installDetails.GitLfsZipUrl, installDetails.GitLfsZipMd5Url, installDetails.ZipPath); - return downloader.Then((success, data) => + if (!state.GitIsValid && state.GitPackage != null) { state.GitZipExists = installDetails.GitZipPath.FileExists(); + if (!Utils.VerifyFileIntegrity(installDetails.GitZipPath, state.GitPackage.Md5)) + { + installDetails.GitZipPath.DeleteIfExists(); + } + state.GitZipExists = installDetails.GitZipPath.FileExists(); + } + + if (!state.GitLfsIsValid && state.GitLfsPackage != null) + { + state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); + if (!Utils.VerifyFileIntegrity(installDetails.GitLfsZipPath, state.GitLfsPackage.Md5)) + { + installDetails.GitLfsZipPath.DeleteIfExists(); + } state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); - installationTask.UpdateProgress(40, 100); + } + return state; + } + + private GitInstallationState GetZipsIfNeeded(GitInstallationState state) + { + if (state.GitZipExists && state.GitLfsZipExists) + return state; + + var downloader = new Downloader(environment.FileSystem); + downloader.Catch(e => + { + LogHelper.Trace(e, "Failed to download"); + return true; + }); + downloader.Progress(p => Progress.UpdateProgress(20 + (long)(20 * p.Percentage), 100, downloader.Message)); + if (!state.GitZipExists && !state.GitIsValid && state.GitPackage != null) + downloader.QueueDownload(state.GitPackage.Uri, installDetails.ZipPath); + if (!state.GitLfsZipExists && !state.GitLfsIsValid && state.GitLfsPackage != null) + downloader.QueueDownload(state.GitLfsPackage.Uri, installDetails.ZipPath); + downloader.RunSynchronously(); + + state.GitZipExists = installDetails.GitZipPath.FileExists(); + state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); + Progress.UpdateProgress(30, 100); + + return state; + } + + private GitInstallationState GrabZipFromResourcesIfNeeded(GitInstallationState state) + { + if (!state.GitZipExists && !state.GitIsValid && state.GitInstallationPath == installDetails.GitInstallationPath) + AssemblyResources.ToFile(ResourceType.Platform, "git.zip", installDetails.ZipPath, environment); + state.GitZipExists = installDetails.GitZipPath.FileExists(); + + if (state.GitLfsInstallationPath != installDetails.GitLfsInstallationPath) return state; - }); + + if (!state.GitLfsZipExists && !state.GitLfsIsValid && state.GitLfsInstallationPath == installDetails.GitLfsInstallationPath) + AssemblyResources.ToFile(ResourceType.Platform, "git-lfs.zip", installDetails.ZipPath, environment); + state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); + return state; } - private void ExtractPortableGit(ITask thisTask, - GitInstallationState state, bool s, Exception exception) + private GitInstallationState ExtractGit(GitInstallationState state) { - ITask task = null; var tempZipExtractPath = NPath.CreateTempDirectory("git_zip_extract_zip_paths"); if (state.GitZipExists && !state.GitIsValid) @@ -219,26 +296,24 @@ private void ExtractPortableGit(ITask thisTask, var unzipTask = new UnzipTask(cancellationToken, installDetails.GitZipPath, gitExtractPath, sharpZipLibHelper, environment.FileSystem) - .Catch(e => true); - unzipTask.Progress(p => installationTask.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Name)); - - unzipTask = unzipTask.Then((success, path) => - { - var target = installDetails.GitInstallationPath; - if (success) + .Catch(e => { - var source = path; - target.DeleteIfExists(); - target.EnsureParentDirectoryExists(); - Logger.Trace($"Moving '{source}' to '{target}'"); - source.Move(target); - state.GitInstallationPath = installDetails.GitInstallationPath; - state.GitExecutablePath = installDetails.GitExecutablePath; - state.GitIsValid = success; - } - return target; - }); - task = unzipTask; + LogHelper.Trace(e, "Failed to unzip " + installDetails.GitZipPath); + return true; + }); + unzipTask.Progress(p => Progress.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Message)); + unzipTask.RunSynchronously(); + var target = state.GitInstallationPath; + if (unzipTask.Successful) + { + Logger.Trace("Moving Git source:{0} target:{1}", gitExtractPath.ToString(), target.ToString()); + + CopyHelper.Copy(gitExtractPath, target); + + state.GitIsValid = true; + + state.IsCustomGitPath = state.GitExecutablePath != installDetails.GitExecutablePath; + } } if (state.GitLfsZipExists && !state.GitLfsIsValid) @@ -247,42 +322,26 @@ private void ExtractPortableGit(ITask thisTask, var unzipTask = new UnzipTask(cancellationToken, installDetails.GitLfsZipPath, gitLfsExtractPath, sharpZipLibHelper, environment.FileSystem) - .Catch(e => true); - unzipTask.Progress(p => installationTask.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Name)); - - unzipTask = unzipTask.Then((success, path) => - { - var target = installDetails.GetGitLfsExecutablePath(state.GitInstallationPath); - if (success) + .Catch(e => { - var source = path.Combine(installDetails.GitLfsExecutable); - target.DeleteIfExists(); - target.EnsureParentDirectoryExists(); - Logger.Trace($"Moving '{source}' to '{target}'"); - source.Move(target); - state.GitLfsInstallationPath = state.GitInstallationPath; - state.GitLfsExecutablePath = target; - state.GitLfsIsValid = success; - } - return target; - }); - task = task?.Then(unzipTask) ?? unzipTask; - } + LogHelper.Trace(e, "Failed to unzip " + installDetails.GitLfsZipPath); + return true; + }); + unzipTask.Progress(p => Progress.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Message)); + unzipTask.RunSynchronously(); + var target = state.GitLfsInstallationPath; + if (unzipTask.Successful) + { + Logger.Trace("Moving GitLFS source:{0} target:{1}", gitLfsExtractPath.ToString(), target.ToString()); - var endTask = new FuncTask(cancellationToken, (success) => - { - tempZipExtractPath.DeleteIfExists(); - return state; - }); + CopyHelper.Copy(gitLfsExtractPath, target); - if (task != null) - { - endTask = task.Then(endTask); + state.GitLfsIsValid = true; + } } - thisTask - .Then(endTask) - .Then(installationTask); + tempZipExtractPath.DeleteIfExists(); + return state; } public class GitInstallationState @@ -295,26 +354,26 @@ public class GitInstallationState public NPath GitExecutablePath { get; set; } public NPath GitLfsInstallationPath { get; set; } public NPath GitLfsExecutablePath { get; set; } + public Package GitPackage { get; set; } + public Package GitLfsPackage { get; set; } + public DateTimeOffset GitLastCheckTime { get; set; } + public bool IsCustomGitPath { get; set; } + public TheVersion GitVersion { get; set; } + public TheVersion GitLfsVersion { get; set; } } public class GitInstallDetails { - public const string DefaultGitZipMd5Url = "https://ghfvs-installer.github.com/unity/git/windows/git.zip.md5"; - public const string DefaultGitZipUrl = "https://ghfvs-installer.github.com/unity/git/windows/git.zip"; - public const string DefaultGitLfsZipMd5Url = "https://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip.md5"; - public const string DefaultGitLfsZipUrl = "https://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip"; - - public const string GitExtractedMD5 = "e6cfc0c294a2312042f27f893dfc9c0a"; - public const string GitLfsExtractedMD5 = "36e3ae968b69fbf42dff72311040d24a"; + public const string GitPackageName = "git.json"; + public const string GitLfsPackageName = "git-lfs.json"; +#if DEBUG + private const string packageFeed = "http://localhost:50000/unity/git/"; +#else + private const string packageFeed = "http://github-vs.s3.amazonaws.com/unity/git/"; +#endif - public const string WindowsGitExecutableMD5 = "50570ed932559f294d1a1361801740b9"; - public const string MacGitExecutableMD5 = ""; - - public const string WindowsGitLfsExecutableMD5 = "177bb14d0c08f665a24f0d5516c3b080"; - public const string MacGitLfsExecutableMD5 = "f81a1a065a26a4123193e8fd96c561ad"; - - private const string PackageVersion = "f02737a78695063deace08e96d5042710d3e32db"; - private const string PackageName = "PortableGit"; + public const string GitDirectory = "git"; + public const string GitLfsDirectory = "git-lfs"; private const string gitZip = "git.zip"; private const string gitLfsZip = "git-lfs.zip"; @@ -330,47 +389,33 @@ public GitInstallDetails(NPath baseDataPath, bool onWindows) GitZipPath = ZipPath.Combine(gitZip); GitLfsZipPath = ZipPath.Combine(gitLfsZip); - var gitInstallPath = baseDataPath.Combine(PackageNameWithVersion); - GitInstallationPath = gitInstallPath; + GitInstallationPath = baseDataPath.Combine(GitDirectory); + GitExecutablePath = GitInstallationPath.Combine(onWindows ? "cmd" : "bin", "git" + DefaultEnvironment.ExecutableExt); + + GitLfsInstallationPath = baseDataPath.Combine(GitLfsDirectory); + GitLfsExecutablePath = GitLfsInstallationPath.Combine("git-lfs" + DefaultEnvironment.ExecutableExt); if (onWindows) { - GitExecutable += "git.exe"; - GitLfsExecutable += "git-lfs.exe"; - - GitExecutablePath = gitInstallPath.Combine("cmd", GitExecutable); + GitPackageFeed = packageFeed + $"windows/{GitPackageName}"; + GitLfsPackageFeed = packageFeed + $"windows/{GitLfsPackageName}"; } else { - GitExecutable = "git"; - GitLfsExecutable = "git-lfs"; - - GitExecutablePath = gitInstallPath.Combine("bin", GitExecutable); + GitPackageFeed = packageFeed + $"mac/{GitPackageName}"; + GitLfsPackageFeed = packageFeed + $"mac/{GitLfsPackageName}"; } - - GitLfsExecutablePath = GetGitLfsExecutablePath(gitInstallPath); - } - - public NPath GetGitLfsExecutablePath(NPath gitInstallRoot) - { - return onWindows - ? gitInstallRoot.Combine("mingw32", "libexec", "git-core", GitLfsExecutable) - : gitInstallRoot.Combine("libexec", "git-core", GitLfsExecutable); } public NPath ZipPath { get; } public NPath GitZipPath { get; } public NPath GitLfsZipPath { get; } public NPath GitInstallationPath { get; } - public string GitExecutable { get; } + public NPath GitLfsInstallationPath { get; } public NPath GitExecutablePath { get; } - public string GitLfsExecutable { get; } public NPath GitLfsExecutablePath { get; } - public UriString GitZipMd5Url { get; set; } = DefaultGitZipMd5Url; - public UriString GitZipUrl { get; set; } = DefaultGitZipUrl; - public UriString GitLfsZipMd5Url { get; set; } = DefaultGitLfsZipMd5Url; - public UriString GitLfsZipUrl { get; set; } = DefaultGitLfsZipUrl; - public string PackageNameWithVersion => PackageName + "_" + PackageVersion; + public UriString GitPackageFeed { get; set; } + public UriString GitLfsPackageFeed { get; set; } } } } diff --git a/src/GitHub.Api/Installer/IZipHelper.cs b/src/GitHub.Api/Installer/IZipHelper.cs index 5f3de3db8..aa6f1f2fa 100644 --- a/src/GitHub.Api/Installer/IZipHelper.cs +++ b/src/GitHub.Api/Installer/IZipHelper.cs @@ -6,6 +6,6 @@ namespace GitHub.Unity interface IZipHelper { bool Extract(string archive, string outFolder, CancellationToken cancellationToken, - Func onProgress); + Func onProgress, Func onFilter = null); } } diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index ad5855795..11ec351a2 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -24,53 +24,40 @@ public OctorunInstaller(IEnvironment environment, ITaskManager taskManager, this.taskManager = taskManager; } - public ITask SetupOctorunIfNeeded() + public NPath SetupOctorunIfNeeded() { - //Logger.Trace("SetupOctorunIfNeeded"); - - var task = new FuncTask(taskManager.Token, () => - { - var isOctorunExtracted = IsOctorunExtracted(); - Logger.Trace("isOctorunExtracted: {0}", isOctorunExtracted); - if (isOctorunExtracted) - return installDetails.ExecutablePath; - GrabZipFromResources(); - return NPath.Default; - }); - - task.OnEnd += (t, path, _, __) => - { - if (!path.IsInitialized) - { - var tempZipExtractPath = NPath.CreateTempDirectory("octorun_extract_archive_path"); - var unzipTask = new UnzipTask(taskManager.Token, installDetails.ZipFile, - tempZipExtractPath, sharpZipLibHelper, - fileSystem) - .Then((success, extractPath) => MoveOctorun(extractPath.Combine("octorun"))); - t.Then(unzipTask); - } - }; - - return task; + NPath path = NPath.Default; + var isOctorunExtracted = IsOctorunExtracted(); + if (isOctorunExtracted) + return installDetails.ExecutablePath; + + GrabZipFromResources(); + + var extractPath = NPath.CreateTempDirectory("octorun_extract_archive_path"); + var unzipTask = new UnzipTask(taskManager.Token, installDetails.ZipFile, + extractPath, sharpZipLibHelper, + fileSystem) + .Catch(e => { Logger.Error(e, "Error extracting octorun"); return true; }); + unzipTask.RunSynchronously(); + + if (unzipTask.Successful) + path = MoveOctorun(extractPath.Combine("octorun")); + return path; } private NPath GrabZipFromResources() { - installDetails.ZipFile.DeleteIfExists(); - - AssemblyResources.ToFile(ResourceType.Generic, "octorun.zip", installDetails.BaseZipPath, environment); - - return installDetails.ZipFile; + return AssemblyResources.ToFile(ResourceType.Generic, "octorun.zip", installDetails.BaseZipPath, environment); } private NPath MoveOctorun(NPath fromPath) { var toPath = installDetails.InstallationPath; - Logger.Trace($"Moving tempDirectory:'{fromPath}' to extractTarget:'{toPath}'"); - toPath.DeleteIfExists(); - toPath.EnsureParentDirectoryExists(); - fromPath.Move(toPath); + Logger.Trace("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + + CopyHelper.Copy(fromPath, toPath); + return installDetails.ExecutablePath; } @@ -78,17 +65,15 @@ private bool IsOctorunExtracted() { if (!installDetails.InstallationPath.DirectoryExists()) { - //Logger.Warning($"{octorunPath} does not exist"); return false; } if (!installDetails.VersionFile.FileExists()) { - //Logger.Warning($"{versionFilePath} does not exist"); return false; } - var octorunVersion = installDetails.VersionFile.ReadAllText(); + var octorunVersion = installDetails.VersionFile.ReadAllText().Trim(); if (!OctorunInstallDetails.PackageVersion.Equals(octorunVersion)) { Logger.Warning("Current version {0} does not match expected {1}", octorunVersion, OctorunInstallDetails.PackageVersion); @@ -99,10 +84,10 @@ private bool IsOctorunExtracted() public class OctorunInstallDetails { - public const string DefaultZipMd5Url = "https://ghfvs-installer.github.com/unity/octorun/octorun.zip.md5"; - public const string DefaultZipUrl = "https://ghfvs-installer.github.com/unity/octorun/octorun.zip"; + public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; + public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "9fcd9faa"; + public const string PackageVersion = "902910f48"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; @@ -129,4 +114,4 @@ public OctorunInstallDetails(NPath baseDataPath) public NPath VersionFile => InstallationPath.Combine("version"); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Installer/UnzipTask.cs b/src/GitHub.Api/Installer/UnzipTask.cs index 1f23df669..6be81e1f9 100644 --- a/src/GitHub.Api/Installer/UnzipTask.cs +++ b/src/GitHub.Api/Installer/UnzipTask.cs @@ -16,7 +16,7 @@ public UnzipTask(CancellationToken token, NPath archiveFilePath, NPath extracted { this.archiveFilePath = archiveFilePath; this.extractedPath = extractedPath; - this.zipHelper = zipHelper; + this.zipHelper = zipHelper ?? ZipHelper.Instance; this.fileSystem = fileSystem; Name = $"Unzip {archiveFilePath.FileName}"; } @@ -29,22 +29,14 @@ protected NPath BaseRun(bool success) protected override NPath RunWithReturn(bool success) { var ret = BaseRun(success); - - RaiseOnStart(); - try { ret = RunUnzip(success); } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(ret); + throw exception; } return ret; } @@ -93,6 +85,7 @@ protected virtual NPath RunUnzip(bool success) return extractedPath; } protected int RetryCount { get; } + public override string Message { get; set; } = "Extracting zip..."; } public class UnzipException : Exception { diff --git a/src/GitHub.Api/Installer/ZipHelper.cs b/src/GitHub.Api/Installer/ZipHelper.cs index 57f55ea49..3d581c5e4 100644 --- a/src/GitHub.Api/Installer/ZipHelper.cs +++ b/src/GitHub.Api/Installer/ZipHelper.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Threading; -using ICSharpCode.SharpZipLib.Zip; +using GitHub.ICSharpCode.SharpZipLib.Zip; using GitHub.Logging; using System.Collections.Generic; @@ -23,7 +23,7 @@ public static IZipHelper Instance } public bool Extract(string archive, string outFolder, CancellationToken cancellationToken, - Func onProgress) + Func onProgress, Func onFilter = null) { const int chunkSize = 4096; // 4K is optimum ZipFile zf = null; @@ -53,6 +53,8 @@ public bool Extract(string archive, string outFolder, CancellationToken cancella cancellationToken.ThrowIfCancellationRequested(); var entryFileName = zipEntry.Name; + if (!onFilter?.Invoke(entryFileName) ?? false) + continue; // to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName); // Optionally match entrynames against a selection list here to skip as desired. // The unpacked length is available in the zipEntry.Size property. @@ -65,18 +67,28 @@ public bool Extract(string archive, string outFolder, CancellationToken cancella { Directory.CreateDirectory(directoryName); } - //#if !WINDOWS - // if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) - // { - // if (zipEntry.ExternalFileAttributes > 0) - // { - // int fd = Mono.Unix.Native.Syscall.open(fullZipToPath, - // Mono.Unix.Native.OpenFlags.O_CREAT | Mono.Unix.Native.OpenFlags.O_TRUNC, - // (Mono.Unix.Native.FilePermissions)zipEntry.ExternalFileAttributes); - // Mono.Unix.Native.Syscall.close(fd); - // } - // } - //#endif + + try + { + if (NPath.IsUnix) + { + if (zipEntry.ExternalFileAttributes == -2115174400) + { + int fd = Mono.Unix.Native.Syscall.open(fullZipToPath, + Mono.Unix.Native.OpenFlags.O_CREAT | Mono.Unix.Native.OpenFlags.O_TRUNC, + Mono.Unix.Native.FilePermissions.S_IRWXU | + Mono.Unix.Native.FilePermissions.S_IRGRP | + Mono.Unix.Native.FilePermissions.S_IXGRP | + Mono.Unix.Native.FilePermissions.S_IROTH | + Mono.Unix.Native.FilePermissions.S_IXOTH); + Mono.Unix.Native.Syscall.close(fd); + } + } + } + catch (Exception ex) + { + LogHelper.Error(ex, "Error setting file attributes in " + fullZipToPath); + } // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size // of the file, but does not waste memory. @@ -88,7 +100,7 @@ public bool Extract(string archive, string outFolder, CancellationToken cancella progress: (totalRead, timeToFinish) => { totalBytes += totalRead; - return onProgress(totalBytes, totalSize); + return onProgress?.Invoke(totalBytes, totalSize) ?? true; })) return false; } diff --git a/src/GitHub.Api/Localization.Designer.cs b/src/GitHub.Api/Localization.Designer.cs index a05e18c2f..ca67ee71e 100644 --- a/src/GitHub.Api/Localization.Designer.cs +++ b/src/GitHub.Api/Localization.Designer.cs @@ -60,6 +60,15 @@ internal Localization() { } } + /// + /// Looks up a localized string similar to Account. + /// + public static string AccountButton { + get { + return ResourceManager.GetString("AccountButton", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}. /// @@ -70,14 +79,23 @@ public static string BasePathLabel { } /// - /// Looks up a localized string similar to cancel. + /// Looks up a localized string similar to Branches. + /// + public static string BranchesTitle { + get { + return ResourceManager.GetString("BranchesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. /// public static string Cancel { get { return ResourceManager.GetString("Cancel", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} changed files. /// @@ -87,6 +105,15 @@ public static string ChangedFilesLabel { } } + /// + /// Looks up a localized string similar to Changes. + /// + public static string ChangesTitle { + get { + return ResourceManager.GetString("ChangesTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// @@ -114,6 +141,15 @@ public static string CommitSelectNoneButton { } } + /// + /// Looks up a localized string similar to No remote configured. + /// + public static string DefaultRepoUrl { + get { + return ResourceManager.GetString("DefaultRepoUrl", resourceCulture); + } + } + /// /// Looks up a localized string similar to Commit description. /// @@ -123,6 +159,78 @@ public static string DescriptionLabel { } } + /// + /// Looks up a localized string similar to Are you sure you want to discard these changes?. + /// + public static string DiscardConfirmDescription { + get { + return ResourceManager.GetString("DiscardConfirmDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Discard Changes?. + /// + public static string DiscardConfirmTitle { + get { + return ResourceManager.GetString("DiscardConfirmTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Discard. + /// + public static string DiscardConfirmYes { + get { + return ResourceManager.GetString("DiscardConfirmYes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetch Changes. + /// + public static string FetchActionTitle { + get { + return ResourceManager.GetString("FetchActionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetch. + /// + public static string FetchButton { + get { + return ResourceManager.GetString("FetchButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not fetch changes. + /// + public static string FetchFailureDescription { + get { + return ResourceManager.GetString("FetchFailureDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assets/Release Lock (forced). + /// + public static string ForceUnlockFileAssetsMenuItem { + get { + return ResourceManager.GetString("ForceUnlockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Release Lock (forced). + /// + public static string ForceUnlockFileMenuItem { + get { + return ResourceManager.GetString("ForceUnlockFileMenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to .... /// @@ -150,6 +258,42 @@ public static string GitInitButton { } } + /// + /// Looks up a localized string similar to We could not find Git in the system.. + /// + public static string GitLFSNotFound { + get { + return ResourceManager.GetString("GitLFSNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The detected LFS at {0} has version {1}, which is too low. The minimum LFS version is {2}.. + /// + public static string GitLfsVersionTooLow { + get { + return ResourceManager.GetString("GitLfsVersionTooLow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to We could not find Git in the system.. + /// + public static string GitNotFound { + get { + return ResourceManager.GetString("GitNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The detected Git at {0} has version {1}, which is too low. The minimum Git version is {2}.. + /// + public static string GitVersionTooLow { + get { + return ResourceManager.GetString("GitVersionTooLow", resourceCulture); + } + } + /// /// Looks up a localized string similar to (All). /// @@ -169,7 +313,16 @@ public static string HistoryFocusSingle { } /// - /// Looks up a localized string similar to Initialize repository. + /// Looks up a localized string similar to History. + /// + public static string HistoryTitle { + get { + return ResourceManager.GetString("HistoryTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Initialize a git repository for this project. /// public static string InitializeRepositoryButtonText { get { @@ -177,6 +330,15 @@ public static string InitializeRepositoryButtonText { } } + /// + /// Looks up a localized string similar to Initialize. + /// + public static string InitializeTitle { + get { + return ResourceManager.GetString("InitializeTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your selected folder '{0}' is not a valid repository root for your current project.. /// @@ -213,6 +375,33 @@ public static string LockedOut { } } + /// + /// Looks up a localized string similar to Assets/Request Lock. + /// + public static string LockFileAssetsMenuItem { + get { + return ResourceManager.GetString("LockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request Lock. + /// + public static string LockFileMenuItem { + get { + return ResourceManager.GetString("LockFileMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locks. + /// + public static string LocksTitle { + get { + return ResourceManager.GetString("LocksTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Login failed. /// @@ -222,6 +411,159 @@ public static string LoginFailed { } } + /// + /// Looks up a localized string similar to Created branch {0}. + /// + public static string MessageBranchCreated { + get { + return ResourceManager.GetString("MessageBranchCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted branch {0}. + /// + public static string MessageBranchDeleted { + get { + return ResourceManager.GetString("MessageBranchDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switched to branch {0}. + /// + public static string MessageBranchSwitched { + get { + return ResourceManager.GetString("MessageBranchSwitched", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Commit failed. + /// + public static string MessageCommitFailed { + get { + return ResourceManager.GetString("MessageCommitFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Committed. + /// + public static string MessageCommitted { + get { + return ResourceManager.GetString("MessageCommitted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Committing. + /// + public static string MessageCommitting { + get { + return ResourceManager.GetString("MessageCommitting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetched. + /// + public static string MessageFetched { + get { + return ResourceManager.GetString("MessageFetched", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetch failed. + /// + public static string MessageFetchFailed { + get { + return ResourceManager.GetString("MessageFetchFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetching. + /// + public static string MessageFetching { + get { + return ResourceManager.GetString("MessageFetching", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulled. + /// + public static string MessagePulled { + get { + return ResourceManager.GetString("MessagePulled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to pull. + /// + public static string MessagePullFailed { + get { + return ResourceManager.GetString("MessagePullFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulling. + /// + public static string MessagePulling { + get { + return ResourceManager.GetString("MessagePulling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pushed. + /// + public static string MessagePushed { + get { + return ResourceManager.GetString("MessagePushed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to push. + /// + public static string MessagePushFailed { + get { + return ResourceManager.GetString("MessagePushFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pushing. + /// + public static string MessagePushing { + get { + return ResourceManager.GetString("MessagePushing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshed. + /// + public static string MessageRefreshed { + get { + return ResourceManager.GetString("MessageRefreshed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshing. + /// + public static string MessageRefreshing { + get { + return ResourceManager.GetString("MessageRefreshing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your current project is not currently in an active git repository:. /// @@ -294,6 +636,15 @@ public static string OneChangedFileLabel { } } + /// + /// Looks up a localized string similar to Publish. + /// + public static string PublishButton { + get { + return ResourceManager.GetString("PublishButton", resourceCulture); + } + } + /// /// Looks up a localized string similar to Pull. /// @@ -313,7 +664,7 @@ public static string PullButton { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to Pull ({0}). /// public static string PullButtonCount { get { @@ -321,15 +672,6 @@ public static string PullButtonCount { } } - /// - /// Looks up a localized string similar to Cancel. - /// - public static string PullConfirmCancel { - get { - return ResourceManager.GetString("PullConfirmCancel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Would you like to pull changes from remote '{0}'?. /// @@ -394,7 +736,7 @@ public static string PushButton { } /// - /// Looks up a localized string similar to . + /// Looks up a localized string similar to Push ({0}). /// public static string PushButtonCount { get { @@ -402,15 +744,6 @@ public static string PushButtonCount { } } - /// - /// Looks up a localized string similar to Cancel. - /// - public static string PushConfirmCancel { - get { - return ResourceManager.GetString("PushConfirmCancel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Would you like to push changes to remote '{0}'?. /// @@ -465,6 +798,15 @@ public static string RefreshButton { } } + /// + /// Looks up a localized string similar to Release Lock. + /// + public static string ReleaseLockActionTitle { + get { + return ResourceManager.GetString("ReleaseLockActionTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Access. /// @@ -510,6 +852,24 @@ public static string RemoteUserTitle { } } + /// + /// Looks up a localized string similar to Request Lock. + /// + public static string RequestLockActionTitle { + get { + return ResourceManager.GetString("RequestLockActionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string SettingsTitle { + get { + return ResourceManager.GetString("SettingsTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Commit summary. /// @@ -527,7 +887,7 @@ public static string SwitchBranchFailedDescription { return ResourceManager.GetString("SwitchBranchFailedDescription", resourceCulture); } } - + /// /// Looks up a localized string similar to Switch branch. /// @@ -536,7 +896,7 @@ public static string SwitchBranchTitle { return ResourceManager.GetString("SwitchBranchTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to GitHub. /// @@ -555,6 +915,24 @@ public static string UnknownViewModeError { } } + /// + /// Looks up a localized string similar to Assets/Release Lock. + /// + public static string UnlockFileAssetsMenuItem { + get { + return ResourceManager.GetString("UnlockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Release Lock. + /// + public static string UnlockFileMenuItem { + get { + return ResourceManager.GetString("UnlockFileMenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Changes. /// @@ -582,6 +960,33 @@ public static string ViewModeSettingsTab { } } + /// + /// Looks up a localized string similar to Active branch. + /// + public static string Window_RepoBranchTooltip { + get { + return ResourceManager.GetString("Window_RepoBranchTooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a remote in the Settings tab. + /// + public static string Window_RepoNoUrlTooltip { + get { + return ResourceManager.GetString("Window_RepoNoUrlTooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Url of the {0} remote. + /// + public static string Window_RepoUrlTooltip { + get { + return ResourceManager.GetString("Window_RepoUrlTooltip", resourceCulture); + } + } + /// /// Looks up a localized string similar to Wrong 2FA code. /// diff --git a/src/GitHub.Api/Localization.resx b/src/GitHub.Api/Localization.resx index ba8fc9667..d8c6ef16e 100644 --- a/src/GitHub.Api/Localization.resx +++ b/src/GitHub.Api/Localization.resx @@ -1,17 +1,17 @@  - @@ -181,13 +181,13 @@ Pull - + Pull ({0}) Push - + Push ({0}) Pull Changes? @@ -198,9 +198,6 @@ Pull - - Cancel - Push Changes? @@ -210,9 +207,6 @@ Push - - Cancel - Commit summary @@ -262,10 +256,10 @@ GitHub - ok + OK - cancel + Cancel Pull @@ -294,4 +288,145 @@ Could not switch to branch {0} + + We could not find Git in the system. + + + The detected LFS at {0} has version {1}, which is too low. The minimum LFS version is {2}. + + + We could not find Git in the system. + + + The detected Git at {0} has version {1}, which is too low. The minimum Git version is {2}. + + + Branches + + + Changes + + + No remote configured + + + Fetch Changes + + + Fetch + + + Could not fetch changes + + + History + + + Initialize + + + Publish + + + Settings + + + Active branch + + + Add a remote in the Settings tab + + + Url of the {0} remote + + + Release Lock + + + Request Lock + + + Assets/Release Lock (forced) + + + Release Lock (forced) + + + Assets/Request Lock + + + Request Lock + + + Locks + + + Assets/Release Lock + + + Release Lock + + + Account + + + Created branch {0} + + + Deleted branch {0} + + + Switched to branch {0} + + + Commit failed + + + Committed + + + Committing + + + Fetched + + + Fetch failed + + + Fetching + + + Pulled + + + Failed to pull + + + Pulling + + + Pushed + + + Failed to push + + + Pushing + + + Refreshed + + + Refreshing + + + Are you sure you want to discard these changes? + + + Discard Changes? + + + Discard + diff --git a/src/GitHub.Api/Managers/Downloader.cs b/src/GitHub.Api/Managers/Downloader.cs deleted file mode 100644 index b4fcbc342..000000000 --- a/src/GitHub.Api/Managers/Downloader.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using GitHub.Logging; -using System.Linq; - -namespace GitHub.Unity -{ - class DownloadData - { - public UriString Url { get; } - public NPath File { get; } - public DownloadData(UriString url, NPath file) - { - this.Url = url; - this.File = file; - } - } - - class Downloader : FuncListTask - { - public event Action DownloadStart; - public event Action DownloadComplete; - public event Action DownloadFailed; - - private readonly List downloaders = new List(); - - public Downloader() : base(TaskManager.Instance.Token, RunDownloaders) - { - Name = "Downloader"; - } - - public void QueueDownload(UriString url, UriString md5Url, NPath targetDirectory) - { - var pairDownloader = new PairDownloader(); - pairDownloader.QueueDownload(url, md5Url, targetDirectory); - downloaders.Add(pairDownloader); - } - - private static List RunDownloaders(bool success, FuncListTask source) - { - Downloader self = (Downloader)source; - List result = null; - var listOfTasks = new List>(); - foreach (var downloader in self.downloaders) - { - downloader.DownloadStart += self.DownloadStart; - downloader.DownloadComplete += self.DownloadComplete; - downloader.DownloadFailed += self.DownloadFailed; - listOfTasks.Add(downloader.Run()); - } - var res = TaskEx.WhenAll(listOfTasks).Result; - if (res != null) - result = new List(res); - return result; - } - - class PairDownloader - { - public event Action DownloadStart; - public event Action DownloadComplete; - public event Action DownloadFailed; - - private readonly List> queuedTasks = new List>(); - private readonly TaskCompletionSource aggregateDownloads = new TaskCompletionSource(); - private readonly IFileSystem fs; - private readonly CancellationToken cancellationToken; - - private int finishedTaskCount; - private volatile bool isSuccessful = true; - private volatile Exception exception; - private DownloadData result; - - public PairDownloader() - { - fs = NPath.FileSystem; - cancellationToken = TaskManager.Instance.Token; - DownloadComplete += d => aggregateDownloads.TrySetResult(d); - DownloadFailed += (_, e) => aggregateDownloads.TrySetException(e); - } - - public Task Run() - { - foreach (var task in queuedTasks) - task.Start(); - if (queuedTasks.Count == 0) - DownloadComplete(result); - return aggregateDownloads.Task; - } - - public Task QueueDownload(UriString url, UriString md5Url, NPath targetDirectory) - { - var destinationFile = targetDirectory.Combine(url.Filename); - var destinationMd5 = targetDirectory.Combine(md5Url.Filename); - result = new DownloadData(url, destinationFile); - - Action, NPath, bool, Exception> verifyDownload = (t, res, success, ex) => - { - var count = Interlocked.Increment(ref finishedTaskCount); - isSuccessful &= success; - if (!success) - exception = ex; - if (count == queuedTasks.Count) - { - if (!isSuccessful) - { - DownloadFailed(result, exception); - } - else - { - if (!Utils.VerifyFileIntegrity(destinationFile, destinationMd5)) - { - destinationMd5.Delete(); - destinationFile.Delete(); - DownloadFailed(result, new DownloadException($"Verification of {url} failed")); - } - else - DownloadComplete(result); - } - } - }; - - var md5Exists = destinationMd5.FileExists(); - var fileExists = destinationFile.FileExists(); - - if (!md5Exists) - { - var md5Download = DownloadFile(md5Url, targetDirectory, result, verifyDownload); - md5Download.OnStart += _ => DownloadStart?.Invoke(result); - queuedTasks.Add(md5Download); - } - - if (!fileExists) - { - var fileDownload = DownloadFile(url, targetDirectory, result, verifyDownload); - if (md5Exists) // only invoke DownloadStart if it hasn't been invoked before in the md5 download - fileDownload.OnStart += _ => DownloadStart?.Invoke(result); - queuedTasks.Add(fileDownload); - } - return aggregateDownloads.Task; - } - - private ITask DownloadFile(UriString url, NPath targetDirectory, DownloadData result, Action, NPath, bool, Exception> verifyDownload) - { - var download = new DownloadTask(cancellationToken, fs, url, targetDirectory) - .Catch(e => { DownloadFailed(result, e); return true; }); - download.OnEnd += verifyDownload; - return download; - } - } - - public static bool Download(ILogging logger, UriString url, - Stream destinationStream, - Func onProgress) - { - long bytes = destinationStream.Length; - - var expectingResume = bytes > 0; - - var webRequest = (HttpWebRequest)WebRequest.Create(url); - - if (expectingResume) - { - // classlib for 3.5 doesn't take long overloads... - webRequest.AddRange((int)bytes); - } - - webRequest.Method = "GET"; - webRequest.Timeout = ApplicationConfiguration.WebTimeout; - - if (expectingResume) - logger.Trace($"Resuming download of {url}"); - else - logger.Trace($"Downloading {url}"); - - using (var webResponse = (HttpWebResponse)webRequest.GetResponseWithoutException()) - { - var httpStatusCode = webResponse.StatusCode; - logger.Trace($"Downloading {url} StatusCode:{(int)webResponse.StatusCode}"); - - if (expectingResume && httpStatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) - { - onProgress(bytes, bytes); - return true; - } - - if (!(httpStatusCode == HttpStatusCode.OK || httpStatusCode == HttpStatusCode.PartialContent)) - { - return false; - } - - if (expectingResume && httpStatusCode == HttpStatusCode.OK) - { - expectingResume = false; - destinationStream.Seek(0, SeekOrigin.Begin); - } - - var responseLength = webResponse.ContentLength; - if (expectingResume) - { - if (!onProgress(bytes, bytes + responseLength)) - return false; - } - - using (var responseStream = webResponse.GetResponseStream()) - { - return Utils.Copy(responseStream, destinationStream, responseLength, - progress: (totalRead, timeToFinish) => - { - return onProgress(totalRead, responseLength); - }); - } - } - } - } -} diff --git a/src/GitHub.Api/Metrics/IMetricsService.cs b/src/GitHub.Api/Metrics/IMetricsService.cs index 78f18bb7d..5eefbe833 100644 --- a/src/GitHub.Api/Metrics/IMetricsService.cs +++ b/src/GitHub.Api/Metrics/IMetricsService.cs @@ -3,11 +3,11 @@ namespace GitHub.Unity { - interface IMetricsService + public interface IMetricsService { /// /// Posts the provided usage model. /// - Task PostUsage(List model); + void PostUsage(List model); } } diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index cf2be6149..c72bfe5ac 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -3,15 +3,24 @@ public interface IUsageTracker { bool Enabled { get; set; } - void IncrementLaunchCount(); - } - - class NullUsageTracker : IUsageTracker - { - public bool Enabled { get; set; } - - public void IncrementLaunchCount(){ } - public void SetMetricsService(IMetricsService instance) - { } + IMetricsService MetricsService { get; set; } + void IncrementNumberOfStartups(); + void IncrementChangesViewButtonCommit(); + void IncrementHistoryViewToolbarFetch(); + void IncrementHistoryViewToolbarPush(); + void IncrementHistoryViewToolbarPull(); + void IncrementAuthenticationViewButtonAuthentication(); + void IncrementProjectsInitialized(); + void IncrementBranchesViewButtonCreateBranch(); + void IncrementBranchesViewButtonDeleteBranch(); + void IncrementBranchesViewButtonCheckoutLocalBranch(); + void IncrementBranchesViewButtonCheckoutRemoteBranch(); + void IncrementSettingsViewButtonLfsUnlock(); + void IncrementUnityProjectViewContextLfsLock(); + void IncrementUnityProjectViewContextLfsUnlock(); + void IncrementPublishViewButtonPublish(); + void IncrementApplicationMenuMenuItemCommandLine(); + void UpdateRepoSize(int kilobytes); + void UpdateLfsDiskUsage(int kilobytes); } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 66ae88362..91a02c1d2 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -1,17 +1,50 @@ -using System; +using GitHub.Logging; +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text; namespace GitHub.Unity { public class Usage + { + public string InstanceId { get; set; } + public Dimensions Dimensions { get; set; } = new Dimensions(); + public Measures Measures { get; set; } = new Measures(); + } + + public class Dimensions { public string Guid { get; set; } - public DateTime Date { get; set; } + public DateTimeOffset Date { get; set; } public string AppVersion { get; set; } public string UnityVersion { get; set; } public string Lang { get; set; } + public string CurrentLang { get; set; } + public string GitHubUser { get; set; } + } + + public class Measures + { public int NumberOfStartups { get; set; } + public int ProjectsInitialized { get; set; } + public int ChangesViewButtonCommit { get; set; } + public int HistoryViewToolbarFetch { get; set; } + public int HistoryViewToolbarPush { get; set; } + public int HistoryViewToolbarPull { get; set; } + public int AuthenticationViewButtonAuthentication { get; set; } + public int BranchesViewButtonCreateBranch { get; set; } + public int BranchesViewButtonDeleteBranch { get; set; } + public int BranchesViewButtonCheckoutLocalBranch { get; set; } + public int BranchesViewButtonCheckoutRemoteBranch { get; set; } + public int SettingsViewButtonLfsUnlock { get; set; } + public int UnityProjectViewContextLfsLock { get; set; } + public int UnityProjectViewContextLfsUnlock { get; set; } + public int PublishViewButtonPublish { get; set; } + public int ApplicationMenuMenuItemCommandLine { get; set; } + public int GitRepoSize { get; set; } + public int LfsDiskUsage { get; set; } } class UsageModel @@ -21,23 +54,32 @@ class UsageModel private Usage currentUsage; - public Usage GetCurrentUsage() + public Usage GetCurrentUsage(string appVersion, string unityVersion, string instanceId) { - var date = DateTime.UtcNow.Date; + Guard.ArgumentNotNullOrWhiteSpace(appVersion, "appVersion"); + Guard.ArgumentNotNullOrWhiteSpace(unityVersion, "unityVersion"); + + var now = DateTimeOffset.Now; if (currentUsage == null) { - currentUsage = Reports.FirstOrDefault(usage => usage.Date == date); + currentUsage = Reports + .FirstOrDefault(usage => usage.InstanceId == instanceId); } - if (currentUsage?.Date == date) - { - // update any fields that might be missing, if we've changed the format - if (currentUsage.Guid != Guid) - currentUsage.Guid = Guid; - } - else + if (currentUsage == null) { - currentUsage = new Usage { Date = date, Guid = Guid }; + currentUsage = new Usage + { + InstanceId = instanceId, + Dimensions = { + Date = now, + Guid = Guid, + AppVersion = appVersion, + UnityVersion = unityVersion, + Lang = CultureInfo.InstalledUICulture.IetfLanguageTag, + CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag + } + }; Reports.Add(currentUsage); } @@ -46,18 +88,23 @@ public Usage GetCurrentUsage() public List SelectReports(DateTime beforeDate) { - return Reports.Where(usage => usage.Date.Date != beforeDate.Date).ToList(); + return Reports.Where(usage => usage.Dimensions.Date.Date < beforeDate.Date).ToList(); } public void RemoveReports(DateTime beforeDate) { - Reports.RemoveAll(usage => usage.Date.Date != beforeDate.Date); + Reports.RemoveAll(usage => usage.Dimensions.Date.Date < beforeDate.Date); } } class UsageStore { - public DateTimeOffset LastUpdated { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset LastSubmissionDate { get; set; } = DateTimeOffset.Now; public UsageModel Model { get; set; } = new UsageModel(); + + public Measures GetCurrentMeasures(string appVersion, string unityVersion, string instanceId) + { + return Model.GetCurrentUsage(appVersion, unityVersion, instanceId).Measures; + } } } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 737209420..0e8bce059 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -1,176 +1,336 @@ using System; using System.Linq; using System.Text; -using System.Threading.Tasks; -using System.Timers; -using System.Globalization; using System.Threading; -using Timer = System.Threading.Timer; using GitHub.Logging; namespace GitHub.Unity { - class UsageTracker : IUsageTracker + class UsageTrackerSync : IUsageTracker { + +#if DEVELOPER_BUILD + protected internal const int MetrisReportTimeout = 30; +#else + protected internal const int MetrisReportTimeout = 3 * 60; +#endif + private static ILogging Logger { get; } = LogHelper.GetLogger(); - private readonly NPath storePath; + private static object _lock = new object(); + private readonly ISettings userSettings; - private readonly IMetricsService metricsService; - private readonly string guid; + private readonly IUsageLoader usageLoader; + private readonly string userId; + private readonly string appVersion; private readonly string unityVersion; + private readonly string instanceId; private Timer timer; - public UsageTracker(IMetricsService metricsService, ISettings userSettings, NPath storePath, string guid, string unityVersion) + public IMetricsService MetricsService { get; set; } + + public UsageTrackerSync(ISettings userSettings, IUsageLoader usageLoader, + string unityVersion, string instanceId) { this.userSettings = userSettings; - this.metricsService = metricsService; - this.guid = guid; - this.storePath = storePath; + this.usageLoader = usageLoader; + this.appVersion = ApplicationInfo.Version; this.unityVersion = unityVersion; + this.instanceId = instanceId; + + if (userSettings.Exists(Constants.GuidKey)) + { + userId = userSettings.Get(Constants.GuidKey); + } - Logger.Trace("guid:{0}", guid); + if (String.IsNullOrEmpty(userId)) + { + userId = Guid.NewGuid().ToString(); + userSettings.Set(Constants.GuidKey, userId); + } + + Logger.Trace("userId:{0} instanceId:{1}", userId, instanceId); if (Enabled) - RunTimer(3*60); + RunTimer(MetrisReportTimeout); } - private UsageStore LoadUsage() + private void RunTimer(int seconds) { - UsageStore result = null; - string json = null; - if (storePath.FileExists()) + timer = new System.Threading.Timer(_ => { - Logger.Trace("LoadUsage: \"{0}\"", storePath); - try { - json = storePath.ReadAllText(Encoding.UTF8); - if (json != null) - { - result = SimpleJson.DeserializeObject(json); - } + timer.Dispose(); + SendUsage(); } - catch (Exception ex) - { - Logger.Warning(ex, "Error Loading Usage: {0}; Deleting File", storePath); + catch { } + }, null, seconds * 1000, Timeout.Infinite); + } - try - { - storePath.DeleteIfExists(); - } - catch {} - } + private void SendUsage() + { + if (MetricsService == null) + { + Logger.Warning("Metrics disabled: no service"); + return; } - if (result == null) - result = new UsageStore(); + if (!Enabled) + { + Logger.Trace("Metrics disabled"); + return; + } - if (String.IsNullOrEmpty(result.Model.Guid)) - result.Model.Guid = guid; + UsageStore usageStore = null; + lock (_lock) + { + usageStore = usageLoader.Load(userId); + } - return result; - } + var currentTimeOffset = DateTimeOffset.UtcNow; + if (usageStore.LastSubmissionDate.Date == currentTimeOffset.Date) + { + Logger.Trace("Already sent today"); + return; + } - private void SaveUsage(UsageStore store) - { - if (!Enabled) + var extractReports = usageStore.Model.SelectReports(currentTimeOffset.Date); + if (!extractReports.Any()) { + Logger.Trace("No items to send"); return; } - var pathString = storePath.ToString(); - Logger.Trace("SaveUsage: \"{0}\"", pathString); + var username = GetUsername(); + if (!String.IsNullOrEmpty(username)) { + extractReports.ForEach(x => x.Dimensions.GitHubUser = username); + } try { - var json = SimpleJson.SerializeObject(store); - storePath.WriteAllText(json, Encoding.UTF8); + MetricsService.PostUsage(extractReports); } catch (Exception ex) { - Logger.Error(ex, "SaveUsage Error: \"{0}\"", pathString); + Logger.Warning(@"Error sending usage:""{0}"" Message:""{1}""", ex.GetType(), ex.GetExceptionMessageShort()); + return; + } + + // if we're here, success! + lock (_lock) + { + usageStore = usageLoader.Load(userId); + usageStore.LastSubmissionDate = currentTimeOffset; + usageStore.Model.RemoveReports(currentTimeOffset.Date); + usageLoader.Save(usageStore); } + + // update the repo size for the current report, while we're at it + CaptureRepoSize(); } - private void RunTimer(int seconds) + protected virtual void CaptureRepoSize() + {} + + public virtual void IncrementNumberOfStartups() { - Logger.Trace($"Scheduling timer for {seconds} seconds from now"); - timer = new Timer(async _ => + lock (_lock) { - try - { - timer.Dispose(); - await SendUsage(); - } - catch {} - }, null, seconds * 1000, Timeout.Infinite); + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .NumberOfStartups++; + usageLoader.Save(usage); + } } + public virtual void IncrementProjectsInitialized() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ProjectsInitialized++; + usageLoader.Save(usage); + } + } - private async Task SendUsage() + public virtual void IncrementChangesViewButtonCommit() { - var usage = LoadUsage(); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ChangesViewButtonCommit++; + usageLoader.Save(usage); + } + } - if (metricsService == null) + public virtual void IncrementHistoryViewToolbarFetch() + { + lock (_lock) { - Logger.Warning("No service, not sending usage"); - return; + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarFetch++; + usageLoader.Save(usage); } + } - if (usage.LastUpdated.Date != DateTimeOffset.UtcNow.Date) + public virtual void IncrementHistoryViewToolbarPush() + { + lock (_lock) { - Logger.Trace("Sending Usage"); + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarPush++; + usageLoader.Save(usage); + } + } - var currentTimeOffset = DateTimeOffset.UtcNow; - var beforeDate = currentTimeOffset.Date; + public virtual void IncrementHistoryViewToolbarPull() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarPull++; + usageLoader.Save(usage); + } + } - var success = false; - var extractReports = usage.Model.SelectReports(beforeDate); - if (!extractReports.Any()) - { - Logger.Trace("No items to send"); - } - else - { - if (!Enabled) - { - Logger.Trace("Metrics disabled"); - return; - } + public virtual void IncrementBranchesViewButtonCreateBranch() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCreateBranch++; + usageLoader.Save(usage); + } + } - try - { - await metricsService.PostUsage(extractReports); - success = true; - } - catch (Exception ex) - { - Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.Message); - } - } + public virtual void IncrementBranchesViewButtonDeleteBranch() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonDeleteBranch++; + usageLoader.Save(usage); + } + } - if (success) - { - usage.Model.RemoveReports(beforeDate); - usage.LastUpdated = currentTimeOffset; - SaveUsage(usage); - } + public virtual void IncrementBranchesViewButtonCheckoutLocalBranch() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCheckoutLocalBranch++; + usageLoader.Save(usage); + } + } + + public virtual void IncrementBranchesViewButtonCheckoutRemoteBranch() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCheckoutRemoteBranch++; + usageLoader.Save(usage); } } - public void IncrementLaunchCount() + public virtual void IncrementSettingsViewButtonLfsUnlock() { - var usageStore = LoadUsage(); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .SettingsViewButtonLfsUnlock++; + usageLoader.Save(usage); + } + } - var usage = usageStore.Model.GetCurrentUsage(); - usage.NumberOfStartups++; - usage.UnityVersion = unityVersion; - usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; - usage.AppVersion = ApplicationConfiguration.AssemblyName.Version.ToString(); + public virtual void IncrementAuthenticationViewButtonAuthentication() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .AuthenticationViewButtonAuthentication++; + usageLoader.Save(usage); + } + } + + public virtual void IncrementUnityProjectViewContextLfsLock() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .UnityProjectViewContextLfsLock++; + usageLoader.Save(usage); + } + } - Logger.Trace("IncrementLaunchCount Date:{0} NumberOfStartups:{1}", usage.Date, usage.NumberOfStartups); + public virtual void IncrementUnityProjectViewContextLfsUnlock() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .UnityProjectViewContextLfsUnlock++; + usageLoader.Save(usage); + } + } - SaveUsage(usageStore); + public virtual void IncrementPublishViewButtonPublish() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .PublishViewButtonPublish++; + usageLoader.Save(usage); + } + } + + public virtual void IncrementApplicationMenuMenuItemCommandLine() + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ApplicationMenuMenuItemCommandLine++; + usageLoader.Save(usage); + } + } + + public virtual void UpdateRepoSize(int kilobytes) + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).GitRepoSize = kilobytes; + usageLoader.Save(usage); + } + } + + public virtual void UpdateLfsDiskUsage(int kilobytes) + { + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).LfsDiskUsage = kilobytes; + usageLoader.Save(usage); + } + } + + protected virtual string GetUsername() + { + return ""; } public bool Enabled @@ -196,4 +356,142 @@ public bool Enabled } } } + + class UsageTracker : UsageTrackerSync + { + public UsageTracker(ITaskManager taskManager, IGitClient gitClient, IProcessManager processManager, + ISettings userSettings, + IEnvironment environment, + IKeychain keychain, + string instanceId) + : base(userSettings, + new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), + environment.UnityVersion, instanceId) + { + TaskManager = taskManager; + Environment = environment; + GitClient = gitClient; + ProcessManager = processManager; + Keychain = keychain; + } + + protected override void CaptureRepoSize() + { + try + { + var gitSize = GitClient.CountObjects() + .Catch(_ => true) + .RunSynchronously(); + base.UpdateRepoSize(gitSize); + + var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); + if (gitLfsDataPath.Exists()) + { + var lfsSize = new LinuxDiskUsageTask(gitLfsDataPath, TaskManager.Token) + .Configure(ProcessManager) + .Catch(_ => true) + .RunSynchronously(); + base.UpdateLfsDiskUsage(lfsSize); + } + } + catch {} + } + + protected override string GetUsername() + { + string username = ""; + try { + var apiClient = new ApiClient(Keychain, ProcessManager, TaskManager, Environment); + var user = apiClient.GetCurrentUser(); + username = user.Login; + } catch { + } + return username; + } + + public override void IncrementApplicationMenuMenuItemCommandLine() => TaskManager.Run(base.IncrementApplicationMenuMenuItemCommandLine); + public override void IncrementAuthenticationViewButtonAuthentication() => TaskManager.Run(base.IncrementAuthenticationViewButtonAuthentication); + public override void IncrementBranchesViewButtonCheckoutLocalBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCheckoutLocalBranch); + public override void IncrementBranchesViewButtonCheckoutRemoteBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCheckoutRemoteBranch); + public override void IncrementBranchesViewButtonCreateBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCreateBranch); + public override void IncrementBranchesViewButtonDeleteBranch() => TaskManager.Run(base.IncrementBranchesViewButtonDeleteBranch); + public override void IncrementChangesViewButtonCommit() => TaskManager.Run(base.IncrementChangesViewButtonCommit); + public override void IncrementHistoryViewToolbarFetch() => TaskManager.Run(base.IncrementHistoryViewToolbarFetch); + public override void IncrementHistoryViewToolbarPull() => TaskManager.Run(base.IncrementHistoryViewToolbarPull); + public override void IncrementHistoryViewToolbarPush() => TaskManager.Run(base.IncrementHistoryViewToolbarPush); + public override void IncrementNumberOfStartups() => TaskManager.Run(base.IncrementNumberOfStartups); + public override void IncrementProjectsInitialized() => TaskManager.Run(base.IncrementProjectsInitialized); + public override void IncrementPublishViewButtonPublish() => TaskManager.Run(base.IncrementPublishViewButtonPublish); + public override void IncrementSettingsViewButtonLfsUnlock() => TaskManager.Run(base.IncrementSettingsViewButtonLfsUnlock); + public override void IncrementUnityProjectViewContextLfsLock() => TaskManager.Run(base.IncrementUnityProjectViewContextLfsLock); + public override void IncrementUnityProjectViewContextLfsUnlock() => TaskManager.Run(base.IncrementUnityProjectViewContextLfsUnlock); + public override void UpdateLfsDiskUsage(int kilobytes) => TaskManager.Run(() => base.UpdateLfsDiskUsage(kilobytes)); + public override void UpdateRepoSize(int kilobytes) => TaskManager.Run(() => base.UpdateRepoSize(kilobytes)); + + protected ITaskManager TaskManager { get; } + protected IEnvironment Environment { get; } + protected IGitClient GitClient { get; } + public IProcessManager ProcessManager { get; } + protected IKeychain Keychain { get; } + } + + interface IUsageLoader + { + UsageStore Load(string userId); + void Save(UsageStore store); + } + + class UsageLoader : IUsageLoader + { + private readonly NPath path; + + public UsageLoader(NPath path) + { + this.path = path; + } + + public UsageStore Load(string userId) + { + UsageStore result = null; + string json = null; + if (path.FileExists()) + { + try + { + json = path.ReadAllText(Encoding.UTF8); + result = json?.FromJson(lowerCase: true); + } + catch (Exception ex) + { + LogHelper.Instance.Warning(ex, "Error Loading Usage: {0}; Deleting File", path); + try + { + path.DeleteIfExists(); + } + catch { } + } + } + + if (result == null) + result = new UsageStore(); + + if (String.IsNullOrEmpty(result.Model.Guid)) + result.Model.Guid = userId; + + return result; + } + + public void Save(UsageStore store) + { + try + { + var json = store.ToJson(lowerCase: true); + path.WriteAllText(json, Encoding.UTF8); + } + catch (Exception ex) + { + LogHelper.Instance.Error(ex, "SaveUsage Error: \"{0}\"", path); + } + } + } } diff --git a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs index 320ea9435..2b6427f40 100644 --- a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs @@ -1,3 +1,5 @@ +using System; +using System.Security.AccessControl; using System.Text.RegularExpressions; namespace GitHub.Unity @@ -15,30 +17,42 @@ public override void LineReceived(string line) if (proc.IsAtEnd) return; - var active = proc.Matches('*'); - proc.SkipWhitespace(); - var detached = proc.Matches("(HEAD "); - var name = "detached"; - if (detached) + try { - proc.MoveToAfter(')'); - } - else - { - name = proc.ReadUntilWhitespace(); + string name; + string trackingName = null; + + if (proc.Matches('*')) + proc.MoveNext(); + proc.SkipWhitespace(); + if (proc.Matches("(HEAD ")) + { + name = "detached"; + proc.MoveToAfter(')'); + } + else + { + name = proc.ReadUntilWhitespace(); + } + + proc.ReadUntilWhitespaceTrim(); + if (proc.Matches(trackingBranchRegex)) + { + trackingName = proc.ReadChunk('[', ']'); + var indexOf = trackingName.IndexOf(':'); + if (indexOf != -1) + { + trackingName = trackingName.Substring(0, indexOf); + } + } + + var branch = new GitBranch(name, trackingName); + RaiseOnEntry(branch); } - proc.SkipWhitespace(); - proc.ReadUntilWhitespace(); - var tracking = proc.Matches(trackingBranchRegex); - var trackingName = ""; - if (tracking) + catch(Exception ex) { - trackingName = proc.ReadChunk('[', ']'); + Logger.Warning(ex, "Unexpected input when listing branches"); } - - var branch = new GitBranch(name, trackingName); - - RaiseOnEntry(branch); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs new file mode 100644 index 000000000..3fe0f31b7 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs @@ -0,0 +1,27 @@ +namespace GitHub.Unity +{ + public class GitCountObjectsProcessor : BaseOutputProcessor + { + public override void LineReceived(string line) + { + if (line == null) + { + return; + } + + //2488 objects, 4237 kilobytes + + try + { + var proc = new LineParser(line); + + proc.MoveToAfter(','); + var kilobytes = int.Parse(proc.ReadUntilWhitespaceTrim()); + + RaiseOnEntry(kilobytes); + } + catch {} + return; + } + } +} diff --git a/src/GitHub.Api/OutputProcessors/LfsVersionOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LfsVersionOutputProcessor.cs index 495eb8e03..465747c60 100644 --- a/src/GitHub.Api/OutputProcessors/LfsVersionOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/LfsVersionOutputProcessor.cs @@ -3,23 +3,17 @@ namespace GitHub.Unity { - class LfsVersionOutputProcessor : BaseOutputProcessor + class LfsVersionOutputProcessor : BaseOutputProcessor { - public static Regex GitLfsVersionRegex = new Regex(@"git-lfs/([\d]+)\.([\d]+)\.([\d]+)"); - public override void LineReceived(string line) { if (String.IsNullOrEmpty(line)) return; - var match = GitLfsVersionRegex.Match(line); - - if (match.Groups.Count > 0) + var parts = line.Split('/', ' '); + if (parts.Length > 1) { - var major = Int32.Parse(match.Groups[1].Value); - var minor = Int32.Parse(match.Groups[2].Value); - var build = Int32.Parse(match.Groups[3].Value); - var version = new Version(major, minor, build); + var version = TheVersion.Parse(parts[1]); RaiseOnEntry(version); } } diff --git a/src/GitHub.Api/OutputProcessors/LineProcessor.cs b/src/GitHub.Api/OutputProcessors/LineProcessor.cs index 213717162..8d90ea413 100644 --- a/src/GitHub.Api/OutputProcessors/LineProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/LineProcessor.cs @@ -61,19 +61,30 @@ public void SkipWhitespace() if (IsAtEnd) throw new InvalidOperationException("Reached end of line"); - while (!Char.IsWhiteSpace(line[current]) && current < line.Length) - current++; - while (Char.IsWhiteSpace(line[current]) && current < line.Length) + while (current < line.Length && char.IsWhiteSpace(line[current])) current++; } - public string ReadUntil(char separator) + /// + /// Reads until it finds the separator and returns what it read. + /// + /// + /// If the current character matches the + /// separator and you actually want to read the next match, set this to true (if you're tokenizing, for instance) + /// + public string ReadUntil(char separator, bool skipCurrentIfMatch = false) { if (IsAtEnd) throw new InvalidOperationException("Reached end of line"); - if (line[current] == separator) - current++; + if (Matches(separator)) + { + if (skipCurrentIfMatch) + MoveNext(); + else + return null; + } + var end = line.IndexOf(separator, current); if (end == -1) return null; @@ -82,23 +93,30 @@ public string ReadUntil(char separator) return LastSubstring; } + + public string ReadUntilWhitespaceTrim() + { + SkipWhitespace(); + if (IsAtEnd) + return null; + return ReadUntilWhitespace(); + } + public string ReadUntilWhitespace() { if (IsAtEnd) throw new InvalidOperationException("Reached end of line"); - if (Char.IsWhiteSpace(line[current])) - SkipWhitespace(); + if (char.IsWhiteSpace(line[current])) + return null; + + var end = current; + while (end < line.Length && !char.IsWhiteSpace(line[end])) + end++; + + if (end == current) // current character is a whitespace, read nothing + return null; - int end = line.Length; - for (var i = current; i < end; i++) - { - if (Char.IsWhiteSpace(line[i])) - { - end = i; - break; - } - } LastSubstring = line.Substring(current, end - current); current = end; return LastSubstring; @@ -125,6 +143,30 @@ public string ReadToEnd() return LastSubstring; } + public string Read(int howMany) + { + if (IsAtEnd) + throw new InvalidOperationException("Reached end of line"); + + if (current + howMany > line.Length) + return null; + + LastSubstring = line.Substring(current, howMany); + current += howMany; + return LastSubstring; + } + + public char ReadChar() + { + if (IsAtEnd) + throw new InvalidOperationException("Reached end of line"); + + var ret = line[current]; + LastSubstring = ret.ToString(); + MoveNext(); + return ret; + } + public string ReadUntilLast(string str) { if (IsAtEnd) @@ -138,10 +180,10 @@ public string ReadUntilLast(string str) return LastSubstring; } - public bool IsAtEnd { get { return line != null ? line.Length == current : true; } } - public bool IsAtWhitespace { get { return line != null && Char.IsWhiteSpace(line[current]); } } - public bool IsAtDigit { get { return line != null && Char.IsDigit(line[current]); } } - public bool IsAtLetter { get { return line != null && Char.IsLetter(line[current]); } } + public bool IsAtEnd => line == null || line.Length == current; + public bool IsAtWhitespace => line != null && Char.IsWhiteSpace(line[current]); + public bool IsAtDigit => line != null && Char.IsDigit(line[current]); + public bool IsAtLetter => line != null && Char.IsLetter(line[current]); public string LastSubstring { get; private set; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs new file mode 100644 index 000000000..0ce27f2ad --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs @@ -0,0 +1,18 @@ +using System; + +namespace GitHub.Unity +{ + public class LinuxDiskUsageOutputProcessor : BaseOutputProcessor + { + public override void LineReceived(string line) + { + if (line == null) + return; + + int kb; + var proc = new LineParser(line); + if (int.TryParse(proc.ReadUntilWhitespace(), out kb)) + RaiseOnEntry(kb); + } + } +} diff --git a/src/GitHub.Api/OutputProcessors/LockOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LockOutputProcessor.cs deleted file mode 100644 index d77f4478e..000000000 --- a/src/GitHub.Api/OutputProcessors/LockOutputProcessor.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace GitHub.Unity -{ - class LockOutputProcessor : BaseOutputListProcessor - { - private static readonly Regex locksSummaryLineRegex = new Regex(@".*?lock\s?\(s\) matched query.", - RegexOptions.Compiled); - - private IGitObjectFactory gitObjectFactory; - - public LockOutputProcessor(IGitObjectFactory gitObjectFactory) - { - Guard.ArgumentNotNull(gitObjectFactory, "gitObjectFactory"); - this.gitObjectFactory = gitObjectFactory; - } - - public override void LineReceived(string line) - { - if (string.IsNullOrEmpty(line)) - { - //Do Nothing - return; - } - - var proc = new LineParser(line); - if (proc.Matches(locksSummaryLineRegex)) - { - return; - } - Logger.Trace(line); - var path = proc.ReadUntil('\t').Trim(); - var user = proc.ReadUntilLast("ID:").Trim(); - proc.MoveToAfter("ID:"); - var id = int.Parse(proc.ReadToEnd().Trim()); - - RaiseOnEntry(gitObjectFactory.CreateGitLock(path, user, id)); - } - } -} diff --git a/src/GitHub.Api/OutputProcessors/LocksOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LocksOutputProcessor.cs new file mode 100644 index 000000000..2c27c2cb1 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/LocksOutputProcessor.cs @@ -0,0 +1,29 @@ +using System; + +namespace GitHub.Unity +{ + class LocksOutputProcessor : BaseOutputListProcessor + { + public override void LineReceived(string line) + { + if (string.IsNullOrEmpty(line)) + { + //Do Nothing + return; + } + + try + { + var locks = line.FromJson(lowerCase: true); + foreach (var lck in locks) + { + RaiseOnEntry(lck); + } + } + catch(Exception ex) + { + Logger.Error(ex, $"Failed to parse lock line {line}"); + } + } + } +} diff --git a/src/GitHub.Api/OutputProcessors/LogEntryOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LogEntryOutputProcessor.cs index 4ad7f6560..79d1fdeef 100644 --- a/src/GitHub.Api/OutputProcessors/LogEntryOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/LogEntryOutputProcessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -143,7 +144,6 @@ public override void LineReceived(string line) } summary = line; - descriptionLines.Add(line); phase++; // there's no description so skip it if (oneliner) @@ -190,101 +190,45 @@ public override void LineReceived(string line) var proc = new LineParser(line); string file = null; - GitFileStatus status; + GitFileStatus status = GitFileStatus.None; string originalPath = null; - if (proc.Matches('M')) - { - status = GitFileStatus.Modified; - } - else if (proc.Matches('A')) - { - status = GitFileStatus.Added; - } - else if (proc.Matches('D')) - { - status = GitFileStatus.Deleted; - } - else if (proc.Matches('R')) - { - status = GitFileStatus.Renamed; - } - else if (proc.Matches('C')) - { - status = GitFileStatus.Copied; - } - else if (proc.Matches('T')) - { - status = GitFileStatus.TypeChange; - } - else if (proc.Matches('U')) - { - status = GitFileStatus.Unmerged; - } - else if (proc.Matches('X')) - { - status = GitFileStatus.Unknown; - } - else if (proc.Matches('B')) - { - status = GitFileStatus.Broken; - } - else if (String.IsNullOrEmpty(line)) + if (proc.IsAtEnd) { // there's no files on this commit, it's a new one! ReturnGitLogEntry(); return; } else + { + status = GitStatusEntry.ParseStatusMarker(proc.ReadChar()); + } + + if (status == GitFileStatus.None) { HandleUnexpected(line); return; } - switch (status) + proc.ReadUntilWhitespace(); + if (status == GitFileStatus.Copied || status == GitFileStatus.Renamed) { - case GitFileStatus.Modified: - case GitFileStatus.Added: - case GitFileStatus.Deleted: - proc.SkipWhitespace(); - - file = proc.Matches('"') - ? proc.ReadUntil('"') - : proc.ReadToEnd(); - - break; - case GitFileStatus.Renamed: - - proc.SkipWhitespace(); - - originalPath = - proc.Matches('"') - ? proc.ReadUntil('"') - : proc.ReadUntilWhitespace(); - - proc.SkipWhitespace(); - - file = proc.Matches('"') - ? proc.ReadUntil('"') - : proc.ReadToEnd(); - - break; - - default: - proc.SkipWhitespace(); - - file = proc.Matches('"') - ? proc.ReadUntil('"') - : proc.ReadUntilWhitespace(); - if (file == null) - { - file = proc.ReadToEnd(); - } - - break; + var files = + proc.ReadToEnd().Trim() + .Split(new char[] { '\t' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Select(s => s.Trim('"')) + .ToArray(); + + originalPath = files[0]; + file = files[1]; + } + else + { + file = proc.ReadToEnd().Trim().Trim('"'); } - changes.Add(gitObjectFactory.CreateGitStatusEntry(file, status, originalPath)); + changes.Add(gitObjectFactory.CreateGitStatusEntry(file, status, GitFileStatus.None, originalPath)); break; @@ -313,7 +257,8 @@ private void ReturnGitLogEntry() { PopNewlines(); - var description = string.Join(Environment.NewLine, descriptionLines.ToArray()); + var filteredDescriptionLines = (descriptionLines.Any() && string.IsNullOrEmpty(descriptionLines.First()) ? descriptionLines.Skip(1) : descriptionLines).ToArray(); + var description = string.Join(Environment.NewLine, filteredDescriptionLines); if (time.HasValue) { @@ -347,4 +292,4 @@ private enum ProcessingPhase Files = 10, } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/RemoteListOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/RemoteListOutputProcessor.cs index 03f8f9d68..08332bbe6 100644 --- a/src/GitHub.Api/OutputProcessors/RemoteListOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/RemoteListOutputProcessor.cs @@ -110,4 +110,4 @@ private void Reset() currentUrl = null; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs index 1b95daca4..26be0f9fd 100644 --- a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { - class StatusOutputProcessor : BaseOutputProcessor + class GitStatusOutputProcessor : BaseOutputProcessor { private static readonly Regex branchTrackedAndDelta = new Regex(@"(.*)\.\.\.(.*)\s\[(.*)\]", RegexOptions.Compiled); @@ -13,7 +13,7 @@ class StatusOutputProcessor : BaseOutputProcessor private readonly IGitObjectFactory gitObjectFactory; GitStatus gitStatus; - public StatusOutputProcessor(IGitObjectFactory gitObjectFactory) + public GitStatusOutputProcessor(IGitObjectFactory gitObjectFactory) { Guard.ArgumentNotNull(gitObjectFactory, "gitObjectFactory"); this.gitObjectFactory = gitObjectFactory; @@ -89,97 +89,76 @@ public override void LineReceived(string line) } else { - // M GitHubVS.sln - //R README.md -> README2.md - // D deploy.cmd - //A something added.txt - //?? something.txt + var gitStatusMarker = proc.Read(2); + if (gitStatusMarker == null) + { + HandleUnexpected(line); + return; + } + + + /* + X Y Meaning + ------------------------------------------------- + [AMD] not updated + M [ MD] updated in index + A [ MD] added to index + D deleted from index + R [ MD] renamed in index + C [ MD] copied in index + [MARC] index and work tree matches + [ MARC] M work tree changed since index + [ MARC] D deleted in work tree + [ D] R renamed in work tree + [ D] C copied in work tree + ------------------------------------------------- + D D unmerged, both deleted + A A unmerged, both added + A U unmerged, added by us + D U unmerged, deleted by us + U A unmerged, added by them + U D unmerged, deleted by them + U U unmerged, both modified + ------------------------------------------------- + ? ? untracked + ! ! ignored + ------------------------------------------------- + */ string originalPath = null; string path = null; - var status = GitFileStatus.Added; - var staged = false; - if (proc.Matches('?')) - { - //?? something.txt - proc.MoveToAfter('?'); - proc.SkipWhitespace(); + var indexStatusMarker = gitStatusMarker[0]; + var workTreeStatusMarker = gitStatusMarker[1]; - path = proc.ReadToEnd().Trim('"'); - status = GitFileStatus.Untracked; - } - else if (proc.Matches('!')) + GitFileStatus indexStatus = GitStatusEntry.ParseStatusMarker(indexStatusMarker); + GitFileStatus workTreeStatus = GitStatusEntry.ParseStatusMarker(workTreeStatusMarker); + GitFileStatus status = workTreeStatus != GitFileStatus.None ? workTreeStatus : indexStatus; + + if (status == GitFileStatus.None) { - //?? something.txt - proc.MoveToAfter('!'); - proc.SkipWhitespace(); + HandleUnexpected(line); + return; + } - path = proc.ReadToEnd().Trim('"'); - status = GitFileStatus.Ignored; + if (status == GitFileStatus.Copied || status == GitFileStatus.Renamed) + { + var files = + proc.ReadToEnd() + .Split(new[] { "->" }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Select(s => s.Trim('"')) + .ToArray(); + + originalPath = files[0]; + path = files[1]; } else { - if (proc.IsAtWhitespace) - { - proc.SkipWhitespace(); - } - else - { - staged = true; - } - - if (proc.Matches('M')) - { - //M GitHubVS.sln - proc.MoveNext(); - proc.SkipWhitespace(); - - path = proc.ReadToEnd().Trim('"'); - status = GitFileStatus.Modified; - } - else if (proc.Matches('D')) - { - //D deploy.cmd - proc.MoveNext(); - proc.SkipWhitespace(); - - path = proc.ReadToEnd().Trim('"'); - status = GitFileStatus.Deleted; - } - else if (proc.Matches('R')) - { - //R README.md -> README2.md - proc.MoveNext(); - proc.SkipWhitespace(); - - var files = - proc.ReadToEnd() - .Split(new[] { "->" }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .Select(s => s.Trim('"')) - .ToArray(); - - originalPath = files[0]; - path = files[1]; - status = GitFileStatus.Renamed; - } - else if (proc.Matches('A')) - { - //A something added.txt - proc.MoveNext(); - proc.SkipWhitespace(); - - path = proc.ReadToEnd().Trim('"'); - status = GitFileStatus.Added; - } - else - { - HandleUnexpected(line); - } + path = proc.ReadToEnd().Trim().Trim('"'); } - var gitStatusEntry = gitObjectFactory.CreateGitStatusEntry(path, status, originalPath, staged); + var gitStatusEntry = gitObjectFactory.CreateGitStatusEntry(path, indexStatus, workTreeStatus, originalPath); gitStatus.Entries.Add(gitStatusEntry); } } @@ -190,6 +169,10 @@ private void ReturnStatus() if (gitStatus.Entries == null) return; + gitStatus.Entries = gitStatus.Entries + .OrderBy(entry => entry.Path, StatusOutputPathComparer.Instance) + .ToList(); + RaiseOnEntry(gitStatus); gitStatus = new GitStatus(); @@ -210,5 +193,32 @@ private void HandleUnexpected(string line) { Logger.Error("Unexpected Input:\"{0}\"", line); } + + public class StatusOutputPathComparer : IComparer + { + public static StatusOutputPathComparer Instance => new StatusOutputPathComparer(); + + public int Compare(string x, string y) + { + Guard.ArgumentNotNull(x, nameof(x)); + Guard.ArgumentNotNull(y, nameof(y)); + + var meta = ".meta"; + var xHasMeta = x.EndsWith(meta); + var yHasMeta = y.EndsWith(meta); + + if(!xHasMeta && !yHasMeta) return StringComparer.InvariantCulture.Compare(x, y); + + var xPure = xHasMeta ? x.Substring(0, x.Length - meta.Length) : x; + var yPure = yHasMeta ? y.Substring(0, y.Length - meta.Length) : y; + + if (xHasMeta) + { + return xPure.Equals(y) ? 1 : StringComparer.InvariantCulture.Compare(xPure, yPure); + } + + return yPure.Equals(x) ? -1 : StringComparer.InvariantCulture.Compare(xPure, yPure); + } + } } } diff --git a/src/GitHub.Api/OutputProcessors/VersionOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/VersionOutputProcessor.cs index 1fcacfaa6..901098c35 100644 --- a/src/GitHub.Api/OutputProcessors/VersionOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/VersionOutputProcessor.cs @@ -5,9 +5,9 @@ namespace GitHub.Unity { - class VersionOutputProcessor : BaseOutputProcessor + class VersionOutputProcessor : BaseOutputProcessor { - public static Regex GitVersionRegex = new Regex(@"git version ([\d]+)\.([\d]+)\.([\d]+)"); + public static Regex GitVersionRegex = new Regex(@"git version (.*)"); public override void LineReceived(string line) { @@ -15,13 +15,9 @@ public override void LineReceived(string line) return; var match = GitVersionRegex.Match(line); - - if (match.Groups.Count > 0) + if (match.Groups.Count > 1) { - var major = Int32.Parse(match.Groups[1].Value); - var minor = Int32.Parse(match.Groups[2].Value); - var build = Int32.Parse(match.Groups[3].Value); - var version = new Version(major, minor, build); + var version = TheVersion.Parse(match.Groups[1].Value); RaiseOnEntry(version); } } diff --git a/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs new file mode 100644 index 000000000..5de075701 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs @@ -0,0 +1,46 @@ +using System; +using System.Text.RegularExpressions; + +namespace GitHub.Unity +{ + public class WindowsDiskUsageOutputProcessor : BaseOutputProcessor + { + private int index = -1; + private int lineCount = 0; + private string[] buffer = new string[2]; + // 199854 File(s) 25,835,841,045 bytes + private static readonly Regex totalFileCount = new Regex(@"[\s]*[\d]+[\s]+File\(s\)[\s]+(?[^\s]+)", + RegexOptions.Compiled); + public override void LineReceived(string line) + { + lineCount++; + index = (index + 1) % 2; + + if (line == null) + { + if (lineCount <= 2) + { + throw new InvalidOperationException("Not enough input"); + } + + var output = buffer[index]; + + Logger.Trace("Processing: {0}", output); + + var match = totalFileCount.Match(output); + long kilobytes = 0; + if (match.Success) + { + var bytes = long.Parse(match.Groups["bytes"].Value.Replace(",", String.Empty).Replace(".", String.Empty)); + kilobytes = bytes / 1024; + } + + RaiseOnEntry(kilobytes); + } + else + { + buffer[index] = line; + } + } + } +} diff --git a/src/GitHub.Api/Platform/DefaultEnvironment.cs b/src/GitHub.Api/Platform/DefaultEnvironment.cs index 1fd414d0b..eece16e48 100644 --- a/src/GitHub.Api/Platform/DefaultEnvironment.cs +++ b/src/GitHub.Api/Platform/DefaultEnvironment.cs @@ -1,5 +1,6 @@ using GitHub.Logging; using System; +using System.Globalization; using System.IO; using System.Linq; @@ -12,8 +13,6 @@ public class DefaultEnvironment : IEnvironment private static bool? onLinux; private static bool? onMac; - private NPath gitExecutablePath; - private NPath gitLfsExecutablePath; private NPath nodeJsExecutablePath; private NPath octorunScriptPath; @@ -35,12 +34,20 @@ public DefaultEnvironment() else { localAppData = GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData).ToNPath(); - commonAppData = "/usr/local/share/".ToNPath(); + commonAppData = GetSpecialFolder(Environment.SpecialFolder.ApplicationData).ToNPath(); } UserCachePath = localAppData.Combine(ApplicationInfo.ApplicationName); SystemCachePath = commonAppData.Combine(ApplicationInfo.ApplicationName); - LogPath = UserCachePath.Combine(logFile); + if (IsMac) + { + LogPath = NPath.HomeDirectory.Combine("Library/Logs").Combine(ApplicationInfo.ApplicationName).Combine(logFile); + } + else + { + LogPath = UserCachePath.Combine(logFile); + } + LogPath.EnsureParentDirectoryExists(); } public DefaultEnvironment(ICacheContainer cacheContainer) : this() @@ -67,42 +74,37 @@ public void Initialize(string unityVersion, NPath extensionInstallPath, NPath un UnityProjectPath = assetsPath.Parent; UnityVersion = unityVersion; User = new User(CacheContainer); + UserSettings = new UserSettings(this); + LocalSettings = new LocalSettings(this); + SystemSettings = new SystemSettings(this); } public void InitializeRepository(NPath? repositoryPath = null) { Guard.NotNull(this, FileSystem, nameof(FileSystem)); - //Logger.Trace("InitializeRepository expectedRepositoryPath:{0}", repositoryPath); - NPath expectedRepositoryPath; - if (!RepositoryPath.IsInitialized) + if (!RepositoryPath.IsInitialized || (repositoryPath != null && RepositoryPath != repositoryPath.Value)) { Guard.NotNull(this, UnityProjectPath, nameof(UnityProjectPath)); - //Logger.Trace("RepositoryPath is null"); - expectedRepositoryPath = repositoryPath != null ? repositoryPath.Value : UnityProjectPath; - if (!expectedRepositoryPath.DirectoryExists(".git")) + if (!expectedRepositoryPath.Exists(".git")) { - Logger.Trace(".git folder exists"); - - NPath reporoot = UnityProjectPath.RecursiveParents.FirstOrDefault(d => d.DirectoryExists(".git")); + NPath reporoot = UnityProjectPath.RecursiveParents.FirstOrDefault(d => d.Exists(".git")); if (reporoot.IsInitialized) expectedRepositoryPath = reporoot; } } else { - //Logger.Trace("Set to RepositoryPath"); expectedRepositoryPath = RepositoryPath; } FileSystem.SetCurrentDirectory(expectedRepositoryPath); - if (expectedRepositoryPath.DirectoryExists(".git")) + if (expectedRepositoryPath.Exists(".git")) { - //Logger.Trace("Determined expectedRepositoryPath:{0}", expectedRepositoryPath); RepositoryPath = expectedRepositoryPath; Repository = new Repository(RepositoryPath, CacheContainer); } @@ -115,12 +117,25 @@ public string GetSpecialFolder(Environment.SpecialFolder folder) public string ExpandEnvironmentVariables(string name) { - return Environment.ExpandEnvironmentVariables(name); + var key = GetEnvironmentVariableKey(name); + return Environment.ExpandEnvironmentVariables(key); + } + + public string GetEnvironmentVariable(string name) + { + var key = GetEnvironmentVariableKey(name); + return Environment.GetEnvironmentVariable(key); } - public string GetEnvironmentVariable(string variable) + public string GetEnvironmentVariableKey(string name) { - return Environment.GetEnvironmentVariable(variable); + return GetEnvironmentVariableKeyInternal(name); + } + + private static string GetEnvironmentVariableKeyInternal(string name) + { + return Environment.GetEnvironmentVariables().Keys.Cast() + .FirstOrDefault(k => string.Compare(name, k, true, CultureInfo.InvariantCulture) == 0) ?? name; } public NPath LogPath { get; } @@ -133,7 +148,7 @@ public string GetEnvironmentVariable(string variable) public NPath ExtensionInstallPath { get; set; } public NPath UserCachePath { get; set; } public NPath SystemCachePath { get; set; } - public string Path { get; set; } = Environment.GetEnvironmentVariable("PATH"); + public string Path { get; set; } = Environment.GetEnvironmentVariable(GetEnvironmentVariableKeyInternal("PATH")); public string NewLine => Environment.NewLine; public NPath OctorunScriptPath @@ -150,31 +165,27 @@ public NPath OctorunScriptPath } } - public bool IsCustomGitExecutable { get; set; } - - public NPath GitExecutablePath + public bool IsCustomGitExecutable => GitInstallationState?.IsCustomGitPath ?? false; + public NPath GitInstallPath => GitInstallationState?.GitInstallationPath ?? NPath.Default; + public NPath GitExecutablePath => GitInstallationState?.GitExecutablePath ?? NPath.Default; + public NPath GitLfsInstallPath => GitInstallationState?.GitLfsInstallationPath ?? NPath.Default; + public NPath GitLfsExecutablePath => GitInstallationState?.GitLfsExecutablePath ?? NPath.Default; + public GitInstaller.GitInstallationState GitInstallationState { - get { return gitExecutablePath; } - set + get { - gitExecutablePath = value; - if (!gitExecutablePath.IsInitialized) - GitInstallPath = NPath.Default; - else - GitInstallPath = GitExecutablePath.Resolve().Parent.Parent; + return SystemSettings.Get(Constants.GitInstallationState, new GitInstaller.GitInstallationState()); } - } - - public NPath GitLfsExecutablePath - { - get { return gitLfsExecutablePath; } set { - gitLfsExecutablePath = value; - GitLfsInstallPath = gitLfsExecutablePath.IsInitialized ? gitLfsExecutablePath.Parent : NPath.Default; + if (value == null) + SystemSettings.Unset(Constants.GitInstallationState); + else + SystemSettings.Set(Constants.GitInstallationState, value); } } + public NPath NodeJsExecutablePath { get @@ -188,12 +199,13 @@ public NPath NodeJsExecutablePath return nodeJsExecutablePath; } } - public NPath GitInstallPath { get; private set; } - public NPath GitLfsInstallPath { get; private set; } public NPath RepositoryPath { get; private set; } public ICacheContainer CacheContainer { get; private set; } public IRepository Repository { get; set; } public IUser User { get; set; } + public ISettings LocalSettings { get; protected set; } + public ISettings SystemSettings { get; protected set; } + public ISettings UserSettings { get; protected set; } public bool IsWindows { get { return OnWindows; } } public bool IsLinux { get { return OnLinux; } } @@ -234,7 +246,8 @@ public static bool OnMac set { onMac = value; } } + public static string ExecutableExt { get { return OnWindows ? ".exe" : string.Empty; } } public string ExecutableExtension { get { return IsWindows ? ".exe" : string.Empty; } } protected static ILogging Logger { get; } = LogHelper.GetLogger(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Platform/FindExecTask.cs b/src/GitHub.Api/Platform/FindExecTask.cs index dcf4e5feb..941948919 100644 --- a/src/GitHub.Api/Platform/FindExecTask.cs +++ b/src/GitHub.Api/Platform/FindExecTask.cs @@ -2,7 +2,7 @@ namespace GitHub.Unity { - class FindExecTask : ProcessTask + public class FindExecTask : ProcessTask { private readonly string arguments; diff --git a/src/GitHub.Api/Platform/IEnvironment.cs b/src/GitHub.Api/Platform/IEnvironment.cs index f25161da7..f21c22eab 100644 --- a/src/GitHub.Api/Platform/IEnvironment.cs +++ b/src/GitHub.Api/Platform/IEnvironment.cs @@ -12,8 +12,12 @@ public interface IEnvironment string Path { get; set; } string NewLine { get; } - bool IsCustomGitExecutable { get; set; } - NPath GitExecutablePath { get; set; } + GitInstaller.GitInstallationState GitInstallationState { get; set; } + bool IsCustomGitExecutable { get; } + NPath GitExecutablePath { get; } + NPath GitInstallPath { get; } + NPath GitLfsInstallPath { get; } + NPath GitLfsExecutablePath { get; } NPath NodeJsExecutablePath { get; } NPath OctorunScriptPath { get; set; } bool IsWindows { get; } @@ -26,7 +30,6 @@ public interface IEnvironment NPath UnityProjectPath { get; } NPath ExtensionInstallPath { get; } NPath RepositoryPath { get; } - NPath GitInstallPath { get; } NPath UserCachePath { get; set; } NPath SystemCachePath { get; set; } NPath LogPath { get; } @@ -35,7 +38,9 @@ public interface IEnvironment IRepository Repository { get; set; } string ExecutableExtension { get; } ICacheContainer CacheContainer { get; } - NPath GitLfsInstallPath { get; } - NPath GitLfsExecutablePath { get; set; } + ISettings LocalSettings { get; } + ISettings SystemSettings { get; } + ISettings UserSettings { get; } + string GetEnvironmentVariableKey(string name); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Platform/ISettings.cs b/src/GitHub.Api/Platform/ISettings.cs index f90f6c85c..9fb923454 100644 --- a/src/GitHub.Api/Platform/ISettings.cs +++ b/src/GitHub.Api/Platform/ISettings.cs @@ -9,5 +9,6 @@ public interface ISettings void Set(string key, T value); void Unset(string key); void Rename(string oldKey, string newKey); + NPath SettingsPath { get; set; } } } \ No newline at end of file diff --git a/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs new file mode 100644 index 000000000..962f62f1a --- /dev/null +++ b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs @@ -0,0 +1,21 @@ +using System.Threading; + +namespace GitHub.Unity +{ + class LinuxDiskUsageTask : ProcessTask + { + private readonly string arguments; + + public LinuxDiskUsageTask(NPath directory, CancellationToken token) + : base(token, new LinuxDiskUsageOutputProcessor()) + { + Name = "du" + DefaultEnvironment.ExecutableExt; + arguments = string.Format("-sH \"{0}\"", directory); + } + + public override string ProcessName { get { return Name; } } + public override string ProcessArguments { get { return arguments; } } + public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Getting directory size..."; + } +} diff --git a/src/GitHub.Api/Platform/Platform.cs b/src/GitHub.Api/Platform/Platform.cs index 71456a5a5..34b07817e 100644 --- a/src/GitHub.Api/Platform/Platform.cs +++ b/src/GitHub.Api/Platform/Platform.cs @@ -12,7 +12,7 @@ public interface IPlatform IKeychain Keychain { get; } } - class Platform : IPlatform + public class Platform : IPlatform { public Platform(IEnvironment environment) { diff --git a/src/GitHub.Api/Platform/Settings.cs b/src/GitHub.Api/Platform/Settings.cs index 8f622cf3a..d49fa1893 100644 --- a/src/GitHub.Api/Platform/Settings.cs +++ b/src/GitHub.Api/Platform/Settings.cs @@ -1,12 +1,13 @@ using GitHub.Logging; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; namespace GitHub.Unity { - abstract class BaseSettings : ISettings + public abstract class BaseSettings : ISettings { public abstract bool Exists(string key); public abstract string Get(string key, string fallback = ""); @@ -15,15 +16,15 @@ abstract class BaseSettings : ISettings public abstract void Rename(string oldKey, string newKey); public abstract void Set(string key, T value); public abstract void Unset(string key); + public NPath SettingsPath { get; set; } protected virtual string SettingsFileName { get; set; } - protected NPath SettingsPath { get; set; } } - class JsonBackedSettings : BaseSettings + public class JsonBackedSettings : BaseSettings { private string cachePath; - private CacheData cacheData = new CacheData(); + protected Dictionary cacheData; private Action dirCreate; private Func dirExists; private Action fileDelete; @@ -45,16 +46,16 @@ public JsonBackedSettings() public override void Initialize() { - logger.Debug($"Initializing settings {GetType()}"); cachePath = SettingsPath.Combine(SettingsFileName); - - logger.Debug("Initializing settings file at {0}", cachePath); LoadFromCache(cachePath); } public override bool Exists(string key) { - return cacheData.GitHubUnity.ContainsKey(key); + if (cacheData == null) + Initialize(); + + return cacheData.ContainsKey(key); } public override string Get(string key, string fallback = "") @@ -64,27 +65,60 @@ public override string Get(string key, string fallback = "") public override T Get(string key, T fallback = default(T)) { + if (cacheData == null) + Initialize(); + object value = null; - if (cacheData.GitHubUnity.TryGetValue(key, out value)) + if (cacheData.TryGetValue(key, out value)) { - logger.Debug("Get: {0}", key); + if (typeof(T) == typeof(DateTimeOffset)) + { + DateTimeOffset dt; + if (DateTimeOffset.TryParseExact(value?.ToString().ToEmptyIfNull(), Constants.Iso8601Formats, + CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) + { + value = dt; + cacheData[key] = dt; + } + } + + if (value == null && fallback != null) + { + value = fallback; + cacheData[key] = fallback; + } + else if (!(value is T)) + { + try + { + value = value.FromObject(); + cacheData[key] = value; + } + catch + { + value = fallback; + cacheData[key] = fallback; + } + } return (T)value; } - - logger.Debug("Miss: {0}", key); return fallback; } public override void Set(string key, T value) { + if (cacheData == null) + Initialize(); + try { - logger.Trace("Set: {0}", key); - - if (!cacheData.GitHubUnity.ContainsKey(key)) - cacheData.GitHubUnity.Add(key, value); + object val = value; + if (value is DateTimeOffset) + val = ((DateTimeOffset)(object)value).ToString(Constants.Iso8601Format); + if (!cacheData.ContainsKey(key)) + cacheData.Add(key, val); else - cacheData.GitHubUnity[key] = value; + cacheData[key] = val; SaveToCache(cachePath); } catch (Exception e) @@ -96,36 +130,57 @@ public override void Set(string key, T value) public override void Unset(string key) { - if (cacheData.GitHubUnity.ContainsKey(key)) - cacheData.GitHubUnity.Remove(key); + if (cacheData == null) + Initialize(); + + if (cacheData.ContainsKey(key)) + cacheData.Remove(key); SaveToCache(cachePath); } public override void Rename(string oldKey, string newKey) { + if (cacheData == null) + Initialize(); + object value = null; - if (cacheData.GitHubUnity.TryGetValue(oldKey, out value)) + if (cacheData.TryGetValue(oldKey, out value)) { - cacheData.GitHubUnity.Remove(oldKey); + cacheData.Remove(oldKey); Set(newKey, value); } SaveToCache(cachePath); } - private void LoadFromCache(string path) + protected virtual void LoadFromCache(string path) { - logger.Trace("LoadFromCache: {0}", path); - EnsureCachePath(path); if (!fileExists(path)) + { + cacheData = new Dictionary(); return; + } var data = readAllText(path, Encoding.UTF8); try { - cacheData = SimpleJson.DeserializeObject(data); + var c = data.FromJson>(); + if (c != null) + { + // upgrade from old format + if (c.ContainsKey("GitHubUnity")) + { + var oldRoot = c["GitHubUnity"]; + cacheData = oldRoot.FromObject>(); + SaveToCache(path); + } + else + cacheData = c; + } + else + cacheData = null; } catch(Exception ex) { @@ -137,19 +192,17 @@ private void LoadFromCache(string path) { // cache is corrupt, remove fileDelete(path); - return; + cacheData = new Dictionary(); } } - private bool SaveToCache(string path) + protected virtual bool SaveToCache(string path) { - logger.Trace("SaveToCache: {0}", path); - EnsureCachePath(path); try { - var data = SimpleJson.SerializeObject(cacheData); + var data = cacheData.ToJson(); writeAllText(path, data); } catch (Exception ex) @@ -170,15 +223,9 @@ private void EnsureCachePath(string path) if (!dirExists(di)) dirCreate(di); } - - private class CacheData - { - public Dictionary GitHubUnity { get; set; } = new Dictionary(); - } - } - class LocalSettings : JsonBackedSettings + public class LocalSettings : JsonBackedSettings { private const string RelativeSettingsPath = "ProjectSettings"; private const string settingsFileName = "GitHub.local.json"; @@ -191,27 +238,53 @@ public LocalSettings(IEnvironment environment) protected override string SettingsFileName { get { return settingsFileName; } } } - class UserSettings : JsonBackedSettings + public class UserSettings : JsonBackedSettings { - private const string settingsFileName = "settings.json"; + private const string settingsFileName = "usersettings.json"; + private const string oldSettingsFileName = "settings.json"; public UserSettings(IEnvironment environment) { SettingsPath = environment.UserCachePath; } + public override void Initialize() + { + var cachePath = SettingsPath.Combine(settingsFileName); + if (!cachePath.FileExists()) + { + var oldSettings = SettingsPath.Combine(oldSettingsFileName); + if (oldSettings.FileExists()) + oldSettings.Copy(cachePath); + } + base.Initialize(); + } + protected override string SettingsFileName { get { return settingsFileName; } } } - class SystemSettings : JsonBackedSettings + public class SystemSettings : JsonBackedSettings { - private const string settingsFileName = "settings.json"; + private const string settingsFileName = "systemsettings.json"; + private const string oldSettingsFileName = "settings.json"; public SystemSettings(IEnvironment environment) { SettingsPath = environment.SystemCachePath; } + public override void Initialize() + { + var cachePath = SettingsPath.Combine(settingsFileName); + if (!cachePath.FileExists()) + { + var oldSettings = SettingsPath.Combine(oldSettingsFileName); + if (oldSettings.FileExists()) + oldSettings.Copy(cachePath); + } + base.Initialize(); + } + protected override string SettingsFileName { get { return settingsFileName; } } } } diff --git a/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs new file mode 100644 index 000000000..d2fb89d12 --- /dev/null +++ b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; + +namespace GitHub.Unity +{ + class WindowsDiskUsageTask : ProcessTask + { + private readonly string arguments; + + public WindowsDiskUsageTask(NPath directory, CancellationToken token) + : base(token, new WindowsDiskUsageOutputProcessor()) + { + Name = "cmd"; + arguments = string.Format("/c dir /a/s \"{0}\"", directory); + } + + public override string ProcessName { get { return Name; } } + public override string ProcessArguments { get { return arguments; } } + public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Getting directory size..."; + } +} diff --git a/src/GitHub.Api/PlatformResources/linux/git-lfs.json b/src/GitHub.Api/PlatformResources/linux/git-lfs.json new file mode 100644 index 000000000..3eb484c66 --- /dev/null +++ b/src/GitHub.Api/PlatformResources/linux/git-lfs.json @@ -0,0 +1 @@ +{"md5":"81ea7bb262838e3779e4bdc426086e27","url":"http://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"executableMd5":"d0d59164a4b7b35685502d7c5f747f2f","version":"2.4.0"} diff --git a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip index c1efa41e2..9ab5a9b0a 100644 --- a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c86ab0b766e2d523ba2bf2ae427fe755e553989f7160217380b0f4dead7b802b -size 2631425 +oid sha256:15269ac992592484612ddcb760363294c1978fd79807f75b8536818db7c6f9e7 +size 2681983 diff --git a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 deleted file mode 100644 index 68e72bb08..000000000 --- a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 +++ /dev/null @@ -1 +0,0 @@ -3cde251dc13fe09ef62a2a2227fcc310 \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/mac/git-lfs.json b/src/GitHub.Api/PlatformResources/mac/git-lfs.json new file mode 100644 index 000000000..d8ddeef88 --- /dev/null +++ b/src/GitHub.Api/PlatformResources/mac/git-lfs.json @@ -0,0 +1 @@ +{"md5":"e2941215f99afa99f002e96c0ae70966","url":"http://ghfvs-installer.github.com/unity/git/mac/git-lfs.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"2.4.0"} \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip index 3932710f3..446aba3d8 100644 --- a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bde4722bdbb24ec6651aa2ab559bfa6e85d31ee9e4195c81f6d6fa7548cacc6 -size 2905910 +oid sha256:4e1ea7c1c6b78c05293039c634426760dfc36cedd6a8a8e92ab26809abcf829a +size 2936128 diff --git a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 deleted file mode 100644 index e1f3c0a06..000000000 --- a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 +++ /dev/null @@ -1 +0,0 @@ -fb5862c66d8d53ba4eb9599419dffa1f \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/windows/git-lfs.json b/src/GitHub.Api/PlatformResources/windows/git-lfs.json new file mode 100644 index 000000000..ac7b6d3d1 --- /dev/null +++ b/src/GitHub.Api/PlatformResources/windows/git-lfs.json @@ -0,0 +1 @@ +{"md5":"78ff68661485e1c09f7b8d82c4317ab5","url":"http://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"2.4.0"} diff --git a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip index 5a56712a7..ed0b4e3f6 100644 --- a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a4699fe6028a3727d76b218a10a7e9c6276f097b8ebd782f2e7b3418dacda07 -size 2652291 +oid sha256:451d565d2f2b54910dc50f8522b848457001ad76fdcec0f5207e53cff80b8a7a +size 2677508 diff --git a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 deleted file mode 100644 index f99b259ad..000000000 --- a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 +++ /dev/null @@ -1 +0,0 @@ -105df1302560c5f6aa64d1930284c126 \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/windows/git.json b/src/GitHub.Api/PlatformResources/windows/git.json new file mode 100644 index 000000000..a8f6a18c9 --- /dev/null +++ b/src/GitHub.Api/PlatformResources/windows/git.json @@ -0,0 +1 @@ +{"md5":"58ee14cb4ce8767167db64b271a0a599","url":"http://ghfvs-installer.github.com/unity/git/windows/git.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"2.17.0.1-windows.1"} \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/windows/git.zip b/src/GitHub.Api/PlatformResources/windows/git.zip index c575bd970..7e5aff5b5 100644 --- a/src/GitHub.Api/PlatformResources/windows/git.zip +++ b/src/GitHub.Api/PlatformResources/windows/git.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24864bd6ed4d60a516330107932082ae17ae5b98b0819d6cb6eba4a96b7ae0e4 -size 83230267 +oid sha256:b5fbf6c5a45df0297328402bdedaed95b33b6288e7b25257213f992a07e3ec67 +size 97180593 diff --git a/src/GitHub.Api/PlatformResources/windows/git.zip.md5 b/src/GitHub.Api/PlatformResources/windows/git.zip.md5 deleted file mode 100644 index 867a674b0..000000000 --- a/src/GitHub.Api/PlatformResources/windows/git.zip.md5 +++ /dev/null @@ -1 +0,0 @@ -ea5d5a38a6b9e9bc2b10011602c65a0d \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/windows/gitconfig b/src/GitHub.Api/PlatformResources/windows/gitconfig index 07adb5f18..2d15e427f 100644 --- a/src/GitHub.Api/PlatformResources/windows/gitconfig +++ b/src/GitHub.Api/PlatformResources/windows/gitconfig @@ -16,5 +16,10 @@ textconv = astextplain [rebase] autosquash = true +[filter "lfs"] + clean = git-lfs clean -- %f + smudge = git-lfs smudge -- %f + process = git-lfs filter-process + required = true [credential] helper = wincred diff --git a/src/GitHub.Api/Primitives/HostAddress.cs b/src/GitHub.Api/Primitives/HostAddress.cs index fd738b649..2382f98c1 100644 --- a/src/GitHub.Api/Primitives/HostAddress.cs +++ b/src/GitHub.Api/Primitives/HostAddress.cs @@ -75,14 +75,23 @@ public static bool IsGitHubDotCom(Uri hostUri) || hostUri.IsSameHost(gistUri); } - public static bool IsGitHubDotCom(string url) + public static bool IsGitHubDotCom(UriString hostUri) { - if (String.IsNullOrEmpty(url)) - return false; - Uri uri = null; - if (!Uri.TryCreate(url, UriKind.Absolute, out uri)) + return hostUri.Host == GitHubDotComHostAddress.WebUri.Host + || hostUri.Host == GitHubDotComHostAddress.ApiUri.Host + || hostUri.Host == gistUri.Host; + } + + public static bool IsGitHubDotCom(Connection connection) + { + if (connection == null || String.IsNullOrEmpty(connection.Host)) return false; - return IsGitHubDotCom(uri); + + var connectionHost = connection.Host.ToUriString(); + + return connectionHost.Host == GitHubDotComHostAddress.WebUri.Host + || connectionHost.Host == GitHubDotComHostAddress.ApiUri.Host + || connectionHost.Host == gistUri.Host; } public bool IsGitHubDotCom() diff --git a/src/GitHub.Api/Primitives/Package.cs b/src/GitHub.Api/Primitives/Package.cs new file mode 100644 index 000000000..3cd3eb73c --- /dev/null +++ b/src/GitHub.Api/Primitives/Package.cs @@ -0,0 +1,70 @@ +using GitHub.Logging; +using System; + +namespace GitHub.Unity +{ + public class Package + { + private string version; + public string Md5 { get; set; } + public string Url { get; set; } + [NotSerialized] private UriString uri; + [NotSerialized] public UriString Uri + { + get + { + if (uri == null) + uri = Url.ToString(); + return uri; + } + } + public string ReleaseNotes { get; set; } + public string ReleaseNotesUrl { get; set; } + public string Message { get; set; } + [NotSerialized] public TheVersion Version { get { return TheVersion.Parse(version); } set { version = value.ToString(); } } + + public static Package Load(IEnvironment environment, UriString packageFeed) + { + Package package = null; + var filename = packageFeed.Filename.ToNPath(); + if (!filename.IsInitialized || filename.IsRoot) + return package; + var key = filename.FileNameWithoutExtension + "_updatelastCheckTime"; + var now = DateTimeOffset.Now; + NPath feed = environment.UserCachePath.Combine(packageFeed.Filename); + + if (!feed.FileExists() || now.Date > environment.UserSettings.Get(key).Date) + { + feed = new DownloadTask(TaskManager.Instance.Token, environment.FileSystem, packageFeed, environment.UserCachePath) + .Catch(ex => + { + LogHelper.Warning(@"Error downloading package feed:{0} ""{1}"" Message:""{2}""", packageFeed, ex.GetType().ToString(), ex.GetExceptionMessageShort()); + return true; + }) + .RunSynchronously(); + + if (feed.IsInitialized) + environment.UserSettings.Set(key, now); + } + + if (!feed.IsInitialized) + { + // try from assembly resources + feed = AssemblyResources.ToFile(ResourceType.Platform, packageFeed.Filename, environment.UserCachePath, environment); + } + + if (feed.IsInitialized) + { + try + { + package = feed.ReadAllText().FromJson(true, false); + } + catch (Exception ex) + { + LogHelper.Error(ex); + } + } + return package; + } + } +} diff --git a/src/GitHub.Api/Primitives/StringEquivalent.cs b/src/GitHub.Api/Primitives/StringEquivalent.cs index 5dd82b3c7..5e360e2c5 100644 --- a/src/GitHub.Api/Primitives/StringEquivalent.cs +++ b/src/GitHub.Api/Primitives/StringEquivalent.cs @@ -1,14 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; namespace GitHub.Unity { [Serializable] - public abstract class StringEquivalent : ISerializable, IXmlSerializable where T : StringEquivalent + public abstract class StringEquivalent : ISerializable where T : StringEquivalent { protected string Value; @@ -86,21 +83,6 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte info.AddValue("Value", Value); } - public XmlSchema GetSchema() - { - return null; - } - - public void ReadXml(XmlReader reader) - { - Value = reader.ReadString(); - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteString(Value); - } - public int Length { get { return Value != null ? Value.Length : 0; } diff --git a/src/GitHub.Api/Primitives/TheVersion.cs b/src/GitHub.Api/Primitives/TheVersion.cs new file mode 100644 index 000000000..2afc062f0 --- /dev/null +++ b/src/GitHub.Api/Primitives/TheVersion.cs @@ -0,0 +1,284 @@ +using GitHub.Logging; +using System; +using System.Text.RegularExpressions; + +namespace GitHub.Unity +{ + [Serializable] + public struct TheVersion : IComparable + { + private const string versionRegex = @"^(?\d+)(\.?(?[^.]+))?(\.?(?[^.]+))?(\.?(?.+))?"; + private const int PART_COUNT = 4; + public static TheVersion Default { get; } = default(TheVersion).Initialize(null); + + [NotSerialized] private int major; + [NotSerialized] public int Major { get { Initialize(Version); return major; } } + [NotSerialized] private int minor; + [NotSerialized] public int Minor { get { Initialize(Version); return minor; } } + [NotSerialized] private int patch; + [NotSerialized] public int Patch { get { Initialize(Version); return patch; } } + [NotSerialized] private int build; + [NotSerialized] public int Build { get { Initialize(Version); return build; } } + [NotSerialized] private string special; + [NotSerialized] public string Special { get { Initialize(Version); return special; } } + [NotSerialized] private bool isAlpha; + [NotSerialized] public bool IsAlpha { get { Initialize(Version); return isAlpha; } } + [NotSerialized] private bool isBeta; + [NotSerialized] public bool IsBeta { get { Initialize(Version); return isBeta; } } + [NotSerialized] private bool isUnstable; + [NotSerialized] public bool IsUnstable { get { Initialize(Version); return isUnstable; } } + + [NotSerialized] private int[] intParts; + [NotSerialized] private string[] stringParts; + [NotSerialized] private int parts; + [NotSerialized] private bool initialized; + [NotSerialized] private string version; + public string Version { get { if (version == null) version = String.Empty; return version; } set { version = value; } } + + private static readonly Regex regex = new Regex(versionRegex); + + public static TheVersion Parse(string version) + { + return default(TheVersion).Initialize(version); + } + + private TheVersion Initialize(string theVersion) + { + if (initialized) + return this; + + this.Version = theVersion?.Trim() ?? String.Empty; + + isAlpha = false; + isBeta = false; + major = 0; + minor = 0; + patch = 0; + build = 0; + special = null; + parts = 0; + + intParts = new int[PART_COUNT]; + stringParts = new string[PART_COUNT]; + for (var i = 0; i < PART_COUNT; i++) + stringParts[i] = intParts[i].ToString(); + + if (String.IsNullOrEmpty(theVersion)) + return this; + + var match = regex.Match(theVersion); + if (!match.Success) + { + LogHelper.Error(new ArgumentException("Invalid version: " + theVersion, "theVersion")); + return this; + } + + major = int.Parse(match.Groups["major"].Value); + intParts[parts] = major; + stringParts[parts] = major.ToString(); + parts = 1; + + var minorMatch = match.Groups["minor"]; + var patchMatch = match.Groups["patch"]; + var buildMatch = match.Groups["build"]; + + if (minorMatch.Success) + { + if (!int.TryParse(minorMatch.Value, out minor)) + { + special = minorMatch.Value.TrimEnd(); + stringParts[parts] = special ?? "0"; + } + else + { + intParts[parts] = minor; + stringParts[parts] = minor.ToString(); + parts++; + + if (patchMatch.Success) + { + if (!int.TryParse(patchMatch.Value, out patch)) + { + special = patchMatch.Value.TrimEnd(); + stringParts[parts] = special ?? "0"; + } + else + { + intParts[parts] = patch; + stringParts[parts] = patch.ToString(); + parts++; + + if (buildMatch.Success) + { + if (!int.TryParse(buildMatch.Value, out build)) + { + special = buildMatch.Value.TrimEnd(); + stringParts[parts] = special ?? "0"; + } + else + { + intParts[parts] = build; + stringParts[parts] = build.ToString(); + parts++; + } + } + } + } + } + } + + isUnstable = special != null; + if (isUnstable) + { + isAlpha = special.IndexOf("alpha") >= 0; + isBeta = special.IndexOf("beta") >= 0; + } + initialized = true; + return this; + } + + public override string ToString() + { + return Version; + } + + public int CompareTo(TheVersion other) + { + if (this > other) + return 1; + if (this == other) + return 0; + return -1; + } + + public override int GetHashCode() + { + int hash = 17; + hash = hash * 23 + Major.GetHashCode(); + hash = hash * 23 + Minor.GetHashCode(); + hash = hash * 23 + Patch.GetHashCode(); + hash = hash * 23 + Build.GetHashCode(); + hash = hash * 23 + (Special != null ? Special.GetHashCode() : 0); + return hash; + } + + public override bool Equals(object obj) + { + if (obj is TheVersion) + return Equals((TheVersion)obj); + return false; + } + + public bool Equals(TheVersion other) + { + return this == other; + } + + public static bool operator==(TheVersion lhs, TheVersion rhs) + { + if (lhs.Version == rhs.Version) + return true; + return + (lhs.Major == rhs.Major) && + (lhs.Minor == rhs.Minor) && + (lhs.Patch == rhs.Patch) && + (lhs.Build == rhs.Build) && + (lhs.Special == rhs.Special); + } + + public static bool operator!=(TheVersion lhs, TheVersion rhs) + { + return !(lhs == rhs); + } + + public static bool operator>(TheVersion lhs, TheVersion rhs) + { + if (lhs.Version == rhs.Version) + return false; + if (!lhs.initialized) + return false; + if (!rhs.initialized) + return true; + + for (var i = 0; i < lhs.parts && i < rhs.parts; i++) + { + if (lhs.intParts[i] != rhs.intParts[i]) + return lhs.intParts[i] > rhs.intParts[i]; + } + + for (var i = 1; i < PART_COUNT; i++) + { + var ret = CompareVersionStrings(lhs.stringParts[i], rhs.stringParts[i]); + if (ret != 0) + return ret > 0; + } + + return false; + } + + public static bool operator<(TheVersion lhs, TheVersion rhs) + { + return !(lhs > rhs); + } + + public static bool operator>=(TheVersion lhs, TheVersion rhs) + { + return lhs > rhs || lhs == rhs; + } + + public static bool operator<=(TheVersion lhs, TheVersion rhs) + { + return lhs < rhs || lhs == rhs; + } + + private static int CompareVersionStrings(string lhs, string rhs) + { + int lhsNonDigitPos; + var lhsNumber = GetNumberFromVersionString(lhs, out lhsNonDigitPos); + + int rhsNonDigitPos; + var rhsNumber = GetNumberFromVersionString(rhs, out rhsNonDigitPos); + + if (lhsNumber != rhsNumber) + return lhsNumber.CompareTo(rhsNumber); + + if (lhsNonDigitPos < 0 && rhsNonDigitPos < 0) + return 0; + + // versions with alphanumeric characters are always lower than ones without + // i.e. 1.1alpha is lower than 1.1 + if (lhsNonDigitPos < 0) + return 1; + if (rhsNonDigitPos < 0) + return -1; + return lhs.Substring(lhsNonDigitPos).CompareTo(rhs.Substring(rhsNonDigitPos)); + } + + private static int GetNumberFromVersionString(string lhs, out int nonDigitPos) + { + nonDigitPos = IndexOfFirstNonDigit(lhs); + var number = -1; + if (nonDigitPos > -1) + { + int.TryParse(lhs.Substring(0, nonDigitPos), out number); + } + else + { + int.TryParse(lhs, out number); + } + return number; + } + + private static int IndexOfFirstNonDigit(string str) + { + for (var i = 0; i < str.Length; i++) + { + if (!char.IsDigit(str[i])) + { + return i; + } + } + return -1; + } + } +} \ No newline at end of file diff --git a/src/GitHub.Api/Primitives/UriString.cs b/src/GitHub.Api/Primitives/UriString.cs index 7e9b58cce..17c725ca3 100644 --- a/src/GitHub.Api/Primitives/UriString.cs +++ b/src/GitHub.Api/Primitives/UriString.cs @@ -28,7 +28,8 @@ public class UriString : StringEquivalent, IEquatable public UriString(string uriString) : base(NormalizePath(uriString)) { if (uriString == null || uriString.Length == 0) return; - if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) + if (Uri.TryCreate(uriString, UriKind.Absolute, out url) + || Uri.TryCreate("https://" + uriString, UriKind.Absolute, out url)) { if (!url.IsFile) SetUri(url); @@ -267,7 +268,7 @@ static string GetSerializedValue(SerializationInfo info) static string NormalizePath(string path) { - return path?.Replace('\\', '/'); + return path?.Replace('\\', '/').TrimEnd('/'); } static string GetRepositoryName(string repositoryNameSegment) @@ -277,7 +278,6 @@ static string GetRepositoryName(string repositoryNameSegment) { return null; } - return repositoryNameSegment.TrimEnd('/').TrimEnd(".git"); } @@ -286,4 +286,12 @@ bool IEquatable.Equals(UriString other) return other != null && ToString().Equals(other.ToString()); } } + + public static class UriStringExtensions + { + public static UriString ToUriString(this string str) + { + return new UriString(str); + } + } } diff --git a/src/GitHub.Api/Platform/IProcessEnvironment.cs b/src/GitHub.Api/Process/IProcessEnvironment.cs similarity index 100% rename from src/GitHub.Api/Platform/IProcessEnvironment.cs rename to src/GitHub.Api/Process/IProcessEnvironment.cs diff --git a/src/GitHub.Api/OutputProcessors/IProcessManager.cs b/src/GitHub.Api/Process/IProcessManager.cs similarity index 95% rename from src/GitHub.Api/OutputProcessors/IProcessManager.cs rename to src/GitHub.Api/Process/IProcessManager.cs index a41b7ccc5..c46ff495d 100644 --- a/src/GitHub.Api/OutputProcessors/IProcessManager.cs +++ b/src/GitHub.Api/Process/IProcessManager.cs @@ -10,5 +10,6 @@ T Configure(T processTask, NPath? executable = null, string arguments = null, IProcess Reconnect(IProcess processTask, int i); CancellationToken CancellationToken { get; } void RunCommandLineWindow(NPath workingDirectory); + void Stop(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Platform/ProcessEnvironment.cs b/src/GitHub.Api/Process/ProcessEnvironment.cs similarity index 68% rename from src/GitHub.Api/Platform/ProcessEnvironment.cs rename to src/GitHub.Api/Process/ProcessEnvironment.cs index b78c5fdbb..bf97546cb 100644 --- a/src/GitHub.Api/Platform/ProcessEnvironment.cs +++ b/src/GitHub.Api/Process/ProcessEnvironment.cs @@ -1,11 +1,10 @@ using GitHub.Logging; -using System; using System.Collections.Generic; using System.Diagnostics; namespace GitHub.Unity { - class ProcessEnvironment : IProcessEnvironment + public class ProcessEnvironment : IProcessEnvironment { protected IEnvironment Environment { get; private set; } protected ILogging Logger { get; private set; } @@ -24,11 +23,12 @@ public void Configure(ProcessStartInfo psi, NPath workingDirectory, bool dontSet var path = Environment.Path; psi.EnvironmentVariables["GHU_WORKINGDIR"] = workingDirectory; + var pathEnvVarKey = Environment.GetEnvironmentVariableKey("PATH"); if (dontSetupGit) { psi.EnvironmentVariables["GHU_FULLPATH"] = path; - psi.EnvironmentVariables["PATH"] = path; + psi.EnvironmentVariables[pathEnvVarKey] = path; return; } @@ -37,9 +37,11 @@ public void Configure(ProcessStartInfo psi, NPath workingDirectory, bool dontSet var pathEntries = new List(); string separator = Environment.IsWindows ? ";" : ":"; + NPath libexecPath = NPath.Default; + List gitPathEntries = new List(); if (Environment.GitInstallPath.IsInitialized) { - var gitPathRoot = Environment.GitInstallPath; + var gitPathRoot = Environment.GitExecutablePath.Resolve().Parent.Parent; var gitExecutableDir = Environment.GitExecutablePath.Parent; // original path to git (might be different from install path if it's a symlink) var baseExecPath = gitPathRoot; @@ -53,40 +55,42 @@ public void Configure(ProcessStartInfo psi, NPath workingDirectory, bool dontSet binPath = baseExecPath.Combine("bin"); } - var execPath = baseExecPath.Combine("libexec", "git-core"); - if (!execPath.DirectoryExists()) - execPath = NPath.Default; + libexecPath = baseExecPath.Combine("libexec", "git-core"); + if (!libexecPath.DirectoryExists()) + libexecPath = NPath.Default; if (Environment.IsWindows) { - pathEntries.AddRange(new[] { gitPathRoot.Combine("cmd").ToString(), gitPathRoot.Combine("usr", "bin") }); + gitPathEntries.AddRange(new[] { gitPathRoot.Combine("cmd").ToString(), gitPathRoot.Combine("usr", "bin") }); } else { - pathEntries.Add(gitExecutableDir.ToString()); + gitPathEntries.Add(gitExecutableDir.ToString()); } - if (execPath.IsInitialized) - pathEntries.Add(execPath); - pathEntries.Add(binPath); + if (libexecPath.IsInitialized) + gitPathEntries.Add(libexecPath); + gitPathEntries.Add(binPath); // we can only set this env var if there is a libexec/git-core. git will bypass internally bundled tools if this env var // is set, which will break Apple's system git on certain tools (like osx-credentialmanager) - if (execPath.IsInitialized) - psi.EnvironmentVariables["GIT_EXEC_PATH"] = execPath.ToString(); + if (libexecPath.IsInitialized) + psi.EnvironmentVariables["GIT_EXEC_PATH"] = libexecPath.ToString(); } - if (Environment.GitLfsInstallPath.IsInitialized && Environment.GitInstallPath != Environment.GitLfsInstallPath) + if (Environment.GitLfsInstallPath.IsInitialized && libexecPath != Environment.GitLfsInstallPath) { pathEntries.Add(Environment.GitLfsInstallPath); } + if (gitPathEntries.Count > 0) + pathEntries.AddRange(gitPathEntries); pathEntries.Add("END"); - path = String.Join(separator, pathEntries.ToArray()) + separator + path; + path = string.Join(separator, pathEntries.ToArray()) + separator + path; psi.EnvironmentVariables["GHU_FULLPATH"] = path; - psi.EnvironmentVariables["PATH"] = path; + psi.EnvironmentVariables[pathEnvVarKey] = path; //TODO: Remove with Git LFS Locking becomes standard psi.EnvironmentVariables["GITLFSLOCKSENABLED"] = "1"; @@ -98,12 +102,13 @@ public void Configure(ProcessStartInfo psi, NPath workingDirectory, bool dontSet } var httpProxy = Environment.GetEnvironmentVariable("HTTP_PROXY"); - if (!String.IsNullOrEmpty(httpProxy)) + if (!string.IsNullOrEmpty(httpProxy)) psi.EnvironmentVariables["HTTP_PROXY"] = httpProxy; var httpsProxy = Environment.GetEnvironmentVariable("HTTPS_PROXY"); - if (!String.IsNullOrEmpty(httpsProxy)) + if (!string.IsNullOrEmpty(httpsProxy)) psi.EnvironmentVariables["HTTPS_PROXY"] = httpsProxy; + psi.EnvironmentVariables["DISPLAY"] = "0"; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/ProcessManager.cs b/src/GitHub.Api/Process/ProcessManager.cs similarity index 75% rename from src/GitHub.Api/OutputProcessors/ProcessManager.cs rename to src/GitHub.Api/Process/ProcessManager.cs index 1be20e2c5..3cf767a91 100644 --- a/src/GitHub.Api/OutputProcessors/ProcessManager.cs +++ b/src/GitHub.Api/Process/ProcessManager.cs @@ -1,21 +1,20 @@ using GitHub.Logging; -using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Text; using System.Threading; namespace GitHub.Unity { - class ProcessManager : IProcessManager + public class ProcessManager : IProcessManager { private static readonly ILogging logger = LogHelper.GetLogger(); private readonly IEnvironment environment; private readonly IProcessEnvironment gitEnvironment; private readonly CancellationToken cancellationToken; + private readonly HashSet processes = new HashSet(); public ProcessManager(IEnvironment environment, IProcessEnvironment gitEnvironment, CancellationToken cancellationToken) { @@ -48,14 +47,20 @@ public T Configure(T processTask, NPath? executable = null, string arguments gitEnvironment.Configure(startInfo, workingDirectory ?? environment.RepositoryPath, dontSetupGit); - if (executable.Value.IsRelative) + string filename = executable.Value; + if (executable.Value.IsRelative && filename.StartsWith("git")) { - executable = executable.Value.FileName.ToNPath(); - executable = FindExecutableInPath(executable.Value, startInfo.EnvironmentVariables["PATH"]) ?? executable; + var file = FindExecutableInPath(executable.Value.FileName, false, startInfo.EnvironmentVariables["PATH"].ToNPathList(environment).ToArray()); + filename = file.IsInitialized ? file : executable.Value.FileName; } - startInfo.FileName = executable; + startInfo.FileName = filename; startInfo.Arguments = arguments ?? processTask.ProcessArguments; processTask.Configure(startInfo); + processTask.OnStartProcess += p => processes.Add(p); + processTask.OnEndProcess += p => { + if (processes.Contains(p)) + processes.Remove(p); + }; return processTask; } @@ -113,33 +118,20 @@ public IProcess Reconnect(IProcess processTask, int pid) return processTask; } - private NPath? FindExecutableInPath(NPath executable, string searchPaths = null) + public void Stop() + { + foreach (var p in processes.ToArray()) + p.Stop(); + } + + public static NPath FindExecutableInPath(string executable, bool recurse = false, params NPath[] searchPaths) { Guard.ArgumentNotNullOrWhiteSpace(executable, "executable"); - if (executable.IsRelative) return executable; - - searchPaths = searchPaths ?? environment.GetEnvironmentVariable("PATH"); - var executablePath = searchPaths.Split(Path.PathSeparator) - .Where(x => !String.IsNullOrEmpty(x)) - .Select(directory => - { - try - { - var unquoted = directory.RemoveSurroundingQuotes(); - var expanded = environment.ExpandEnvironmentVariables(unquoted); - return expanded.ToNPath().Combine(executable); - } - catch (Exception e) - { - logger.Error("Error while looking for {0} in {1}\n{2}", executable, directory, e); - return new NPath?(); - } - }) - .Where(x => x != null) - .FirstOrDefault(x => x.Value.FileExists()); - - return executablePath; + return searchPaths + .Where(x => x.IsInitialized && !x.IsRelative && x.DirectoryExists()) + .SelectMany(x => x.Files(executable, recurse)) + .FirstOrDefault(); } public CancellationToken CancellationToken { get { return cancellationToken; } } diff --git a/src/GitHub.Api/Properties/AssemblyInfo.cs b/src/GitHub.Api/Properties/AssemblyInfo.cs index 1c8fb5107..ecf892a01 100644 --- a/src/GitHub.Api/Properties/AssemblyInfo.cs +++ b/src/GitHub.Api/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("GitHub.Api")] -[assembly: AssemblyDescription("GitHub Api")] +[assembly: AssemblyDescription("GitHub for Unity API")] [assembly: Guid("4B424108-D0E8-4BF9-9B0C-4FB49E532AB9")] [assembly: InternalsVisibleTo("GitHub.Unity")] +[assembly: InternalsVisibleTo("GitHub.Unity.45")] diff --git a/src/GitHub.Api/Resources/.gitattributes b/src/GitHub.Api/Resources/.gitattributes index 8f8c2db53..c8cb4a6c4 100644 --- a/src/GitHub.Api/Resources/.gitattributes +++ b/src/GitHub.Api/Resources/.gitattributes @@ -1,10 +1,28 @@ * text=auto # Unity files -*.meta -text -merge=unityamlmerge -*.unity -text -merge=unityamlmerge -*.asset -text -merge=unityamlmerge -*.prefab -text -merge=unityamlmerge +*.meta -text merge=unityyamlmerge diff +*.unity -text merge=unityyamlmerge diff +*.asset -text merge=unityyamlmerge diff +*.prefab -text merge=unityyamlmerge diff +*.mat -text merge=unityyamlmerge diff +*.anim -text merge=unityyamlmerge diff +*.controller -text merge=unityyamlmerge diff +*.overrideController -text merge=unityyamlmerge diff +*.physicMaterial -text merge=unityyamlmerge diff +*.physicsMaterial2D -text merge=unityyamlmerge diff +*.playable -text merge=unityyamlmerge diff +*.mask -text merge=unityyamlmerge diff +*.brush -text merge=unityyamlmerge diff +*.flare -text merge=unityyamlmerge diff +*.fontsettings -text merge=unityyamlmerge diff +*.guiskin -text merge=unityyamlmerge diff +*.giparams -text merge=unityyamlmerge diff +*.renderTexture -text merge=unityyamlmerge diff +*.spriteatlas -text merge=unityyamlmerge diff +*.terrainlayer -text merge=unityyamlmerge diff +*.mixer -text merge=unityyamlmerge diff +*.shadervariants -text merge=unityyamlmerge diff # Image formats *.psd filter=lfs diff=lfs merge=lfs -text @@ -14,9 +32,11 @@ *.bmp filter=lfs diff=lfs merge=lfs -text *.tga filter=lfs diff=lfs merge=lfs -text *.tiff filter=lfs diff=lfs merge=lfs -text +*.tif filter=lfs diff=lfs merge=lfs -text *.iff filter=lfs diff=lfs merge=lfs -text *.pict filter=lfs diff=lfs merge=lfs -text *.dds filter=lfs diff=lfs merge=lfs -text +*.xcf filter=lfs diff=lfs merge=lfs -text # Audio formats *.mp3 filter=lfs diff=lfs merge=lfs -text @@ -63,4 +83,4 @@ *.7z filter=lfs diff=lfs merge=lfs -text *.gz filter=lfs diff=lfs merge=lfs -text *.rar filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text \ No newline at end of file +*.tar filter=lfs diff=lfs merge=lfs -text diff --git a/src/GitHub.Api/Resources/.gitignore b/src/GitHub.Api/Resources/.gitignore index 25f674308..2538b1d2e 100644 --- a/src/GitHub.Api/Resources/.gitignore +++ b/src/GitHub.Api/Resources/.gitignore @@ -1,13 +1,18 @@ -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ -/[Bb]uilds/ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ -/Assets/AssetStoreTools* +# Uncomment this line if you wish to ignore the asset store tools plugin +# [Aa]ssets/AssetStoreTools* -# Visual Studio 2015 cache directory -/.vs/ +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ @@ -23,14 +28,21 @@ ExportedObj/ *.booproj *.svd *.pdb - +*.mdb +*.opendb +*.VC.db # Unity3D generated meta files *.pidb.meta +*.pdb.meta +*.mdb.meta -# Unity3D Generated File On Crash Reports +# Unity3D generated file on crash reports sysinfo.txt # Builds *.apk -*.unitypackage \ No newline at end of file +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index b4b2b9310..7b1374c9d 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac6b09ebe88bb66f5aefdd262bd9cbf9819fd6a0b7f996acfa746dc33f4dbe74 -size 219653 +oid sha256:95fe1967a6d00af4abb3d34b897769451f78bdf87ad132ba2b526264ac36e14a +size 214195 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 0c673acef..70d55debe 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -e562a8ccf9ef1e1d00a2e9a72f8234cf \ No newline at end of file +3ad23df7f5076a6fbd7d3ce03ad919cc diff --git a/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs b/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs new file mode 100644 index 000000000..b0fc04a43 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs @@ -0,0 +1,237 @@ +// Adler32.cs - Computes Adler32 data checksum of a data stream +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Computes Adler32 checksum for a stream of data. An Adler32 + /// checksum is not as reliable as a CRC32 checksum, but a lot faster to + /// compute. + /// + /// The specification for Adler32 may be found in RFC 1950. + /// ZLIB Compressed Data Format Specification version 3.3) + /// + /// + /// From that document: + /// + /// "ADLER32 (Adler-32 checksum) + /// This contains a checksum value of the uncompressed data + /// (excluding any dictionary data) computed according to Adler-32 + /// algorithm. This algorithm is a 32-bit extension and improvement + /// of the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073 + /// standard. + /// + /// Adler-32 is composed of two sums accumulated per byte: s1 is + /// the sum of all bytes, s2 is the sum of all s1 values. Both sums + /// are done modulo 65521. s1 is initialized to 1, s2 to zero. The + /// Adler-32 checksum is stored as s2*65536 + s1 in most- + /// significant-byte first (network) order." + /// + /// "8.2. The Adler-32 algorithm + /// + /// The Adler-32 algorithm is much faster than the CRC32 algorithm yet + /// still provides an extremely low probability of undetected errors. + /// + /// The modulo on unsigned long accumulators can be delayed for 5552 + /// bytes, so the modulo operation time is negligible. If the bytes + /// are a, b, c, the second sum is 3a + 2b + c + 3, and so is position + /// and order sensitive, unlike the first sum, which is just a + /// checksum. That 65521 is prime is important to avoid a possible + /// large class of two-byte errors that leave the check unchanged. + /// (The Fletcher checksum uses 255, which is not prime and which also + /// makes the Fletcher check insensitive to single byte changes 0 - + /// 255.) + /// + /// The sum s1 is initialized to 1 instead of zero to make the length + /// of the sequence part of s2, so that the length does not have to be + /// checked separately. (Any sequence of zeroes has a Fletcher + /// checksum of zero.)" + /// + /// + /// + public sealed class Adler32 : IChecksum + { + /// + /// largest prime smaller than 65536 + /// + const uint BASE = 65521; + + /// + /// Returns the Adler32 data checksum computed so far. + /// + public long Value { + get { + return checksum; + } + } + + /// + /// Creates a new instance of the Adler32 class. + /// The checksum starts off with a value of 1. + /// + public Adler32() + { + Reset(); + } + + /// + /// Resets the Adler32 checksum to the initial value. + /// + public void Reset() + { + checksum = 1; + } + + /// + /// Updates the checksum with a byte value. + /// + /// + /// The data value to add. The high byte of the int is ignored. + /// + public void Update(int value) + { + // We could make a length 1 byte array and call update again, but I + // would rather not have that overhead + uint s1 = checksum & 0xFFFF; + uint s2 = checksum >> 16; + + s1 = (s1 + ((uint)value & 0xFF)) % BASE; + s2 = (s1 + s2) % BASE; + + checksum = (s2 << 16) + s1; + } + + /// + /// Updates the checksum with an array of bytes. + /// + /// + /// The source of the data to update with. + /// + public void Update(byte[] buffer) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Updates the checksum with the bytes taken from the array. + /// + /// + /// an array of bytes + /// + /// + /// the start of the data used for this update + /// + /// + /// the number of bytes to use for this update + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if (offset < 0) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "cannot be negative"); +#endif + } + + if ( count < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "cannot be negative"); +#endif + } + + if (offset >= buffer.Length) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "not a valid index into buffer"); +#endif + } + + if (offset + count > buffer.Length) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "exceeds buffer size"); +#endif + } + + //(By Per Bothner) + uint s1 = checksum & 0xFFFF; + uint s2 = checksum >> 16; + + while (count > 0) { + // We can defer the modulo operation: + // s1 maximally grows from 65521 to 65521 + 255 * 3800 + // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 + int n = 3800; + if (n > count) { + n = count; + } + count -= n; + while (--n >= 0) { + s1 = s1 + (uint)(buffer[offset++] & 0xff); + s2 = s2 + s1; + } + s1 %= BASE; + s2 %= BASE; + } + + checksum = (s2 << 16) | s1; + } + + #region Instance Fields + uint checksum; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs b/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs new file mode 100644 index 000000000..086594dbe --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs @@ -0,0 +1,223 @@ +// CRC32.cs - Computes CRC32 data checksum of a data stream +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: + /// x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1. + /// + /// Polynomials over GF(2) are represented in binary, one bit per coefficient, + /// with the lowest powers in the most significant bit. Then adding polynomials + /// is just exclusive-or, and multiplying a polynomial by x is a right shift by + /// one. If we call the above polynomial p, and represent a byte as the + /// polynomial q, also with the lowest power in the most significant bit (so the + /// byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, + /// where a mod b means the remainder after dividing a by b. + /// + /// This calculation is done using the shift-register method of multiplying and + /// taking the remainder. The register is initialized to zero, and for each + /// incoming bit, x^32 is added mod p to the register if the bit is a one (where + /// x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by + /// x (which is shifting right by one and adding x^32 mod p if the bit shifted + /// out is a one). We start with the highest power (least significant bit) of + /// q and repeat for all eight bits of q. + /// + /// The table is simply the CRC of all possible eight bit values. This is all + /// the information needed to generate CRC's on data a byte at a time for all + /// combinations of CRC register values and incoming bytes. + /// + public sealed class Crc32 : IChecksum + { + const uint CrcSeed = 0xFFFFFFFF; + + readonly static uint[] CrcTable = new uint[] { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, + 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, + 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, + 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, + 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, + 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, + 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, + 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, + 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, + 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, + 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, + 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, + 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, + 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, + 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, + 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, + 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, + 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, + 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, + 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, + 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, + 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, + 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, + 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, + 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, + 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, + 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, + 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, + 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, + 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, + 0x2D02EF8D + }; + + internal static uint ComputeCrc32(uint oldCrc, byte value) + { + return (uint)(Crc32.CrcTable[(oldCrc ^ value) & 0xFF] ^ (oldCrc >> 8)); + } + + /// + /// The crc data checksum so far. + /// + uint crc; + + /// + /// Returns the CRC32 data checksum computed so far. + /// + public long Value { + get { + return (long)crc; + } + set { + crc = (uint)value; + } + } + + /// + /// Resets the CRC32 data checksum as if no update was ever called. + /// + public void Reset() + { + crc = 0; + } + + /// + /// Updates the checksum with the int bval. + /// + /// + /// the byte is taken as the lower 8 bits of value + /// + public void Update(int value) + { + crc ^= CrcSeed; + crc = CrcTable[(crc ^ value) & 0xFF] ^ (crc >> 8); + crc ^= CrcSeed; + } + + /// + /// Updates the checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + public void Update(byte[] buffer) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The buffer which contains the data + /// + /// + /// The offset in the buffer where the data starts + /// + /// + /// The number of data bytes to update the CRC with. + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Count cannot be less than zero"); +#endif + } + + if (offset < 0 || offset + count > buffer.Length) { + throw new ArgumentOutOfRangeException("offset"); + } + + crc ^= CrcSeed; + + while (--count >= 0) { + crc = CrcTable[(crc ^ buffer[offset++]) & 0xFF] ^ (crc >> 8); + } + + crc ^= CrcSeed; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs b/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs new file mode 100644 index 000000000..6ff24e024 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs @@ -0,0 +1,93 @@ +// IChecksum.cs - Interface to compute a data checksum +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Interface to compute a data checksum used by checked input/output streams. + /// A data checksum can be updated by one byte or with a byte array. After each + /// update the value of the current checksum can be returned by calling + /// getValue. The complete checksum object can also be reset + /// so it can be used again with new data. + /// + public interface IChecksum + { + /// + /// Returns the data checksum computed so far. + /// + long Value + { + get; + } + + /// + /// Resets the data checksum as if no update was ever called. + /// + void Reset(); + + /// + /// Adds one byte to the data checksum. + /// + /// + /// the data value to add. The high byte of the int is ignored. + /// + void Update(int value); + + /// + /// Updates the data checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + void Update(byte[] buffer); + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The buffer which contains the data + /// + /// + /// The offset in the buffer where the data starts + /// + /// + /// the number of data bytes to add. + /// + void Update(byte[] buffer, int offset, int count); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs b/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs new file mode 100644 index 000000000..6d61a6723 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs @@ -0,0 +1,208 @@ +// StrangeCRC.cs - computes a crc used in the bziplib +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + /// + /// Bzip2 checksum algorithm + /// + public class StrangeCRC : IChecksum + { + readonly static uint[] crc32Table = { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 + }; + + int globalCrc; + + /// + /// Initialise a default instance of + /// + public StrangeCRC() + { + Reset(); + } + + /// + /// Reset the state of Crc. + /// + public void Reset() + { + globalCrc = -1; + } + + /// + /// Get the current Crc value. + /// + public long Value { + get { + return ~globalCrc; + } + } + + /// + /// Update the Crc value. + /// + /// data update is based on + public void Update(int value) + { + int temp = (globalCrc >> 24) ^ value; + if (temp < 0) { + temp = 256 + temp; + } + globalCrc = unchecked((int)((globalCrc << 8) ^ crc32Table[temp])); + } + + /// + /// Update Crc based on a block of data + /// + /// The buffer containing data to update the crc with. + public void Update(byte[] buffer) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Update Crc based on a portion of a block of data + /// + /// block of data + /// index of first byte to use + /// number of bytes to use + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "cannot be less than zero"); +#endif + } + + if ( count < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "cannot be less than zero"); +#endif + } + + if ( offset + count > buffer.Length ) + { + throw new ArgumentOutOfRangeException("count"); + } + + for (int i = 0; i < count; ++i) { + Update(buffer[offset++]); + } + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs b/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs new file mode 100644 index 000000000..52c8bd2e1 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs @@ -0,0 +1,533 @@ +// FileSystemScanner.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + #region EventArgs + /// + /// Event arguments for scanning. + /// + public class ScanEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file or directory name. + public ScanEventArgs(string name) + { + name_ = name; + } + #endregion + + /// + /// The file or directory name for this event. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating if scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + string name_; + bool continueRunning_ = true; + #endregion + } + + /// + /// Event arguments during processing of a single file or directory. + /// + public class ProgressEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file or directory name if known. + /// The number of bytes processed so far + /// The total number of bytes to process, 0 if not known + public ProgressEventArgs(string name, long processed, long target) + { + name_ = name; + processed_ = processed; + target_ = target; + } + #endregion + + /// + /// The name for this event if known. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating wether scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + /// + /// Get a percentage representing how much of the has been processed + /// + /// 0.0 to 100.0 percent; 0 if target is not known. + public float PercentComplete + { + get + { + float result; + if (target_ <= 0) + { + result = 0; + } + else + { + result = ((float)processed_ / (float)target_) * 100.0f; + } + return result; + } + } + + /// + /// The number of bytes processed so far + /// + public long Processed + { + get { return processed_; } + } + + /// + /// The number of bytes to process. + /// + /// Target may be 0 or negative if the value isnt known. + public long Target + { + get { return target_; } + } + + #region Instance Fields + string name_; + long processed_; + long target_; + bool continueRunning_ = true; + #endregion + } + + /// + /// Event arguments for directories. + /// + public class DirectoryEventArgs : ScanEventArgs + { + #region Constructors + /// + /// Initialize an instance of . + /// + /// The name for this directory. + /// Flag value indicating if any matching files are contained in this directory. + public DirectoryEventArgs(string name, bool hasMatchingFiles) + : base (name) + { + hasMatchingFiles_ = hasMatchingFiles; + } + #endregion + + /// + /// Get a value indicating if the directory contains any matching files or not. + /// + public bool HasMatchingFiles + { + get { return hasMatchingFiles_; } + } + + #region Instance Fields + bool hasMatchingFiles_; + #endregion + } + + /// + /// Arguments passed when scan failures are detected. + /// + public class ScanFailureEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The name to apply. + /// The exception to use. + public ScanFailureEventArgs(string name, Exception e) + { + name_ = name; + exception_ = e; + continueRunning_ = true; + } + #endregion + + /// + /// The applicable name. + /// + public string Name + { + get { return name_; } + } + + /// + /// The applicable exception. + /// + public Exception Exception + { + get { return exception_; } + } + + /// + /// Get / set a value indicating wether scanning should continue. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + string name_; + Exception exception_; + bool continueRunning_; + #endregion + } + + #endregion + + #region Delegates + /// + /// Delegate invoked before starting to process a directory. + /// + public delegate void ProcessDirectoryHandler(object sender, DirectoryEventArgs e); + + /// + /// Delegate invoked before starting to process a file. + /// + /// The source of the event + /// The event arguments. + public delegate void ProcessFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked during processing of a file or directory + /// + /// The source of the event + /// The event arguments. + public delegate void ProgressHandler(object sender, ProgressEventArgs e); + + /// + /// Delegate invoked when a file has been completely processed. + /// + /// The source of the event + /// The event arguments. + public delegate void CompletedFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked when a directory failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void DirectoryFailureHandler(object sender, ScanFailureEventArgs e); + + /// + /// Delegate invoked when a file failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void FileFailureHandler(object sender, ScanFailureEventArgs e); + #endregion + + /// + /// FileSystemScanner provides facilities scanning of files and directories. + /// + public class FileSystemScanner + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file filter to apply when scanning. + public FileSystemScanner(string filter) + { + fileFilter_ = new PathFilter(filter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(string fileFilter, string directoryFilter) + { + fileFilter_ = new PathFilter(fileFilter); + directoryFilter_ = new PathFilter(directoryFilter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + public FileSystemScanner(IScanFilter fileFilter) + { + fileFilter_ = fileFilter; + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(IScanFilter fileFilter, IScanFilter directoryFilter) + { + fileFilter_ = fileFilter; + directoryFilter_ = directoryFilter; + } + #endregion + + #region Delegates + /// + /// Delegate to invoke when a directory is processed. + /// + public ProcessDirectoryHandler ProcessDirectory; + + /// + /// Delegate to invoke when a file is processed. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke when processing for a file has finished. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when a directory failure is detected. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when a file failure is detected. + /// + public FileFailureHandler FileFailure; + #endregion + + /// + /// Raise the DirectoryFailure event. + /// + /// The directory name. + /// The exception detected. + bool OnDirectoryFailure(string directory, Exception e) + { + DirectoryFailureHandler handler = DirectoryFailure; + bool result = (handler != null); + if ( result ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(directory, e); + handler(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the FileFailure event. + /// + /// The file name. + /// The exception detected. + bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + + bool result = (handler != null); + + if ( result ){ + ScanFailureEventArgs args = new ScanFailureEventArgs(file, e); + FileFailure(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the ProcessFile event. + /// + /// The file name. + void OnProcessFile(string file) + { + ProcessFileHandler handler = ProcessFile; + + if ( handler!= null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the complete file event + /// + /// The file name + void OnCompleteFile(string file) + { + CompletedFileHandler handler = CompletedFile; + + if (handler != null) + { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the ProcessDirectory event. + /// + /// The directory name. + /// Flag indicating if the directory has matching files. + void OnProcessDirectory(string directory, bool hasMatchingFiles) + { + ProcessDirectoryHandler handler = ProcessDirectory; + + if ( handler != null ) { + DirectoryEventArgs args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Scan a directory. + /// + /// The base directory to scan. + /// True to recurse subdirectories, false to scan a single directory. + public void Scan(string directory, bool recurse) + { + alive_ = true; + ScanDir(directory, recurse); + } + + void ScanDir(string directory, bool recurse) + { + + try { + string[] names = System.IO.Directory.GetFiles(directory); + bool hasMatch = false; + for (int fileIndex = 0; fileIndex < names.Length; ++fileIndex) { + if ( !fileFilter_.IsMatch(names[fileIndex]) ) { + names[fileIndex] = null; + } else { + hasMatch = true; + } + } + + OnProcessDirectory(directory, hasMatch); + + if ( alive_ && hasMatch ) { + foreach (string fileName in names) { + try { + if ( fileName != null ) { + OnProcessFile(fileName); + if ( !alive_ ) { + break; + } + } + } + catch (Exception e) { + if (!OnFileFailure(fileName, e)) { + throw; + } + } + } + } + } + catch (Exception e) { + if (!OnDirectoryFailure(directory, e)) { + throw; + } + } + + if ( alive_ && recurse ) { + try { + string[] names = System.IO.Directory.GetDirectories(directory); + foreach (string fulldir in names) { + if ((directoryFilter_ == null) || (directoryFilter_.IsMatch(fulldir))) { + ScanDir(fulldir, true); + if ( !alive_ ) { + break; + } + } + } + } + catch (Exception e) { + if (!OnDirectoryFailure(directory, e)) { + throw; + } + } + } + } + + #region Instance Fields + /// + /// The file filter currently in use. + /// + IScanFilter fileFilter_; + /// + /// The directory filter currently in use. + /// + IScanFilter directoryFilter_; + /// + /// Flag indicating if scanning should continue running. + /// + bool alive_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs b/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs new file mode 100644 index 000000000..7e5b025ae --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs @@ -0,0 +1,57 @@ +// INameTransform.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// INameTransform defines how file system names are transformed for use with archives, or vice versa. + /// + public interface INameTransform + { + /// + /// Given a file name determine the transformed value. + /// + /// The name to transform. + /// The transformed file name. + string TransformFile(string name); + + /// + /// Given a directory name determine the transformed value. + /// + /// The name to transform. + /// The transformed directory name + string TransformDirectory(string name); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs b/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs new file mode 100644 index 000000000..1a29bca1f --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs @@ -0,0 +1,50 @@ +// IScanFilter.cs +// +// Copyright 2006 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// Scanning filters support filtering of names. + /// + public interface IScanFilter + { + /// + /// Test a name to see if it 'matches' the filter. + /// + /// The name to test. + /// Returns true if the name matches the filter, false if it does not match. + bool IsMatch(string name); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs b/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs new file mode 100644 index 000000000..83fee778d --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs @@ -0,0 +1,290 @@ +// NameFilter.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2010-03-03 Z-1654 Fixed bug where escape characters were excluded in SplitQuoted() + +using System; +using System.Collections; +using System.Text; +using System.Text.RegularExpressions; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// NameFilter is a string matching class which allows for both positive and negative + /// matching. + /// A filter is a sequence of independant regular expressions separated by semi-colons ';'. + /// To include a semi-colon it may be quoted as in \;. Each expression can be prefixed by a plus '+' sign or + /// a minus '-' sign to denote the expression is intended to include or exclude names. + /// If neither a plus or minus sign is found include is the default. + /// A given name is tested for inclusion before checking exclusions. Only names matching an include spec + /// and not matching an exclude spec are deemed to match the filter. + /// An empty filter matches any name. + /// + /// The following expression includes all name ending in '.dat' with the exception of 'dummy.dat' + /// "+\.dat$;-^dummy\.dat$" + /// + public class NameFilter : IScanFilter + { + #region Constructors + /// + /// Construct an instance based on the filter expression passed + /// + /// The filter expression. + public NameFilter(string filter) + { + filter_ = filter; + inclusions_ = new ArrayList(); + exclusions_ = new ArrayList(); + Compile(); + } + #endregion + + /// + /// Test a string to see if it is a valid regular expression. + /// + /// The expression to test. + /// True if expression is a valid false otherwise. + public static bool IsValidExpression(string expression) + { + bool result = true; + try { + Regex exp = new Regex(expression, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + catch (ArgumentException) { + result = false; + } + return result; + } + + /// + /// Test an expression to see if it is valid as a filter. + /// + /// The filter expression to test. + /// True if the expression is valid, false otherwise. + public static bool IsValidFilterExpression(string toTest) + { + if ( toTest == null ) { + throw new ArgumentNullException("toTest"); + } + + bool result = true; + + try { + string[] items = SplitQuoted(toTest); + for (int i = 0; i < items.Length; ++i) { + if ((items[i] != null) && (items[i].Length > 0)) { + string toCompile; + + if (items[i][0] == '+') { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if (items[i][0] == '-') { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else { + toCompile = items[i]; + } + + Regex testRegex = new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + } + } + catch (ArgumentException) { + result = false; + } + + return result; + } + + /// + /// Split a string into its component pieces + /// + /// The original string + /// Returns an array of values containing the individual filter elements. + public static string[] SplitQuoted(string original) + { + char escape = '\\'; + char[] separators = { ';' }; + + ArrayList result = new ArrayList(); + + if ((original != null) && (original.Length > 0)) { + int endIndex = -1; + StringBuilder b = new StringBuilder(); + + while (endIndex < original.Length) { + endIndex += 1; + if (endIndex >= original.Length) { + result.Add(b.ToString()); + } + else if (original[endIndex] == escape) { + endIndex += 1; + if (endIndex >= original.Length) { +#if NETCF_1_0 + throw new ArgumentException("Missing terminating escape character"); +#else + throw new ArgumentException("Missing terminating escape character", "original"); +#endif + } + // include escape if this is not an escaped separator + if (Array.IndexOf(separators, original[endIndex]) < 0) + b.Append(escape); + + b.Append(original[endIndex]); + } + else { + if (Array.IndexOf(separators, original[endIndex]) >= 0) { + result.Add(b.ToString()); + b.Length = 0; + } + else { + b.Append(original[endIndex]); + } + } + } + } + + return (string[])result.ToArray(typeof(string)); + } + + /// + /// Convert this filter to its string equivalent. + /// + /// The string equivalent for this filter. + public override string ToString() + { + return filter_; + } + + /// + /// Test a value to see if it is included by the filter. + /// + /// The value to test. + /// True if the value is included, false otherwise. + public bool IsIncluded(string name) + { + bool result = false; + if ( inclusions_.Count == 0 ) { + result = true; + } + else { + foreach ( Regex r in inclusions_ ) { + if ( r.IsMatch(name) ) { + result = true; + break; + } + } + } + return result; + } + + /// + /// Test a value to see if it is excluded by the filter. + /// + /// The value to test. + /// True if the value is excluded, false otherwise. + public bool IsExcluded(string name) + { + bool result = false; + foreach ( Regex r in exclusions_ ) { + if ( r.IsMatch(name) ) { + result = true; + break; + } + } + return result; + } + + #region IScanFilter Members + /// + /// Test a value to see if it matches the filter. + /// + /// The value to test. + /// True if the value matches, false otherwise. + public bool IsMatch(string name) + { + return (IsIncluded(name) && !IsExcluded(name)); + } + #endregion + + /// + /// Compile this filter. + /// + void Compile() + { + // TODO: Check to see if combining RE's makes it faster/smaller. + // simple scheme would be to have one RE for inclusion and one for exclusion. + if ( filter_ == null ) { + return; + } + + string[] items = SplitQuoted(filter_); + for ( int i = 0; i < items.Length; ++i ) { + if ( (items[i] != null) && (items[i].Length > 0) ) { + bool include = (items[i][0] != '-'); + string toCompile; + + if ( items[i][0] == '+' ) { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if ( items[i][0] == '-' ) { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else { + toCompile = items[i]; + } + + // NOTE: Regular expressions can fail to compile here for a number of reasons that cause an exception + // these are left unhandled here as the caller is responsible for ensuring all is valid. + // several functions IsValidFilterExpression and IsValidExpression are provided for such checking + if ( include ) { + inclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + else { + exclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + } + } + } + + #region Instance Fields + string filter_; + ArrayList inclusions_; + ArrayList exclusions_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs b/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs new file mode 100644 index 000000000..662bce785 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs @@ -0,0 +1,334 @@ +// PathFilter.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// PathFilter filters directories and files using a form of regular expressions + /// by full path name. + /// See NameFilter for more detail on filtering. + /// + public class PathFilter : IScanFilter + { + #region Constructors + /// + /// Initialise a new instance of . + /// + /// The filter expression to apply. + public PathFilter(string filter) + { + nameFilter_ = new NameFilter(filter); + } + #endregion + + #region IScanFilter Members + /// + /// Test a name to see if it matches the filter. + /// + /// The name to test. + /// True if the name matches, false otherwise. + /// is used to get the full path before matching. + public virtual bool IsMatch(string name) + { + bool result = false; + + if ( name != null ) { + string cooked = (name.Length > 0) ? Path.GetFullPath(name) : ""; + result = nameFilter_.IsMatch(cooked); + } + return result; + } + #endregion + + #region Instance Fields + NameFilter nameFilter_; + #endregion + } + + /// + /// ExtendedPathFilter filters based on name, file size, and the last write time of the file. + /// + /// Provides an example of how to customise filtering. + public class ExtendedPathFilter : PathFilter + { + #region Constructors + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinDate = minDate; + MaxDate = maxDate; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + MinDate = minDate; + MaxDate = maxDate; + } + #endregion + + #region IScanFilter Members + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + /// The doesnt exist + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if ( result ) { + FileInfo fileInfo = new FileInfo(name); + result = + (MinSize <= fileInfo.Length) && + (MaxSize >= fileInfo.Length) && + (MinDate <= fileInfo.LastWriteTime) && + (MaxDate >= fileInfo.LastWriteTime) + ; + } + return result; + } + #endregion + + #region Properties + /// + /// Get/set the minimum size/length for a file that will match this filter. + /// + /// The default value is zero. + /// value is less than zero; greater than + public long MinSize + { + get { return minSize_; } + set + { + if ( (value < 0) || (maxSize_ < value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size/length for a file that will match this filter. + /// + /// The default value is + /// value is less than zero or less than + public long MaxSize + { + get { return maxSize_; } + set + { + if ( (value < 0) || (minSize_ > value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + maxSize_ = value; + } + } + + /// + /// Get/set the minimum value that will match for this filter. + /// + /// Files with a LastWrite time less than this value are excluded by the filter. + public DateTime MinDate + { + get + { + return minDate_; + } + + set + { + if ( value > maxDate_ ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "Exceeds MaxDate"); +#endif + } + + minDate_ = value; + } + } + + /// + /// Get/set the maximum value that will match for this filter. + /// + /// Files with a LastWrite time greater than this value are excluded by the filter. + public DateTime MaxDate + { + get + { + return maxDate_; + } + + set + { + if ( minDate_ > value ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "Exceeds MinDate"); +#endif + } + + maxDate_ = value; + } + } + #endregion + + #region Instance Fields + long minSize_; + long maxSize_ = long.MaxValue; + DateTime minDate_ = DateTime.MinValue; + DateTime maxDate_ = DateTime.MaxValue; + #endregion + } + + /// + /// NameAndSizeFilter filters based on name and file size. + /// + /// A sample showing how filters might be extended. + [Obsolete("Use ExtendedPathFilter instead")] + public class NameAndSizeFilter : PathFilter + { + + /// + /// Initialise a new instance of NameAndSizeFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public NameAndSizeFilter(string filter, long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if ( result ) { + FileInfo fileInfo = new FileInfo(name); + long length = fileInfo.Length; + result = + (MinSize <= length) && + (MaxSize >= length); + } + return result; + } + + /// + /// Get/set the minimum size for a file that will match this filter. + /// + public long MinSize + { + get { return minSize_; } + set { + if ( (value < 0) || (maxSize_ < value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size for a file that will match this filter. + /// + public long MaxSize + { + get { return maxSize_; } + set + { + if ( (value < 0) || (minSize_ > value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + maxSize_ = value; + } + } + + #region Instance Fields + long minSize_; + long maxSize_ = long.MaxValue; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs b/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs new file mode 100644 index 000000000..59344d54d --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs @@ -0,0 +1,246 @@ +// StreamUtils.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// Provides simple " utilities. + /// + public sealed class StreamUtils + { + /// + /// Read from a ensuring all the required data is read. + /// + /// The stream to read. + /// The buffer to fill. + /// + static public void ReadFully(Stream stream, byte[] buffer) + { + ReadFully(stream, buffer, 0, buffer.Length); + } + + /// + /// Read from a " ensuring all the required data is read. + /// + /// The stream to read data from. + /// The buffer to store data in. + /// The offset at which to begin storing data. + /// The number of bytes of data to store. + /// Required parameter is null + /// and or are invalid. + /// End of stream is encountered before all the data has been read. + static public void ReadFully(Stream stream, byte[] buffer, int offset, int count) + { + if ( stream == null ) { + throw new ArgumentNullException("stream"); + } + + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + // Offset can equal length when buffer and count are 0. + if ( (offset < 0) || (offset > buffer.Length) ) { + throw new ArgumentOutOfRangeException("offset"); + } + + if ( (count < 0) || (offset + count > buffer.Length) ) { + throw new ArgumentOutOfRangeException("count"); + } + + while ( count > 0 ) { + int readCount = stream.Read(buffer, offset, count); + if ( readCount <= 0 ) { + throw new EndOfStreamException(); + } + offset += readCount; + count -= readCount; + } + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + static public void Copy(Stream source, Stream destination, byte[] buffer) + { + if (source == null) { + throw new ArgumentNullException("source"); + } + + if (destination == null) { + throw new ArgumentNullException("destination"); + } + + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) { + throw new ArgumentException("Buffer is too small", "buffer"); + } + + bool copying = true; + + while (copying) { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) { + destination.Write(buffer, 0, bytesRead); + } + else { + destination.Flush(); + copying = false; + } + } + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// This form is specialised for use within #Zip to support events during archive operations. + static public void Copy(Stream source, Stream destination, + byte[] buffer, ProgressHandler progressHandler, TimeSpan updateInterval, object sender, string name) + { + Copy(source, destination, buffer, progressHandler, updateInterval, sender, name, -1); + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// A predetermined fixed target value to use with progress updates. + /// If the value is negative the target is calculated by looking at the stream. + /// This form is specialised for use within #Zip to support events during archive operations. + static public void Copy(Stream source, Stream destination, + byte[] buffer, + ProgressHandler progressHandler, TimeSpan updateInterval, + object sender, string name, long fixedTarget) + { + if (source == null) { + throw new ArgumentNullException("source"); + } + + if (destination == null) { + throw new ArgumentNullException("destination"); + } + + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) { + throw new ArgumentException("Buffer is too small", "buffer"); + } + + if (progressHandler == null) { + throw new ArgumentNullException("progressHandler"); + } + + bool copying = true; + + DateTime marker = DateTime.Now; + long processed = 0; + long target = 0; + + if (fixedTarget >= 0) { + target = fixedTarget; + } + else if (source.CanSeek) { + target = source.Length - source.Position; + } + + // Always fire 0% progress.. + ProgressEventArgs args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + bool progressFired = true; + + while (copying) { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) { + processed += bytesRead; + progressFired = false; + destination.Write(buffer, 0, bytesRead); + } + else { + destination.Flush(); + copying = false; + } + + if (DateTime.Now - marker > updateInterval) { + progressFired = true; + marker = DateTime.Now; + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + copying = args.ContinueRunning; + } + } + + if (!progressFired) { + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + } + } + + /// + /// Initialise an instance of + /// + private StreamUtils() + { + // Do nothing. + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs b/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs new file mode 100644 index 000000000..820461c56 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs @@ -0,0 +1,94 @@ +// WindowsPathUtils.cs +// +// Copyright 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// WindowsPathUtils provides simple utilities for handling windows paths. + /// + public abstract class WindowsPathUtils + { + /// + /// Initializes a new instance of the class. + /// + internal WindowsPathUtils() + { + } + + /// + /// Remove any path root present in the path + /// + /// A containing path information. + /// The path with the root removed if it was present; path otherwise. + /// Unlike the class the path isnt otherwise checked for validity. + public static string DropPathRoot(string path) + { + string result = path; + + if ( (path != null) && (path.Length > 0) ) { + if ((path[0] == '\\') || (path[0] == '/')) { + // UNC name ? + if ((path.Length > 1) && ((path[1] == '\\') || (path[1] == '/'))) { + int index = 2; + int elements = 2; + + // Scan for two separate elements \\machine\share\restofpath + while ((index <= path.Length) && + (((path[index] != '\\') && (path[index] != '/')) || (--elements > 0))) { + index++; + } + + index++; + + if (index < path.Length) { + result = path.Substring(index); + } + else { + result = ""; + } + } + } + else if ((path.Length > 1) && (path[1] == ':')) { + int dropCount = 2; + if ((path.Length > 2) && ((path[2] == '\\') || (path[2] == '/'))) { + dropCount = 3; + } + result = result.Remove(0, dropCount); + } + } + return result; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs b/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs new file mode 100644 index 000000000..e030aaa64 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs @@ -0,0 +1,498 @@ +// +// PkzipClassic encryption +// +// Copyright 2004 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + + +#if !NETCF_1_0 + +using System; +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Checksums; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption +{ + /// + /// PkzipClassic embodies the classic or original encryption facilities used in Pkzip archives. + /// While it has been superceded by more recent and more powerful algorithms, its still in use and + /// is viable for preventing casual snooping + /// + public abstract class PkzipClassic : SymmetricAlgorithm + { + /// + /// Generates new encryption keys based on given seed + /// + /// The seed value to initialise keys with. + /// A new key value. + static public byte[] GenerateKeys(byte[] seed) + { + if ( seed == null ) { + throw new ArgumentNullException("seed"); + } + + if ( seed.Length == 0 ) { + throw new ArgumentException("Length is zero", "seed"); + } + + uint[] newKeys = new uint[] { + 0x12345678, + 0x23456789, + 0x34567890 + }; + + for (int i = 0; i < seed.Length; ++i) { + newKeys[0] = Crc32.ComputeCrc32(newKeys[0], seed[i]); + newKeys[1] = newKeys[1] + (byte)newKeys[0]; + newKeys[1] = newKeys[1] * 134775813 + 1; + newKeys[2] = Crc32.ComputeCrc32(newKeys[2], (byte)(newKeys[1] >> 24)); + } + + byte[] result = new byte[12]; + result[0] = (byte)(newKeys[0] & 0xff); + result[1] = (byte)((newKeys[0] >> 8) & 0xff); + result[2] = (byte)((newKeys[0] >> 16) & 0xff); + result[3] = (byte)((newKeys[0] >> 24) & 0xff); + result[4] = (byte)(newKeys[1] & 0xff); + result[5] = (byte)((newKeys[1] >> 8) & 0xff); + result[6] = (byte)((newKeys[1] >> 16) & 0xff); + result[7] = (byte)((newKeys[1] >> 24) & 0xff); + result[8] = (byte)(newKeys[2] & 0xff); + result[9] = (byte)((newKeys[2] >> 8) & 0xff); + result[10] = (byte)((newKeys[2] >> 16) & 0xff); + result[11] = (byte)((newKeys[2] >> 24) & 0xff); + return result; + } + } + + /// + /// PkzipClassicCryptoBase provides the low level facilities for encryption + /// and decryption using the PkzipClassic algorithm. + /// + class PkzipClassicCryptoBase + { + /// + /// Transform a single byte + /// + /// + /// The transformed value + /// + protected byte TransformByte() + { + uint temp = ((keys[2] & 0xFFFF) | 2); + return (byte)((temp * (temp ^ 1)) >> 8); + } + + /// + /// Set the key schedule for encryption/decryption. + /// + /// The data use to set the keys from. + protected void SetKeys(byte[] keyData) + { + if ( keyData == null ) { + throw new ArgumentNullException("keyData"); + } + + if ( keyData.Length != 12 ) { + throw new InvalidOperationException("Key length is not valid"); + } + + keys = new uint[3]; + keys[0] = (uint)((keyData[3] << 24) | (keyData[2] << 16) | (keyData[1] << 8) | keyData[0]); + keys[1] = (uint)((keyData[7] << 24) | (keyData[6] << 16) | (keyData[5] << 8) | keyData[4]); + keys[2] = (uint)((keyData[11] << 24) | (keyData[10] << 16) | (keyData[9] << 8) | keyData[8]); + } + + /// + /// Update encryption keys + /// + protected void UpdateKeys(byte ch) + { + keys[0] = Crc32.ComputeCrc32(keys[0], ch); + keys[1] = keys[1] + (byte)keys[0]; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = Crc32.ComputeCrc32(keys[2], (byte)(keys[1] >> 24)); + } + + /// + /// Reset the internal state. + /// + protected void Reset() + { + keys[0] = 0; + keys[1] = 0; + keys[2] = 0; + } + + #region Instance Fields + uint[] keys; + #endregion + } + + /// + /// PkzipClassic CryptoTransform for encryption. + /// + class PkzipClassicEncryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of + /// + /// The key block to use. + internal PkzipClassicEncryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) { + byte oldbyte = inputBuffer[i]; + outputBuffer[outputOffset++] = (byte)(inputBuffer[i] ^ TransformByte()); + UpdateKeys(oldbyte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get { + return true; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion + } + + + /// + /// PkzipClassic CryptoTransform for decryption. + /// + class PkzipClassicDecryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of . + /// + /// The key block to decrypt with. + internal PkzipClassicDecryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) { + byte newByte = (byte)(inputBuffer[i] ^ TransformByte()); + outputBuffer[outputOffset++] = newByte; + UpdateKeys(newByte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get { + return true; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion + } + + /// + /// Defines a wrapper object to access the Pkzip algorithm. + /// This class cannot be inherited. + /// + public sealed class PkzipClassicManaged : PkzipClassic + { + /// + /// Get / set the applicable block size in bits. + /// + /// The only valid block size is 8. + public override int BlockSize + { + get { + return 8; + } + + set { + if (value != 8) { + throw new CryptographicException("Block size is invalid"); + } + } + } + + /// + /// Get an array of legal key sizes. + /// + public override KeySizes[] LegalKeySizes + { + get { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(12 * 8, 12 * 8, 0); + return keySizes; + } + } + + /// + /// Generate an initial vector. + /// + public override void GenerateIV() + { + // Do nothing. + } + + /// + /// Get an array of legal block sizes. + /// + public override KeySizes[] LegalBlockSizes + { + get { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(1 * 8, 1 * 8, 0); + return keySizes; + } + } + + /// + /// Get / set the key value applicable. + /// + public override byte[] Key + { + get { + if ( key_ == null ) { + GenerateKey(); + } + + return (byte[]) key_.Clone(); + } + + set { + if ( value == null ) { + throw new ArgumentNullException("value"); + } + + if ( value.Length != 12 ) { + throw new CryptographicException("Key size is illegal"); + } + + key_ = (byte[]) value.Clone(); + } + } + + /// + /// Generate a new random key. + /// + public override void GenerateKey() + { + key_ = new byte[12]; + Random rnd = new Random(); + rnd.NextBytes(key_); + } + + /// + /// Create an encryptor. + /// + /// The key to use for this encryptor. + /// Initialisation vector for the new encryptor. + /// Returns a new PkzipClassic encryptor + public override ICryptoTransform CreateEncryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicEncryptCryptoTransform(Key); + } + + /// + /// Create a decryptor. + /// + /// Keys to use for this new decryptor. + /// Initialisation vector for the new decryptor. + /// Returns a new decryptor. + public override ICryptoTransform CreateDecryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicDecryptCryptoTransform(Key); + } + + #region Instance Fields + byte[] key_; + #endregion + } +} +#endif diff --git a/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs new file mode 100644 index 000000000..8721b4a92 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs @@ -0,0 +1,170 @@ +// +// ZipAESStream.cs +// +// Copyright 2009 David Pierson +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + +#if !NET_1_1 && !NETCF_2_0 + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption { + + // Based on information from http://www.winzip.com/aes_info.htm + // and http://www.gladman.me.uk/cryptography_technology/fileencrypt/ + + /// + /// Encrypts and decrypts AES ZIP + /// + internal class ZipAESStream : CryptoStream { + + /// + /// Constructor + /// + /// The stream on which to perform the cryptographic transformation. + /// Instance of ZipAESTransform + /// Read or Write + public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode mode) + : base(stream, transform, mode) { + + _stream = stream; + _transform = transform; + _slideBuffer = new byte[1024]; + + _blockAndAuth = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH; + + // mode: + // CryptoStreamMode.Read means we read from "stream" and pass decrypted to our Read() method. + // Write bypasses this stream and uses the Transform directly. + if (mode != CryptoStreamMode.Read) { + throw new Exception("ZipAESStream only for read"); + } + } + + // The final n bytes of the AES stream contain the Auth Code. + private const int AUTH_CODE_LENGTH = 10; + + private Stream _stream; + private ZipAESTransform _transform; + private byte[] _slideBuffer; + private int _slideBufStartPos; + private int _slideBufFreePos; + // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. + private const int CRYPTO_BLOCK_SIZE = 16; + private int _blockAndAuth; + + /// + /// Reads a sequence of bytes from the current CryptoStream into buffer, + /// and advances the position within the stream by the number of bytes read. + /// + public override int Read(byte[] outBuffer, int offset, int count) { + int nBytes = 0; + while (nBytes < count) { + // Calculate buffer quantities vs read-ahead size, and check for sufficient free space + int byteCount = _slideBufFreePos - _slideBufStartPos; + + // Need to handle final block and Auth Code specially, but don't know total data length. + // Maintain a read-ahead equal to the length of (crypto block + Auth Code). + // When that runs out we can detect these final sections. + int lengthToRead = _blockAndAuth - byteCount; + if (_slideBuffer.Length - _slideBufFreePos < lengthToRead) { + // Shift the data to the beginning of the buffer + int iTo = 0; + for (int iFrom = _slideBufStartPos; iFrom < _slideBufFreePos; iFrom++, iTo++) { + _slideBuffer[iTo] = _slideBuffer[iFrom]; + } + _slideBufFreePos -= _slideBufStartPos; // Note the -= + _slideBufStartPos = 0; + } + int obtained = _stream.Read(_slideBuffer, _slideBufFreePos, lengthToRead); + _slideBufFreePos += obtained; + + // Recalculate how much data we now have + byteCount = _slideBufFreePos - _slideBufStartPos; + if (byteCount >= _blockAndAuth) { + // At least a 16 byte block and an auth code remains. + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + CRYPTO_BLOCK_SIZE, + outBuffer, + offset); + nBytes += CRYPTO_BLOCK_SIZE; + offset += CRYPTO_BLOCK_SIZE; + _slideBufStartPos += CRYPTO_BLOCK_SIZE; + } else { + // Last round. + if (byteCount > AUTH_CODE_LENGTH) { + // At least one byte of data plus auth code + int finalBlock = byteCount - AUTH_CODE_LENGTH; + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + finalBlock, + outBuffer, + offset); + + nBytes += finalBlock; + _slideBufStartPos += finalBlock; + } + else if (byteCount < AUTH_CODE_LENGTH) + throw new Exception("Internal error missed auth code"); // Coding bug + // Final block done. Check Auth code. + byte[] calcAuthCode = _transform.GetAuthCode(); + for (int i = 0; i < AUTH_CODE_LENGTH; i++) { + if (calcAuthCode[i] != _slideBuffer[_slideBufStartPos + i]) { + throw new Exception("AES Authentication Code does not match. This is a super-CRC check on the data in the file after compression and encryption. \r\n" + + "The file may be damaged."); + } + } + + break; // Reached the auth code + } + } + return nBytes; + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) { + // ZipAESStream is used for reading but not for writing. Writing uses the ZipAESTransform directly. + throw new NotImplementedException(); + } + } +} +#endif \ No newline at end of file diff --git a/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs new file mode 100644 index 000000000..002036cc3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs @@ -0,0 +1,219 @@ +// +// ZipAESTransform.cs +// +// Copyright 2009 David Pierson +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + +#if !NET_1_1 && !NETCF_2_0 +// Framework version 2.0 required for Rfc2898DeriveBytes + +using System; +using System.Security.Cryptography; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption { + + /// + /// Transforms stream using AES in CTR mode + /// + internal class ZipAESTransform : ICryptoTransform { + + private const int PWD_VER_LENGTH = 2; + + // WinZip use iteration count of 1000 for PBKDF2 key generation + private const int KEY_ROUNDS = 1000; + + // For 128-bit AES (16 bytes) the encryption is implemented as expected. + // For 256-bit AES (32 bytes) WinZip do full 256 bit AES of the nonce to create the encryption + // block but use only the first 16 bytes of it, and discard the second half. + private const int ENCRYPT_BLOCK = 16; + + private int _blockSize; + private ICryptoTransform _encryptor; + private readonly byte[] _counterNonce; + private byte[] _encryptBuffer; + private int _encrPos; + private byte[] _pwdVerifier; + private HMACSHA1 _hmacsha1; + private bool _finalised; + + private bool _writeMode; + + /// + /// Constructor. + /// + /// Password string + /// Random bytes, length depends on encryption strength. + /// 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + /// The encryption strength, in bytes eg 16 for 128 bits. + /// True when creating a zip, false when reading. For the AuthCode. + /// + public ZipAESTransform(string key, byte[] saltBytes, int blockSize, bool writeMode) { + + if (blockSize != 16 && blockSize != 32) // 24 valid for AES but not supported by Winzip + throw new Exception("Invalid blocksize " + blockSize + ". Must be 16 or 32."); + if (saltBytes.Length != blockSize / 2) + throw new Exception("Invalid salt len. Must be " + blockSize / 2 + " for blocksize " + blockSize); + // initialise the encryption buffer and buffer pos + _blockSize = blockSize; + _encryptBuffer = new byte[_blockSize]; + _encrPos = ENCRYPT_BLOCK; + + // Performs the equivalent of derive_key in Dr Brian Gladman's pwd2key.c + Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(key, saltBytes, KEY_ROUNDS); + RijndaelManaged rm = new RijndaelManaged(); + rm.Mode = CipherMode.ECB; // No feedback from cipher for CTR mode + _counterNonce = new byte[_blockSize]; + byte[] byteKey1 = pdb.GetBytes(_blockSize); + byte[] byteKey2 = pdb.GetBytes(_blockSize); + _encryptor = rm.CreateEncryptor(byteKey1, byteKey2); + _pwdVerifier = pdb.GetBytes(PWD_VER_LENGTH); + // + _hmacsha1 = new HMACSHA1(byteKey2); + _writeMode = writeMode; + } + + /// + /// Implement the ICryptoTransform method. + /// + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) { + + // Pass the data stream to the hash algorithm for generating the Auth Code. + // This does not change the inputBuffer. Do this before decryption for read mode. + if (!_writeMode) { + _hmacsha1.TransformBlock(inputBuffer, inputOffset, inputCount, inputBuffer, inputOffset); + } + // Encrypt with AES in CTR mode. Regards to Dr Brian Gladman for this. + int ix = 0; + while (ix < inputCount) { + if (_encrPos == ENCRYPT_BLOCK) { + /* increment encryption nonce */ + int j = 0; + while (++_counterNonce[j] == 0) { + ++j; + } + /* encrypt the nonce to form next xor buffer */ + _encryptor.TransformBlock(_counterNonce, 0, _blockSize, _encryptBuffer, 0); + _encrPos = 0; + } + outputBuffer[ix + outputOffset] = (byte)(inputBuffer[ix + inputOffset] ^ _encryptBuffer[_encrPos++]); + // + ix++; + } + if (_writeMode) { + // This does not change the buffer. + _hmacsha1.TransformBlock(outputBuffer, outputOffset, inputCount, outputBuffer, outputOffset); + } + return inputCount; + } + + /// + /// Returns the 2 byte password verifier + /// + public byte[] PwdVerifier { + get { + return _pwdVerifier; + } + } + + /// + /// Returns the 10 byte AUTH CODE to be checked or appended immediately following the AES data stream. + /// + public byte[] GetAuthCode() { + // We usually don't get advance notice of final block. Hash requres a TransformFinal. + if (!_finalised) { + byte[] dummy = new byte[0]; + _hmacsha1.TransformFinalBlock(dummy, 0, 0); + _finalised = true; + } + return _hmacsha1.Hash; + } + + #region ICryptoTransform Members + + /// + /// Not implemented. + /// + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) { + + throw new NotImplementedException("ZipAESTransform.TransformFinalBlock"); + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize { + get { + return _blockSize; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize { + get { + return _blockSize; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks { + get { + return true; + } + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform { + get { + return true; + } + } + + /// + /// Cleanup internal state. + /// + public void Dispose() { + _encryptor.Dispose(); + } + + #endregion + + } +} +#endif \ No newline at end of file diff --git a/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs b/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs new file mode 100644 index 000000000..227a951cb --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs @@ -0,0 +1,94 @@ +// SharpZipBaseException.cs +// +// Copyright 2004 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +#if !NETCF_1_0 && !NETCF_2_0 +using System.Runtime.Serialization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib +{ + /// + /// SharpZipBaseException is the base exception class for the SharpZipLibrary. + /// All library exceptions are derived from this. + /// + /// NOTE: Not all exceptions thrown will be derived from this class. + /// A variety of other exceptions are possible for example +#if !NETCF_1_0 && !NETCF_2_0 + [Serializable] +#endif + public class SharpZipBaseException : ApplicationException + { +#if !NETCF_1_0 && !NETCF_2_0 + /// + /// Deserialization constructor + /// + /// for this constructor + /// for this constructor + protected SharpZipBaseException(SerializationInfo info, StreamingContext context ) + : base( info, context ) + { + } +#endif + + /// + /// Initializes a new instance of the SharpZipBaseException class. + /// + public SharpZipBaseException() + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified error message. + /// + /// A message describing the exception. + public SharpZipBaseException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public SharpZipBaseException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs new file mode 100644 index 000000000..3ba505385 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs @@ -0,0 +1,557 @@ +// Deflater.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This is the Deflater class. The deflater class compresses input + /// with the deflate algorithm described in RFC 1951. It has several + /// compression levels and three different strategies described below. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of deflate and setInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class Deflater + { + #region Deflater Documentation + /* + * The Deflater can do the following state transitions: + * + * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. + * / | (2) (5) | + * / v (5) | + * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) + * \ | (3) | ,--------' + * | | | (3) / + * v v (5) v v + * (1) -> BUSY_STATE ----> FINISHING_STATE + * | (6) + * v + * FINISHED_STATE + * \_____________________________________/ + * | (7) + * v + * CLOSED_STATE + * + * (1) If we should produce a header we start in INIT_STATE, otherwise + * we start in BUSY_STATE. + * (2) A dictionary may be set only when we are in INIT_STATE, then + * we change the state as indicated. + * (3) Whether a dictionary is set or not, on the first call of deflate + * we change to BUSY_STATE. + * (4) -- intentionally left blank -- :) + * (5) FINISHING_STATE is entered, when flush() is called to indicate that + * there is no more INPUT. There are also states indicating, that + * the header wasn't written yet. + * (6) FINISHED_STATE is entered, when everything has been flushed to the + * internal pending output buffer. + * (7) At any time (7) + * + */ + #endregion + #region Public Constants + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BEST_COMPRESSION = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BEST_SPEED = 1; + + /// + /// The default compression level. + /// + public const int DEFAULT_COMPRESSION = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + public const int NO_COMPRESSION = 0; + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + public const int DEFLATED = 8; + #endregion + #region Local Constants + private const int IS_SETDICT = 0x01; + private const int IS_FLUSHING = 0x04; + private const int IS_FINISHING = 0x08; + + private const int INIT_STATE = 0x00; + private const int SETDICT_STATE = 0x01; + // private static int INIT_FINISHING_STATE = 0x08; + // private static int SETDICT_FINISHING_STATE = 0x09; + private const int BUSY_STATE = 0x10; + private const int FLUSHING_STATE = 0x14; + private const int FINISHING_STATE = 0x1c; + private const int FINISHED_STATE = 0x1e; + private const int CLOSED_STATE = 0x7f; + #endregion + #region Constructors + /// + /// Creates a new deflater with default compression level. + /// + public Deflater() : this(DEFAULT_COMPRESSION, false) + { + + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION, or DEFAULT_COMPRESSION. + /// + /// if lvl is out of range. + public Deflater(int level) : this(level, false) + { + + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION. + /// + /// + /// true, if we should suppress the Zlib/RFC1950 header at the + /// beginning and the adler checksum at the end of the output. This is + /// useful for the GZIP/PKZIP formats. + /// + /// if lvl is out of range. + public Deflater(int level, bool noZlibHeaderOrFooter) + { + if (level == DEFAULT_COMPRESSION) { + level = 6; + } else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) { + throw new ArgumentOutOfRangeException("level"); + } + + pending = new DeflaterPending(); + engine = new DeflaterEngine(pending); + this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; + SetStrategy(DeflateStrategy.Default); + SetLevel(level); + Reset(); + } + #endregion + + /// + /// Resets the deflater. The deflater acts afterwards as if it was + /// just created with the same compression level and strategy as it + /// had before. + /// + public void Reset() + { + state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); + totalOut = 0; + pending.Reset(); + engine.Reset(); + } + + /// + /// Gets the current adler checksum of the data that was processed so far. + /// + public int Adler { + get { + return engine.Adler; + } + } + + /// + /// Gets the number of input bytes processed so far. + /// + public long TotalIn { + get { + return engine.TotalIn; + } + } + + /// + /// Gets the number of output bytes so far. + /// + public long TotalOut { + get { + return totalOut; + } + } + + /// + /// Flushes the current input block. Further calls to deflate() will + /// produce enough output to inflate everything in the current input + /// block. This is not part of Sun's JDK so I have made it package + /// private. It is used by DeflaterOutputStream to implement + /// flush(). + /// + public void Flush() + { + state |= IS_FLUSHING; + } + + /// + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must + /// be called to force all bytes to be flushed. + /// + public void Finish() + { + state |= (IS_FLUSHING | IS_FINISHING); + } + + /// + /// Returns true if the stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished { + get { + return (state == FINISHED_STATE) && pending.IsFlushed; + } + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput { + get { + return engine.NeedsInput(); + } + } + + /// + /// Sets the data which should be compressed next. This should be only + /// called when needsInput indicates that more input is needed. + /// If you call setInput when needsInput() returns false, the + /// previous input that is still pending will be thrown away. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// This call is equivalent to setInput(input, 0, input.length). + /// + /// + /// the buffer containing the input data. + /// + /// + /// if the buffer was finished() or ended(). + /// + public void SetInput(byte[] input) + { + SetInput(input, 0, input.Length); + } + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// + /// + /// the buffer containing the input data. + /// + /// + /// the start of the data. + /// + /// + /// the number of data bytes of input. + /// + /// + /// if the buffer was Finish()ed or if previous input is still pending. + /// + public void SetInput(byte[] input, int offset, int count) + { + if ((state & IS_FINISHING) != 0) { + throw new InvalidOperationException("Finish() already called"); + } + engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DEFAULT_COMPRESSION) { + level = 6; + } else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) { + throw new ArgumentOutOfRangeException("level"); + } + + if (this.level != level) { + this.level = level; + engine.SetLevel(level); + } + } + + /// + /// Get current compression level + /// + /// Returns the current compression level + public int GetLevel() { + return level; + } + + /// + /// Sets the compression strategy. Strategy is one of + /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact + /// position where the strategy is changed, the same as for + /// SetLevel() applies. + /// + /// + /// The new compression strategy. + /// + public void SetStrategy(DeflateStrategy strategy) + { + engine.Strategy = strategy; + } + + /// + /// Deflates the current input block with to the given array. + /// + /// + /// The buffer where compressed data is stored + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// IsNeedingInput() or IsFinished returns true or length is zero. + /// + public int Deflate(byte[] output) + { + return Deflate(output, 0, output.Length); + } + + /// + /// Deflates the current input block to the given array. + /// + /// + /// Buffer to store the compressed data. + /// + /// + /// Offset into the output array. + /// + /// + /// The maximum number of bytes that may be stored. + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// needsInput() or finished() returns true or length is zero. + /// + /// + /// If Finish() was previously called. + /// + /// + /// If offset or length don't match the array length. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (state == CLOSED_STATE) { + throw new InvalidOperationException("Deflater closed"); + } + + if (state < BUSY_STATE) { + // output header + int header = (DEFLATED + + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int level_flags = (level - 1) >> 1; + if (level_flags < 0 || level_flags > 3) { + level_flags = 3; + } + header |= level_flags << 6; + if ((state & IS_SETDICT) != 0) { + // Dictionary was set + header |= DeflaterConstants.PRESET_DICT; + } + header += 31 - (header % 31); + + pending.WriteShortMSB(header); + if ((state & IS_SETDICT) != 0) { + int chksum = engine.Adler; + engine.ResetAdler(); + pending.WriteShortMSB(chksum >> 16); + pending.WriteShortMSB(chksum & 0xffff); + } + + state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + } + + for (;;) { + int count = pending.Flush(output, offset, length); + offset += count; + totalOut += count; + length -= count; + + if (length == 0 || state == FINISHED_STATE) { + break; + } + + if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) { + if (state == BUSY_STATE) { + // We need more input now + return origLength - length; + } else if (state == FLUSHING_STATE) { + if (level != NO_COMPRESSION) { + /* We have to supply some lookahead. 8 bit lookahead + * is needed by the zlib inflater, and we must fill + * the next byte, so that all bits are flushed. + */ + int neededbits = 8 + ((-pending.BitCount) & 7); + while (neededbits > 0) { + /* write a static tree block consisting solely of + * an EOF: + */ + pending.WriteBits(2, 10); + neededbits -= 10; + } + } + state = BUSY_STATE; + } else if (state == FINISHING_STATE) { + pending.AlignToByte(); + + // Compressed data is complete. Write footer information if required. + if (!noZlibHeaderOrFooter) { + int adler = engine.Adler; + pending.WriteShortMSB(adler >> 16); + pending.WriteShortMSB(adler & 0xffff); + } + state = FINISHED_STATE; + } + } + } + return origLength - length; + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// This call is equivalent to setDictionary(dict, 0, dict.Length). + /// + /// + /// the dictionary. + /// + /// + /// if SetInput () or Deflate () were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary) + { + SetDictionary(dictionary, 0, dictionary.Length); + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// The dictionary is a byte array containing strings that are + /// likely to occur in the data which should be compressed. The + /// dictionary is not stored in the compressed output, only a + /// checksum. To decompress the output you need to supply the same + /// dictionary again. + /// + /// + /// The dictionary data + /// + /// + /// The index where dictionary information commences. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// If SetInput () or Deflate() were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary, int index, int count) + { + if (state != INIT_STATE) { + throw new InvalidOperationException(); + } + + state = SETDICT_STATE; + engine.SetDictionary(dictionary, index, count); + } + + #region Instance Fields + /// + /// Compression level. + /// + int level; + + /// + /// If true no Zlib/RFC1950 headers or footers are generated + /// + bool noZlibHeaderOrFooter; + + /// + /// The current state. + /// + int state; + + /// + /// The total bytes of output written. + /// + long totalOut; + + /// + /// The pending output. + /// + DeflaterPending pending; + + /// + /// The deflater engine. + /// + DeflaterEngine engine; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs new file mode 100644 index 000000000..abc68cbac --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs @@ -0,0 +1,186 @@ +// DeflaterConstants.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class contains constants used for deflation. + /// + public class DeflaterConstants + { + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs new file mode 100644 index 000000000..f3a39d8e9 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs @@ -0,0 +1,869 @@ +// DeflaterEngine.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// Strategies for deflater + /// + public enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + public class DeflaterEngine : DeflaterConstants + { + #region Constants + const int TooFar = 4096; + #endregion + + #region Constructors + /// + /// Construct instance with pending buffer + /// + /// + /// Pending buffer to use + /// > + public DeflaterEngine(DeflaterPending pending) + { + this.pending = pending; + huffman = new DeflaterHuffman(pending); + adler = new Adler32(); + + window = new byte[2 * WSIZE]; + head = new short[HASH_SIZE]; + prev = new short[WSIZE]; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + blockStart = strstart = 1; + } + + #endregion + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress; + do + { + FillWindow(); + bool canFlush = flush && (inputOff == inputEnd); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("window: [" + blockStart + "," + strstart + "," + + lookahead + "], " + compressionFunction + "," + canFlush); + } +#endif + switch (compressionFunction) + { + case DEFLATE_STORED: + progress = DeflateStored(canFlush, finish); + break; + case DEFLATE_FAST: + progress = DeflateFast(canFlush, finish); + break; + case DEFLATE_SLOW: + progress = DeflateSlow(canFlush, finish); + break; + default: + throw new InvalidOperationException("unknown compressionFunction"); + } + } while (pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when NeedsInput() + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[] buffer, int offset, int count) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) + { + throw new ArgumentOutOfRangeException("offset"); + } + + if ( count < 0 ) + { + throw new ArgumentOutOfRangeException("count"); + } + + if (inputOff < inputEnd) + { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + /* We want to throw an ArrayIndexOutOfBoundsException early. The + * check is very tricky: it also handles integer wrap around. + */ + if ((offset > end) || (end > buffer.Length) ) + { + throw new ArgumentOutOfRangeException("count"); + } + + inputBuf = buffer; + inputOff = offset; + inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + public bool NeedsInput() + { + return (inputEnd == inputOff); + } + + /// + /// Set compression dictionary + /// + /// The buffer containing the dictionary data + /// The offset in the buffer for the first byte of data + /// The length of the dictionary data. + public void SetDictionary(byte[] buffer, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (strstart != 1) ) + { + throw new InvalidOperationException("strstart not 1"); + } +#endif + adler.Update(buffer, offset, length); + if (length < MIN_MATCH) + { + return; + } + + if (length > MAX_DIST) + { + offset += length - MAX_DIST; + length = MAX_DIST; + } + + System.Array.Copy(buffer, offset, window, strstart, length); + + UpdateHash(); + --length; + while (--length > 0) + { + InsertString(); + strstart++; + } + strstart += 2; + blockStart = strstart; + } + + /// + /// Reset internal state + /// + public void Reset() + { + huffman.Reset(); + adler.Reset(); + blockStart = strstart = 1; + lookahead = 0; + totalIn = 0; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + + for (int i = 0; i < HASH_SIZE; i++) { + head[i] = 0; + } + + for (int i = 0; i < WSIZE; i++) { + prev[i] = 0; + } + } + + /// + /// Reset Adler checksum + /// + public void ResetAdler() + { + adler.Reset(); + } + + /// + /// Get current value of Adler checksum + /// + public int Adler { + get { + return unchecked((int)adler.Value); + } + } + + /// + /// Total data processed + /// + public long TotalIn { + get { + return totalIn; + } + } + + /// + /// Get/set the deflate strategy + /// + public DeflateStrategy Strategy { + get { + return strategy; + } + set { + strategy = value; + } + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ( (level < 0) || (level > 9) ) + { + throw new ArgumentOutOfRangeException("level"); + } + + goodLength = DeflaterConstants.GOOD_LENGTH[level]; + max_lazy = DeflaterConstants.MAX_LAZY[level]; + niceLength = DeflaterConstants.NICE_LENGTH[level]; + max_chain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != compressionFunction) { + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("Change from " + compressionFunction + " to " + + DeflaterConstants.COMPR_FUNC[level]); + } +#endif + switch (compressionFunction) { + case DEFLATE_STORED: + if (strstart > blockStart) { + huffman.FlushStoredBlock(window, blockStart, + strstart - blockStart, false); + blockStart = strstart; + } + UpdateHash(); + break; + + case DEFLATE_FAST: + if (strstart > blockStart) { + huffman.FlushBlock(window, blockStart, strstart - blockStart, + false); + blockStart = strstart; + } + break; + + case DEFLATE_SLOW: + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + if (strstart > blockStart) { + huffman.FlushBlock(window, blockStart, strstart - blockStart, false); + blockStart = strstart; + } + prevAvailable = false; + matchLen = MIN_MATCH - 1; + break; + } + compressionFunction = COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (strstart >= WSIZE + MAX_DIST) + { + SlideWindow(); + } + + /* If there is not enough lookahead, but still some input left, + * read in the input + */ + while (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) + { + int more = 2 * WSIZE - lookahead - strstart; + + if (more > inputEnd - inputOff) + { + more = inputEnd - inputOff; + } + + System.Array.Copy(inputBuf, inputOff, window, strstart + lookahead, more); + adler.Update(inputBuf, inputOff, more); + + inputOff += more; + totalIn += more; + lookahead += more; + } + + if (lookahead >= MIN_MATCH) + { + UpdateHash(); + } + } + + void UpdateHash() + { +/* + if (DEBUGGING) { + Console.WriteLine("updateHash: "+strstart); + } +*/ + ins_h = (window[strstart] << HASH_SHIFT) ^ window[strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + int InsertString() + { + short match; + int hash = ((ins_h << HASH_SHIFT) ^ window[strstart + (MIN_MATCH -1)]) & HASH_MASK; + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + if (hash != (((window[strstart] << (2*HASH_SHIFT)) ^ + (window[strstart + 1] << HASH_SHIFT) ^ + (window[strstart + 2])) & HASH_MASK)) { + throw new SharpZipBaseException("hash inconsistent: " + hash + "/" + +window[strstart] + "," + +window[strstart + 1] + "," + +window[strstart + 2] + "," + HASH_SHIFT); + } + } +#endif + prev[strstart & WMASK] = match = head[hash]; + head[hash] = unchecked((short)strstart); + ins_h = hash; + return match & 0xffff; + } + + void SlideWindow() + { + Array.Copy(window, WSIZE, window, 0, WSIZE); + matchStart -= WSIZE; + strstart -= WSIZE; + blockStart -= WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + for (int i = 0; i < HASH_SIZE; ++i) { + int m = head[i] & 0xffff; + head[i] = (short)(m >= WSIZE ? (m - WSIZE) : 0); + } + + // Slide the prev table. + for (int i = 0; i < WSIZE; i++) { + int m = prev[i] & 0xffff; + prev[i] = (short)(m >= WSIZE ? (m - WSIZE) : 0); + } + } + + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// Preconditions: + /// + /// strstart + MAX_MATCH <= window.length. + /// + /// + /// True if a match greater than the minimum length is found + bool FindLongestMatch(int curMatch) + { + int chainLength = this.max_chain; + int niceLength = this.niceLength; + short[] prev = this.prev; + int scan = this.strstart; + int match; + int best_end = this.strstart + matchLen; + int best_len = Math.Max(matchLen, MIN_MATCH - 1); + + int limit = Math.Max(strstart - MAX_DIST, 0); + + int strend = strstart + MAX_MATCH - 1; + byte scan_end1 = window[best_end - 1]; + byte scan_end = window[best_end]; + + // Do not waste too much time if we already have a good match: + if (best_len >= this.goodLength) { + chainLength >>= 2; + } + + /* Do not look for matches beyond the end of the input. This is necessary + * to make deflate deterministic. + */ + if (niceLength > lookahead) { + niceLength = lookahead; + } + +#if DebugDeflation + + if (DeflaterConstants.DEBUGGING && (strstart > 2 * WSIZE - MIN_LOOKAHEAD)) + { + throw new InvalidOperationException("need lookahead"); + } +#endif + + do { + +#if DebugDeflation + + if (DeflaterConstants.DEBUGGING && (curMatch >= strstart) ) + { + throw new InvalidOperationException("no future"); + } +#endif + if (window[curMatch + best_len] != scan_end || + window[curMatch + best_len - 1] != scan_end1 || + window[curMatch] != window[scan] || + window[curMatch + 1] != window[scan + 1]) { + continue; + } + + match = curMatch + 2; + scan += 2; + + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258. + */ + while ( + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + (scan < strend)) + { + // Do nothing + } + + if (scan > best_end) { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (ins_h == 0) ) + Console.Error.WriteLine("Found match: " + curMatch + "-" + (scan - strstart)); +#endif + matchStart = curMatch; + best_end = scan; + best_len = scan - strstart; + + if (best_len >= niceLength) { + break; + } + + scan_end1 = window[best_end - 1]; + scan_end = window[best_end]; + } + scan = strstart; + } while ((curMatch = (prev[curMatch & WMASK] & 0xffff)) > limit && --chainLength != 0); + + matchLen = Math.Min(best_len, lookahead); + return matchLen >= MIN_MATCH; + } + + bool DeflateStored(bool flush, bool finish) + { + if (!flush && (lookahead == 0)) { + return false; + } + + strstart += lookahead; + lookahead = 0; + + int storedLength = strstart - blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (blockStart < WSIZE && storedLength >= MAX_DIST) || // Block may move out of window + flush) { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + Console.WriteLine("storedBlock[" + storedLength + "," + lastBlock + "]"); + } +#endif + + huffman.FlushStoredBlock(window, blockStart, storedLength, lastBlock); + blockStart += storedLength; + return !lastBlock; + } + return true; + } + + bool DeflateFast(bool flush, bool finish) + { + if (lookahead < MIN_LOOKAHEAD && !flush) { + return false; + } + + while (lookahead >= MIN_LOOKAHEAD || flush) { + if (lookahead == 0) { + // We are flushing everything + huffman.FlushBlock(window, blockStart, strstart - blockStart, finish); + blockStart = strstart; + return false; + } + + if (strstart > 2 * WSIZE - MIN_LOOKAHEAD) { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int hashHead; + if (lookahead >= MIN_MATCH && + (hashHead = InsertString()) != 0 && + strategy != DeflateStrategy.HuffmanOnly && + strstart - hashHead <= MAX_DIST && + FindLongestMatch(hashHead)) { + // longestMatch sets matchStart and matchLen +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart + i] != window[matchStart + i]) { + throw new SharpZipBaseException("Match failure"); + } + } + } +#endif + + bool full = huffman.TallyDist(strstart - matchStart, matchLen); + + lookahead -= matchLen; + if (matchLen <= max_lazy && lookahead >= MIN_MATCH) { + while (--matchLen > 0) { + ++strstart; + InsertString(); + } + ++strstart; + } else { + strstart += matchLen; + if (lookahead >= MIN_MATCH - 1) { + UpdateHash(); + } + } + matchLen = MIN_MATCH - 1; + if (!full) { + continue; + } + } else { + // No match found + huffman.TallyLit(window[strstart] & 0xff); + ++strstart; + --lookahead; + } + + if (huffman.IsFull()) { + bool lastBlock = finish && (lookahead == 0); + huffman.FlushBlock(window, blockStart, strstart - blockStart, lastBlock); + blockStart = strstart; + return !lastBlock; + } + } + return true; + } + + bool DeflateSlow(bool flush, bool finish) + { + if (lookahead < MIN_LOOKAHEAD && !flush) { + return false; + } + + while (lookahead >= MIN_LOOKAHEAD || flush) { + if (lookahead == 0) { + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + prevAvailable = false; + + // We are flushing everything +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && !flush) + { + throw new SharpZipBaseException("Not flushing, but no lookahead"); + } +#endif + huffman.FlushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart >= 2 * WSIZE - MIN_LOOKAHEAD) { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int prevMatch = matchStart; + int prevLen = matchLen; + if (lookahead >= MIN_MATCH) { + + int hashHead = InsertString(); + + if (strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + strstart - hashHead <= MAX_DIST && + FindLongestMatch(hashHead)) { + + // longestMatch sets matchStart and matchLen + + // Discard match if too small and too far away + if (matchLen <= 5 && (strategy == DeflateStrategy.Filtered || (matchLen == MIN_MATCH && strstart - matchStart > TooFar))) { + matchLen = MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= MIN_MATCH) && (matchLen <= prevLen) ) { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart-1+i] != window[prevMatch + i]) + throw new SharpZipBaseException(); + } + } +#endif + huffman.TallyDist(strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do { + strstart++; + lookahead--; + if (lookahead >= MIN_MATCH) { + InsertString(); + } + } while (--prevLen > 0); + + strstart ++; + lookahead--; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + } else { + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + prevAvailable = true; + strstart++; + lookahead--; + } + + if (huffman.IsFull()) { + int len = strstart - blockStart; + if (prevAvailable) { + len--; + } + bool lastBlock = (finish && (lookahead == 0) && !prevAvailable); + huffman.FlushBlock(window, blockStart, len, lastBlock); + blockStart += len; + return !lastBlock; + } + } + return true; + } + + #region Instance Fields + + // Hash index of string to be inserted + int ins_h; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + short[] head; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + short[] prev; + + int matchStart; + // Length of best match + int matchLen; + // Set if previous match exists + bool prevAvailable; + int blockStart; + + /// + /// Points to the current character in the window. + /// + int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + int lookahead; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + byte[] window; + + DeflateStrategy strategy; + int max_chain, max_lazy, niceLength, goodLength; + + /// + /// The current compression function. + /// + int compressionFunction; + + /// + /// The input data for compression. + /// + byte[] inputBuf; + + /// + /// The total bytes of input read. + /// + long totalIn; + + /// + /// The offset into inputBuf, where input data starts. + /// + int inputOff; + + /// + /// The end offset of the input data. + /// + int inputEnd; + + DeflaterPending pending; + DeflaterHuffman huffman; + + /// + /// The adler checksum + /// + Adler32 adler; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs new file mode 100644 index 000000000..b7251e95f --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs @@ -0,0 +1,908 @@ +// DeflaterHuffman.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This is the DeflaterHuffman class. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of Deflate and SetInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterHuffman + { + const int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + const int LITERAL_NUM = 286; + + // Number of distance codes + const int DIST_NUM = 30; + // Number of codes used to transfer bit lengths + const int BITLEN_NUM = 19; + + // repeat previous bit length 3-6 times (2 bits of repeat count) + const int REP_3_6 = 16; + // repeat a zero length 3-10 times (3 bits of repeat count) + const int REP_3_10 = 17; + // repeat a zero length 11-138 times (7 bits of repeat count) + const int REP_11_138 = 18; + + const int EOF_SYMBOL = 256; + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit length codes. + static readonly int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + static readonly byte[] bit4Reverse = { + 0, + 8, + 4, + 12, + 2, + 10, + 6, + 14, + 1, + 9, + 5, + 13, + 3, + 11, + 7, + 15 + }; + + static short[] staticLCodes; + static byte[] staticLLength; + static short[] staticDCodes; + static byte[] staticDLength; + + class Tree + { + #region Instance Fields + public short[] freqs; + + public byte[] length; + + public int minNumCodes; + + public int numCodes; + + short[] codes; + int[] bl_counts; + int maxLength; + DeflaterHuffman dh; + #endregion + + #region Constructors + public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) + { + this.dh = dh; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + freqs = new short[elems]; + bl_counts = new int[maxLength]; + } + + #endregion + + /// + /// Resets the internal state of the tree + /// + public void Reset() + { + for (int i = 0; i < freqs.Length; i++) { + freqs[i] = 0; + } + codes = null; + length = null; + } + + public void WriteSymbol(int code) + { + // if (DeflaterConstants.DEBUGGING) { + // freqs[code]--; + // // Console.Write("writeSymbol("+freqs.length+","+code+"): "); + // } + dh.pending.WriteBits(codes[code] & 0xffff, length[code]); + } + + /// + /// Check that all frequencies are zero + /// + /// + /// At least one frequency is non-zero + /// + public void CheckEmpty() + { + bool empty = true; + for (int i = 0; i < freqs.Length; i++) { + if (freqs[i] != 0) { + //Console.WriteLine("freqs[" + i + "] == " + freqs[i]); + empty = false; + } + } + + if (!empty) { + throw new SharpZipBaseException("!Empty"); + } + } + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) + { + codes = staticCodes; + length = staticLengths; + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + int numSymbols = freqs.Length; + int[] nextCode = new int[maxLength]; + int code = 0; + + codes = new short[freqs.Length]; + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("buildCodes: "+freqs.Length); + // } + + for (int bits = 0; bits < maxLength; bits++) { + nextCode[bits] = code; + code += bl_counts[bits] << (15 - bits); + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("bits: " + ( bits + 1) + " count: " + bl_counts[bits] + // +" nextCode: "+code); + // } + } + +#if DebugDeflation + if ( DeflaterConstants.DEBUGGING && (code != 65536) ) + { + throw new SharpZipBaseException("Inconsistent bl_counts!"); + } +#endif + for (int i=0; i < numCodes; i++) { + int bits = length[i]; + if (bits > 0) { + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("codes["+i+"] = rev(" + nextCode[bits-1]+"), + // +bits); + // } + + codes[i] = BitReverse(nextCode[bits-1]); + nextCode[bits-1] += 1 << (16 - bits); + } + } + } + + public void BuildTree() + { + int numSymbols = freqs.Length; + + /* heap is a priority queue, sorted by frequency, least frequent + * nodes first. The heap is a binary tree, with the property, that + * the parent node is smaller than both child nodes. This assures + * that the smallest node is the first parent. + * + * The binary tree is encoded in an array: 0 is root node and + * the nodes 2*n+1, 2*n+2 are the child nodes of node n. + */ + int[] heap = new int[numSymbols]; + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) { + int freq = freqs[n]; + if (freq != 0) { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && freqs[heap[ppos = (pos - 1) / 2]] > freq) { + heap[pos] = heap[ppos]; + pos = ppos; + } + heap[pos] = n; + + maxCode = n; + } + } + + /* We could encode a single literal with 0 bits but then we + * don't see the literals. Therefore we force at least two + * literals to avoid this case. We don't care about order in + * this case, both literals get a 1 bit code. + */ + while (heapLen < 2) { + int node = maxCode < 2 ? ++maxCode : 0; + heap[heapLen++] = node; + } + + numCodes = Math.Max(maxCode + 1, minNumCodes); + + int numLeafs = heapLen; + int[] childs = new int[4 * heapLen - 2]; + int[] values = new int[2 * heapLen - 1]; + int numNodes = numLeafs; + for (int i = 0; i < heapLen; i++) { + int node = heap[i]; + childs[2 * i] = node; + childs[2 * i + 1] = -1; + values[i] = freqs[node] << 8; + heap[i] = i; + } + + /* Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + do { + int first = heap[0]; + int last = heap[--heapLen]; + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path+1]]) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = path * 2 + 1; + } + + /* Now propagate the last element down along path. Normally + * it shouldn't go too deep. + */ + int lastVal = values[last]; + while ((path = ppos) > 0 && values[heap[ppos = (path - 1)/2]] > lastVal) { + heap[path] = heap[ppos]; + } + heap[path] = last; + + + int second = heap[0]; + + // Create a new node father of first and second + last = numNodes++; + childs[2 * last] = first; + childs[2 * last + 1] = second; + int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); + values[last] = lastVal = values[first] + values[second] - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path+1]]) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = ppos * 2 + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && values[heap[ppos = (path - 1)/2]] > lastVal) { + heap[path] = heap[ppos]; + } + heap[path] = last; + } while (heapLen > 1); + + if (heap[0] != childs.Length / 2 - 1) { + throw new SharpZipBaseException("Heap invariant violated"); + } + + BuildLength(childs); + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < freqs.Length; i++) { + len += freqs[i] * length[i]; + } + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.freqs[nextlen]++; + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + blTree.freqs[curlen] += (short)count; + } else if (curlen != 0) { + blTree.freqs[REP_3_6]++; + } else if (count <= 10) { + blTree.freqs[REP_3_10]++; + } else { + blTree.freqs[REP_11_138]++; + } + } + } + + /// + /// Write tree values + /// + /// Tree to write + public void WriteTree(Tree blTree) + { + int max_count; // max repeat count + int min_count; // min repeat count + int count; // repeat count of the current code + int curlen = -1; // length of current code + + int i = 0; + while (i < numCodes) { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.WriteSymbol(nextlen); + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + while (count-- > 0) { + blTree.WriteSymbol(curlen); + } + } else if (curlen != 0) { + blTree.WriteSymbol(REP_3_6); + dh.pending.WriteBits(count - 3, 2); + } else if (count <= 10) { + blTree.WriteSymbol(REP_3_10); + dh.pending.WriteBits(count - 3, 3); + } else { + blTree.WriteSymbol(REP_11_138); + dh.pending.WriteBits(count - 11, 7); + } + } + } + + void BuildLength(int[] childs) + { + this.length = new byte [freqs.Length]; + int numNodes = childs.Length / 2; + int numLeafs = (numNodes + 1) / 2; + int overflow = 0; + + for (int i = 0; i < maxLength; i++) { + bl_counts[i] = 0; + } + + // First calculate optimal bit lengths + int[] lengths = new int[numNodes]; + lengths[numNodes-1] = 0; + + for (int i = numNodes - 1; i >= 0; i--) { + if (childs[2 * i + 1] != -1) { + int bitLength = lengths[i] + 1; + if (bitLength > maxLength) { + bitLength = maxLength; + overflow++; + } + lengths[childs[2 * i]] = lengths[childs[2 * i + 1]] = bitLength; + } else { + // A leaf node + int bitLength = lengths[i]; + bl_counts[bitLength - 1]++; + this.length[childs[2*i]] = (byte) lengths[i]; + } + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Tree "+freqs.Length+" lengths:"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + + if (overflow == 0) { + return; + } + + int incrBitLen = maxLength - 1; + do { + // Find the first bit length which could increase: + while (bl_counts[--incrBitLen] == 0) + ; + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do { + bl_counts[incrBitLen]--; + bl_counts[++incrBitLen]++; + overflow -= 1 << (maxLength - 1 - incrBitLen); + } while (overflow > 0 && incrBitLen < maxLength - 1); + } while (overflow > 0); + + /* We may have overshot above. Move some nodes from maxLength to + * maxLength-1 in that case. + */ + bl_counts[maxLength-1] += overflow; + bl_counts[maxLength-2] -= overflow; + + /* Now recompute all bit lengths, scanning in increasing + * frequency. It is simpler to reconstruct all lengths instead of + * fixing only the wrong ones. This idea is taken from 'ar' + * written by Haruhiko Okumura. + * + * The nodes were inserted with decreasing frequency into the childs + * array. + */ + int nodePtr = 2 * numLeafs; + for (int bits = maxLength; bits != 0; bits--) { + int n = bl_counts[bits-1]; + while (n > 0) { + int childPtr = 2*childs[nodePtr++]; + if (childs[childPtr + 1] == -1) { + // We found another leaf + length[childs[childPtr]] = (byte) bits; + n--; + } + } + } + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("*** After overflow elimination. ***"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + } + + } + + #region Instance Fields + /// + /// Pending buffer to use + /// + public DeflaterPending pending; + + Tree literalTree; + Tree distTree; + Tree blTree; + + // Buffer for distances + short[] d_buf; + byte[] l_buf; + int last_lit; + int extra_bits; + #endregion + + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + staticLCodes = new short[LITERAL_NUM]; + staticLLength = new byte[LITERAL_NUM]; + + int i = 0; + while (i < 144) { + staticLCodes[i] = BitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + + while (i < 256) { + staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + staticLLength[i++] = 9; + } + + while (i < 280) { + staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + staticLLength[i++] = 7; + } + + while (i < LITERAL_NUM) { + staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + staticLLength[i++] = 8; + } + + // Distance codes + staticDCodes = new short[DIST_NUM]; + staticDLength = new byte[DIST_NUM]; + for (i = 0; i < DIST_NUM; i++) { + staticDCodes[i] = BitReverse(i << 11); + staticDLength[i] = 5; + } + } + + /// + /// Construct instance with pending buffer + /// + /// Pending buffer to use + public DeflaterHuffman(DeflaterPending pending) + { + this.pending = pending; + + literalTree = new Tree(this, LITERAL_NUM, 257, 15); + distTree = new Tree(this, DIST_NUM, 1, 15); + blTree = new Tree(this, BITLEN_NUM, 4, 7); + + d_buf = new short[BUFSIZE]; + l_buf = new byte [BUFSIZE]; + } + + /// + /// Reset internal state + /// + public void Reset() + { + last_lit = 0; + extra_bits = 0; + literalTree.Reset(); + distTree.Reset(); + blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + blTree.BuildCodes(); + literalTree.BuildCodes(); + distTree.BuildCodes(); + pending.WriteBits(literalTree.numCodes - 257, 5); + pending.WriteBits(distTree.numCodes - 1, 5); + pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) { + pending.WriteBits(blTree.length[BL_ORDER[rank]], 3); + } + literalTree.WriteTree(blTree); + distTree.WriteTree(blTree); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + blTree.CheckEmpty(); + } +#endif + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + for (int i = 0; i < last_lit; i++) { + int litlen = l_buf[i] & 0xff; + int dist = d_buf[i]; + if (dist-- != 0) { + // if (DeflaterConstants.DEBUGGING) { + // Console.Write("["+(dist+1)+","+(litlen+3)+"]: "); + // } + + int lc = Lcode(litlen); + literalTree.WriteSymbol(lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) { + pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + distTree.WriteSymbol(dc); + + bits = dc / 2 - 1; + if (bits > 0) { + pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } else { + // if (DeflaterConstants.DEBUGGING) { + // if (litlen > 32 && litlen < 127) { + // Console.Write("("+(char)litlen+"): "); + // } else { + // Console.Write("{"+litlen+"}: "); + // } + // } + literalTree.WriteSymbol(litlen); + } + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.Write("EOF: "); + } +#endif + literalTree.WriteSymbol(EOF_SYMBOL); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + literalTree.CheckEmpty(); + distTree.CheckEmpty(); + } +#endif + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { +#if DebugDeflation + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Flushing stored block "+ storedLength); + // } +#endif + pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + pending.AlignToByte(); + pending.WriteShort(storedLength); + pending.WriteShort(~storedLength); + pending.WriteBlock(stored, storedOffset, storedLength); + Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + literalTree.freqs[EOF_SYMBOL]++; + + // Build trees + literalTree.BuildTree(); + distTree.BuildTree(); + + // Calculate bitlen frequency + literalTree.CalcBLFreq(blTree); + distTree.CalcBLFreq(blTree); + + // Build bitlen tree + blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) { + if (blTree.length[BL_ORDER[i]] > 0) { + blTreeCodes = i+1; + } + } + int opt_len = 14 + blTreeCodes * 3 + blTree.GetEncodedLength() + + literalTree.GetEncodedLength() + distTree.GetEncodedLength() + + extra_bits; + + int static_len = extra_bits; + for (int i = 0; i < LITERAL_NUM; i++) { + static_len += literalTree.freqs[i] * staticLLength[i]; + } + for (int i = 0; i < DIST_NUM; i++) { + static_len += distTree.freqs[i] * staticDLength[i]; + } + if (opt_len >= static_len) { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) { + // Store Block + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Storing, since " + storedLength + " < " + opt_len + // + " <= " + static_len); + // } + FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } else if (opt_len == static_len) { + // Encode with static tree + pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + literalTree.SetStaticCodes(staticLCodes, staticLLength); + distTree.SetStaticCodes(staticDCodes, staticDLength); + CompressBlock(); + Reset(); + } else { + // Encode with dynamic tree + pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + SendAllTrees(blTreeCodes); + CompressBlock(); + Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + public bool IsFull() + { + return last_lit >= BUFSIZE; + } + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + public bool TallyLit(int literal) + { + // if (DeflaterConstants.DEBUGGING) { + // if (lit > 32 && lit < 127) { + // //Console.WriteLine("("+(char)lit+")"); + // } else { + // //Console.WriteLine("{"+lit+"}"); + // } + // } + d_buf[last_lit] = 0; + l_buf[last_lit++] = (byte)literal; + literalTree.freqs[literal]++; + return IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + public bool TallyDist(int distance, int length) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("[" + distance + "," + length + "]"); + // } + + d_buf[last_lit] = (short)distance; + l_buf[last_lit++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + literalTree.freqs[lc]++; + if (lc >= 265 && lc < 285) { + extra_bits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + distTree.freqs[dc]++; + if (dc >= 4) { + extra_bits += dc / 2 - 1; + } + return IsFull(); + } + + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + public static short BitReverse(int toReverse) + { + return (short) (bit4Reverse[toReverse & 0xF] << 12 | + bit4Reverse[(toReverse >> 4) & 0xF] << 8 | + bit4Reverse[(toReverse >> 8) & 0xF] << 4 | + bit4Reverse[toReverse >> 12]); + } + + static int Lcode(int length) + { + if (length == 255) { + return 285; + } + + int code = 257; + while (length >= 8) { + code += 4; + length >>= 1; + } + return code + length; + } + + static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) { + code += 2; + distance >>= 1; + } + return code + distance; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs new file mode 100644 index 000000000..dbdcac6b2 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs @@ -0,0 +1,57 @@ +// DeflaterPending.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class stores the pending output of the Deflater. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterPending : PendingBuffer + { + /// + /// Construct instance with default buffer size + /// + public DeflaterPending() : base(DeflaterConstants.PENDING_BUF_SIZE) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs new file mode 100644 index 000000000..80f98cbaa --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs @@ -0,0 +1,864 @@ +// Inflater.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// Inflater is used to decompress data that has been compressed according + /// to the "deflate" standard described in rfc1951. + /// + /// By default Zlib (rfc1950) headers and footers are expected in the input. + /// You can use constructor public Inflater(bool noHeader) passing true + /// if there is no Zlib header information + /// + /// The usage is as following. First you have to set some input with + /// SetInput(), then Inflate() it. If inflate doesn't + /// inflate any bytes there may be three reasons: + ///
    + ///
  • IsNeedingInput() returns true because the input buffer is empty. + /// You have to provide more input with SetInput(). + /// NOTE: IsNeedingInput() also returns true when, the stream is finished. + ///
  • + ///
  • IsNeedingDictionary() returns true, you have to provide a preset + /// dictionary with SetDictionary().
  • + ///
  • IsFinished returns true, the inflater has finished.
  • + ///
+ /// Once the first output byte is produced, a dictionary will not be + /// needed at a later stage. + /// + /// author of the original java version : John Leuner, Jochen Hoenicke + ///
+ public class Inflater + { + #region Constants/Readonly + /// + /// Copy lengths for literal codes 257..285 + /// + static readonly int[] CPLENS = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 + }; + + /// + /// Extra bits for literal codes 257..285 + /// + static readonly int[] CPLEXT = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 + }; + + /// + /// Copy offsets for distance codes 0..29 + /// + static readonly int[] CPDIST = { + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + }; + + /// + /// Extra bits for distance codes + /// + static readonly int[] CPDEXT = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 + }; + + /// + /// These are the possible states for an inflater + /// + const int DECODE_HEADER = 0; + const int DECODE_DICT = 1; + const int DECODE_BLOCKS = 2; + const int DECODE_STORED_LEN1 = 3; + const int DECODE_STORED_LEN2 = 4; + const int DECODE_STORED = 5; + const int DECODE_DYN_HEADER = 6; + const int DECODE_HUFFMAN = 7; + const int DECODE_HUFFMAN_LENBITS = 8; + const int DECODE_HUFFMAN_DIST = 9; + const int DECODE_HUFFMAN_DISTBITS = 10; + const int DECODE_CHKSUM = 11; + const int FINISHED = 12; + #endregion + + #region Instance Fields + /// + /// This variable contains the current state. + /// + int mode; + + /// + /// The adler checksum of the dictionary or of the decompressed + /// stream, as it is written in the header resp. footer of the + /// compressed stream. + /// Only valid if mode is DECODE_DICT or DECODE_CHKSUM. + /// + int readAdler; + + /// + /// The number of bits needed to complete the current state. This + /// is valid, if mode is DECODE_DICT, DECODE_CHKSUM, + /// DECODE_HUFFMAN_LENBITS or DECODE_HUFFMAN_DISTBITS. + /// + int neededBits; + int repLength; + int repDist; + int uncomprLen; + + /// + /// True, if the last block flag was set in the last block of the + /// inflated stream. This means that the stream ends after the + /// current block. + /// + bool isLastBlock; + + /// + /// The total number of inflated bytes. + /// + long totalOut; + + /// + /// The total number of bytes set with setInput(). This is not the + /// value returned by the TotalIn property, since this also includes the + /// unprocessed input. + /// + long totalIn; + + /// + /// This variable stores the noHeader flag that was given to the constructor. + /// True means, that the inflated stream doesn't contain a Zlib header or + /// footer. + /// + bool noHeader; + + StreamManipulator input; + OutputWindow outputWindow; + InflaterDynHeader dynHeader; + InflaterHuffmanTree litlenTree, distTree; + Adler32 adler; + #endregion + + #region Constructors + /// + /// Creates a new inflater or RFC1951 decompressor + /// RFC1950/Zlib headers and footers will be expected in the input data + /// + public Inflater() : this(false) + { + } + + /// + /// Creates a new inflater. + /// + /// + /// True if no RFC1950/Zlib header and footer fields are expected in the input data + /// + /// This is used for GZIPed/Zipped input. + /// + /// For compatibility with + /// Sun JDK you should provide one byte of input more than needed in + /// this case. + /// + public Inflater(bool noHeader) + { + this.noHeader = noHeader; + this.adler = new Adler32(); + input = new StreamManipulator(); + outputWindow = new OutputWindow(); + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + } + #endregion + + /// + /// Resets the inflater so that a new stream can be decompressed. All + /// pending input and output will be discarded. + /// + public void Reset() + { + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + totalIn = 0; + totalOut = 0; + input.Reset(); + outputWindow.Reset(); + dynHeader = null; + litlenTree = null; + distTree = null; + isLastBlock = false; + adler.Reset(); + } + + /// + /// Decodes a zlib/RFC1950 header. + /// + /// + /// False if more input is needed. + /// + /// + /// The header is invalid. + /// + private bool DecodeHeader() + { + int header = input.PeekBits(16); + if (header < 0) { + return false; + } + input.DropBits(16); + + // The header is written in "wrong" byte order + header = ((header << 8) | (header >> 8)) & 0xffff; + if (header % 31 != 0) { + throw new SharpZipBaseException("Header checksum illegal"); + } + + if ((header & 0x0f00) != (Deflater.DEFLATED << 8)) { + throw new SharpZipBaseException("Compression Method unknown"); + } + + /* Maximum size of the backwards window in bits. + * We currently ignore this, but we could use it to make the + * inflater window more space efficient. On the other hand the + * full window (15 bits) is needed most times, anyway. + int max_wbits = ((header & 0x7000) >> 12) + 8; + */ + + if ((header & 0x0020) == 0) { // Dictionary flag? + mode = DECODE_BLOCKS; + } else { + mode = DECODE_DICT; + neededBits = 32; + } + return true; + } + + /// + /// Decodes the dictionary checksum after the deflate header. + /// + /// + /// False if more input is needed. + /// + private bool DecodeDict() + { + while (neededBits > 0) { + int dictByte = input.PeekBits(8); + if (dictByte < 0) { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | dictByte; + neededBits -= 8; + } + return false; + } + + /// + /// Decodes the huffman encoded symbols in the input stream. + /// + /// + /// false if more input is needed, true if output window is + /// full or the current block ends. + /// + /// + /// if deflated stream is invalid. + /// + private bool DecodeHuffman() + { + int free = outputWindow.GetFreeSpace(); + while (free >= 258) + { + int symbol; + switch (mode) + { + case DECODE_HUFFMAN: + // This is the inner loop so it is optimized a bit + while (((symbol = litlenTree.GetSymbol(input)) & ~0xff) == 0) + { + outputWindow.Write(symbol); + if (--free < 258) + { + return true; + } + } + + if (symbol < 257) + { + if (symbol < 0) + { + return false; + } + else + { + // symbol == 256: end of block + distTree = null; + litlenTree = null; + mode = DECODE_BLOCKS; + return true; + } + } + + try + { + repLength = CPLENS[symbol - 257]; + neededBits = CPLEXT[symbol - 257]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep length code"); + } + goto case DECODE_HUFFMAN_LENBITS; // fall through + + case DECODE_HUFFMAN_LENBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_LENBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repLength += i; + } + mode = DECODE_HUFFMAN_DIST; + goto case DECODE_HUFFMAN_DIST; // fall through + + case DECODE_HUFFMAN_DIST: + symbol = distTree.GetSymbol(input); + if (symbol < 0) + { + return false; + } + + try + { + repDist = CPDIST[symbol]; + neededBits = CPDEXT[symbol]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep dist code"); + } + + goto case DECODE_HUFFMAN_DISTBITS; // fall through + + case DECODE_HUFFMAN_DISTBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_DISTBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repDist += i; + } + + outputWindow.Repeat(repLength, repDist); + free -= repLength; + mode = DECODE_HUFFMAN; + break; + + default: + throw new SharpZipBaseException("Inflater unknown mode"); + } + } + return true; + } + + /// + /// Decodes the adler checksum after the deflate stream. + /// + /// + /// false if more input is needed. + /// + /// + /// If checksum doesn't match. + /// + private bool DecodeChksum() + { + while (neededBits > 0) { + int chkByte = input.PeekBits(8); + if (chkByte < 0) { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | chkByte; + neededBits -= 8; + } + + if ((int) adler.Value != readAdler) { + throw new SharpZipBaseException("Adler chksum doesn't match: " + (int)adler.Value + " vs. " + readAdler); + } + + mode = FINISHED; + return false; + } + + /// + /// Decodes the deflated stream. + /// + /// + /// false if more input is needed, or if finished. + /// + /// + /// if deflated stream is invalid. + /// + private bool Decode() + { + switch (mode) { + case DECODE_HEADER: + return DecodeHeader(); + + case DECODE_DICT: + return DecodeDict(); + + case DECODE_CHKSUM: + return DecodeChksum(); + + case DECODE_BLOCKS: + if (isLastBlock) { + if (noHeader) { + mode = FINISHED; + return false; + } else { + input.SkipToByteBoundary(); + neededBits = 32; + mode = DECODE_CHKSUM; + return true; + } + } + + int type = input.PeekBits(3); + if (type < 0) { + return false; + } + input.DropBits(3); + + if ((type & 1) != 0) { + isLastBlock = true; + } + switch (type >> 1){ + case DeflaterConstants.STORED_BLOCK: + input.SkipToByteBoundary(); + mode = DECODE_STORED_LEN1; + break; + case DeflaterConstants.STATIC_TREES: + litlenTree = InflaterHuffmanTree.defLitLenTree; + distTree = InflaterHuffmanTree.defDistTree; + mode = DECODE_HUFFMAN; + break; + case DeflaterConstants.DYN_TREES: + dynHeader = new InflaterDynHeader(); + mode = DECODE_DYN_HEADER; + break; + default: + throw new SharpZipBaseException("Unknown block type " + type); + } + return true; + + case DECODE_STORED_LEN1: + { + if ((uncomprLen = input.PeekBits(16)) < 0) { + return false; + } + input.DropBits(16); + mode = DECODE_STORED_LEN2; + } + goto case DECODE_STORED_LEN2; // fall through + + case DECODE_STORED_LEN2: + { + int nlen = input.PeekBits(16); + if (nlen < 0) { + return false; + } + input.DropBits(16); + if (nlen != (uncomprLen ^ 0xffff)) { + throw new SharpZipBaseException("broken uncompressed block"); + } + mode = DECODE_STORED; + } + goto case DECODE_STORED; // fall through + + case DECODE_STORED: + { + int more = outputWindow.CopyStored(input, uncomprLen); + uncomprLen -= more; + if (uncomprLen == 0) { + mode = DECODE_BLOCKS; + return true; + } + return !input.IsNeedingInput; + } + + case DECODE_DYN_HEADER: + if (!dynHeader.Decode(input)) { + return false; + } + + litlenTree = dynHeader.BuildLitLenTree(); + distTree = dynHeader.BuildDistTree(); + mode = DECODE_HUFFMAN; + goto case DECODE_HUFFMAN; // fall through + + case DECODE_HUFFMAN: + case DECODE_HUFFMAN_LENBITS: + case DECODE_HUFFMAN_DIST: + case DECODE_HUFFMAN_DISTBITS: + return DecodeHuffman(); + + case FINISHED: + return false; + + default: + throw new SharpZipBaseException("Inflater.Decode unknown mode"); + } + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + public void SetDictionary(byte[] buffer) + { + SetDictionary(buffer, 0, buffer.Length); + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + /// + /// The index into buffer where the dictionary starts. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// No dictionary is needed. + /// + /// + /// The adler checksum for the buffer is invalid + /// + public void SetDictionary(byte[] buffer, int index, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( index < 0 ) { + throw new ArgumentOutOfRangeException("index"); + } + + if ( count < 0 ) { + throw new ArgumentOutOfRangeException("count"); + } + + if (!IsNeedingDictionary) { + throw new InvalidOperationException("Dictionary is not needed"); + } + + adler.Update(buffer, index, count); + + if ((int)adler.Value != readAdler) { + throw new SharpZipBaseException("Wrong adler checksum"); + } + adler.Reset(); + outputWindow.CopyDict(buffer, index, count); + mode = DECODE_BLOCKS; + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// the input. + /// + public void SetInput(byte[] buffer) + { + SetInput(buffer, 0, buffer.Length); + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// The source of input data + /// + /// + /// The index into buffer where the input starts. + /// + /// + /// The number of bytes of input to use. + /// + /// + /// No input is needed. + /// + /// + /// The index and/or count are wrong. + /// + public void SetInput(byte[] buffer, int index, int count) + { + input.SetInput(buffer, index, count); + totalIn += (long)count; + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether IsNeedingDictionary(), + /// IsNeedingInput() or IsFinished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// The number of bytes written to the buffer, 0 if no further + /// output can be produced. + /// + /// + /// if buffer has length 0. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + return Inflate(buffer, 0, buffer.Length); + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether needsDictionary(), + /// needsInput() or finished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// the offset in buffer where storing starts. + /// + /// + /// the maximum number of bytes to output. + /// + /// + /// the number of bytes written to the buffer, 0 if no further output can be produced. + /// + /// + /// if count is less than 0. + /// + /// + /// if the index and / or count are wrong. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer, int offset, int count) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "count cannot be negative"); +#endif + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "offset cannot be negative"); +#endif + } + + if ( offset + count > buffer.Length ) { + throw new ArgumentException("count exceeds buffer bounds"); + } + + // Special case: count may be zero + if (count == 0) + { + if (!IsFinished) { // -jr- 08-Nov-2003 INFLATE_BUG fix.. + Decode(); + } + return 0; + } + + int bytesCopied = 0; + + do { + if (mode != DECODE_CHKSUM) { + /* Don't give away any output, if we are waiting for the + * checksum in the input stream. + * + * With this trick we have always: + * IsNeedingInput() and not IsFinished() + * implies more output can be produced. + */ + int more = outputWindow.CopyOutput(buffer, offset, count); + if ( more > 0 ) { + adler.Update(buffer, offset, more); + offset += more; + bytesCopied += more; + totalOut += (long)more; + count -= more; + if (count == 0) { + return bytesCopied; + } + } + } + } while (Decode() || ((outputWindow.GetAvailable() > 0) && (mode != DECODE_CHKSUM))); + return bytesCopied; + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method also returns true when the stream is finished. + /// + public bool IsNeedingInput { + get { + return input.IsNeedingInput; + } + } + + /// + /// Returns true, if a preset dictionary is needed to inflate the input. + /// + public bool IsNeedingDictionary { + get { + return mode == DECODE_DICT && neededBits == 0; + } + } + + /// + /// Returns true, if the inflater has finished. This means, that no + /// input is needed and no output can be produced. + /// + public bool IsFinished { + get { + return mode == FINISHED && outputWindow.GetAvailable() == 0; + } + } + + /// + /// Gets the adler checksum. This is either the checksum of all + /// uncompressed bytes returned by inflate(), or if needsDictionary() + /// returns true (and thus no output was yet produced) this is the + /// adler checksum of the expected dictionary. + /// + /// + /// the adler checksum. + /// + public int Adler { + get { + return IsNeedingDictionary ? readAdler : (int) adler.Value; + } + } + + /// + /// Gets the total number of output bytes returned by Inflate(). + /// + /// + /// the total number of output bytes. + /// + public long TotalOut { + get { + return totalOut; + } + } + + /// + /// Gets the total number of processed compressed input bytes. + /// + /// + /// The total number of bytes of processed input bytes. + /// + public long TotalIn { + get { + return totalIn - (long)RemainingInput; + } + } + + /// + /// Gets the number of unprocessed input bytes. Useful, if the end of the + /// stream is reached and you want to further process the bytes after + /// the deflate stream. + /// + /// + /// The number of bytes of the input which have not been processed. + /// + public int RemainingInput { + // TODO: This should be a long? + get { + return input.AvailableBytes; + } + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs new file mode 100644 index 000000000..cb019b8b5 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs @@ -0,0 +1,218 @@ +// InflaterDynHeader.cs +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + class InflaterDynHeader + { + #region Constants + const int LNUM = 0; + const int DNUM = 1; + const int BLNUM = 2; + const int BLLENS = 3; + const int LENS = 4; + const int REPS = 5; + + static readonly int[] repMin = { 3, 3, 11 }; + static readonly int[] repBits = { 2, 3, 7 }; + + static readonly int[] BL_ORDER = + { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + #endregion + + #region Constructors + public InflaterDynHeader() + { + } + #endregion + + public bool Decode(StreamManipulator input) + { + decode_loop: + for (;;) { + switch (mode) { + case LNUM: + lnum = input.PeekBits(5); + if (lnum < 0) { + return false; + } + lnum += 257; + input.DropBits(5); + // System.err.println("LNUM: "+lnum); + mode = DNUM; + goto case DNUM; // fall through + case DNUM: + dnum = input.PeekBits(5); + if (dnum < 0) { + return false; + } + dnum++; + input.DropBits(5); + // System.err.println("DNUM: "+dnum); + num = lnum+dnum; + litdistLens = new byte[num]; + mode = BLNUM; + goto case BLNUM; // fall through + case BLNUM: + blnum = input.PeekBits(4); + if (blnum < 0) { + return false; + } + blnum += 4; + input.DropBits(4); + blLens = new byte[19]; + ptr = 0; + // System.err.println("BLNUM: "+blnum); + mode = BLLENS; + goto case BLLENS; // fall through + case BLLENS: + while (ptr < blnum) { + int len = input.PeekBits(3); + if (len < 0) { + return false; + } + input.DropBits(3); + // System.err.println("blLens["+BL_ORDER[ptr]+"]: "+len); + blLens[BL_ORDER[ptr]] = (byte) len; + ptr++; + } + blTree = new InflaterHuffmanTree(blLens); + blLens = null; + ptr = 0; + mode = LENS; + goto case LENS; // fall through + case LENS: + { + int symbol; + while (((symbol = blTree.GetSymbol(input)) & ~15) == 0) { + /* Normal case: symbol in [0..15] */ + + // System.err.println("litdistLens["+ptr+"]: "+symbol); + litdistLens[ptr++] = lastLen = (byte)symbol; + + if (ptr == num) { + /* Finished */ + return true; + } + } + + /* need more input ? */ + if (symbol < 0) { + return false; + } + + /* otherwise repeat code */ + if (symbol >= 17) { + /* repeat zero */ + // System.err.println("repeating zero"); + lastLen = 0; + } else { + if (ptr == 0) { + throw new SharpZipBaseException(); + } + } + repSymbol = symbol-16; + } + mode = REPS; + goto case REPS; // fall through + case REPS: + { + int bits = repBits[repSymbol]; + int count = input.PeekBits(bits); + if (count < 0) { + return false; + } + input.DropBits(bits); + count += repMin[repSymbol]; + // System.err.println("litdistLens repeated: "+count); + + if (ptr + count > num) { + throw new SharpZipBaseException(); + } + while (count-- > 0) { + litdistLens[ptr++] = lastLen; + } + + if (ptr == num) { + /* Finished */ + return true; + } + } + mode = LENS; + goto decode_loop; + } + } + } + + public InflaterHuffmanTree BuildLitLenTree() + { + byte[] litlenLens = new byte[lnum]; + Array.Copy(litdistLens, 0, litlenLens, 0, lnum); + return new InflaterHuffmanTree(litlenLens); + } + + public InflaterHuffmanTree BuildDistTree() + { + byte[] distLens = new byte[dnum]; + Array.Copy(litdistLens, lnum, distLens, 0, dnum); + return new InflaterHuffmanTree(distLens); + } + + #region Instance Fields + byte[] blLens; + byte[] litdistLens; + + InflaterHuffmanTree blTree; + + /// + /// The current decode mode + /// + int mode; + int lnum, dnum, blnum, num; + int repSymbol; + byte lastLen; + int ptr; + #endregion + + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs new file mode 100644 index 000000000..e1467fa82 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs @@ -0,0 +1,232 @@ +// InflaterHuffmanTree.cs +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// Huffman tree used for inflation + /// + public class InflaterHuffmanTree + { + #region Constants + const int MAX_BITLEN = 15; + #endregion + + #region Instance Fields + short[] tree; + #endregion + + /// + /// Literal length tree + /// + public static InflaterHuffmanTree defLitLenTree; + + /// + /// Distance tree + /// + public static InflaterHuffmanTree defDistTree; + + static InflaterHuffmanTree() + { + try { + byte[] codeLengths = new byte[288]; + int i = 0; + while (i < 144) { + codeLengths[i++] = 8; + } + while (i < 256) { + codeLengths[i++] = 9; + } + while (i < 280) { + codeLengths[i++] = 7; + } + while (i < 288) { + codeLengths[i++] = 8; + } + defLitLenTree = new InflaterHuffmanTree(codeLengths); + + codeLengths = new byte[32]; + i = 0; + while (i < 32) { + codeLengths[i++] = 5; + } + defDistTree = new InflaterHuffmanTree(codeLengths); + } catch (Exception) { + throw new SharpZipBaseException("InflaterHuffmanTree: static tree length illegal"); + } + } + + #region Constructors + /// + /// Constructs a Huffman tree from the array of code lengths. + /// + /// + /// the array of code lengths + /// + public InflaterHuffmanTree(byte[] codeLengths) + { + BuildTree(codeLengths); + } + #endregion + + void BuildTree(byte[] codeLengths) + { + int[] blCount = new int[MAX_BITLEN + 1]; + int[] nextCode = new int[MAX_BITLEN + 1]; + + for (int i = 0; i < codeLengths.Length; i++) { + int bits = codeLengths[i]; + if (bits > 0) { + blCount[bits]++; + } + } + + int code = 0; + int treeSize = 512; + for (int bits = 1; bits <= MAX_BITLEN; bits++) { + nextCode[bits] = code; + code += blCount[bits] << (16 - bits); + if (bits >= 10) { + /* We need an extra table for bit lengths >= 10. */ + int start = nextCode[bits] & 0x1ff80; + int end = code & 0x1ff80; + treeSize += (end - start) >> (16 - bits); + } + } + +/* -jr comment this out! doesnt work for dynamic trees and pkzip 2.04g + if (code != 65536) + { + throw new SharpZipBaseException("Code lengths don't add up properly."); + } +*/ + /* Now create and fill the extra tables from longest to shortest + * bit len. This way the sub trees will be aligned. + */ + tree = new short[treeSize]; + int treePtr = 512; + for (int bits = MAX_BITLEN; bits >= 10; bits--) { + int end = code & 0x1ff80; + code -= blCount[bits] << (16 - bits); + int start = code & 0x1ff80; + for (int i = start; i < end; i += 1 << 7) { + tree[DeflaterHuffman.BitReverse(i)] = (short) ((-treePtr << 4) | bits); + treePtr += 1 << (bits-9); + } + } + + for (int i = 0; i < codeLengths.Length; i++) { + int bits = codeLengths[i]; + if (bits == 0) { + continue; + } + code = nextCode[bits]; + int revcode = DeflaterHuffman.BitReverse(code); + if (bits <= 9) { + do { + tree[revcode] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < 512); + } else { + int subTree = tree[revcode & 511]; + int treeLen = 1 << (subTree & 15); + subTree = -(subTree >> 4); + do { + tree[subTree | (revcode >> 9)] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < treeLen); + } + nextCode[bits] = code + (1 << (16 - bits)); + } + + } + + /// + /// Reads the next symbol from input. The symbol is encoded using the + /// huffman tree. + /// + /// + /// input the input source. + /// + /// + /// the next symbol, or -1 if not enough input is available. + /// + public int GetSymbol(StreamManipulator input) + { + int lookahead, symbol; + if ((lookahead = input.PeekBits(9)) >= 0) { + if ((symbol = tree[lookahead]) >= 0) { + input.DropBits(symbol & 15); + return symbol >> 4; + } + int subtree = -(symbol >> 4); + int bitlen = symbol & 15; + if ((lookahead = input.PeekBits(bitlen)) >= 0) { + symbol = tree[subtree | (lookahead >> 9)]; + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[subtree | (lookahead >> 9)]; + if ((symbol & 15) <= bits) { + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } else { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[lookahead]; + if (symbol >= 0 && (symbol & 15) <= bits) { + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } + } +} + diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs new file mode 100644 index 000000000..1ea1fb4b2 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs @@ -0,0 +1,295 @@ +// PendingBuffer.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class is general purpose class for writing data to a buffer. + /// + /// It allows you to write bits as well as bytes + /// Based on DeflaterPending.java + /// + /// author of the original java version : Jochen Hoenicke + /// + public class PendingBuffer + { + #region Instance Fields + /// + /// Internal work buffer + /// + byte[] buffer_; + + int start; + int end; + + uint bits; + int bitCount; + #endregion + + #region Constructors + /// + /// construct instance using default buffer size of 4096 + /// + public PendingBuffer() : this( 4096 ) + { + } + + /// + /// construct instance using specified buffer size + /// + /// + /// size to use for internal buffer + /// + public PendingBuffer(int bufferSize) + { + buffer_ = new byte[bufferSize]; + } + + #endregion + + /// + /// Clear internal state/buffers + /// + public void Reset() + { + start = end = bitCount = 0; + } + + /// + /// Write a byte to buffer + /// + /// + /// The value to write + /// + public void WriteByte(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + } + + /// + /// Write a short value to buffer LSB first + /// + /// + /// The value to write. + /// + public void WriteShort(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + buffer_[end++] = unchecked((byte) (value >> 8)); + } + + /// + /// write an integer LSB first + /// + /// The value to write. + public void WriteInt(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + buffer_[end++] = unchecked((byte) (value >> 8)); + buffer_[end++] = unchecked((byte) (value >> 16)); + buffer_[end++] = unchecked((byte) (value >> 24)); + } + + /// + /// Write a block of data to buffer + /// + /// data to write + /// offset of first byte to write + /// number of bytes to write + public void WriteBlock(byte[] block, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + System.Array.Copy(block, offset, buffer_, end, length); + end += length; + } + + /// + /// The number of bits written to the buffer + /// + public int BitCount { + get { + return bitCount; + } + } + + /// + /// Align internal buffer on a byte boundary + /// + public void AlignToByte() + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + if (bitCount > 0) + { + buffer_[end++] = unchecked((byte) bits); + if (bitCount > 8) { + buffer_[end++] = unchecked((byte) (bits >> 8)); + } + } + bits = 0; + bitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + public void WriteBits(int b, int count) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("writeBits("+b+","+count+")"); + // } +#endif + bits |= (uint)(b << bitCount); + bitCount += count; + if (bitCount >= 16) { + buffer_[end++] = unchecked((byte) bits); + buffer_[end++] = unchecked((byte) (bits >> 8)); + bits >>= 16; + bitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// value to write + public void WriteShortMSB(int s) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) (s >> 8)); + buffer_[end++] = unchecked((byte) s); + } + + /// + /// Indicates if buffer has been flushed + /// + public bool IsFlushed { + get { + return end == 0; + } + } + + /// + /// Flushes the pending buffer into the given output array. If the + /// output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (bitCount >= 8) { + buffer_[end++] = unchecked((byte) bits); + bits >>= 8; + bitCount -= 8; + } + + if (length > end - start) { + length = end - start; + System.Array.Copy(buffer_, start, output, offset, length); + start = 0; + end = 0; + } else { + System.Array.Copy(buffer_, start, output, offset, length); + start += length; + } + return length; + } + + /// + /// Convert internal buffer to byte array. + /// Buffer is empty on completion + /// + /// + /// The internal buffer contents converted to a byte array. + /// + public byte[] ToByteArray() + { + byte[] result = new byte[end - start]; + System.Array.Copy(buffer_, start, result, 0, result.Length); + start = 0; + end = 0; + return result; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs new file mode 100644 index 000000000..9adb557c0 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -0,0 +1,602 @@ +// DeflaterOutputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support + +using System; +using System.IO; + +#if !NETCF_1_0 +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating.
+ /// Authors of the original java version : Tom Tromey, Jochen Hoenicke + ///
+ public class DeflaterOutputStream : Stream + { + #region Constructors + /// + /// Creates a new DeflaterOutputStream with a default Deflater and default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + public DeflaterOutputStream(Stream baseOutputStream) + : this(baseOutputStream, new Deflater(), 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + /// + /// the underlying deflater. + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater) + : this(baseOutputStream, deflater, 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// buffer size. + /// + /// + /// The output stream where deflated output is written. + /// + /// + /// The underlying deflater to use + /// + /// + /// The buffer size in bytes to use when deflating (minimum value 512) + /// + /// + /// bufsize is less than or equal to zero. + /// + /// + /// baseOutputStream does not support writing + /// + /// + /// deflater instance is null + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater, int bufferSize) + { + if ( baseOutputStream == null ) { + throw new ArgumentNullException("baseOutputStream"); + } + + if (baseOutputStream.CanWrite == false) { + throw new ArgumentException("Must support writing", "baseOutputStream"); + } + + if (deflater == null) { + throw new ArgumentNullException("deflater"); + } + + if (bufferSize < 512) { + throw new ArgumentOutOfRangeException("bufferSize"); + } + + baseOutputStream_ = baseOutputStream; + buffer_ = new byte[bufferSize]; + deflater_ = deflater; + } + #endregion + + #region Public API + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// + /// Not all input is deflated + /// + public virtual void Finish() + { + deflater_.Finish(); + while (!deflater_.IsFinished) { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) { + break; + } + +#if NETCF_1_0 + if ( keys != null ) { +#else + if (cryptoTransform_ != null) { +#endif + EncryptBlock(buffer_, 0, len); + } + + baseOutputStream_.Write(buffer_, 0, len); + } + + if (!deflater_.IsFinished) { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + baseOutputStream_.Flush(); + +#if NETCF_1_0 + if ( keys != null ) { + keys = null; + } +#else + if (cryptoTransform_ != null) { +#if !NET_1_1 && !NETCF_2_0 + if (cryptoTransform_ is ZipAESTransform) { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } +#endif + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } +#endif + } + + /// + /// Get/set flag indicating ownership of the underlying stream. + /// When the flag is true will close the underlying stream also. + /// + public bool IsStreamOwner + { + get { return isStreamOwner_; } + set { isStreamOwner_ = value; } + } + + /// + /// Allows client to determine if an entry can be patched after its added + /// + public bool CanPatchEntries { + get { + return baseOutputStream_.CanSeek; + } + } + + #endregion + + #region Encryption + + string password; + +#if NETCF_1_0 + uint[] keys; +#else + ICryptoTransform cryptoTransform_; + + /// + /// Returns the 10 byte AUTH CODE to be appended immediately following the AES data stream. + /// + protected byte[] AESAuthCode; +#endif + + /// + /// Get/set the password used for encryption. + /// + /// When set to null or if the password is empty no encryption is performed + public string Password { + get { + return password; + } + set { + if ( (value != null) && (value.Length == 0) ) { + password = null; + } else { + password = value; + } + } + } + + /// + /// Encrypt a block of data + /// + /// + /// Data to encrypt. NOTE the original contents of the buffer are lost + /// + /// + /// Offset of first byte in buffer to encrypt + /// + /// + /// Number of bytes in buffer to encrypt + /// + protected void EncryptBlock(byte[] buffer, int offset, int length) + { +#if NETCF_1_0 + for (int i = offset; i < offset + length; ++i) { + byte oldbyte = buffer[i]; + buffer[i] ^= EncryptByte(); + UpdateKeys(oldbyte); + } +#else + cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); +#endif + } + + /// + /// Initializes encryption keys based on given . + /// + /// The password. + protected void InitializePassword(string password) + { +#if NETCF_1_0 + keys = new uint[] { + 0x12345678, + 0x23456789, + 0x34567890 + }; + + byte[] rawPassword = ZipConstants.ConvertToArray(password); + + for (int i = 0; i < rawPassword.Length; ++i) { + UpdateKeys((byte)rawPassword[i]); + } + +#else + PkzipClassicManaged pkManaged = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(password)); + cryptoTransform_ = pkManaged.CreateEncryptor(key, null); +#endif + } + +#if !NET_1_1 && !NETCF_2_0 + /// + /// Initializes encryption keys based on given password. + /// + protected void InitializeAESPassword(ZipEntry entry, string rawPassword, + out byte[] salt, out byte[] pwdVerifier) { + salt = new byte[entry.AESSaltLen]; + // Salt needs to be cryptographically random, and unique per file + if (_aesRnd == null) + _aesRnd = new RNGCryptoServiceProvider(); + _aesRnd.GetBytes(salt); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); + pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; + } +#endif + +#if NETCF_1_0 + + /// + /// Encrypt a single byte + /// + /// + /// The encrypted value + /// + protected byte EncryptByte() + { + uint temp = ((keys[2] & 0xFFFF) | 2); + return (byte)((temp * (temp ^ 1)) >> 8); + } + + /// + /// Update encryption keys + /// + protected void UpdateKeys(byte ch) + { + keys[0] = Crc32.ComputeCrc32(keys[0], ch); + keys[1] = keys[1] + (byte)keys[0]; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = Crc32.ComputeCrc32(keys[2], (byte)(keys[1] >> 24)); + } +#endif + + #endregion + + #region Deflation Support + /// + /// Deflates everything in the input buffers. This will call + /// def.deflate() until all bytes from the input buffers + /// are processed. + /// + protected void Deflate() + { + while (!deflater_.IsNeedingInput) + { + int deflateCount = deflater_.Deflate(buffer_, 0, buffer_.Length); + + if (deflateCount <= 0) { + break; + } +#if NETCF_1_0 + if (keys != null) +#else + if (cryptoTransform_ != null) +#endif + { + EncryptBlock(buffer_, 0, deflateCount); + } + + baseOutputStream_.Write(buffer_, 0, deflateCount); + } + + if (!deflater_.IsNeedingInput) { + throw new SharpZipBaseException("DeflaterOutputStream can't deflate all input?"); + } + } + #endregion + + #region Stream Overrides + /// + /// Gets value indicating stream can be read from + /// + public override bool CanRead + { + get { + return false; + } + } + + /// + /// Gets a value indicating if seeking is supported for this stream + /// This property always returns false + /// + public override bool CanSeek { + get { + return false; + } + } + + /// + /// Get value indicating if this stream supports writing + /// + public override bool CanWrite { + get { + return baseOutputStream_.CanWrite; + } + } + + /// + /// Get current length of stream + /// + public override long Length { + get { + return baseOutputStream_.Length; + } + } + + /// + /// Gets the current position within the stream. + /// + /// Any attempt to set position + public override long Position { + get { + return baseOutputStream_.Position; + } + set { + throw new NotSupportedException("Position property not supported"); + } + } + + /// + /// Sets the current position of this stream to the given value. Not supported by this class! + /// + /// The offset relative to the to seek. + /// The to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("DeflaterOutputStream Seek not supported"); + } + + /// + /// Sets the length of this stream to the given value. Not supported by this class! + /// + /// The new stream length. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("DeflaterOutputStream SetLength not supported"); + } + + /// + /// Read a byte from stream advancing position by one + /// + /// The byte read cast to an int. THe value is -1 if at the end of the stream. + /// Any access + public override int ReadByte() + { + throw new NotSupportedException("DeflaterOutputStream ReadByte not supported"); + } + + /// + /// Read a block of bytes from stream + /// + /// The buffer to store read data in. + /// The offset to start storing at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. Zero if end of stream is detected. + /// Any access + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("DeflaterOutputStream Read not supported"); + } + + /// + /// Asynchronous reads are not supported a NotSupportedException is always thrown + /// + /// The buffer to read into. + /// The offset to start storing data at. + /// The number of bytes to read + /// The async callback to use. + /// The state to use. + /// Returns an + /// Any access + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("DeflaterOutputStream BeginRead not currently supported"); + } + + /// + /// Asynchronous writes arent supported, a NotSupportedException is always thrown + /// + /// The buffer to write. + /// The offset to begin writing at. + /// The number of bytes to write. + /// The to use. + /// The state object. + /// Returns an IAsyncResult. + /// Any access + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("BeginWrite is not supported"); + } + + /// + /// Flushes the stream by calling Flush on the deflater and then + /// on the underlying stream. This ensures that all bytes are flushed. + /// + public override void Flush() + { + deflater_.Flush(); + Deflate(); + baseOutputStream_.Flush(); + } + + /// + /// Calls and closes the underlying + /// stream when is true. + /// + public override void Close() + { + if ( !isClosed_ ) { + isClosed_ = true; + + try { + Finish(); +#if NETCF_1_0 + keys=null; +#else + if ( cryptoTransform_ != null ) { + GetAuthCodeIfAES(); + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } +#endif + } + finally { + if( isStreamOwner_ ) { + baseOutputStream_.Close(); + } + } + } + } + + private void GetAuthCodeIfAES() { +#if !NET_1_1 && !NETCF_2_0 + if (cryptoTransform_ is ZipAESTransform) { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } +#endif + } + + /// + /// Writes a single byte to the compressed output stream. + /// + /// + /// The byte value. + /// + public override void WriteByte(byte value) + { + byte[] b = new byte[1]; + b[0] = value; + Write(b, 0, 1); + } + + /// + /// Writes bytes from an array to the compressed stream. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + public override void Write(byte[] buffer, int offset, int count) + { + deflater_.SetInput(buffer, offset, count); + Deflate(); + } + #endregion + + #region Instance Fields + /// + /// This buffer is used temporarily to retrieve the bytes from the + /// deflater and write them to the underlying output stream. + /// + byte[] buffer_; + + /// + /// The deflater which is used to deflate the stream. + /// + protected Deflater deflater_; + + /// + /// Base stream the deflater depends on. + /// + protected Stream baseOutputStream_; + + bool isClosed_; + + bool isStreamOwner_ = true; + #endregion + + #region Static Fields + +#if !NET_1_1 && !NETCF_2_0 + // Static to help ensure that multiple files within a zip will get different random salt + private static RNGCryptoServiceProvider _aesRnd; +#endif + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs new file mode 100644 index 000000000..f1599041b --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -0,0 +1,732 @@ +// InflaterInputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 11-08-2009 GeoffHart T9121 Added Multi-member gzip support + +using System; +using System.IO; + +#if !NETCF_1_0 +using System.Security.Cryptography; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// An input buffer customised for use by + /// + /// + /// The buffer supports decryption of incoming data. + /// + public class InflaterInputBuffer + { + #region Constructors + /// + /// Initialise a new instance of with a default buffer size + /// + /// The stream to buffer. + public InflaterInputBuffer(Stream stream) : this(stream , 4096) + { + } + + /// + /// Initialise a new instance of + /// + /// The stream to buffer. + /// The size to use for the buffer + /// A minimum buffer size of 1KB is permitted. Lower sizes are treated as 1KB. + public InflaterInputBuffer(Stream stream, int bufferSize) + { + inputStream = stream; + if ( bufferSize < 1024 ) { + bufferSize = 1024; + } + rawData = new byte[bufferSize]; + clearText = rawData; + } + #endregion + + /// + /// Get the length of bytes bytes in the + /// + public int RawLength + { + get { + return rawLength; + } + } + + /// + /// Get the contents of the raw data buffer. + /// + /// This may contain encrypted data. + public byte[] RawData + { + get { + return rawData; + } + } + + /// + /// Get the number of useable bytes in + /// + public int ClearTextLength + { + get { + return clearTextLength; + } + } + + /// + /// Get the contents of the clear text buffer. + /// + public byte[] ClearText + { + get { + return clearText; + } + } + + /// + /// Get/set the number of bytes available + /// + public int Available + { + get { return available; } + set { available = value; } + } + + /// + /// Call passing the current clear text buffer contents. + /// + /// The inflater to set input for. + public void SetInflaterInput(Inflater inflater) + { + if ( available > 0 ) { + inflater.SetInput(clearText, clearTextLength - available, available); + available = 0; + } + } + + /// + /// Fill the buffer from the underlying input stream. + /// + public void Fill() + { + rawLength = 0; + int toRead = rawData.Length; + + while (toRead > 0) { + int count = inputStream.Read(rawData, rawLength, toRead); + if ( count <= 0 ) { + break; + } + rawLength += count; + toRead -= count; + } + +#if !NETCF_1_0 + if ( cryptoTransform != null ) { + clearTextLength = cryptoTransform.TransformBlock(rawData, 0, rawLength, clearText, 0); + } + else +#endif + { + clearTextLength = rawLength; + } + + available = clearTextLength; + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to fill + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] buffer) + { + return ReadRawBuffer(buffer, 0, buffer.Length); + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to read into + /// The offset to start reading data into. + /// The number of bytes to read. + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] outBuffer, int offset, int length) + { + if ( length < 0 ) { + throw new ArgumentOutOfRangeException("length"); + } + + int currentOffset = offset; + int currentLength = length; + + while ( currentLength > 0 ) { + if ( available <= 0 ) { + Fill(); + if (available <= 0) { + return 0; + } + } + int toCopy = Math.Min(currentLength, available); + System.Array.Copy(rawData, rawLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read clear text data from the input stream. + /// + /// The buffer to add data to. + /// The offset to start adding data at. + /// The number of bytes to read. + /// Returns the number of bytes actually read. + public int ReadClearTextBuffer(byte[] outBuffer, int offset, int length) + { + if ( length < 0 ) { + throw new ArgumentOutOfRangeException("length"); + } + + int currentOffset = offset; + int currentLength = length; + + while ( currentLength > 0 ) { + if ( available <= 0 ) { + Fill(); + if (available <= 0) { + return 0; + } + } + + int toCopy = Math.Min(currentLength, available); + Array.Copy(clearText, clearTextLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read a from the input stream. + /// + /// Returns the byte read. + public int ReadLeByte() + { + if (available <= 0) { + Fill(); + if (available <= 0) { + throw new ZipException("EOF in header"); + } + } + byte result = rawData[rawLength - available]; + available -= 1; + return result; + } + + /// + /// Read an in little endian byte order. + /// + /// The short value read case to an int. + public int ReadLeShort() + { + return ReadLeByte() | (ReadLeByte() << 8); + } + + /// + /// Read an in little endian byte order. + /// + /// The int value read. + public int ReadLeInt() + { + return ReadLeShort() | (ReadLeShort() << 16); + } + + /// + /// Read a in little endian byte order. + /// + /// The long value read. + public long ReadLeLong() + { + return (uint)ReadLeInt() | ((long)ReadLeInt() << 32); + } + +#if !NETCF_1_0 + /// + /// Get/set the to apply to any data. + /// + /// Set this value to null to have no transform applied. + public ICryptoTransform CryptoTransform + { + set { + cryptoTransform = value; + if ( cryptoTransform != null ) { + if ( rawData == clearText ) { + if ( internalClearText == null ) { + internalClearText = new byte[rawData.Length]; + } + clearText = internalClearText; + } + clearTextLength = rawLength; + if ( available > 0 ) { + cryptoTransform.TransformBlock(rawData, rawLength - available, available, clearText, rawLength - available); + } + } else { + clearText = rawData; + clearTextLength = rawLength; + } + } + } +#endif + + #region Instance Fields + int rawLength; + byte[] rawData; + + int clearTextLength; + byte[] clearText; +#if !NETCF_1_0 + byte[] internalClearText; +#endif + + int available; + +#if !NETCF_1_0 + ICryptoTransform cryptoTransform; +#endif + Stream inputStream; + #endregion + } + + /// + /// This filter stream is used to decompress data compressed using the "deflate" + /// format. The "deflate" format is described in RFC 1951. + /// + /// This stream may form the basis for other decompression filters, such + /// as the GZipInputStream. + /// + /// Author of the original java version : John Leuner. + /// + public class InflaterInputStream : Stream + { + #region Constructors + /// + /// Create an InflaterInputStream with the default decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The InputStream to read bytes from + /// + public InflaterInputStream(Stream baseInputStream) + : this(baseInputStream, new Inflater(), 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The source of input data + /// + /// + /// The decompressor used to decompress data read from baseInputStream + /// + public InflaterInputStream(Stream baseInputStream, Inflater inf) + : this(baseInputStream, inf, 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and the specified buffer size. + /// + /// + /// The InputStream to read bytes from + /// + /// + /// The decompressor to use + /// + /// + /// Size of the buffer to use + /// + public InflaterInputStream(Stream baseInputStream, Inflater inflater, int bufferSize) + { + if (baseInputStream == null) { + throw new ArgumentNullException("baseInputStream"); + } + + if (inflater == null) { + throw new ArgumentNullException("inflater"); + } + + if (bufferSize <= 0) { + throw new ArgumentOutOfRangeException("bufferSize"); + } + + this.baseInputStream = baseInputStream; + this.inf = inflater; + + inputBuffer = new InflaterInputBuffer(baseInputStream, bufferSize); + } + + #endregion + + /// + /// Get/set flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// + /// The default value is true. + /// + public bool IsStreamOwner + { + get { return isStreamOwner; } + set { isStreamOwner = value; } + } + + /// + /// Skip specified number of bytes of uncompressed data + /// + /// + /// Number of bytes to skip + /// + /// + /// The number of bytes skipped, zero if the end of + /// stream has been reached + /// + /// + /// The number of bytes to skip is less than or equal to zero. + /// + public long Skip(long count) + { + if (count <= 0) { + throw new ArgumentOutOfRangeException("count"); + } + + // v0.80 Skip by seeking if underlying stream supports it... + if (baseInputStream.CanSeek) { + baseInputStream.Seek(count, SeekOrigin.Current); + return count; + } + else { + int length = 2048; + if (count < length) { + length = (int) count; + } + + byte[] tmp = new byte[length]; + int readCount = 1; + long toSkip = count; + + while ((toSkip > 0) && (readCount > 0) ) { + if (toSkip < length) { + length = (int)toSkip; + } + + readCount = baseInputStream.Read(tmp, 0, length); + toSkip -= readCount; + } + + return count - toSkip; + } + } + + /// + /// Clear any cryptographic state. + /// + protected void StopDecrypting() + { +#if !NETCF_1_0 + inputBuffer.CryptoTransform = null; +#endif + } + + /// + /// Returns 0 once the end of the stream (EOF) has been reached. + /// Otherwise returns 1. + /// + public virtual int Available + { + get { + return inf.IsFinished ? 0 : 1; + } + } + + /// + /// Fills the buffer with more data to decompress. + /// + /// + /// Stream ends early + /// + protected void Fill() + { + // Protect against redundant calls + if (inputBuffer.Available <= 0) { + inputBuffer.Fill(); + if (inputBuffer.Available <= 0) { + throw new SharpZipBaseException("Unexpected EOF"); + } + } + inputBuffer.SetInflaterInput(inf); + } + + #region Stream Overrides + /// + /// Gets a value indicating whether the current stream supports reading + /// + public override bool CanRead + { + get { + return baseInputStream.CanRead; + } + } + + /// + /// Gets a value of false indicating seeking is not supported for this stream. + /// + public override bool CanSeek { + get { + return false; + } + } + + /// + /// Gets a value of false indicating that this stream is not writeable. + /// + public override bool CanWrite { + get { + return false; + } + } + + /// + /// A value representing the length of the stream in bytes. + /// + public override long Length { + get { + return inputBuffer.RawLength; + } + } + + /// + /// The current position within the stream. + /// Throws a NotSupportedException when attempting to set the position + /// + /// Attempting to set the position + public override long Position { + get { + return baseInputStream.Position; + } + set { + throw new NotSupportedException("InflaterInputStream Position not supported"); + } + } + + /// + /// Flushes the baseInputStream + /// + public override void Flush() + { + baseInputStream.Flush(); + } + + /// + /// Sets the position within the current stream + /// Always throws a NotSupportedException + /// + /// The relative offset to seek to. + /// The defining where to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("Seek not supported"); + } + + /// + /// Set the length of the current stream + /// Always throws a NotSupportedException + /// + /// The new length value for the stream. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("InflaterInputStream SetLength not supported"); + } + + /// + /// Writes a sequence of bytes to stream and advances the current position + /// This method always throws a NotSupportedException + /// + /// Thew buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Any access + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("InflaterInputStream Write not supported"); + } + + /// + /// Writes one byte to the current stream and advances the current position + /// Always throws a NotSupportedException + /// + /// The byte to write. + /// Any access + public override void WriteByte(byte value) + { + throw new NotSupportedException("InflaterInputStream WriteByte not supported"); + } + + /// + /// Entry point to begin an asynchronous write. Always throws a NotSupportedException. + /// + /// The buffer to write data from + /// Offset of first byte to write + /// The maximum number of bytes to write + /// The method to be called when the asynchronous write operation is completed + /// A user-provided object that distinguishes this particular asynchronous write request from other requests + /// An IAsyncResult that references the asynchronous write + /// Any access + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("InflaterInputStream BeginWrite not supported"); + } + + /// + /// Closes the input stream. When + /// is true the underlying stream is also closed. + /// + public override void Close() + { + if ( !isClosed ) { + isClosed = true; + if ( isStreamOwner ) { + baseInputStream.Close(); + } + } + } + + /// + /// Reads decompressed data into the provided buffer byte array + /// + /// + /// The array to read and decompress data into + /// + /// + /// The offset indicating where the data should be placed + /// + /// + /// The number of bytes to decompress + /// + /// The number of bytes read. Zero signals the end of stream + /// + /// Inflater needs a dictionary + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (inf.IsNeedingDictionary) + { + throw new SharpZipBaseException("Need a dictionary"); + } + + int remainingBytes = count; + while (true) { + int bytesRead = inf.Inflate(buffer, offset, remainingBytes); + offset += bytesRead; + remainingBytes -= bytesRead; + + if (remainingBytes == 0 || inf.IsFinished) { + break; + } + + if ( inf.IsNeedingInput ) { + Fill(); + } + else if ( bytesRead == 0 ) { + throw new ZipException("Dont know what to do"); + } + } + return count - remainingBytes; + } + #endregion + + #region Instance Fields + /// + /// Decompressor for this stream + /// + protected Inflater inf; + + /// + /// Input buffer for this stream. + /// + protected InflaterInputBuffer inputBuffer; + + /// + /// Base stream the inflater reads from. + /// + private Stream baseInputStream; + + /// + /// The compressed size + /// + protected long csize; + + /// + /// Flag indicating wether this instance has been closed or not. + /// + bool isClosed; + + /// + /// Flag indicating wether this instance is designated the stream owner. + /// When closing if this flag is true the underlying stream is closed. + /// + bool isStreamOwner = true; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs new file mode 100644 index 000000000..9114d0ca9 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs @@ -0,0 +1,235 @@ +// OutputWindow.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// Contains the output from the Inflation process. + /// We need to have a window so that we can refer backwards into the output stream + /// to repeat stuff.
+ /// Author of the original java version : John Leuner + ///
+ public class OutputWindow + { + #region Constants + const int WindowSize = 1 << 15; + const int WindowMask = WindowSize - 1; + #endregion + + #region Instance Fields + byte[] window = new byte[WindowSize]; //The window is 2^15 bytes + int windowEnd; + int windowFilled; + #endregion + + /// + /// Write a byte to this output window + /// + /// value to write + /// + /// if window is full + /// + public void Write(int value) + { + if (windowFilled++ == WindowSize) { + throw new InvalidOperationException("Window full"); + } + window[windowEnd++] = (byte) value; + windowEnd &= WindowMask; + } + + + private void SlowRepeat(int repStart, int length, int distance) + { + while (length-- > 0) { + window[windowEnd++] = window[repStart++]; + windowEnd &= WindowMask; + repStart &= WindowMask; + } + } + + /// + /// Append a byte pattern already in the window itself + /// + /// length of pattern to copy + /// distance from end of window pattern occurs + /// + /// If the repeated data overflows the window + /// + public void Repeat(int length, int distance) + { + if ((windowFilled += length) > WindowSize) { + throw new InvalidOperationException("Window full"); + } + + int repStart = (windowEnd - distance) & WindowMask; + int border = WindowSize - length; + if ( (repStart <= border) && (windowEnd < border) ) { + if (length <= distance) { + System.Array.Copy(window, repStart, window, windowEnd, length); + windowEnd += length; + } else { + // We have to copy manually, since the repeat pattern overlaps. + while (length-- > 0) { + window[windowEnd++] = window[repStart++]; + } + } + } else { + SlowRepeat(repStart, length, distance); + } + } + + /// + /// Copy from input manipulator to internal window + /// + /// source of data + /// length of data to copy + /// the number of bytes copied + public int CopyStored(StreamManipulator input, int length) + { + length = Math.Min(Math.Min(length, WindowSize - windowFilled), input.AvailableBytes); + int copied; + + int tailLen = WindowSize - windowEnd; + if (length > tailLen) { + copied = input.CopyBytes(window, windowEnd, tailLen); + if (copied == tailLen) { + copied += input.CopyBytes(window, 0, length - tailLen); + } + } else { + copied = input.CopyBytes(window, windowEnd, length); + } + + windowEnd = (windowEnd + copied) & WindowMask; + windowFilled += copied; + return copied; + } + + /// + /// Copy dictionary to window + /// + /// source dictionary + /// offset of start in source dictionary + /// length of dictionary + /// + /// If window isnt empty + /// + public void CopyDict(byte[] dictionary, int offset, int length) + { + if ( dictionary == null ) { + throw new ArgumentNullException("dictionary"); + } + + if (windowFilled > 0) { + throw new InvalidOperationException(); + } + + if (length > WindowSize) { + offset += length - WindowSize; + length = WindowSize; + } + System.Array.Copy(dictionary, offset, window, 0, length); + windowEnd = length & WindowMask; + } + + /// + /// Get remaining unfilled space in window + /// + /// Number of bytes left in window + public int GetFreeSpace() + { + return WindowSize - windowFilled; + } + + /// + /// Get bytes available for output in window + /// + /// Number of bytes filled + public int GetAvailable() + { + return windowFilled; + } + + /// + /// Copy contents of window to output + /// + /// buffer to copy to + /// offset to start at + /// number of bytes to count + /// The number of bytes copied + /// + /// If a window underflow occurs + /// + public int CopyOutput(byte[] output, int offset, int len) + { + int copyEnd = windowEnd; + if (len > windowFilled) { + len = windowFilled; + } else { + copyEnd = (windowEnd - windowFilled + len) & WindowMask; + } + + int copied = len; + int tailLen = len - copyEnd; + + if (tailLen > 0) { + System.Array.Copy(window, WindowSize - tailLen, output, offset, tailLen); + offset += tailLen; + len = copyEnd; + } + System.Array.Copy(window, copyEnd - len, output, offset, len); + windowFilled -= copied; + if (windowFilled < 0) { + throw new InvalidOperationException(); + } + return copied; + } + + /// + /// Reset by clearing window so GetAvailable returns 0 + /// + public void Reset() + { + windowFilled = windowEnd = 0; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs new file mode 100644 index 000000000..0a30e6228 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs @@ -0,0 +1,297 @@ +// StreamManipulator.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// This class allows us to retrieve a specified number of bits from + /// the input buffer, as well as copy big byte blocks. + /// + /// It uses an int buffer to store up to 31 bits for direct + /// manipulation. This guarantees that we can get at least 16 bits, + /// but we only need at most 15, so this is all safe. + /// + /// There are some optimizations in this class, for example, you must + /// never peek more than 8 bits more than needed, and you must first + /// peek bits before you may drop them. This is not a general purpose + /// class but optimized for the behaviour of the Inflater. + /// + /// authors of the original java version : John Leuner, Jochen Hoenicke + /// + public class StreamManipulator + { + #region Constructors + /// + /// Constructs a default StreamManipulator with all buffers empty + /// + public StreamManipulator() + { + } + #endregion + + /// + /// Get the next sequence of bits but don't increase input pointer. bitCount must be + /// less or equal 16 and if this call succeeds, you must drop + /// at least n - 8 bits in the next call. + /// + /// The number of bits to peek. + /// + /// the value of the bits, or -1 if not enough bits available. */ + /// + public int PeekBits(int bitCount) + { + if (bitsInBuffer_ < bitCount) { + if (windowStart_ == windowEnd_) { + return -1; // ok + } + buffer_ |= (uint)((window_[windowStart_++] & 0xff | + (window_[windowStart_++] & 0xff) << 8) << bitsInBuffer_); + bitsInBuffer_ += 16; + } + return (int)(buffer_ & ((1 << bitCount) - 1)); + } + + /// + /// Drops the next n bits from the input. You should have called PeekBits + /// with a bigger or equal n before, to make sure that enough bits are in + /// the bit buffer. + /// + /// The number of bits to drop. + public void DropBits(int bitCount) + { + buffer_ >>= bitCount; + bitsInBuffer_ -= bitCount; + } + + /// + /// Gets the next n bits and increases input pointer. This is equivalent + /// to followed by , except for correct error handling. + /// + /// The number of bits to retrieve. + /// + /// the value of the bits, or -1 if not enough bits available. + /// + public int GetBits(int bitCount) + { + int bits = PeekBits(bitCount); + if (bits >= 0) { + DropBits(bitCount); + } + return bits; + } + + /// + /// Gets the number of bits available in the bit buffer. This must be + /// only called when a previous PeekBits() returned -1. + /// + /// + /// the number of bits available. + /// + public int AvailableBits { + get { + return bitsInBuffer_; + } + } + + /// + /// Gets the number of bytes available. + /// + /// + /// The number of bytes available. + /// + public int AvailableBytes { + get { + return windowEnd_ - windowStart_ + (bitsInBuffer_ >> 3); + } + } + + /// + /// Skips to the next byte boundary. + /// + public void SkipToByteBoundary() + { + buffer_ >>= (bitsInBuffer_ & 7); + bitsInBuffer_ &= ~7; + } + + /// + /// Returns true when SetInput can be called + /// + public bool IsNeedingInput { + get { + return windowStart_ == windowEnd_; + } + } + + /// + /// Copies bytes from input buffer to output buffer starting + /// at output[offset]. You have to make sure, that the buffer is + /// byte aligned. If not enough bytes are available, copies fewer + /// bytes. + /// + /// + /// The buffer to copy bytes to. + /// + /// + /// The offset in the buffer at which copying starts + /// + /// + /// The length to copy, 0 is allowed. + /// + /// + /// The number of bytes copied, 0 if no bytes were available. + /// + /// + /// Length is less than zero + /// + /// + /// Bit buffer isnt byte aligned + /// + public int CopyBytes(byte[] output, int offset, int length) + { + if (length < 0) { + throw new ArgumentOutOfRangeException("length"); + } + + if ((bitsInBuffer_ & 7) != 0) { + // bits_in_buffer may only be 0 or a multiple of 8 + throw new InvalidOperationException("Bit buffer is not byte aligned!"); + } + + int count = 0; + while ((bitsInBuffer_ > 0) && (length > 0)) { + output[offset++] = (byte) buffer_; + buffer_ >>= 8; + bitsInBuffer_ -= 8; + length--; + count++; + } + + if (length == 0) { + return count; + } + + int avail = windowEnd_ - windowStart_; + if (length > avail) { + length = avail; + } + System.Array.Copy(window_, windowStart_, output, offset, length); + windowStart_ += length; + + if (((windowStart_ - windowEnd_) & 1) != 0) { + // We always want an even number of bytes in input, see peekBits + buffer_ = (uint)(window_[windowStart_++] & 0xff); + bitsInBuffer_ = 8; + } + return count + length; + } + + /// + /// Resets state and empties internal buffers + /// + public void Reset() + { + buffer_ = 0; + windowStart_ = windowEnd_ = bitsInBuffer_ = 0; + } + + /// + /// Add more input for consumption. + /// Only call when IsNeedingInput returns true + /// + /// data to be input + /// offset of first byte of input + /// number of bytes of input to add. + public void SetInput(byte[] buffer, int offset, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if (windowStart_ < windowEnd_) { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + // We want to throw an ArrayIndexOutOfBoundsException early. + // Note the check also handles integer wrap around. + if ((offset > end) || (end > buffer.Length) ) { + throw new ArgumentOutOfRangeException("count"); + } + + if ((count & 1) != 0) { + // We always want an even number of bytes in input, see PeekBits + buffer_ |= (uint)((buffer[offset++] & 0xff) << bitsInBuffer_); + bitsInBuffer_ += 8; + } + + window_ = buffer; + windowStart_ = offset; + windowEnd_ = end; + } + + #region Instance Fields + private byte[] window_; + private int windowStart_; + private int windowEnd_; + + private uint buffer_; + private int bitsInBuffer_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs b/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs new file mode 100644 index 000000000..97c063ebd --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs @@ -0,0 +1,729 @@ +// FastZip.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// FastZipEvents supports all events applicable to FastZip operations. + /// + public class FastZipEvents + { + /// + /// Delegate to invoke when processing directories. + /// + public ProcessDirectoryHandler ProcessDirectory; + + /// + /// Delegate to invoke when processing files. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke during processing of files. + /// + public ProgressHandler Progress; + + /// + /// Delegate to invoke when processing for a file has been completed. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when processing directory failures. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when processing file failures. + /// + public FileFailureHandler FileFailure; + + /// + /// Raise the directory failure event. + /// + /// The directory causing the failure. + /// The exception for this event. + /// A boolean indicating if execution should continue or not. + public bool OnDirectoryFailure(string directory, Exception e) + { + bool result = false; + DirectoryFailureHandler handler = DirectoryFailure; + + if ( handler != null ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(directory, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the file failure handler delegate. + /// + /// The file causing the failure. + /// The exception for this failure. + /// A boolean indicating if execution should continue or not. + public bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + bool result = (handler != null); + + if ( result ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(file, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the ProcessFile delegate. + /// + /// The file being processed. + /// A boolean indicating if execution should continue or not. + public bool OnProcessFile(string file) + { + bool result = true; + ProcessFileHandler handler = ProcessFile; + + if ( handler != null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the delegate + /// + /// The file whose processing has been completed. + /// A boolean indicating if execution should continue or not. + public bool OnCompletedFile(string file) + { + bool result = true; + CompletedFileHandler handler = CompletedFile; + if ( handler != null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the process directory delegate. + /// + /// The directory being processed. + /// Flag indicating if the directory has matching files as determined by the current filter. + /// A of true if the operation should continue; false otherwise. + public bool OnProcessDirectory(string directory, bool hasMatchingFiles) + { + bool result = true; + ProcessDirectoryHandler handler = ProcessDirectory; + if ( handler != null ) { + DirectoryEventArgs args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// The minimum timespan between events. + /// + /// The minimum period of time between events. + /// + /// The default interval is three seconds. + public TimeSpan ProgressInterval + { + get { return progressInterval_; } + set { progressInterval_ = value; } + } + + #region Instance Fields + TimeSpan progressInterval_ = TimeSpan.FromSeconds(3); + #endregion + } + + /// + /// FastZip provides facilities for creating and extracting zip files. + /// + public class FastZip + { + #region Enumerations + /// + /// Defines the desired handling when overwriting files during extraction. + /// + public enum Overwrite + { + /// + /// Prompt the user to confirm overwriting + /// + Prompt, + /// + /// Never overwrite files. + /// + Never, + /// + /// Always overwrite files. + /// + Always + } + #endregion + + #region Constructors + /// + /// Initialise a default instance of . + /// + public FastZip() + { + } + + /// + /// Initialise a new instance of + /// + /// The events to use during operations. + public FastZip(FastZipEvents events) + { + events_ = events; + } + #endregion + + #region Properties + /// + /// Get/set a value indicating wether empty directories should be created. + /// + public bool CreateEmptyDirectories + { + get { return createEmptyDirectories_; } + set { createEmptyDirectories_ = value; } + } + +#if !NETCF_1_0 + /// + /// Get / set the password value. + /// + public string Password + { + get { return password_; } + set { password_ = value; } + } +#endif + + /// + /// Get or set the active when creating Zip files. + /// + /// + public INameTransform NameTransform + { + get { return entryFactory_.NameTransform; } + set { + entryFactory_.NameTransform = value; + } + } + + /// + /// Get or set the active when creating Zip files. + /// + public IEntryFactory EntryFactory + { + get { return entryFactory_; } + set { + if ( value == null ) { + entryFactory_ = new ZipEntryFactory(); + } + else { + entryFactory_ = value; + } + } + } + + /// + /// Gets or sets the setting for Zip64 handling when writing. + /// + /// + /// The default value is dynamic which is not backwards compatible with old + /// programs and can cause problems with XP's built in compression which cant + /// read Zip64 archives. However it does avoid the situation were a large file + /// is added and cannot be completed correctly. + /// NOTE: Setting the size for entries before they are added is the best solution! + /// By default the EntryFactory used by FastZip will set fhe file size. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Get/set a value indicating wether file dates and times should + /// be restored when extracting files from an archive. + /// + /// The default value is false. + public bool RestoreDateTimeOnExtract + { + get { + return restoreDateTimeOnExtract_; + } + set { + restoreDateTimeOnExtract_ = value; + } + } + + /// + /// Get/set a value indicating wether file attributes should + /// be restored during extract operations + /// + public bool RestoreAttributesOnExtract + { + get { return restoreAttributesOnExtract_; } + set { restoreAttributesOnExtract_ = value; } + } + #endregion + + #region Delegates + /// + /// Delegate called when confirming overwriting of files. + /// + public delegate bool ConfirmOverwriteDelegate(string fileName); + #endregion + + #region CreateZip + /// + /// Create a zip file. + /// + /// The name of the zip file to create. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, + bool recurse, string fileFilter, string directoryFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter); + } + + /// + /// Create a zip file/archive. + /// + /// The name of the zip file to create. + /// The directory to obtain files and directories from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, null); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + /// The is closed after creation. + public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter) + { + NameTransform = new ZipNameTransform(sourceDirectory); + sourceDirectory_ = sourceDirectory; + + using ( outputStream_ = new ZipOutputStream(outputStream) ) { + +#if !NETCF_1_0 + if ( password_ != null ) { + outputStream_.Password = password_; + } +#endif + + outputStream_.UseZip64 = UseZip64; + FileSystemScanner scanner = new FileSystemScanner(fileFilter, directoryFilter); + scanner.ProcessFile += new ProcessFileHandler(ProcessFile); + if ( this.CreateEmptyDirectories ) { + scanner.ProcessDirectory += new ProcessDirectoryHandler(ProcessDirectory); + } + + if (events_ != null) { + if ( events_.FileFailure != null ) { + scanner.FileFailure += events_.FileFailure; + } + + if ( events_.DirectoryFailure != null ) { + scanner.DirectoryFailure += events_.DirectoryFailure; + } + } + + scanner.Scan(sourceDirectory, recurse); + } + } + + #endregion + + #region ExtractZip + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// A filter to apply to files. + public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter) + { + ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_); + } + + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + public void ExtractZip(string zipFileName, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime) + { + Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read); + ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true); + } + + /// + /// Extract the contents of a zip file held in a stream. + /// + /// The seekable input stream containing the zip to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + /// Flag indicating whether the inputStream will be closed by this method. + public void ExtractZip(Stream inputStream, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime, + bool isStreamOwner) + { + if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) { + throw new ArgumentNullException("confirmDelegate"); + } + + continueRunning_ = true; + overwrite_ = overwrite; + confirmDelegate_ = confirmDelegate; + extractNameTransform_ = new WindowsNameTransform(targetDirectory); + + fileFilter_ = new NameFilter(fileFilter); + directoryFilter_ = new NameFilter(directoryFilter); + restoreDateTimeOnExtract_ = restoreDateTime; + + using (zipFile_ = new ZipFile(inputStream)) { + +#if !NETCF_1_0 + if (password_ != null) { + zipFile_.Password = password_; + } +#endif + zipFile_.IsStreamOwner = isStreamOwner; + System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator(); + while (continueRunning_ && enumerator.MoveNext()) { + ZipEntry entry = (ZipEntry)enumerator.Current; + if (entry.IsFile) + { + // TODO Path.GetDirectory can fail here on invalid characters. + if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) { + ExtractEntry(entry); + } + } + else if (entry.IsDirectory) { + if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) { + ExtractEntry(entry); + } + } + else { + // Do nothing for volume labels etc... + } + } + } + } + #endregion + + #region Internal Processing + void ProcessDirectory(object sender, DirectoryEventArgs e) + { + if ( !e.HasMatchingFiles && CreateEmptyDirectories ) { + if ( events_ != null ) { + events_.OnProcessDirectory(e.Name, e.HasMatchingFiles); + } + + if ( e.ContinueRunning ) { + if (e.Name != sourceDirectory_) { + ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name); + outputStream_.PutNextEntry(entry); + } + } + } + } + + void ProcessFile(object sender, ScanEventArgs e) + { + if ( (events_ != null) && (events_.ProcessFile != null) ) { + events_.ProcessFile(sender, e); + } + + if ( e.ContinueRunning ) { + try { + // The open below is equivalent to OpenRead which gaurantees that if opened the + // file will not be changed by subsequent openers, but precludes opening in some cases + // were it could succeed. + using (FileStream stream = File.Open(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read)) { + ZipEntry entry = entryFactory_.MakeFileEntry(e.Name); + outputStream_.PutNextEntry(entry); + AddFileContents(e.Name, stream); + } + } + catch(Exception ex) { + if (events_ != null) { + continueRunning_ = events_.OnFileFailure(e.Name, ex); + } + else { + continueRunning_ = false; + throw; + } + } + } + } + + void AddFileContents(string name, Stream stream) + { + if( stream==null ) { + throw new ArgumentNullException("stream"); + } + + if( buffer_==null ) { + buffer_=new byte[4096]; + } + + if( (events_!=null)&&(events_.Progress!=null) ) { + StreamUtils.Copy(stream, outputStream_, buffer_, + events_.Progress, events_.ProgressInterval, this, name); + } + else { + StreamUtils.Copy(stream, outputStream_, buffer_); + } + + if( events_!=null ) { + continueRunning_=events_.OnCompletedFile(name); + } + } + + void ExtractFileEntry(ZipEntry entry, string targetName) + { + bool proceed = true; + if ( overwrite_ != Overwrite.Always ) { + if ( File.Exists(targetName) ) { + if ( (overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null) ) { + proceed = confirmDelegate_(targetName); + } + else { + proceed = false; + } + } + } + + if ( proceed ) { + if ( events_ != null ) { + continueRunning_ = events_.OnProcessFile(entry.Name); + } + + if ( continueRunning_ ) { + try { + using ( FileStream outputStream = File.Create(targetName) ) { + if ( buffer_ == null ) { + buffer_ = new byte[4096]; + } + if ((events_ != null) && (events_.Progress != null)) + { + StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_, + events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size); + } + else + { + StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_); + } + + if (events_ != null) { + continueRunning_ = events_.OnCompletedFile(entry.Name); + } + } + +#if !NETCF_1_0 && !NETCF_2_0 + if ( restoreDateTimeOnExtract_ ) { + File.SetLastWriteTime(targetName, entry.DateTime); + } + + if ( RestoreAttributesOnExtract && entry.IsDOSEntry && (entry.ExternalFileAttributes != -1)) { + FileAttributes fileAttributes = (FileAttributes) entry.ExternalFileAttributes; + // TODO: FastZip - Setting of other file attributes on extraction is a little trickier. + fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden); + File.SetAttributes(targetName, fileAttributes); + } +#endif + } + catch(Exception ex) { + if ( events_ != null ) { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + else { + continueRunning_ = false; + throw; + } + } + } + } + } + + void ExtractEntry(ZipEntry entry) + { + bool doExtraction = entry.IsCompressionMethodSupported(); + string targetName = entry.Name; + + if ( doExtraction ) { + if ( entry.IsFile ) { + targetName = extractNameTransform_.TransformFile(targetName); + } + else if ( entry.IsDirectory ) { + targetName = extractNameTransform_.TransformDirectory(targetName); + } + + doExtraction = !((targetName == null) || (targetName.Length == 0)); + } + + // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid? + + string dirName = null; + + if ( doExtraction ) { + if ( entry.IsDirectory ) { + dirName = targetName; + } + else { + dirName = Path.GetDirectoryName(Path.GetFullPath(targetName)); + } + } + + if ( doExtraction && !Directory.Exists(dirName) ) { + if ( !entry.IsDirectory || CreateEmptyDirectories ) { + try { + Directory.CreateDirectory(dirName); + } + catch (Exception ex) { + doExtraction = false; + if ( events_ != null ) { + if ( entry.IsDirectory ) { + continueRunning_ = events_.OnDirectoryFailure(targetName, ex); + } + else { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + } + else { + continueRunning_ = false; + throw; + } + } + } + } + + if ( doExtraction && entry.IsFile ) { + ExtractFileEntry(entry, targetName); + } + } + + static int MakeExternalAttributes(FileInfo info) + { + return (int)info.Attributes; + } + +#if NET_1_0 || NET_1_1 || NETCF_1_0 + static bool NameIsValid(string name) + { + return (name != null) && + (name.Length > 0) && + (name.IndexOfAny(Path.InvalidPathChars) < 0); + } +#else + static bool NameIsValid(string name) + { + return (name != null) && + (name.Length > 0) && + (name.IndexOfAny(Path.GetInvalidPathChars()) < 0); + } +#endif + #endregion + + #region Instance Fields + bool continueRunning_; + byte[] buffer_; + ZipOutputStream outputStream_; + ZipFile zipFile_; + string sourceDirectory_; + NameFilter fileFilter_; + NameFilter directoryFilter_; + Overwrite overwrite_; + ConfirmOverwriteDelegate confirmDelegate_; + + bool restoreDateTimeOnExtract_; + bool restoreAttributesOnExtract_; + bool createEmptyDirectories_; + FastZipEvents events_; + IEntryFactory entryFactory_ = new ZipEntryFactory(); + INameTransform extractNameTransform_; + UseZip64 useZip64_=UseZip64.Dynamic; + +#if !NETCF_1_0 + string password_; +#endif + + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs b/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs new file mode 100644 index 000000000..31c6f40d3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs @@ -0,0 +1,82 @@ +// IEntryFactory.cs +// +// Copyright 2006 John Reilly +// +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// Defines factory methods for creating new values. + /// + public interface IEntryFactory + { + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName); + + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// If true get details from the file system if the file exists. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName, bool useFileSystem); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// If true get details from the file system for this directory if it exists. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem); + + /// + /// Get/set the applicable. + /// + INameTransform NameTransform { get; set; } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs b/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs new file mode 100644 index 000000000..0ab439375 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs @@ -0,0 +1,272 @@ +// WindowsNameTransform.cs +// +// Copyright 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using System.Text; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// WindowsNameTransform transforms names to windows compatible ones. + /// + public class WindowsNameTransform : INameTransform + { + /// + /// Initialises a new instance of + /// + /// + public WindowsNameTransform(string baseDirectory) + { + if ( baseDirectory == null ) { + throw new ArgumentNullException("baseDirectory", "Directory name is invalid"); + } + + BaseDirectory = baseDirectory; + } + + /// + /// Initialise a default instance of + /// + public WindowsNameTransform() + { + // Do nothing. + } + + /// + /// Gets or sets a value containing the target directory to prefix values with. + /// + public string BaseDirectory + { + get { return _baseDirectory; } + set { + if ( value == null ) { + throw new ArgumentNullException("value"); + } + + _baseDirectory = Path.GetFullPath(value); + } + } + + /// + /// Gets or sets a value indicating wether paths on incoming values should be removed. + /// + public bool TrimIncomingPaths + { + get { return _trimIncomingPaths; } + set { _trimIncomingPaths = value; } + } + + /// + /// Transform a Zip directory name to a windows directory name. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) { + while ( name.EndsWith(@"\") ) { + name = name.Remove(name.Length - 1, 1); + } + } + else { + throw new ZipException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a Zip format file name to a windows style one. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) { + name = MakeValidName(name, _replacementChar); + + if ( _trimIncomingPaths ) { + name = Path.GetFileName(name); + } + + // This may exceed windows length restrictions. + // Combine will throw a PathTooLongException in that case. + if ( _baseDirectory != null ) { + name = Path.Combine(_baseDirectory, name); + } + } + else { + name = string.Empty; + } + return name; + } + + /// + /// Test a name to see if it is a valid name for a windows filename as extracted from a Zip archive. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// The filename isnt a true windows path in some fundamental ways like no absolute paths, no rooted paths etc. + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.Length <= MaxPath) && + (string.Compare(name, MakeValidName(name, '_')) == 0) + ; + + return result; + } + + /// + /// Initialise static class information. + /// + static WindowsNameTransform() + { + char[] invalidPathChars; + +#if NET_1_0 || NET_1_1 || NETCF_1_0 + invalidPathChars = Path.InvalidPathChars; +#else + invalidPathChars = Path.GetInvalidPathChars(); +#endif + int howMany = invalidPathChars.Length + 3; + + InvalidEntryChars = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryChars, 0, invalidPathChars.Length); + InvalidEntryChars[howMany - 1] = '*'; + InvalidEntryChars[howMany - 2] = '?'; + InvalidEntryChars[howMany - 3] = ':'; + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to make valid + /// The replacement character to use for any invalid characters. + /// Returns a valid name + public static string MakeValidName(string name, char replacement) + { + if ( name == null ) { + throw new ArgumentNullException("name"); + } + + name = WindowsPathUtils.DropPathRoot(name.Replace("/", @"\")); + + // Drop any leading slashes. + while ( (name.Length > 0) && (name[0] == '\\')) { + name = name.Remove(0, 1); + } + + // Drop any trailing slashes. + while ( (name.Length > 0) && (name[name.Length - 1] == '\\')) { + name = name.Remove(name.Length - 1, 1); + } + + // Convert consecutive \\ characters to \ + int index = name.IndexOf(@"\\"); + while (index >= 0) { + name = name.Remove(index, 1); + index = name.IndexOf(@"\\"); + } + + // Convert any invalid characters using the replacement one. + index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) { + StringBuilder builder = new StringBuilder(name); + + while (index >= 0 ) { + builder[index] = replacement; + + if (index >= name.Length) { + index = -1; + } + else { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + // Check for names greater than MaxPath characters. + // TODO: Were is CLR version of MaxPath defined? Can't find it in Environment. + if ( name.Length > MaxPath ) { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Gets or set the character to replace invalid characters during transformations. + /// + public char Replacement + { + get { return _replacementChar; } + set { + for ( int i = 0; i < InvalidEntryChars.Length; ++i ) { + if ( InvalidEntryChars[i] == value ) { + throw new ArgumentException("invalid path character"); + } + } + + if ((value == '\\') || (value == '/')) { + throw new ArgumentException("invalid replacement character"); + } + + _replacementChar = value; + } + } + + /// + /// The maximum windows path name permitted. + /// + /// This may not valid for all windows systems - CE?, etc but I cant find the equivalent in the CLR. + const int MaxPath = 260; + + #region Instance Fields + string _baseDirectory; + bool _trimIncomingPaths; + char _replacementChar = '_'; + #endregion + + #region Class Fields + static readonly char[] InvalidEntryChars; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs new file mode 100644 index 000000000..e544eafd3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs @@ -0,0 +1,632 @@ +// ZipConstants.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support + +using System; +using System.Text; +using System.Threading; + +#if NETCF_1_0 || NETCF_2_0 +using System.Globalization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + #region Enumerations + + /// + /// Determines how entries are tested to see if they should use Zip64 extensions or not. + /// + public enum UseZip64 + { + /// + /// Zip64 will not be forced on entries during processing. + /// + /// An entry can have this overridden if required + Off, + /// + /// Zip64 should always be used. + /// + On, + /// + /// #ZipLib will determine use based on entry values when added to archive. + /// + Dynamic, + } + + /// + /// The kind of compression used for an entry in an archive + /// + public enum CompressionMethod + { + /// + /// A direct copy of the file contents is held in the archive + /// + Stored = 0, + + /// + /// Common Zip compression method using a sliding dictionary + /// of up to 32KB and secondary compression from Huffman/Shannon-Fano trees + /// + Deflated = 8, + + /// + /// An extension to deflate with a 64KB window. Not supported by #Zip currently + /// + Deflate64 = 9, + + /// + /// BZip2 compression. Not supported by #Zip. + /// + BZip2 = 11, + + /// + /// WinZip special for AES encryption, Now supported by #Zip. + /// + WinZipAES = 99, + + } + + /// + /// Identifies the encryption algorithm used for an entry + /// + public enum EncryptionAlgorithm + { + /// + /// No encryption has been used. + /// + None = 0, + /// + /// Encrypted using PKZIP 2.0 or 'classic' encryption. + /// + PkzipClassic = 1, + /// + /// DES encryption has been used. + /// + Des = 0x6601, + /// + /// RCS encryption has been used for encryption. + /// + RC2 = 0x6602, + /// + /// Triple DES encryption with 168 bit keys has been used for this entry. + /// + TripleDes168 = 0x6603, + /// + /// Triple DES with 112 bit keys has been used for this entry. + /// + TripleDes112 = 0x6609, + /// + /// AES 128 has been used for encryption. + /// + Aes128 = 0x660e, + /// + /// AES 192 has been used for encryption. + /// + Aes192 = 0x660f, + /// + /// AES 256 has been used for encryption. + /// + Aes256 = 0x6610, + /// + /// RC2 corrected has been used for encryption. + /// + RC2Corrected = 0x6702, + /// + /// Blowfish has been used for encryption. + /// + Blowfish = 0x6720, + /// + /// Twofish has been used for encryption. + /// + Twofish = 0x6721, + /// + /// RC4 has been used for encryption. + /// + RC4 = 0x6801, + /// + /// An unknown algorithm has been used for encryption. + /// + Unknown = 0xffff + } + + /// + /// Defines the contents of the general bit flags field for an archive entry. + /// + [Flags] + public enum GeneralBitFlags : int + { + /// + /// Bit 0 if set indicates that the file is encrypted + /// + Encrypted = 0x0001, + /// + /// Bits 1 and 2 - Two bits defining the compression method (only for Method 6 Imploding and 8,9 Deflating) + /// + Method = 0x0006, + /// + /// Bit 3 if set indicates a trailing data desciptor is appended to the entry data + /// + Descriptor = 0x0008, + /// + /// Bit 4 is reserved for use with method 8 for enhanced deflation + /// + ReservedPKware4 = 0x0010, + /// + /// Bit 5 if set indicates the file contains Pkzip compressed patched data. + /// Requires version 2.7 or greater. + /// + Patched = 0x0020, + /// + /// Bit 6 if set indicates strong encryption has been used for this entry. + /// + StrongEncryption = 0x0040, + /// + /// Bit 7 is currently unused + /// + Unused7 = 0x0080, + /// + /// Bit 8 is currently unused + /// + Unused8 = 0x0100, + /// + /// Bit 9 is currently unused + /// + Unused9 = 0x0200, + /// + /// Bit 10 is currently unused + /// + Unused10 = 0x0400, + /// + /// Bit 11 if set indicates the filename and + /// comment fields for this file must be encoded using UTF-8. + /// + UnicodeText = 0x0800, + /// + /// Bit 12 is documented as being reserved by PKware for enhanced compression. + /// + EnhancedCompress = 0x1000, + /// + /// Bit 13 if set indicates that values in the local header are masked to hide + /// their actual values, and the central directory is encrypted. + /// + /// + /// Used when encrypting the central directory contents. + /// + HeaderMasked = 0x2000, + /// + /// Bit 14 is documented as being reserved for use by PKware + /// + ReservedPkware14 = 0x4000, + /// + /// Bit 15 is documented as being reserved for use by PKware + /// + ReservedPkware15 = 0x8000 + } + + #endregion + + /// + /// This class contains constants used for Zip format files + /// + public sealed class ZipConstants + { + #region Versions + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See . + /// + public const int VersionMadeBy = 51; // was 45 before AES + + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See ZipInputStream.CanDecompressEntry. + /// + [Obsolete("Use VersionMadeBy instead")] + public const int VERSION_MADE_BY = 51; + + /// + /// The minimum version required to support strong encryption + /// + public const int VersionStrongEncryption = 50; + + /// + /// The minimum version required to support strong encryption + /// + [Obsolete("Use VersionStrongEncryption instead")] + public const int VERSION_STRONG_ENCRYPTION = 50; + + /// + /// Version indicating AES encryption + /// + public const int VERSION_AES = 51; + + /// + /// The version required for Zip64 extensions (4.5 or higher) + /// + public const int VersionZip64 = 45; + #endregion + + #region Header Sizes + /// + /// Size of local entry header (excluding variable length fields at end) + /// + public const int LocalHeaderBaseSize = 30; + + /// + /// Size of local entry header (excluding variable length fields at end) + /// + [Obsolete("Use LocalHeaderBaseSize instead")] + public const int LOCHDR = 30; + + /// + /// Size of Zip64 data descriptor + /// + public const int Zip64DataDescriptorSize = 24; + + /// + /// Size of data descriptor + /// + public const int DataDescriptorSize = 16; + + /// + /// Size of data descriptor + /// + [Obsolete("Use DataDescriptorSize instead")] + public const int EXTHDR = 16; + + /// + /// Size of central header entry (excluding variable fields) + /// + public const int CentralHeaderBaseSize = 46; + + /// + /// Size of central header entry + /// + [Obsolete("Use CentralHeaderBaseSize instead")] + public const int CENHDR = 46; + + /// + /// Size of end of central record (excluding variable fields) + /// + public const int EndOfCentralRecordBaseSize = 22; + + /// + /// Size of end of central record (excluding variable fields) + /// + [Obsolete("Use EndOfCentralRecordBaseSize instead")] + public const int ENDHDR = 22; + + /// + /// Size of 'classic' cryptographic header stored before any entry data + /// + public const int CryptoHeaderSize = 12; + + /// + /// Size of cryptographic header stored before entry data + /// + [Obsolete("Use CryptoHeaderSize instead")] + public const int CRYPTO_HEADER_SIZE = 12; + #endregion + + #region Header Signatures + + /// + /// Signature for local entry header + /// + public const int LocalHeaderSignature = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for local entry header + /// + [Obsolete("Use LocalHeaderSignature instead")] + public const int LOCSIG = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for spanning entry + /// + public const int SpanningSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for spanning entry + /// + [Obsolete("Use SpanningSignature instead")] + public const int SPANNINGSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for temporary spanning entry + /// + public const int SpanningTempSignature = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for temporary spanning entry + /// + [Obsolete("Use SpanningTempSignature instead")] + public const int SPANTEMPSIG = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + public const int DataDescriptorSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + [Obsolete("Use DataDescriptorSignature instead")] + public const int EXTSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for central header + /// + [Obsolete("Use CentralHeaderSignature instead")] + public const int CENSIG = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for central header + /// + public const int CentralHeaderSignature = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for Zip64 central file header + /// + public const int Zip64CentralFileHeaderSignature = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central file header + /// + [Obsolete("Use Zip64CentralFileHeaderSignature instead")] + public const int CENSIG64 = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central directory locator + /// + public const int Zip64CentralDirLocatorSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Signature for archive extra data signature (were headers are encrypted). + /// + public const int ArchiveExtraDataSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Central header digitial signature + /// + public const int CentralHeaderDigitalSignature = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// Central header digitial signature + /// + [Obsolete("Use CentralHeaderDigitalSignaure instead")] + public const int CENDIGITALSIG = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// End of central directory record signature + /// + public const int EndOfCentralDirectorySignature = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + + /// + /// End of central directory record signature + /// + [Obsolete("Use EndOfCentralDirectorySignature instead")] + public const int ENDSIG = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + #endregion + +#if NETCF_1_0 || NETCF_2_0 + // This isnt so great but is better than nothing. + // Trying to work out an appropriate OEM code page would be good. + // 850 is a good default for english speakers particularly in Europe. + static int defaultCodePage = CultureInfo.CurrentCulture.TextInfo.ANSICodePage; +#else + static int defaultCodePage = Thread.CurrentThread.CurrentCulture.TextInfo.OEMCodePage; +#endif + + /// + /// Default encoding used for string conversion. 0 gives the default system OEM code page. + /// Dont use unicode encodings if you want to be Zip compatible! + /// Using the default code page isnt the full solution neccessarily + /// there are many variable factors, codepage 850 is often a good choice for + /// European users, however be careful about compatability. + /// + public static int DefaultCodePage { + get { + return defaultCodePage; + } + set { + defaultCodePage = value; + } + } + + /// + /// Convert a portion of a byte array to a string. + /// + /// + /// Data to convert to string + /// + /// + /// Number of bytes to convert starting from index 0 + /// + /// + /// data[0]..data[length - 1] converted to a string + /// + public static string ConvertToString(byte[] data, int count) + { + if ( data == null ) { + return string.Empty; + } + + return Encoding.GetEncoding(DefaultCodePage).GetString(data, 0, count); + } + + /// + /// Convert a byte array to string + /// + /// + /// Byte array to convert + /// + /// + /// dataconverted to a string + /// + public static string ConvertToString(byte[] data) + { + if ( data == null ) { + return string.Empty; + } + return ConvertToString(data, data.Length); + } + + /// + /// Convert a byte array to string + /// + /// The applicable general purpose bits flags + /// + /// Byte array to convert + /// + /// The number of bytes to convert. + /// + /// dataconverted to a string + /// + public static string ConvertToStringExt(int flags, byte[] data, int count) + { + if ( data == null ) { + return string.Empty; + } + + if ( (flags & (int)GeneralBitFlags.UnicodeText) != 0 ) { + return Encoding.UTF8.GetString(data, 0, count); + } + else { + return ConvertToString(data, count); + } + } + + /// + /// Convert a byte array to string + /// + /// + /// Byte array to convert + /// + /// The applicable general purpose bits flags + /// + /// dataconverted to a string + /// + public static string ConvertToStringExt(int flags, byte[] data) + { + if ( data == null ) { + return string.Empty; + } + + if ( (flags & (int)GeneralBitFlags.UnicodeText) != 0 ) { + return Encoding.UTF8.GetString(data, 0, data.Length); + } + else { + return ConvertToString(data, data.Length); + } + } + + /// + /// Convert a string to a byte array + /// + /// + /// String to convert to an array + /// + /// Converted array + public static byte[] ConvertToArray(string str) + { + if ( str == null ) { + return new byte[0]; + } + + return Encoding.GetEncoding(DefaultCodePage).GetBytes(str); + } + + /// + /// Convert a string to a byte array + /// + /// The applicable general purpose bits flags + /// + /// String to convert to an array + /// + /// Converted array + public static byte[] ConvertToArray(int flags, string str) + { + if (str == null) { + return new byte[0]; + } + + if ((flags & (int)GeneralBitFlags.UnicodeText) != 0) { + return Encoding.UTF8.GetBytes(str); + } + else { + return ConvertToArray(str); + } + } + + + /// + /// Initialise default instance of ZipConstants + /// + /// + /// Private to prevent instances being created. + /// + ZipConstants() + { + // Do nothing + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs new file mode 100644 index 000000000..b1464c035 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs @@ -0,0 +1,1252 @@ +// ZipEntry.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support +// 02-02-2010 DavidPierson Changed NTFS Extra Data min length to 4 + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Defines known values for the property. + /// + public enum HostSystemID + { + /// + /// Host system = MSDOS + /// + Msdos = 0, + /// + /// Host system = Amiga + /// + Amiga = 1, + /// + /// Host system = Open VMS + /// + OpenVms = 2, + /// + /// Host system = Unix + /// + Unix = 3, + /// + /// Host system = VMCms + /// + VMCms = 4, + /// + /// Host system = Atari ST + /// + AtariST = 5, + /// + /// Host system = OS2 + /// + OS2 = 6, + /// + /// Host system = Macintosh + /// + Macintosh = 7, + /// + /// Host system = ZSystem + /// + ZSystem = 8, + /// + /// Host system = Cpm + /// + Cpm = 9, + /// + /// Host system = Windows NT + /// + WindowsNT = 10, + /// + /// Host system = MVS + /// + MVS = 11, + /// + /// Host system = VSE + /// + Vse = 12, + /// + /// Host system = Acorn RISC + /// + AcornRisc = 13, + /// + /// Host system = VFAT + /// + Vfat = 14, + /// + /// Host system = Alternate MVS + /// + AlternateMvs = 15, + /// + /// Host system = BEOS + /// + BeOS = 16, + /// + /// Host system = Tandem + /// + Tandem = 17, + /// + /// Host system = OS400 + /// + OS400 = 18, + /// + /// Host system = OSX + /// + OSX = 19, + /// + /// Host system = WinZIP AES + /// + WinZipAES = 99, + } + + /// + /// This class represents an entry in a zip archive. This can be a file + /// or a directory + /// ZipFile and ZipInputStream will give you instances of this class as + /// information about the members in an archive. ZipOutputStream + /// uses an instance of this class when creating an entry in a Zip file. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ public class ZipEntry : ICloneable + { + [Flags] + enum Known : byte + { + None = 0, + Size = 0x01, + CompressedSize = 0x02, + Crc = 0x04, + Time = 0x08, + ExternalAttributes = 0x10, + } + + #region Constructors + /// + /// Creates a zip entry with the given name. + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with relative names only. + /// There are with no device names and path elements are separated by '/' characters. + /// + /// + /// The name passed is null + /// + public ZipEntry(string name) + : this(name, 0, ZipConstants.VersionMadeBy, CompressionMethod.Deflated) + { + } + + /// + /// Creates a zip entry with the given name and version required to extract + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with no device names and + /// path elements separated by '/' characters. This is not enforced see CleanName + /// on how to ensure names are valid if this is desired. + /// + /// + /// The minimum 'feature version' required this entry + /// + /// + /// The name passed is null + /// + internal ZipEntry(string name, int versionRequiredToExtract) + : this(name, versionRequiredToExtract, ZipConstants.VersionMadeBy, + CompressionMethod.Deflated) + { + } + + /// + /// Initializes an entry with the given name and made by information + /// + /// Name for this entry + /// Version and HostSystem Information + /// Minimum required zip feature version required to extract this entry + /// Compression method for this entry. + /// + /// The name passed is null + /// + /// + /// versionRequiredToExtract should be 0 (auto-calculate) or > 10 + /// + /// + /// This constructor is used by the ZipFile class when reading from the central header + /// It is not generally useful, use the constructor specifying the name only. + /// + internal ZipEntry(string name, int versionRequiredToExtract, int madeByInfo, + CompressionMethod method) + { + if (name == null) { + throw new System.ArgumentNullException("name"); + } + + if ( name.Length > 0xffff ) { + throw new ArgumentException("Name is too long", "name"); + } + + if ( (versionRequiredToExtract != 0) && (versionRequiredToExtract < 10) ) { + throw new ArgumentOutOfRangeException("versionRequiredToExtract"); + } + + this.DateTime = System.DateTime.Now; + this.name = name; + this.versionMadeBy = (ushort)madeByInfo; + this.versionToExtract = (ushort)versionRequiredToExtract; + this.method = method; + } + + /// + /// Creates a deep copy of the given zip entry. + /// + /// + /// The entry to copy. + /// + [Obsolete("Use Clone instead")] + public ZipEntry(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + known = entry.known; + name = entry.name; + size = entry.size; + compressedSize = entry.compressedSize; + crc = entry.crc; + dosTime = entry.dosTime; + method = entry.method; + comment = entry.comment; + versionToExtract = entry.versionToExtract; + versionMadeBy = entry.versionMadeBy; + externalFileAttributes = entry.externalFileAttributes; + flags = entry.flags; + + zipFileIndex = entry.zipFileIndex; + offset = entry.offset; + + forceZip64_ = entry.forceZip64_; + + if ( entry.extra != null ) { + extra = new byte[entry.extra.Length]; + Array.Copy(entry.extra, 0, extra, 0, entry.extra.Length); + } + } + + #endregion + + /// + /// Get a value indicating wether the entry has a CRC value available. + /// + public bool HasCrc + { + get { + return (known & Known.Crc) != 0; + } + } + + /// + /// Get/Set flag indicating if entry is encrypted. + /// A simple helper routine to aid interpretation of flags + /// + /// This is an assistant that interprets the flags property. + public bool IsCrypted + { + get { + return (flags & 1) != 0; + } + set { + if (value) { + flags |= 1; + } + else { + flags &= ~1; + } + } + } + + /// + /// Get / set a flag indicating wether entry name and comment text are + /// encoded in unicode UTF8. + /// + /// This is an assistant that interprets the flags property. + public bool IsUnicodeText + { + get { + return ( flags & (int)GeneralBitFlags.UnicodeText ) != 0; + } + set { + if ( value ) { + flags |= (int)GeneralBitFlags.UnicodeText; + } + else { + flags &= ~(int)GeneralBitFlags.UnicodeText; + } + } + } + + /// + /// Value used during password checking for PKZIP 2.0 / 'classic' encryption. + /// + internal byte CryptoCheckValue + { + get { + return cryptoCheckValue_; + } + + set { + cryptoCheckValue_ = value; + } + } + + /// + /// Get/Set general purpose bit flag for entry + /// + /// + /// General purpose bit flag
+ ///
+ /// Bit 0: If set, indicates the file is encrypted
+ /// Bit 1-2 Only used for compression type 6 Imploding, and 8, 9 deflating
+ /// Imploding:
+ /// Bit 1 if set indicates an 8K sliding dictionary was used. If clear a 4k dictionary was used
+ /// Bit 2 if set indicates 3 Shannon-Fanno trees were used to encode the sliding dictionary, 2 otherwise
+ ///
+ /// Deflating:
+ /// Bit 2 Bit 1
+ /// 0 0 Normal compression was used
+ /// 0 1 Maximum compression was used
+ /// 1 0 Fast compression was used
+ /// 1 1 Super fast compression was used
+ ///
+ /// Bit 3: If set, the fields crc-32, compressed size + /// and uncompressed size are were not able to be written during zip file creation + /// The correct values are held in a data descriptor immediately following the compressed data.
+ /// Bit 4: Reserved for use by PKZIP for enhanced deflating
+ /// Bit 5: If set indicates the file contains compressed patch data
+ /// Bit 6: If set indicates strong encryption was used.
+ /// Bit 7-10: Unused or reserved
+ /// Bit 11: If set the name and comments for this entry are in unicode.
+ /// Bit 12-15: Unused or reserved
+ ///
+ /// + /// + public int Flags + { + get { + return flags; + } + set { + flags = value; + } + } + + /// + /// Get/Set index of this entry in Zip file + /// + /// This is only valid when the entry is part of a + public long ZipFileIndex + { + get { + return zipFileIndex; + } + set { + zipFileIndex = value; + } + } + + /// + /// Get/set offset for use in central header + /// + public long Offset + { + get { + return offset; + } + set { + offset = value; + } + } + + /// + /// Get/Set external file attributes as an integer. + /// The values of this are operating system dependant see + /// HostSystem for details + /// + public int ExternalFileAttributes + { + get { + if ((known & Known.ExternalAttributes) == 0) { + return -1; + } + else { + return externalFileAttributes; + } + } + + set { + externalFileAttributes = value; + known |= Known.ExternalAttributes; + } + } + + /// + /// Get the version made by for this entry or zero if unknown. + /// The value / 10 indicates the major version number, and + /// the value mod 10 is the minor version number + /// + public int VersionMadeBy + { + get { + return (versionMadeBy & 0xff); + } + } + + /// + /// Get a value indicating this entry is for a DOS/Windows system. + /// + public bool IsDOSEntry + { + get { + return ((HostSystem == ( int )HostSystemID.Msdos) || + (HostSystem == ( int )HostSystemID.WindowsNT)); + } + } + + /// + /// Test the external attributes for this to + /// see if the external attributes are Dos based (including WINNT and variants) + /// and match the values + /// + /// The attributes to test. + /// Returns true if the external attributes are known to be DOS/Windows + /// based and have the same attributes set as the value passed. + bool HasDosAttributes(int attributes) + { + bool result = false; + if ( (known & Known.ExternalAttributes) != 0 ) { + if ( ((HostSystem == (int)HostSystemID.Msdos) || + (HostSystem == (int)HostSystemID.WindowsNT)) && + (ExternalFileAttributes & attributes) == attributes) { + result = true; + } + } + return result; + } + + /// + /// Gets the compatability information for the external file attribute + /// If the external file attributes are compatible with MS-DOS and can be read + /// by PKZIP for DOS version 2.04g then this value will be zero. Otherwise the value + /// will be non-zero and identify the host system on which the attributes are compatible. + /// + /// + /// + /// The values for this as defined in the Zip File format and by others are shown below. The values are somewhat + /// misleading in some cases as they are not all used as shown. You should consult the relevant documentation + /// to obtain up to date and correct information. The modified appnote by the infozip group is + /// particularly helpful as it documents a lot of peculiarities. The document is however a little dated. + /// + /// 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + /// 1 - Amiga + /// 2 - OpenVMS + /// 3 - Unix + /// 4 - VM/CMS + /// 5 - Atari ST + /// 6 - OS/2 HPFS + /// 7 - Macintosh + /// 8 - Z-System + /// 9 - CP/M + /// 10 - Windows NTFS + /// 11 - MVS (OS/390 - Z/OS) + /// 12 - VSE + /// 13 - Acorn Risc + /// 14 - VFAT + /// 15 - Alternate MVS + /// 16 - BeOS + /// 17 - Tandem + /// 18 - OS/400 + /// 19 - OS/X (Darwin) + /// 99 - WinZip AES + /// remainder - unused + /// + /// + public int HostSystem + { + get { + return (versionMadeBy >> 8) & 0xff; + } + + set { + versionMadeBy &= 0xff; + versionMadeBy |= (ushort)((value & 0xff) << 8); + } + } + + /// + /// Get minimum Zip feature version required to extract this entry + /// + /// + /// Minimum features are defined as:
+ /// 1.0 - Default value
+ /// 1.1 - File is a volume label
+ /// 2.0 - File is a folder/directory
+ /// 2.0 - File is compressed using Deflate compression
+ /// 2.0 - File is encrypted using traditional encryption
+ /// 2.1 - File is compressed using Deflate64
+ /// 2.5 - File is compressed using PKWARE DCL Implode
+ /// 2.7 - File is a patch data set
+ /// 4.5 - File uses Zip64 format extensions
+ /// 4.6 - File is compressed using BZIP2 compression
+ /// 5.0 - File is encrypted using DES
+ /// 5.0 - File is encrypted using 3DES
+ /// 5.0 - File is encrypted using original RC2 encryption
+ /// 5.0 - File is encrypted using RC4 encryption
+ /// 5.1 - File is encrypted using AES encryption
+ /// 5.1 - File is encrypted using corrected RC2 encryption
+ /// 5.1 - File is encrypted using corrected RC2-64 encryption
+ /// 6.1 - File is encrypted using non-OAEP key wrapping
+ /// 6.2 - Central directory encryption (not confirmed yet)
+ /// 6.3 - File is compressed using LZMA
+ /// 6.3 - File is compressed using PPMD+
+ /// 6.3 - File is encrypted using Blowfish
+ /// 6.3 - File is encrypted using Twofish
+ ///
+ /// + public int Version + { + get { + // Return recorded version if known. + if (versionToExtract != 0) { + return versionToExtract; + } + else { + int result = 10; + if (AESKeySize > 0) { + result = ZipConstants.VERSION_AES; // Ver 5.1 = AES + } + else if (CentralHeaderRequiresZip64) { + result = ZipConstants.VersionZip64; + } + else if (CompressionMethod.Deflated == method) { + result = 20; + } + else if (IsDirectory == true) { + result = 20; + } + else if (IsCrypted == true) { + result = 20; + } + else if (HasDosAttributes(0x08) ) { + result = 11; + } + return result; + } + } + } + + /// + /// Get a value indicating whether this entry can be decompressed by the library. + /// + /// This is based on the and + /// wether the compression method is supported. + public bool CanDecompress + { + get { + return (Version <= ZipConstants.VersionMadeBy) && + ((Version == 10) || + (Version == 11) || + (Version == 20) || + (Version == 45) || + (Version == 51)) && + IsCompressionMethodSupported(); + } + } + + /// + /// Force this entry to be recorded using Zip64 extensions. + /// + public void ForceZip64() + { + forceZip64_ = true; + } + + /// + /// Get a value indicating wether Zip64 extensions were forced. + /// + /// A value of true if Zip64 extensions have been forced on; false if not. + public bool IsZip64Forced() + { + return forceZip64_; + } + + /// + /// Gets a value indicating if the entry requires Zip64 extensions + /// to store the full entry values. + /// + /// A value of true if a local header requires Zip64 extensions; false if not. + public bool LocalHeaderRequiresZip64 + { + get { + bool result = forceZip64_; + + if ( !result ) { + ulong trueCompressedSize = compressedSize; + + if ( (versionToExtract == 0) && IsCrypted ) { + trueCompressedSize += ZipConstants.CryptoHeaderSize; + } + + // TODO: A better estimation of the true limit based on compression overhead should be used + // to determine when an entry should use Zip64. + result = + ((this.size >= uint.MaxValue) || (trueCompressedSize >= uint.MaxValue)) && + ((versionToExtract == 0) || (versionToExtract >= ZipConstants.VersionZip64)); + } + + return result; + } + } + + /// + /// Get a value indicating wether the central directory entry requires Zip64 extensions to be stored. + /// + public bool CentralHeaderRequiresZip64 + { + get { + return LocalHeaderRequiresZip64 || (offset >= uint.MaxValue); + } + } + + /// + /// Get/Set DosTime value. + /// + /// + /// The MS-DOS date format can only represent dates between 1/1/1980 and 12/31/2107. + /// + public long DosTime + { + get { + if ((known & Known.Time) == 0) { + return 0; + } + else { + return dosTime; + } + } + + set { + unchecked { + dosTime = (uint)value; + } + + known |= Known.Time; + } + } + + /// + /// Gets/Sets the time of last modification of the entry. + /// + /// + /// The property is updated to match this as far as possible. + /// + public DateTime DateTime + { + get { + uint sec = Math.Min(59, 2 * (dosTime & 0x1f)); + uint min = Math.Min(59, (dosTime >> 5) & 0x3f); + uint hrs = Math.Min(23, (dosTime >> 11) & 0x1f); + uint mon = Math.Max(1, Math.Min(12, ((dosTime >> 21) & 0xf))); + uint year = ((dosTime >> 25) & 0x7f) + 1980; + int day = Math.Max(1, Math.Min(DateTime.DaysInMonth((int)year, (int)mon), (int)((dosTime >> 16) & 0x1f))); + return new System.DateTime((int)year, (int)mon, day, (int)hrs, (int)min, (int)sec); + } + + set { + uint year = (uint) value.Year; + uint month = (uint) value.Month; + uint day = (uint) value.Day; + uint hour = (uint) value.Hour; + uint minute = (uint) value.Minute; + uint second = (uint) value.Second; + + if ( year < 1980 ) { + year = 1980; + month = 1; + day = 1; + hour = 0; + minute = 0; + second = 0; + } + else if ( year > 2107 ) { + year = 2107; + month = 12; + day = 31; + hour = 23; + minute = 59; + second = 59; + } + + DosTime = ((year - 1980) & 0x7f) << 25 | + (month << 21) | + (day << 16) | + (hour << 11) | + (minute << 5) | + (second >> 1); + } + } + + /// + /// Returns the entry name. + /// + /// + /// The unix naming convention is followed. + /// Path components in the entry should always separated by forward slashes ('/'). + /// Dos device names like C: should also be removed. + /// See the class, or + /// + public string Name + { + get { + return name; + } + } + + /// + /// Gets/Sets the size of the uncompressed data. + /// + /// + /// The size or -1 if unknown. + /// + /// Setting the size before adding an entry to an archive can help + /// avoid compatability problems with some archivers which dont understand Zip64 extensions. + public long Size + { + get { + return (known & Known.Size) != 0 ? (long)size : -1L; + } + set { + this.size = (ulong)value; + this.known |= Known.Size; + } + } + + /// + /// Gets/Sets the size of the compressed data. + /// + /// + /// The compressed entry size or -1 if unknown. + /// + public long CompressedSize + { + get { + return (known & Known.CompressedSize) != 0 ? (long)compressedSize : -1L; + } + set { + this.compressedSize = (ulong)value; + this.known |= Known.CompressedSize; + } + } + + /// + /// Gets/Sets the crc of the uncompressed data. + /// + /// + /// Crc is not in the range 0..0xffffffffL + /// + /// + /// The crc value or -1 if unknown. + /// + public long Crc + { + get { + return (known & Known.Crc) != 0 ? crc & 0xffffffffL : -1L; + } + set { + if (((ulong)crc & 0xffffffff00000000L) != 0) { + throw new ArgumentOutOfRangeException("value"); + } + this.crc = (uint)value; + this.known |= Known.Crc; + } + } + + /// + /// Gets/Sets the compression method. Only Deflated and Stored are supported. + /// + /// + /// The compression method for this entry + /// + /// + /// + public CompressionMethod CompressionMethod { + get { + return method; + } + + set { + if ( !IsCompressionMethodSupported(value) ) { + throw new NotSupportedException("Compression method not supported"); + } + this.method = value; + } + } + + /// + /// Gets the compression method for outputting to the local or central header. + /// Returns same value as CompressionMethod except when AES encrypting, which + /// places 99 in the method and places the real method in the extra data. + /// + internal CompressionMethod CompressionMethodForHeader { + get { + return (AESKeySize > 0) ? CompressionMethod.WinZipAES : method; + } + } + + /// + /// Gets/Sets the extra data. + /// + /// + /// Extra data is longer than 64KB (0xffff) bytes. + /// + /// + /// Extra data or null if not set. + /// + public byte[] ExtraData { + + get { +// TODO: This is slightly safer but less efficient. Think about wether it should change. +// return (byte[]) extra.Clone(); + return extra; + } + + set { + if (value == null) { + extra = null; + } + else { + if (value.Length > 0xffff) { + throw new System.ArgumentOutOfRangeException("value"); + } + + extra = new byte[value.Length]; + Array.Copy(value, 0, extra, 0, value.Length); + } + } + } + + +#if !NET_1_1 && !NETCF_2_0 + /// + /// For AES encrypted files returns or sets the number of bits of encryption (128, 192 or 256). + /// When setting, only 0 (off), 128 or 256 is supported. + /// + public int AESKeySize { + get { + // the strength (1 or 3) is in the entry header + switch (_aesEncryptionStrength) { + case 0: return 0; // Not AES + case 1: return 128; + case 2: return 192; // Not used by WinZip + case 3: return 256; + default: throw new ZipException("Invalid AESEncryptionStrength " + _aesEncryptionStrength); + } + } + set { + switch (value) { + case 0: _aesEncryptionStrength = 0; break; + case 128: _aesEncryptionStrength = 1; break; + case 256: _aesEncryptionStrength = 3; break; + default: throw new ZipException("AESKeySize must be 0, 128 or 256: " + value); + } + } + } + + /// + /// AES Encryption strength for storage in extra data in entry header. + /// 1 is 128 bit, 2 is 192 bit, 3 is 256 bit. + /// + internal byte AESEncryptionStrength { + get { + return (byte)_aesEncryptionStrength; + } + } +#else + /// + /// AES unsupported prior to .NET 2.0 + /// + internal int AESKeySize; +#endif + + /// + /// Returns the length of the salt, in bytes + /// + internal int AESSaltLen { + get { + // Key size -> Salt length: 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + return AESKeySize / 16; + } + } + + /// + /// Number of extra bytes required to hold the AES Header fields (Salt, Pwd verify, AuthCode) + /// + internal int AESOverheadSize { + get { + // File format: + // Bytes Content + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + return 12 + AESSaltLen; + } + } + + /// + /// Process extra data fields updating the entry based on the contents. + /// + /// True if the extra data fields should be handled + /// for a local header, rather than for a central header. + /// + internal void ProcessExtraData(bool localHeader) + { + ZipExtraData extraData = new ZipExtraData(this.extra); + + if ( extraData.Find(0x0001) ) { + // Version required to extract is ignored here as some archivers dont set it correctly + // in theory it should be version 45 or higher + + // The recorded size will change but remember that this is zip64. + forceZip64_ = true; + + if ( extraData.ValueLength < 4 ) { + throw new ZipException("Extra data extended Zip64 information length is invalid"); + } + + if ( localHeader || (size == uint.MaxValue) ) { + size = (ulong)extraData.ReadLong(); + } + + if ( localHeader || (compressedSize == uint.MaxValue) ) { + compressedSize = (ulong)extraData.ReadLong(); + } + + if ( !localHeader && (offset == uint.MaxValue) ) { + offset = extraData.ReadLong(); + } + + // Disk number on which file starts is ignored + } + else { + if ( + ((versionToExtract & 0xff) >= ZipConstants.VersionZip64) && + ((size == uint.MaxValue) || (compressedSize == uint.MaxValue)) + ) { + throw new ZipException("Zip64 Extended information required but is missing."); + } + } + + if ( extraData.Find(10) ) { + // No room for any tags. + if ( extraData.ValueLength < 4 ) { + throw new ZipException("NTFS Extra data invalid"); + } + + extraData.ReadInt(); // Reserved + + while ( extraData.UnreadCount >= 4 ) { + int ntfsTag = extraData.ReadShort(); + int ntfsLength = extraData.ReadShort(); + if ( ntfsTag == 1 ) { + if ( ntfsLength >= 24 ) { + long lastModification = extraData.ReadLong(); + long lastAccess = extraData.ReadLong(); + long createTime = extraData.ReadLong(); + + DateTime = System.DateTime.FromFileTime(lastModification); + } + break; + } + else { + // An unknown NTFS tag so simply skip it. + extraData.Skip(ntfsLength); + } + } + } + else if ( extraData.Find(0x5455) ) { + int length = extraData.ValueLength; + int flags = extraData.ReadByte(); + + // Can include other times but these are ignored. Length of data should + // actually be 1 + 4 * no of bits in flags. + if ( ((flags & 1) != 0) && (length >= 5) ) { + int iTime = extraData.ReadInt(); + + DateTime = (new System.DateTime ( 1970, 1, 1, 0, 0, 0 ).ToUniversalTime() + + new TimeSpan ( 0, 0, 0, iTime, 0 )).ToLocalTime(); + } + } + if (method == CompressionMethod.WinZipAES) { + ProcessAESExtraData(extraData); + } + } + + // For AES the method in the entry is 99, and the real compression method is in the extradata + // + private void ProcessAESExtraData(ZipExtraData extraData) { + +#if !NET_1_1 && !NETCF_2_0 + if (extraData.Find(0x9901)) { + // Set version and flag for Zipfile.CreateAndInitDecryptionStream + versionToExtract = ZipConstants.VERSION_AES; // Ver 5.1 = AES see "Version" getter + // Set StrongEncryption flag for ZipFile.CreateAndInitDecryptionStream + Flags = Flags | (int)GeneralBitFlags.StrongEncryption; + // + // Unpack AES extra data field see http://www.winzip.com/aes_info.htm + int length = extraData.ValueLength; // Data size currently 7 + if (length < 7) + throw new ZipException("AES Extra Data Length " + length + " invalid."); + int ver = extraData.ReadShort(); // Version number (1=AE-1 2=AE-2) + int vendorId = extraData.ReadShort(); // 2-character vendor ID 0x4541 = "AE" + int encrStrength = extraData.ReadByte(); // encryption strength 1 = 128 2 = 192 3 = 256 + int actualCompress = extraData.ReadShort(); // The actual compression method used to compress the file + _aesVer = ver; + _aesEncryptionStrength = encrStrength; + method = (CompressionMethod)actualCompress; + } else + throw new ZipException("AES Extra Data missing"); +#else + throw new ZipException("AES unsupported"); +#endif + } + + /// + /// Gets/Sets the entry comment. + /// + /// + /// If comment is longer than 0xffff. + /// + /// + /// The comment or null if not set. + /// + /// + /// A comment is only available for entries when read via the class. + /// The class doesnt have the comment data available. + /// + public string Comment { + get { + return comment; + } + set { + // This test is strictly incorrect as the length is in characters + // while the storage limit is in bytes. + // While the test is partially correct in that a comment of this length or greater + // is definitely invalid, shorter comments may also have an invalid length + // where there are multi-byte characters + // The full test is not possible here however as the code page to apply conversions with + // isnt available. + if ( (value != null) && (value.Length > 0xffff) ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "cannot exceed 65535"); +#endif + } + + comment = value; + } + } + + /// + /// Gets a value indicating if the entry is a directory. + /// however. + /// + /// + /// A directory is determined by an entry name with a trailing slash '/'. + /// The external file attributes can also indicate an entry is for a directory. + /// Currently only dos/windows attributes are tested in this manner. + /// The trailing slash convention should always be followed. + /// + public bool IsDirectory + { + get { + int nameLength = name.Length; + bool result = + ((nameLength > 0) && + ((name[nameLength - 1] == '/') || (name[nameLength - 1] == '\\'))) || + HasDosAttributes(16) + ; + return result; + } + } + + /// + /// Get a value of true if the entry appears to be a file; false otherwise + /// + /// + /// This only takes account of DOS/Windows attributes. Other operating systems are ignored. + /// For linux and others the result may be incorrect. + /// + public bool IsFile + { + get { + return !IsDirectory && !HasDosAttributes(8); + } + } + + /// + /// Test entry to see if data can be extracted. + /// + /// Returns true if data can be extracted for this entry; false otherwise. + public bool IsCompressionMethodSupported() + { + return IsCompressionMethodSupported(CompressionMethod); + } + + #region ICloneable Members + /// + /// Creates a copy of this zip entry. + /// + /// An that is a copy of the current instance. + public object Clone() + { + ZipEntry result = (ZipEntry)this.MemberwiseClone(); + + // Ensure extra data is unique if it exists. + if ( extra != null ) { + result.extra = new byte[extra.Length]; + Array.Copy(extra, 0, result.extra, 0, extra.Length); + } + + return result; + } + + #endregion + + /// + /// Gets a string representation of this ZipEntry. + /// + /// A readable textual representation of this + public override string ToString() + { + return name; + } + + /// + /// Test a compression method to see if this library + /// supports extracting data compressed with that method + /// + /// The compression method to test. + /// Returns true if the compression method is supported; false otherwise + public static bool IsCompressionMethodSupported(CompressionMethod method) + { + return + ( method == CompressionMethod.Deflated ) || + ( method == CompressionMethod.Stored ); + } + + /// + /// Cleans a name making it conform to Zip file conventions. + /// Devices names ('c:\') and UNC share names ('\\server\share') are removed + /// and forward slashes ('\') are converted to back slashes ('/'). + /// Names are made relative by trimming leading slashes which is compatible + /// with the ZIP naming convention. + /// + /// The name to clean + /// The 'cleaned' name. + /// + /// The Zip name transform class is more flexible. + /// + public static string CleanName(string name) + { + if (name == null) { + return string.Empty; + } + + if (Path.IsPathRooted(name) == true) { + // NOTE: + // for UNC names... \\machine\share\zoom\beet.txt gives \zoom\beet.txt + name = name.Substring(Path.GetPathRoot(name).Length); + } + + name = name.Replace(@"\", "/"); + + while ( (name.Length > 0) && (name[0] == '/')) { + name = name.Remove(0, 1); + } + return name; + } + + #region Instance Fields + Known known; + int externalFileAttributes = -1; // contains external attributes (O/S dependant) + + ushort versionMadeBy; // Contains host system and version information + // only relevant for central header entries + + string name; + ulong size; + ulong compressedSize; + ushort versionToExtract; // Version required to extract (library handles <= 2.0) + uint crc; + uint dosTime; + + CompressionMethod method = CompressionMethod.Deflated; + byte[] extra; + string comment; + + int flags; // general purpose bit flags + + long zipFileIndex = -1; // used by ZipFile + long offset; // used by ZipFile and ZipOutputStream + + bool forceZip64_; + byte cryptoCheckValue_; +#if !NET_1_1 && !NETCF_2_0 + int _aesVer; // Version number (2 = AE-2 ?). Assigned but not used. + int _aesEncryptionStrength; // Encryption strength 1 = 128 2 = 192 3 = 256 +#endif + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs new file mode 100644 index 000000000..4e5e1c1e8 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs @@ -0,0 +1,413 @@ +// ZipEntryFactory.cs +// +// Copyright 2006 John Reilly +// +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// Basic implementation of + /// + public class ZipEntryFactory : IEntryFactory + { + #region Enumerations + /// + /// Defines the possible values to be used for the . + /// + public enum TimeSetting + { + /// + /// Use the recorded LastWriteTime value for the file. + /// + LastWriteTime, + /// + /// Use the recorded LastWriteTimeUtc value for the file + /// + LastWriteTimeUtc, + /// + /// Use the recorded CreateTime value for the file. + /// + CreateTime, + /// + /// Use the recorded CreateTimeUtc value for the file. + /// + CreateTimeUtc, + /// + /// Use the recorded LastAccessTime value for the file. + /// + LastAccessTime, + /// + /// Use the recorded LastAccessTimeUtc value for the file. + /// + LastAccessTimeUtc, + /// + /// Use a fixed value. + /// + /// The actual value used can be + /// specified via the constructor or + /// using the with the setting set + /// to which will use the when this class was constructed. + /// The property can also be used to set this value. + Fixed, + } + #endregion + + #region Constructors + /// + /// Initialise a new instance of the class. + /// + /// A default , and the LastWriteTime for files is used. + public ZipEntryFactory() + { + nameTransform_ = new ZipNameTransform(); + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time setting to use when creating Zip entries. + public ZipEntryFactory(TimeSetting timeSetting) + { + timeSetting_ = timeSetting; + nameTransform_ = new ZipNameTransform(); + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time to set all values to. + public ZipEntryFactory(DateTime time) + { + timeSetting_ = TimeSetting.Fixed; + FixedDateTime = time; + nameTransform_ = new ZipNameTransform(); + } + + #endregion + + #region Properties + /// + /// Get / set the to be used when creating new values. + /// + /// + /// Setting this property to null will cause a default name transform to be used. + /// + public INameTransform NameTransform + { + get { return nameTransform_; } + set + { + if (value == null) { + nameTransform_ = new ZipNameTransform(); + } + else { + nameTransform_ = value; + } + } + } + + /// + /// Get / set the in use. + /// + public TimeSetting Setting + { + get { return timeSetting_; } + set { timeSetting_ = value; } + } + + /// + /// Get / set the value to use when is set to + /// + public DateTime FixedDateTime + { + get { return fixedDateTime_; } + set + { + if (value.Year < 1970) { + throw new ArgumentException("Value is too old to be valid", "value"); + } + fixedDateTime_ = value; + } + } + + /// + /// A bitmask defining the attributes to be retrieved from the actual file. + /// + /// The default is to get all possible attributes from the actual file. + public int GetAttributes + { + get { return getAttributes_; } + set { getAttributes_ = value; } + } + + /// + /// A bitmask defining which attributes are to be set on. + /// + /// By default no attributes are set on. + public int SetAttributes + { + get { return setAttributes_; } + set { setAttributes_ = value; } + } + + /// + /// Get set a value indicating wether unidoce text should be set on. + /// + public bool IsUnicodeText + { + get { return isUnicodeText_; } + set { isUnicodeText_ = value; } + } + + #endregion + + #region IEntryFactory Members + + /// + /// Make a new for a file. + /// + /// The name of the file to create a new entry for. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName) + { + return MakeFileEntry(fileName, true); + } + + /// + /// Make a new from a name. + /// + /// The name of the file to create a new entry for. + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName, bool useFileSystem) + { + ZipEntry result = new ZipEntry(nameTransform_.TransformFile(fileName)); + result.IsUnicodeText = isUnicodeText_; + + int externalAttributes = 0; + bool useAttributes = (setAttributes_ != 0); + + FileInfo fi = null; + if (useFileSystem) + { + fi = new FileInfo(fileName); + } + + if ((fi != null) && fi.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = fi.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.CreationTime.ToUniversalTime(); +#else + result.DateTime = fi.CreationTimeUtc; +#endif + break; + + case TimeSetting.LastAccessTime: + result.DateTime = fi.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.LastAccessTime.ToUniversalTime(); +#else + result.DateTime = fi.LastAccessTimeUtc; +#endif + break; + + case TimeSetting.LastWriteTime: + result.DateTime = fi.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.LastWriteTime.ToUniversalTime(); +#else + result.DateTime = fi.LastWriteTimeUtc; +#endif + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeFileEntry"); + } + + result.Size = fi.Length; + + useAttributes = true; + externalAttributes = ((int)fi.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + if (useAttributes) + { + externalAttributes |= setAttributes_; + result.ExternalFileAttributes = externalAttributes; + } + + return result; + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName) + { + return MakeDirectoryEntry(directoryName, true); + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem) + { + + ZipEntry result = new ZipEntry(nameTransform_.TransformDirectory(directoryName)); + result.IsUnicodeText = isUnicodeText_; + result.Size = 0; + + int externalAttributes = 0; + + DirectoryInfo di = null; + + if (useFileSystem) + { + di = new DirectoryInfo(directoryName); + } + + + if ((di != null) && di.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = di.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.CreationTime.ToUniversalTime(); +#else + result.DateTime = di.CreationTimeUtc; +#endif + break; + + case TimeSetting.LastAccessTime: + result.DateTime = di.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.LastAccessTime.ToUniversalTime(); +#else + result.DateTime = di.LastAccessTimeUtc; +#endif + break; + + case TimeSetting.LastWriteTime: + result.DateTime = di.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.LastWriteTime.ToUniversalTime(); +#else + result.DateTime = di.LastWriteTimeUtc; +#endif + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeDirectoryEntry"); + } + + externalAttributes = ((int)di.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + // Always set directory attribute on. + externalAttributes |= (setAttributes_ | 16); + result.ExternalFileAttributes = externalAttributes; + + return result; + } + + #endregion + + #region Instance Fields + INameTransform nameTransform_; + DateTime fixedDateTime_ = DateTime.Now; + TimeSetting timeSetting_; + bool isUnicodeText_; + + int getAttributes_ = -1; + int setAttributes_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs new file mode 100644 index 000000000..7ceb4a063 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs @@ -0,0 +1,94 @@ +// ZipException.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +#if !NETCF_1_0 && !NETCF_2_0 +using System.Runtime.Serialization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Represents exception conditions specific to Zip archive handling + /// +#if !NETCF_1_0 && !NETCF_2_0 + [Serializable] +#endif + public class ZipException : SharpZipBaseException + { +#if !NETCF_1_0 && !NETCF_2_0 + /// + /// Deserialization constructor + /// + /// for this constructor + /// for this constructor + protected ZipException(SerializationInfo info, StreamingContext context ) + : base( info, context ) + { + } +#endif + + /// + /// Initializes a new instance of the ZipException class. + /// + public ZipException() + { + } + + /// + /// Initializes a new instance of the ZipException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public ZipException(string message) + : base(message) + { + } + + /// + /// Initialise a new instance of ZipException. + /// + /// A message describing the error. + /// The exception that is the cause of the current exception. + public ZipException(string message, Exception exception) + : base(message, exception) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs new file mode 100644 index 000000000..533ba9c33 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs @@ -0,0 +1,987 @@ +// +// ZipExtraData.cs +// +// Copyright 2004-2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + // TODO: Sort out wether tagged data is useful and what a good implementation might look like. + // Its just a sketch of an idea at the moment. + + /// + /// ExtraData tagged value interface. + /// + public interface ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + short TagID { get; } + + /// + /// Set the contents of this instance from the data passed. + /// + /// The data to extract contents from. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + void SetData(byte[] data, int offset, int count); + + /// + /// Get the data representing this instance. + /// + /// Returns the data for this instance. + byte[] GetData(); + } + + /// + /// A raw binary tagged value + /// + public class RawTaggedData : ITaggedData + { + /// + /// Initialise a new instance. + /// + /// The tag ID. + public RawTaggedData(short tag) + { + _tag = tag; + } + + #region ITaggedData Members + + /// + /// Get the ID for this tagged data value. + /// + public short TagID + { + get { return _tag; } + set { _tag = value; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int offset, int count) + { + if( data==null ) + { + throw new ArgumentNullException("data"); + } + + _data=new byte[count]; + Array.Copy(data, offset, _data, 0, count); + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + return _data; + } + + #endregion + + /// + /// Get /set the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] Data + { + get { return _data; } + set { _data=value; } + } + + #region Instance Fields + /// + /// The tag ID for this instance. + /// + short _tag; + + byte[] _data; + #endregion + } + + /// + /// Class representing extended unix date time values. + /// + public class ExtendedUnixData : ITaggedData + { + /// + /// Flags indicate which values are included in this instance. + /// + [Flags] + public enum Flags : byte + { + /// + /// The modification time is included + /// + ModificationTime = 0x01, + + /// + /// The access time is included + /// + AccessTime = 0x02, + + /// + /// The create time is included. + /// + CreateTime = 0x04, + } + + #region ITaggedData Members + + /// + /// Get the ID + /// + public short TagID + { + get { return 0x5455; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + // bit 0 if set, modification time is present + // bit 1 if set, access time is present + // bit 2 if set, creation time is present + + _flags = (Flags)helperStream.ReadByte(); + if (((_flags & Flags.ModificationTime) != 0) && (count >= 5)) + { + int iTime = helperStream.ReadLEInt(); + + _modificationTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + + if ((_flags & Flags.AccessTime) != 0) + { + int iTime = helperStream.ReadLEInt(); + + _lastAccessTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + + if ((_flags & Flags.CreateTime) != 0) + { + int iTime = helperStream.ReadLEInt(); + + _createTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.IsStreamOwner = false; + helperStream.WriteByte((byte)_flags); // Flags + if ( (_flags & Flags.ModificationTime) != 0) { + TimeSpan span = _modificationTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + if ( (_flags & Flags.AccessTime) != 0) { + TimeSpan span = _lastAccessTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + if ( (_flags & Flags.CreateTime) != 0) { + TimeSpan span = _createTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + return ms.ToArray(); + } + } + + #endregion + + /// + /// Test a value to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// The standard Unix time is a signed integer data type, directly encoding the Unix time number, + /// which is the number of seconds since 1970-01-01. + /// Being 32 bits means the values here cover a range of about 136 years. + /// The minimum representable time is 1901-12-13 20:45:52, + /// and the maximum representable time is 2038-01-19 03:14:07. + /// + public static bool IsValidValue(DateTime value) + { + return (( value >= new DateTime(1901, 12, 13, 20, 45, 52)) || + ( value <= new DateTime(2038, 1, 19, 03, 14, 07) )); + } + + /// + /// Get /set the Modification Time + /// + /// + /// + public DateTime ModificationTime + { + get { return _modificationTime; } + set + { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.ModificationTime; + _modificationTime=value; + } + } + + /// + /// Get / set the Access Time + /// + /// + /// + public DateTime AccessTime + { + get { return _lastAccessTime; } + set { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.AccessTime; + _lastAccessTime=value; + } + } + + /// + /// Get / Set the Create Time + /// + /// + /// + public DateTime CreateTime + { + get { return _createTime; } + set { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.CreateTime; + _createTime=value; + } + } + + /// + /// Get/set the values to include. + /// + Flags Include + { + get { return _flags; } + set { _flags = value; } + } + + #region Instance Fields + Flags _flags; + DateTime _modificationTime = new DateTime(1970,1,1); + DateTime _lastAccessTime = new DateTime(1970, 1, 1); + DateTime _createTime = new DateTime(1970, 1, 1); + #endregion + } + + /// + /// Class handling NT date time values. + /// + public class NTTaggedData : ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + public short TagID + { + get { return 10; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.ReadLEInt(); // Reserved + while (helperStream.Position < helperStream.Length) + { + int ntfsTag = helperStream.ReadLEShort(); + int ntfsLength = helperStream.ReadLEShort(); + if (ntfsTag == 1) + { + if (ntfsLength >= 24) + { + long lastModificationTicks = helperStream.ReadLELong(); + _lastModificationTime = DateTime.FromFileTime(lastModificationTicks); + + long lastAccessTicks = helperStream.ReadLELong(); + _lastAccessTime = DateTime.FromFileTime(lastAccessTicks); + + long createTimeTicks = helperStream.ReadLELong(); + _createTime = DateTime.FromFileTime(createTimeTicks); + } + break; + } + else + { + // An unknown NTFS tag so simply skip it. + helperStream.Seek(ntfsLength, SeekOrigin.Current); + } + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.IsStreamOwner = false; + helperStream.WriteLEInt(0); // Reserved + helperStream.WriteLEShort(1); // Tag + helperStream.WriteLEShort(24); // Length = 3 x 8. + helperStream.WriteLELong(_lastModificationTime.ToFileTime()); + helperStream.WriteLELong(_lastAccessTime.ToFileTime()); + helperStream.WriteLELong(_createTime.ToFileTime()); + return ms.ToArray(); + } + } + + /// + /// Test a valuie to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// + /// NTFS filetimes are 64-bit unsigned integers, stored in Intel + /// (least significant byte first) byte order. They determine the + /// number of 1.0E-07 seconds (1/10th microseconds!) past WinNT "epoch", + /// which is "01-Jan-1601 00:00:00 UTC". 28 May 60056 is the upper limit + /// + public static bool IsValidValue(DateTime value) + { + bool result = true; + try + { + value.ToFileTimeUtc(); + } + catch + { + result = false; + } + return result; + } + + /// + /// Get/set the last modification time. + /// + public DateTime LastModificationTime + { + get { return _lastModificationTime; } + set { + if (! IsValidValue(value)) + { + throw new ArgumentOutOfRangeException("value"); + } + _lastModificationTime = value; + } + } + + /// + /// Get /set the create time + /// + public DateTime CreateTime + { + get { return _createTime; } + set { + if ( !IsValidValue(value)) { + throw new ArgumentOutOfRangeException("value"); + } + _createTime = value; + } + } + + /// + /// Get /set the last access time. + /// + public DateTime LastAccessTime + { + get { return _lastAccessTime; } + set { + if (!IsValidValue(value)) { + throw new ArgumentOutOfRangeException("value"); + } + _lastAccessTime = value; + } + } + + #region Instance Fields + DateTime _lastAccessTime = DateTime.FromFileTime(0); + DateTime _lastModificationTime = DateTime.FromFileTime(0); + DateTime _createTime = DateTime.FromFileTime(0); + #endregion + } + + /// + /// A factory that creates tagged data instances. + /// + interface ITaggedDataFactory + { + /// + /// Get data for a specific tag value. + /// + /// The tag ID to find. + /// The data to search. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + /// The located value found, or null if not found. + ITaggedData Create(short tag, byte[] data, int offset, int count); + } + + /// + /// + /// A class to handle the extra data field for Zip entries + /// + /// + /// Extra data contains 0 or more values each prefixed by a header tag and length. + /// They contain zero or more bytes of actual data. + /// The data is held internally using a copy on write strategy. This is more efficient but + /// means that for extra data created by passing in data can have the values modified by the caller + /// in some circumstances. + /// + sealed public class ZipExtraData : IDisposable + { + #region Constructors + /// + /// Initialise a default instance. + /// + public ZipExtraData() + { + Clear(); + } + + /// + /// Initialise with known extra data. + /// + /// The extra data. + public ZipExtraData(byte[] data) + { + if ( data == null ) + { + _data = new byte[0]; + } + else + { + _data = data; + } + } + #endregion + + /// + /// Get the raw extra data value + /// + /// Returns the raw byte[] extra data this instance represents. + public byte[] GetEntryData() + { + if ( Length > ushort.MaxValue ) { + throw new ZipException("Data exceeds maximum length"); + } + + return (byte[])_data.Clone(); + } + + /// + /// Clear the stored data. + /// + public void Clear() + { + if ( (_data == null) || (_data.Length != 0) ) { + _data = new byte[0]; + } + } + + /// + /// Gets the current extra data length. + /// + public int Length + { + get { return _data.Length; } + } + + /// + /// Get a read-only for the associated tag. + /// + /// The tag to locate data for. + /// Returns a containing tag data or null if no tag was found. + public Stream GetStreamForTag(int tag) + { + Stream result = null; + if ( Find(tag) ) { + result = new MemoryStream(_data, _index, _readValueLength, false); + } + return result; + } + + /// + /// Get the tagged data for a tag. + /// + /// The tag to search for. + /// Returns a tagged value or null if none found. + private ITaggedData GetData(short tag) + { + ITaggedData result = null; + if (Find(tag)) + { + result = Create(tag, _data, _readValueStart, _readValueLength); + } + return result; + } + + static ITaggedData Create(short tag, byte[] data, int offset, int count) + { + ITaggedData result = null; + switch ( tag ) + { + case 0x000A: + result = new NTTaggedData(); + break; + case 0x5455: + result = new ExtendedUnixData(); + break; + default: + result = new RawTaggedData(tag); + break; + } + result.SetData(data, offset, count); + return result; + } + + /// + /// Get the length of the last value found by + /// + /// This is only valid if has previously returned true. + public int ValueLength + { + get { return _readValueLength; } + } + + /// + /// Get the index for the current read value. + /// + /// This is only valid if has previously returned true. + /// Initially the result will be the index of the first byte of actual data. The value is updated after calls to + /// , and . + public int CurrentReadIndex + { + get { return _index; } + } + + /// + /// Get the number of bytes remaining to be read for the current value; + /// + public int UnreadCount + { + get + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4) ) { + throw new ZipException("Find must be called before calling a Read method"); + } + + return _readValueStart + _readValueLength - _index; + } + } + + /// + /// Find an extra data value + /// + /// The identifier for the value to find. + /// Returns true if the value was found; false otherwise. + public bool Find(int headerID) + { + _readValueStart = _data.Length; + _readValueLength = 0; + _index = 0; + + int localLength = _readValueStart; + int localTag = headerID - 1; + + // Trailing bytes that cant make up an entry (as there arent enough + // bytes for a tag and length) are ignored! + while ( (localTag != headerID) && (_index < _data.Length - 3) ) { + localTag = ReadShortInternal(); + localLength = ReadShortInternal(); + if ( localTag != headerID ) { + _index += localLength; + } + } + + bool result = (localTag == headerID) && ((_index + localLength) <= _data.Length); + + if ( result ) { + _readValueStart = _index; + _readValueLength = localLength; + } + + return result; + } + + /// + /// Add a new entry to extra data. + /// + /// The value to add. + public void AddEntry(ITaggedData taggedData) + { + if (taggedData == null) + { + throw new ArgumentNullException("taggedData"); + } + AddEntry(taggedData.TagID, taggedData.GetData()); + } + + /// + /// Add a new entry to extra data + /// + /// The ID for this entry. + /// The data to add. + /// If the ID already exists its contents are replaced. + public void AddEntry(int headerID, byte[] fieldData) + { + if ( (headerID > ushort.MaxValue) || (headerID < 0)) { + throw new ArgumentOutOfRangeException("headerID"); + } + + int addLength = (fieldData == null) ? 0 : fieldData.Length; + + if ( addLength > ushort.MaxValue ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("fieldData"); +#else + throw new ArgumentOutOfRangeException("fieldData", "exceeds maximum length"); +#endif + } + + // Test for new length before adjusting data. + int newLength = _data.Length + addLength + 4; + + if ( Find(headerID) ) + { + newLength -= (ValueLength + 4); + } + + if ( newLength > ushort.MaxValue ) { + throw new ZipException("Data exceeds maximum length"); + } + + Delete(headerID); + + byte[] newData = new byte[newLength]; + _data.CopyTo(newData, 0); + int index = _data.Length; + _data = newData; + SetShort(ref index, headerID); + SetShort(ref index, addLength); + if ( fieldData != null ) { + fieldData.CopyTo(newData, index); + } + } + + /// + /// Start adding a new entry. + /// + /// Add data using , , , or . + /// The new entry is completed and actually added by calling + /// + public void StartNewEntry() + { + _newEntry = new MemoryStream(); + } + + /// + /// Add entry data added since using the ID passed. + /// + /// The identifier to use for this entry. + public void AddNewEntry(int headerID) + { + byte[] newData = _newEntry.ToArray(); + _newEntry = null; + AddEntry(headerID, newData); + } + + /// + /// Add a byte of data to the pending new entry. + /// + /// The byte to add. + /// + public void AddData(byte data) + { + _newEntry.WriteByte(data); + } + + /// + /// Add data to a pending new entry. + /// + /// The data to add. + /// + public void AddData(byte[] data) + { + if ( data == null ) { + throw new ArgumentNullException("data"); + } + + _newEntry.Write(data, 0, data.Length); + } + + /// + /// Add a short value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeShort(int toAdd) + { + unchecked { + _newEntry.WriteByte(( byte )toAdd); + _newEntry.WriteByte(( byte )(toAdd >> 8)); + } + } + + /// + /// Add an integer value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeInt(int toAdd) + { + unchecked { + AddLeShort(( short )toAdd); + AddLeShort(( short )(toAdd >> 16)); + } + } + + /// + /// Add a long value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeLong(long toAdd) + { + unchecked { + AddLeInt(( int )(toAdd & 0xffffffff)); + AddLeInt(( int )(toAdd >> 32)); + } + } + + /// + /// Delete an extra data field. + /// + /// The identifier of the field to delete. + /// Returns true if the field was found and deleted. + public bool Delete(int headerID) + { + bool result = false; + + if ( Find(headerID) ) { + result = true; + int trueStart = _readValueStart - 4; + + byte[] newData = new byte[_data.Length - (ValueLength + 4)]; + Array.Copy(_data, 0, newData, 0, trueStart); + + int trueEnd = trueStart + ValueLength + 4; + Array.Copy(_data, trueEnd, newData, trueStart, _data.Length - trueEnd); + _data = newData; + } + return result; + } + + #region Reading Support + /// + /// Read a long in little endian form from the last found data value + /// + /// Returns the long value read. + public long ReadLong() + { + ReadCheck(8); + return (ReadInt() & 0xffffffff) | ((( long )ReadInt()) << 32); + } + + /// + /// Read an integer in little endian form from the last found data value. + /// + /// Returns the integer read. + public int ReadInt() + { + ReadCheck(4); + + int result = _data[_index] + (_data[_index + 1] << 8) + + (_data[_index + 2] << 16) + (_data[_index + 3] << 24); + _index += 4; + return result; + } + + /// + /// Read a short value in little endian form from the last found data value. + /// + /// Returns the short value read. + public int ReadShort() + { + ReadCheck(2); + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + /// + /// Read a byte from an extra data + /// + /// The byte value read or -1 if the end of data has been reached. + public int ReadByte() + { + int result = -1; + if ( (_index < _data.Length) && (_readValueStart + _readValueLength > _index) ) { + result = _data[_index]; + _index += 1; + } + return result; + } + + /// + /// Skip data during reading. + /// + /// The number of bytes to skip. + public void Skip(int amount) + { + ReadCheck(amount); + _index += amount; + } + + void ReadCheck(int length) + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4) ) { + throw new ZipException("Find must be called before calling a Read method"); + } + + if (_index > _readValueStart + _readValueLength - length ) { + throw new ZipException("End of extra data"); + } + + if ( _index + length < 4 ) { + throw new ZipException("Cannot read before start of tag"); + } + } + + /// + /// Internal form of that reads data at any location. + /// + /// Returns the short value read. + int ReadShortInternal() + { + if ( _index > _data.Length - 2) { + throw new ZipException("End of extra data"); + } + + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + void SetShort(ref int index, int source) + { + _data[index] = (byte)source; + _data[index + 1] = (byte)(source >> 8); + index += 2; + } + + #endregion + + #region IDisposable Members + + /// + /// Dispose of this instance. + /// + public void Dispose() + { + if ( _newEntry != null ) { + _newEntry.Close(); + } + } + + #endregion + + #region Instance Fields + int _index; + int _readValueStart; + int _readValueLength; + + MemoryStream _newEntry; + byte[] _data; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs new file mode 100644 index 000000000..b896c1b3a --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs @@ -0,0 +1,4486 @@ +// ZipFile.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2009-12-22 Z-1649 Added AES support +// 2010-03-02 Z-1650 Fixed updating ODT archives in memory. Exposed exceptions in updating. +// 2010-05-25 Z-1663 Fixed exception when testing local header compressed size of -1 + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Globalization; + +#if !NETCF_1_0 +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +using GitHub.ICSharpCode.SharpZipLib.Core; +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + #region Keys Required Event Args + /// + /// Arguments used with KeysRequiredEvent + /// + public class KeysRequiredEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + public KeysRequiredEventArgs(string name) + { + fileName = name; + } + + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + /// The current key value. + public KeysRequiredEventArgs(string name, byte[] keyValue) + { + fileName = name; + key = keyValue; + } + + #endregion + #region Properties + /// + /// Gets the name of the file for which keys are required. + /// + public string FileName + { + get { return fileName; } + } + + /// + /// Gets or sets the key value + /// + public byte[] Key + { + get { return key; } + set { key = value; } + } + #endregion + + #region Instance Fields + string fileName; + byte[] key; + #endregion + } + #endregion + + #region Test Definitions + /// + /// The strategy to apply to testing. + /// + public enum TestStrategy + { + /// + /// Find the first error only. + /// + FindFirstError, + /// + /// Find all possible errors. + /// + FindAllErrors, + } + + /// + /// The operation in progress reported by a during testing. + /// + /// TestArchive + public enum TestOperation + { + /// + /// Setting up testing. + /// + Initialising, + + /// + /// Testing an individual entries header + /// + EntryHeader, + + /// + /// Testing an individual entries data + /// + EntryData, + + /// + /// Testing an individual entry has completed. + /// + EntryComplete, + + /// + /// Running miscellaneous tests + /// + MiscellaneousTests, + + /// + /// Testing is complete + /// + Complete, + } + + /// + /// Status returned returned by during testing. + /// + /// TestArchive + public class TestStatus + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The this status applies to. + public TestStatus(ZipFile file) + { + file_ = file; + } + #endregion + + #region Properties + + /// + /// Get the current in progress. + /// + public TestOperation Operation + { + get { return operation_; } + } + + /// + /// Get the this status is applicable to. + /// + public ZipFile File + { + get { return file_; } + } + + /// + /// Get the current/last entry tested. + /// + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the number of errors detected so far. + /// + public int ErrorCount + { + get { return errorCount_; } + } + + /// + /// Get the number of bytes tested so far for the current entry. + /// + public long BytesTested + { + get { return bytesTested_; } + } + + /// + /// Get a value indicating wether the last entry test was valid. + /// + public bool EntryValid + { + get { return entryValid_; } + } + #endregion + + #region Internal API + internal void AddError() + { + errorCount_++; + entryValid_ = false; + } + + internal void SetOperation(TestOperation operation) + { + operation_ = operation; + } + + internal void SetEntry(ZipEntry entry) + { + entry_ = entry; + entryValid_ = true; + bytesTested_ = 0; + } + + internal void SetBytesTested(long value) + { + bytesTested_ = value; + } + #endregion + + #region Instance Fields + ZipFile file_; + ZipEntry entry_; + bool entryValid_; + int errorCount_; + long bytesTested_; + TestOperation operation_; + #endregion + } + + /// + /// Delegate invoked during testing if supplied indicating current progress and status. + /// + /// If the message is non-null an error has occured. If the message is null + /// the operation as found in status has started. + public delegate void ZipTestResultHandler(TestStatus status, string message); + #endregion + + #region Update Definitions + /// + /// The possible ways of applying updates to an archive. + /// + public enum FileUpdateMode + { + /// + /// Perform all updates on temporary files ensuring that the original file is saved. + /// + Safe, + /// + /// Update the archive directly, which is faster but less safe. + /// + Direct, + } + #endregion + + #region ZipFile Class + /// + /// This class represents a Zip archive. You can ask for the contained + /// entries, or get an input stream for a file entry. The entry is + /// automatically decompressed. + /// + /// You can also update the archive adding or deleting entries. + /// + /// This class is thread safe for input: You can open input streams for arbitrary + /// entries in different threads. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// + /// using System; + /// using System.Text; + /// using System.Collections; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// static public void Main(string[] args) + /// { + /// using (ZipFile zFile = new ZipFile(args[0])) { + /// Console.WriteLine("Listing of : " + zFile.Name); + /// Console.WriteLine(""); + /// Console.WriteLine("Raw Size Size Date Time Name"); + /// Console.WriteLine("-------- -------- -------- ------ ---------"); + /// foreach (ZipEntry e in zFile) { + /// if ( e.IsFile ) { + /// DateTime d = e.DateTime; + /// Console.WriteLine("{0, -10}{1, -10}{2} {3} {4}", e.Size, e.CompressedSize, + /// d.ToString("dd-MM-yy"), d.ToString("HH:mm"), + /// e.Name); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipFile : IEnumerable, IDisposable + { + #region KeyHandling + + /// + /// Delegate for handling keys/password setting during compresion/decompression. + /// + public delegate void KeysRequiredEventHandler( + object sender, + KeysRequiredEventArgs e + ); + + /// + /// Event handler for handling encryption keys. + /// + public KeysRequiredEventHandler KeysRequired; + + /// + /// Handles getting of encryption keys when required. + /// + /// The file for which encryption keys are required. + void OnKeysRequired(string fileName) + { + if (KeysRequired != null) { + KeysRequiredEventArgs krea = new KeysRequiredEventArgs(fileName, key); + KeysRequired(this, krea); + key = krea.Key; + } + } + + /// + /// Get/set the encryption key value. + /// + byte[] Key + { + get { return key; } + set { key = value; } + } + +#if !NETCF_1_0 + /// + /// Password to be used for encrypting/decrypting files. + /// + /// Set to null if no password is required. + public string Password + { + set + { + if ( (value == null) || (value.Length == 0) ) { + key = null; + } + else { + rawPassword_ = value; + key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(value)); + } + } + } +#endif + + /// + /// Get a value indicating wether encryption keys are currently available. + /// + bool HaveKeys + { + get { return key != null; } + } + #endregion + + #region Constructors + /// + /// Opens a Zip file with the given name for reading. + /// + /// The name of the file to open. + /// The argument supplied is null. + /// + /// An i/o error occurs + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(string name) + { + if ( name == null ) { + throw new ArgumentNullException("name"); + } + + name_ = name; + + baseStream_ = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + isStreamOwner = true; + + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// The supplied argument is null. + /// + /// An i/o error occurs. + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(FileStream file) + { + if ( file == null ) { + throw new ArgumentNullException("file"); + } + + if ( !file.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "file"); + } + + baseStream_ = file; + name_ = file.Name; + isStreamOwner = true; + + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// + /// An i/o error occurs + /// + /// + /// The stream doesn't contain a valid zip archive.
+ ///
+ /// + /// The stream doesnt support seeking. + /// + /// + /// The stream argument is null. + /// + public ZipFile(Stream stream) + { + if ( stream == null ) { + throw new ArgumentNullException("stream"); + } + + if ( !stream.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "stream"); + } + + baseStream_ = stream; + isStreamOwner = true; + + if ( baseStream_.Length > 0 ) { + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } else { + entries_ = new ZipEntry[0]; + isNewArchive_ = true; + } + } + + /// + /// Initialises a default instance with no entries and no file storage. + /// + internal ZipFile() + { + entries_ = new ZipEntry[0]; + isNewArchive_ = true; + } + + #endregion + + #region Destructors and Closing + /// + /// Finalize this instance. + /// + ~ZipFile() + { + Dispose(false); + } + + /// + /// Closes the ZipFile. If the stream is owned then this also closes the underlying input stream. + /// Once closed, no further instance methods should be called. + /// + /// + /// An i/o error occurs. + /// + public void Close() + { + DisposeInternal(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region Creators + /// + /// Create a new whose data will be stored in a file. + /// + /// The name of the archive to create. + /// Returns the newly created + /// is null + public static ZipFile Create(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + FileStream fs = File.Create(fileName); + + ZipFile result = new ZipFile(); + result.name_ = fileName; + result.baseStream_ = fs; + result.isStreamOwner = true; + return result; + } + + /// + /// Create a new whose data will be stored on a stream. + /// + /// The stream providing data storage. + /// Returns the newly created + /// is null + /// doesnt support writing. + public static ZipFile Create(Stream outStream) + { + if ( outStream == null ) { + throw new ArgumentNullException("outStream"); + } + + if ( !outStream.CanWrite ) { + throw new ArgumentException("Stream is not writeable", "outStream"); + } + + if ( !outStream.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "outStream"); + } + + ZipFile result = new ZipFile(); + result.baseStream_ = outStream; + return result; + } + + #endregion + + #region Properties + /// + /// Get/set a flag indicating if the underlying stream is owned by the ZipFile instance. + /// If the flag is true then the stream will be closed when Close is called. + /// + /// + /// The default value is true in all cases. + /// + public bool IsStreamOwner + { + get { return isStreamOwner; } + set { isStreamOwner = value; } + } + + /// + /// Get a value indicating wether + /// this archive is embedded in another file or not. + /// + public bool IsEmbeddedArchive + { + // Not strictly correct in all circumstances currently + get { return offsetOfFirstEntry > 0; } + } + + /// + /// Get a value indicating that this archive is a new one. + /// + public bool IsNewArchive + { + get { return isNewArchive_; } + } + + /// + /// Gets the comment for the zip file. + /// + public string ZipFileComment + { + get { return comment_; } + } + + /// + /// Gets the name of this zip file. + /// + public string Name + { + get { return name_; } + } + + /// + /// Gets the number of entries in this zip file. + /// + /// + /// The Zip file has been closed. + /// + [Obsolete("Use the Count property instead")] + public int Size + { + get + { + return entries_.Length; + } + } + + /// + /// Get the number of entries contained in this . + /// + public long Count + { + get + { + return entries_.Length; + } + } + + /// + /// Indexer property for ZipEntries + /// + [System.Runtime.CompilerServices.IndexerNameAttribute("EntryByIndex")] + public ZipEntry this[int index] + { + get { + return (ZipEntry) entries_[index].Clone(); + } + } + + #endregion + + #region Input Handling + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + public IEnumerator GetEnumerator() + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + return new ZipEntryEnumerator(entries_); + } + + /// + /// Return the index of the entry with a matching name + /// + /// Entry name to find + /// If true the comparison is case insensitive + /// The index position of the matching entry or -1 if not found + /// + /// The Zip file has been closed. + /// + public int FindEntry(string name, bool ignoreCase) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + // TODO: This will be slow as the next ice age for huge archives! + for (int i = 0; i < entries_.Length; i++) { + if (string.Compare(name, entries_[i].Name, ignoreCase, CultureInfo.InvariantCulture) == 0) { + return i; + } + } + return -1; + } + + /// + /// Searches for a zip entry in this archive with the given name. + /// String comparisons are case insensitive + /// + /// + /// The name to find. May contain directory components separated by slashes ('/'). + /// + /// + /// A clone of the zip entry, or null if no entry with that name exists. + /// + /// + /// The Zip file has been closed. + /// + public ZipEntry GetEntry(string name) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + int index = FindEntry(name, true); + return (index >= 0) ? (ZipEntry) entries_[index].Clone() : null; + } + + /// + /// Gets an input stream for reading the given zip entry data in an uncompressed form. + /// Normally the should be an entry returned by GetEntry(). + /// + /// The to obtain a data for + /// An input containing data for this + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + long index = entry.ZipFileIndex; + if ( (index < 0) || (index >= entries_.Length) || (entries_[index].Name != entry.Name) ) { + index = FindEntry(entry.Name, true); + if (index < 0) { + throw new ZipException("Entry cannot be found"); + } + } + return GetInputStream(index); + } + + /// + /// Creates an input stream reading a zip entry + /// + /// The index of the entry to obtain an input stream for. + /// + /// An input containing data for this + /// + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(long entryIndex) + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + long start = LocateEntry(entries_[entryIndex]); + CompressionMethod method = entries_[entryIndex].CompressionMethod; + Stream result = new PartialInputStream(this, start, entries_[entryIndex].CompressedSize); + + if (entries_[entryIndex].IsCrypted == true) { +#if NETCF_1_0 + throw new ZipException("decryption not supported for Compact Framework 1.0"); +#else + result = CreateAndInitDecryptionStream(result, entries_[entryIndex]); + if (result == null) { + throw new ZipException("Unable to decrypt this entry"); + } +#endif + } + + switch (method) { + case CompressionMethod.Stored: + // read as is. + break; + + case CompressionMethod.Deflated: + // No need to worry about ownership and closing as underlying stream close does nothing. + result = new InflaterInputStream(result, new Inflater(true)); + break; + + default: + throw new ZipException("Unsupported compression method " + method); + } + + return result; + } + + #endregion + + #region Archive Testing + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// true if all tests pass, false otherwise + /// Testing will terminate on the first error found. + public bool TestArchive(bool testData) + { + return TestArchive(testData, TestStrategy.FindFirstError, null); + } + + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// The to apply. + /// The handler to call during testing. + /// true if all tests pass, false otherwise + /// The object has already been closed. + public bool TestArchive(bool testData, TestStrategy strategy, ZipTestResultHandler resultHandler) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + TestStatus status = new TestStatus(this); + + if ( resultHandler != null ) { + resultHandler(status, null); + } + + HeaderTest test = testData ? (HeaderTest.Header | HeaderTest.Extract) : HeaderTest.Header; + + bool testing = true; + + try { + int entryIndex = 0; + + while ( testing && (entryIndex < Count) ) { + if ( resultHandler != null ) { + status.SetEntry(this[entryIndex]); + status.SetOperation(TestOperation.EntryHeader); + resultHandler(status, null); + } + + try { + TestLocalHeader(this[entryIndex], test); + } + catch(ZipException ex) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, + string.Format("Exception during test - '{0}'", ex.Message)); + } + + if ( strategy == TestStrategy.FindFirstError ) { + testing = false; + } + } + + if ( testing && testData && this[entryIndex].IsFile ) { + if ( resultHandler != null ) { + status.SetOperation(TestOperation.EntryData); + resultHandler(status, null); + } + + Crc32 crc = new Crc32(); + + using (Stream entryStream = this.GetInputStream(this[entryIndex])) + { + + byte[] buffer = new byte[4096]; + long totalBytes = 0; + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + crc.Update(buffer, 0, bytesRead); + + if (resultHandler != null) + { + totalBytes += bytesRead; + status.SetBytesTested(totalBytes); + resultHandler(status, null); + } + } + } + + if (this[entryIndex].Crc != crc.Value) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, "CRC mismatch"); + } + + if ( strategy == TestStrategy.FindFirstError ) { + testing = false; + } + } + + if (( this[entryIndex].Flags & (int)GeneralBitFlags.Descriptor) != 0 ) { + ZipHelperStream helper = new ZipHelperStream(baseStream_); + DescriptorData data = new DescriptorData(); + helper.ReadDataDescriptor(this[entryIndex].LocalHeaderRequiresZip64, data); + if (this[entryIndex].Crc != data.Crc) { + status.AddError(); + } + + if (this[entryIndex].CompressedSize != data.CompressedSize) { + status.AddError(); + } + + if (this[entryIndex].Size != data.Size) { + status.AddError(); + } + } + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.EntryComplete); + resultHandler(status, null); + } + + entryIndex += 1; + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.MiscellaneousTests); + resultHandler(status, null); + } + + // TODO: the 'Corrina Johns' test where local headers are missing from + // the central directory. They are therefore invisible to many archivers. + } + catch (Exception ex) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, string.Format("Exception during test - '{0}'", ex.Message)); + } + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.Complete); + status.SetEntry(null); + resultHandler(status, null); + } + + return (status.ErrorCount == 0); + } + + [Flags] + enum HeaderTest + { + Extract = 0x01, // Check that this header represents an entry whose data can be extracted + Header = 0x02, // Check that this header contents are valid + } + + /// + /// Test a local header against that provided from the central directory + /// + /// + /// The entry to test against + /// + /// The type of tests to carry out. + /// The offset of the entries data in the file + long TestLocalHeader(ZipEntry entry, HeaderTest tests) + { + lock(baseStream_) + { + bool testHeader = (tests & HeaderTest.Header) != 0; + bool testData = (tests & HeaderTest.Extract) != 0; + + baseStream_.Seek(offsetOfFirstEntry + entry.Offset, SeekOrigin.Begin); + if ((int)ReadLEUint() != ZipConstants.LocalHeaderSignature) { + throw new ZipException(string.Format("Wrong local header signature @{0:X}", offsetOfFirstEntry + entry.Offset)); + } + + short extractVersion = ( short )ReadLEUshort(); + short localFlags = ( short )ReadLEUshort(); + short compressionMethod = ( short )ReadLEUshort(); + short fileTime = ( short )ReadLEUshort(); + short fileDate = ( short )ReadLEUshort(); + uint crcValue = ReadLEUint(); + long compressedSize = ReadLEUint(); + long size = ReadLEUint(); + int storedNameLength = ReadLEUshort(); + int extraDataLength = ReadLEUshort(); + + byte[] nameData = new byte[storedNameLength]; + StreamUtils.ReadFully(baseStream_, nameData); + + byte[] extraData = new byte[extraDataLength]; + StreamUtils.ReadFully(baseStream_, extraData); + + ZipExtraData localExtraData = new ZipExtraData(extraData); + + // Extra data / zip64 checks + if (localExtraData.Find(1)) + { + // 2010-03-04 Forum 10512: removed checks for version >= ZipConstants.VersionZip64 + // and size or compressedSize = MaxValue, due to rogue creators. + + size = localExtraData.ReadLong(); + compressedSize = localExtraData.ReadLong(); + + if ((localFlags & (int)GeneralBitFlags.Descriptor) != 0) + { + // These may be valid if patched later + if ( (size != -1) && (size != entry.Size)) { + throw new ZipException("Size invalid for descriptor"); + } + + if ((compressedSize != -1) && (compressedSize != entry.CompressedSize)) { + throw new ZipException("Compressed size invalid for descriptor"); + } + } + } + else + { + // No zip64 extra data but entry requires it. + if ((extractVersion >= ZipConstants.VersionZip64) && + (((uint)size == uint.MaxValue) || ((uint)compressedSize == uint.MaxValue))) + { + throw new ZipException("Required Zip64 extended information missing"); + } + } + + if ( testData ) { + if ( entry.IsFile ) { + if ( !entry.IsCompressionMethodSupported() ) { + throw new ZipException("Compression method not supported"); + } + + if ( (extractVersion > ZipConstants.VersionMadeBy) + || ((extractVersion > 20) && (extractVersion < ZipConstants.VersionZip64)) ) { + throw new ZipException(string.Format("Version required to extract this entry not supported ({0})", extractVersion)); + } + + if ( (localFlags & ( int )(GeneralBitFlags.Patched | GeneralBitFlags.StrongEncryption | GeneralBitFlags.EnhancedCompress | GeneralBitFlags.HeaderMasked)) != 0 ) { + throw new ZipException("The library does not support the zip version required to extract this entry"); + } + } + } + + if (testHeader) + { + if ((extractVersion <= 63) && // Ignore later versions as we dont know about them.. + (extractVersion != 10) && + (extractVersion != 11) && + (extractVersion != 20) && + (extractVersion != 21) && + (extractVersion != 25) && + (extractVersion != 27) && + (extractVersion != 45) && + (extractVersion != 46) && + (extractVersion != 50) && + (extractVersion != 51) && + (extractVersion != 52) && + (extractVersion != 61) && + (extractVersion != 62) && + (extractVersion != 63) + ) + { + throw new ZipException(string.Format("Version required to extract this entry is invalid ({0})", extractVersion)); + } + + // Local entry flags dont have reserved bit set on. + if ((localFlags & (int)(GeneralBitFlags.ReservedPKware4 | GeneralBitFlags.ReservedPkware14 | GeneralBitFlags.ReservedPkware15)) != 0) + { + throw new ZipException("Reserved bit flags cannot be set."); + } + + // Encryption requires extract version >= 20 + if (((localFlags & (int)GeneralBitFlags.Encrypted) != 0) && (extractVersion < 20)) + { + throw new ZipException(string.Format("Version required to extract this entry is too low for encryption ({0})", extractVersion)); + } + + // Strong encryption requires encryption flag to be set and extract version >= 50. + if ((localFlags & (int)GeneralBitFlags.StrongEncryption) != 0) + { + if ((localFlags & (int)GeneralBitFlags.Encrypted) == 0) + { + throw new ZipException("Strong encryption flag set but encryption flag is not set"); + } + + if (extractVersion < 50) + { + throw new ZipException(string.Format("Version required to extract this entry is too low for encryption ({0})", extractVersion)); + } + } + + // Patched entries require extract version >= 27 + if (((localFlags & (int)GeneralBitFlags.Patched) != 0) && (extractVersion < 27)) + { + throw new ZipException(string.Format("Patched data requires higher version than ({0})", extractVersion)); + } + + // Central header flags match local entry flags. + if (localFlags != entry.Flags) + { + throw new ZipException("Central header/local header flags mismatch"); + } + + // Central header compression method matches local entry + if (entry.CompressionMethod != (CompressionMethod)compressionMethod) + { + throw new ZipException("Central header/local header compression method mismatch"); + } + + if (entry.Version != extractVersion) + { + throw new ZipException("Extract version mismatch"); + } + + // Strong encryption and extract version match + if ((localFlags & (int)GeneralBitFlags.StrongEncryption) != 0) + { + if (extractVersion < 62) + { + throw new ZipException("Strong encryption flag set but version not high enough"); + } + } + + if ((localFlags & (int)GeneralBitFlags.HeaderMasked) != 0) + { + if ((fileTime != 0) || (fileDate != 0)) + { + throw new ZipException("Header masked set but date/time values non-zero"); + } + } + + if ((localFlags & (int)GeneralBitFlags.Descriptor) == 0) + { + if (crcValue != (uint)entry.Crc) + { + throw new ZipException("Central header/local header crc mismatch"); + } + } + + // Crc valid for empty entry. + // This will also apply to streamed entries where size isnt known and the header cant be patched + if ((size == 0) && (compressedSize == 0)) + { + if (crcValue != 0) + { + throw new ZipException("Invalid CRC for empty entry"); + } + } + + // TODO: make test more correct... can't compare lengths as was done originally as this can fail for MBCS strings + // Assuming a code page at this point is not valid? Best is to store the name length in the ZipEntry probably + if (entry.Name.Length > storedNameLength) + { + throw new ZipException("File name length mismatch"); + } + + // Name data has already been read convert it and compare. + string localName = ZipConstants.ConvertToStringExt(localFlags, nameData); + + // Central directory and local entry name match + if (localName != entry.Name) + { + throw new ZipException("Central header and local header file name mismatch"); + } + + // Directories have zero actual size but can have compressed size + if (entry.IsDirectory) + { + if (size > 0) + { + throw new ZipException("Directory cannot have size"); + } + + // There may be other cases where the compressed size can be greater than this? + // If so until details are known we will be strict. + if (entry.IsCrypted) + { + if (compressedSize > ZipConstants.CryptoHeaderSize + 2) + { + throw new ZipException("Directory compressed size invalid"); + } + } + else if (compressedSize > 2) + { + // When not compressed the directory size can validly be 2 bytes + // if the true size wasnt known when data was originally being written. + // NOTE: Versions of the library 0.85.4 and earlier always added 2 bytes + throw new ZipException("Directory compressed size invalid"); + } + } + + if (!ZipNameTransform.IsValidName(localName, true)) + { + throw new ZipException("Name is invalid"); + } + } + + // Tests that apply to both data and header. + + // Size can be verified only if it is known in the local header. + // it will always be known in the central header. + if (((localFlags & (int)GeneralBitFlags.Descriptor) == 0) || + ((size > 0) || (compressedSize > 0))) { + + if (size != entry.Size) { + throw new ZipException( + string.Format("Size mismatch between central header({0}) and local header({1})", + entry.Size, size)); + } + + if (compressedSize != entry.CompressedSize && + compressedSize != 0xFFFFFFFF && compressedSize != -1) { + throw new ZipException( + string.Format("Compressed size mismatch between central header({0}) and local header({1})", + entry.CompressedSize, compressedSize)); + } + } + + int extraLength = storedNameLength + extraDataLength; + return offsetOfFirstEntry + entry.Offset + ZipConstants.LocalHeaderBaseSize + extraLength; + } + } + + #endregion + + #region Updating + + const int DefaultBufferSize = 4096; + + /// + /// The kind of update to apply. + /// + enum UpdateCommand + { + Copy, // Copy original file contents. + Modify, // Change encryption, compression, attributes, name, time etc, of an existing file. + Add, // Add a new file to the archive. + } + + #region Properties + /// + /// Get / set the to apply to names when updating. + /// + public INameTransform NameTransform + { + get { + return updateEntryFactory_.NameTransform; + } + + set { + updateEntryFactory_.NameTransform = value; + } + } + + /// + /// Get/set the used to generate values + /// during updates. + /// + public IEntryFactory EntryFactory + { + get { + return updateEntryFactory_; + } + + set { + if (value == null) { + updateEntryFactory_ = new ZipEntryFactory(); + } + else { + updateEntryFactory_ = value; + } + } + } + + /// + /// Get /set the buffer size to be used when updating this zip file. + /// + public int BufferSize + { + get { return bufferSize_; } + set { + if ( value < 1024 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "cannot be below 1024"); +#endif + } + + if ( bufferSize_ != value ) { + bufferSize_ = value; + copyBuffer_ = null; + } + } + } + + /// + /// Get a value indicating an update has been started. + /// + public bool IsUpdating + { + get { return updates_ != null; } + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + #endregion + + #region Immediate updating +// TBD: Direct form of updating +// +// public void Update(IEntryMatcher deleteMatcher) +// { +// } +// +// public void Update(IScanner addScanner) +// { +// } + #endregion + + #region Deferred Updating + /// + /// Begin updating this archive. + /// + /// The archive storage for use during the update. + /// The data source to utilise during updating. + /// ZipFile has been closed. + /// One of the arguments provided is null + /// ZipFile has been closed. + public void BeginUpdate(IArchiveStorage archiveStorage, IDynamicDataSource dataSource) + { + if ( archiveStorage == null ) { + throw new ArgumentNullException("archiveStorage"); + } + + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + if ( IsEmbeddedArchive ) { + throw new ZipException ("Cannot update embedded/SFX archives"); + } + + archiveStorage_ = archiveStorage; + updateDataSource_ = dataSource; + + // NOTE: the baseStream_ may not currently support writing or seeking. + + updateIndex_ = new Hashtable(); + + updates_ = new ArrayList(entries_.Length); + foreach(ZipEntry entry in entries_) { + int index = updates_.Add(new ZipUpdate(entry)); + updateIndex_.Add(entry.Name, index); + } + + // We must sort by offset before using offset's calculated sizes + updates_.Sort(new UpdateComparer()); + + int idx = 0; + foreach (ZipUpdate update in updates_) { + //If last entry, there is no next entry offset to use + if (idx == updates_.Count - 1) + break; + + update.OffsetBasedSize = ((ZipUpdate)updates_[idx + 1]).Entry.Offset - update.Entry.Offset; + idx++; + } + updateCount_ = updates_.Count; + + contentsEdited_ = false; + commentEdited_ = false; + newComment_ = null; + } + + /// + /// Begin updating to this archive. + /// + /// The storage to use during the update. + public void BeginUpdate(IArchiveStorage archiveStorage) + { + BeginUpdate(archiveStorage, new DynamicDiskDataSource()); + } + + /// + /// Begin updating this archive. + /// + /// + /// + /// + public void BeginUpdate() + { + if ( Name == null ) { + BeginUpdate(new MemoryArchiveStorage(), new DynamicDiskDataSource()); + } + else { + BeginUpdate(new DiskArchiveStorage(this), new DynamicDiskDataSource()); + } + } + + /// + /// Commit current updates, updating this archive. + /// + /// + /// + /// ZipFile has been closed. + public void CommitUpdate() + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + try { + updateIndex_.Clear(); + updateIndex_=null; + + if( contentsEdited_ ) { + RunUpdates(); + } + else if( commentEdited_ ) { + UpdateCommentOnly(); + } + else { + // Create an empty archive if none existed originally. + if( entries_.Length==0 ) { + byte[] theComment=(newComment_!=null)?newComment_.RawComment:ZipConstants.ConvertToArray(comment_); + using( ZipHelperStream zhs=new ZipHelperStream(baseStream_) ) { + zhs.WriteEndOfCentralDirectory(0, 0, 0, theComment); + } + } + } + + } + finally { + PostUpdateCleanup(); + } + } + + /// + /// Abort updating leaving the archive unchanged. + /// + /// + /// + public void AbortUpdate() + { + PostUpdateCleanup(); + } + + /// + /// Set the file comment to be recorded when the current update is commited. + /// + /// The comment to record. + /// ZipFile has been closed. + public void SetComment(string comment) + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + newComment_ = new ZipString(comment); + + if ( newComment_.RawLength > 0xffff ) { + newComment_ = null; + throw new ZipException("Comment length exceeds maximum - 65535"); + } + + // We dont take account of the original and current comment appearing to be the same + // as encoding may be different. + commentEdited_ = true; + } + + #endregion + + #region Adding Entries + + void AddUpdate(ZipUpdate update) + { + contentsEdited_ = true; + + int index = FindExistingUpdate(update.Entry.Name); + + if (index >= 0) { + if ( updates_[index] == null ) { + updateCount_ += 1; + } + + // Direct replacement is faster than delete and add. + updates_[index] = update; + } + else { + index = updates_.Add(update); + updateCount_ += 1; + updateIndex_.Add(update.Entry.Name, index); + } + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// Ensure Unicode text is used for name and comment for this entry. + /// Argument supplied is null. + /// ZipFile has been closed. + /// Compression method is not supported. + public void Add(string fileName, CompressionMethod compressionMethod, bool useUnicodeText ) + { + if (fileName == null) { + throw new ArgumentNullException("fileName"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + if (!ZipEntry.IsCompressionMethodSupported(compressionMethod)) { + throw new ArgumentOutOfRangeException("compressionMethod"); + } + + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// ZipFile has been closed. + /// The compression method is not supported. + public void Add(string fileName, CompressionMethod compressionMethod) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + if ( !ZipEntry.IsCompressionMethodSupported(compressionMethod) ) { + throw new ArgumentOutOfRangeException("compressionMethod"); + } + + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.CompressionMethod = compressionMethod; + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// Argument supplied is null. + public void Add(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(fileName))); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// The name to use for the on the Zip file created. + /// Argument supplied is null. + public void Add(string fileName, string entryName) + { + if (fileName == null) { + throw new ArgumentNullException("fileName"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(entryName))); + } + + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + public void Add(IStaticDataSource dataSource, string entryName) + { + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(dataSource, EntryFactory.MakeFileEntry(entryName, false))); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + /// Ensure Unicode text is used for name and comments for this entry. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod, bool useUnicodeText) + { + if (dataSource == null) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a that contains no data. + /// + /// The entry to add. + /// This can be used to add directories, volume labels, or empty file entries. + public void Add(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + CheckUpdating(); + + if ( (entry.Size != 0) || (entry.CompressedSize != 0) ) { + throw new ZipException("Entry cannot have any data"); + } + + AddUpdate(new ZipUpdate(UpdateCommand.Add, entry)); + } + + /// + /// Add a directory entry to the archive. + /// + /// The directory to add. + public void AddDirectory(string directoryName) + { + if ( directoryName == null ) { + throw new ArgumentNullException("directoryName"); + } + + CheckUpdating(); + + ZipEntry dirEntry = EntryFactory.MakeDirectoryEntry(directoryName); + AddUpdate(new ZipUpdate(UpdateCommand.Add, dirEntry)); + } + + #endregion + + #region Modifying Entries +/* Modify not yet ready for public consumption. + Direct modification of an entry should not overwrite original data before its read. + Safe mode is trivial in this sense. + public void Modify(ZipEntry original, ZipEntry updated) + { + if ( original == null ) { + throw new ArgumentNullException("original"); + } + + if ( updated == null ) { + throw new ArgumentNullException("updated"); + } + + CheckUpdating(); + contentsEdited_ = true; + updates_.Add(new ZipUpdate(original, updated)); + } +*/ + #endregion + + #region Deleting Entries + /// + /// Delete an entry by name + /// + /// The filename to delete + /// True if the entry was found and deleted; false otherwise. + public bool Delete(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + CheckUpdating(); + + bool result = false; + int index = FindExistingUpdate(fileName); + if ( (index >= 0) && (updates_[index] != null) ) { + result = true; + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else { + throw new ZipException("Cannot find entry to delete"); + } + return result; + } + + /// + /// Delete a from the archive. + /// + /// The entry to delete. + public void Delete(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + CheckUpdating(); + + int index = FindExistingUpdate(entry); + if ( index >= 0 ) { + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else { + throw new ZipException("Cannot find entry to delete"); + } + } + + #endregion + + #region Update Support + + #region Writing Values/Headers + void WriteLEShort(int value) + { + baseStream_.WriteByte(( byte )(value & 0xff)); + baseStream_.WriteByte(( byte )((value >> 8) & 0xff)); + } + + /// + /// Write an unsigned short in little endian byte order. + /// + void WriteLEUshort(ushort value) + { + baseStream_.WriteByte(( byte )(value & 0xff)); + baseStream_.WriteByte(( byte )(value >> 8)); + } + + /// + /// Write an int in little endian byte order. + /// + void WriteLEInt(int value) + { + WriteLEShort(value & 0xffff); + WriteLEShort(value >> 16); + } + + /// + /// Write an unsigned int in little endian byte order. + /// + void WriteLEUint(uint value) + { + WriteLEUshort((ushort)(value & 0xffff)); + WriteLEUshort((ushort)(value >> 16)); + } + + /// + /// Write a long in little endian byte order. + /// + void WriteLeLong(long value) + { + WriteLEInt(( int )(value & 0xffffffff)); + WriteLEInt(( int )(value >> 32)); + } + + void WriteLEUlong(ulong value) + { + WriteLEUint(( uint )(value & 0xffffffff)); + WriteLEUint(( uint )(value >> 32)); + } + + void WriteLocalEntryHeader(ZipUpdate update) + { + ZipEntry entry = update.OutEntry; + + // TODO: Local offset will require adjusting for multi-disk zip files. + entry.Offset = baseStream_.Position; + + // TODO: Need to clear any entry flags that dont make sense or throw an exception here. + if (update.Command != UpdateCommand.Copy) { + if (entry.CompressionMethod == CompressionMethod.Deflated) { + if (entry.Size == 0) { + // No need to compress - no data. + entry.CompressedSize = entry.Size; + entry.Crc = 0; + entry.CompressionMethod = CompressionMethod.Stored; + } + } + else if (entry.CompressionMethod == CompressionMethod.Stored) { + entry.Flags &= ~(int)GeneralBitFlags.Descriptor; + } + + if (HaveKeys) { + entry.IsCrypted = true; + if (entry.Crc < 0) { + entry.Flags |= (int)GeneralBitFlags.Descriptor; + } + } + else { + entry.IsCrypted = false; + } + + switch (useZip64_) { + case UseZip64.Dynamic: + if (entry.Size < 0) { + entry.ForceZip64(); + } + break; + + case UseZip64.On: + entry.ForceZip64(); + break; + + case UseZip64.Off: + // Do nothing. The entry itself may be using Zip64 independantly. + break; + } + } + + // Write the local file header + WriteLEInt(ZipConstants.LocalHeaderSignature); + + WriteLEShort(entry.Version); + WriteLEShort(entry.Flags); + + WriteLEShort((byte)entry.CompressionMethod); + WriteLEInt(( int )entry.DosTime); + + if ( !entry.HasCrc ) { + // Note patch address for updating CRC later. + update.CrcPatchOffset = baseStream_.Position; + WriteLEInt(( int )0); + } + else { + WriteLEInt(unchecked(( int )entry.Crc)); + } + + if (entry.LocalHeaderRequiresZip64) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + if ( (entry.CompressedSize < 0) || (entry.Size < 0) ) { + update.SizePatchOffset = baseStream_.Position; + } + + WriteLEInt(( int )entry.CompressedSize); + WriteLEInt(( int )entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if ( name.Length > 0xFFFF ) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.LocalHeaderRequiresZip64 ) { + ed.StartNewEntry(); + + // Local entry header always includes size and compressed size. + // NOTE the order of these fields is reversed when compared to the normal headers! + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + ed.AddNewEntry(1); + } + else { + ed.Delete(1); + } + + entry.ExtraData = ed.GetEntryData(); + + WriteLEShort(name.Length); + WriteLEShort(entry.ExtraData.Length); + + if ( name.Length > 0 ) { + baseStream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 ) { + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cannot find extra data"); + } + + update.SizePatchOffset = baseStream_.Position + ed.CurrentReadIndex; + } + + if ( entry.ExtraData.Length > 0 ) { + baseStream_.Write(entry.ExtraData, 0, entry.ExtraData.Length); + } + } + + int WriteCentralDirectoryHeader(ZipEntry entry) + { + if ( entry.CompressedSize < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown csize"); + } + + if ( entry.Size < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown size"); + } + + if ( entry.Crc < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown crc"); + } + + // Write the central file header + WriteLEInt(ZipConstants.CentralHeaderSignature); + + // Version made by + WriteLEShort(ZipConstants.VersionMadeBy); + + // Version required to extract + WriteLEShort(entry.Version); + + WriteLEShort(entry.Flags); + + unchecked { + WriteLEShort((byte)entry.CompressionMethod); + WriteLEInt((int)entry.DosTime); + WriteLEInt((int)entry.Crc); + } + + if ( (entry.IsZip64Forced()) || (entry.CompressedSize >= 0xffffffff) ) { + WriteLEInt(-1); + } + else { + WriteLEInt((int)(entry.CompressedSize & 0xffffffff)); + } + + if ( (entry.IsZip64Forced()) || (entry.Size >= 0xffffffff) ) { + WriteLEInt(-1); + } + else { + WriteLEInt((int)entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if ( name.Length > 0xFFFF ) { + throw new ZipException("Entry name is too long."); + } + + WriteLEShort(name.Length); + + // Central header extra data is different to local header version so regenerate. + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.CentralHeaderRequiresZip64 ) { + ed.StartNewEntry(); + + if ( (entry.Size >= 0xffffffff) || (useZip64_ == UseZip64.On) ) + { + ed.AddLeLong(entry.Size); + } + + if ( (entry.CompressedSize >= 0xffffffff) || (useZip64_ == UseZip64.On) ) + { + ed.AddLeLong(entry.CompressedSize); + } + + if ( entry.Offset >= 0xffffffff ) { + ed.AddLeLong(entry.Offset); + } + + // Number of disk on which this file starts isnt supported and is never written here. + ed.AddNewEntry(1); + } + else { + // Should have already be done when local header was added. + ed.Delete(1); + } + + byte[] centralExtraData = ed.GetEntryData(); + + WriteLEShort(centralExtraData.Length); + WriteLEShort(entry.Comment != null ? entry.Comment.Length : 0); + + WriteLEShort(0); // disk number + WriteLEShort(0); // internal file attributes + + // External file attributes... + if ( entry.ExternalFileAttributes != -1 ) { + WriteLEInt(entry.ExternalFileAttributes); + } + else { + if ( entry.IsDirectory ) { + WriteLEUint(16); + } + else { + WriteLEUint(0); + } + } + + if ( entry.Offset >= 0xffffffff ) { + WriteLEUint(0xffffffff); + } + else { + WriteLEUint((uint)(int)entry.Offset); + } + + if ( name.Length > 0 ) { + baseStream_.Write(name, 0, name.Length); + } + + if ( centralExtraData.Length > 0 ) { + baseStream_.Write(centralExtraData, 0, centralExtraData.Length); + } + + byte[] rawComment = (entry.Comment != null) ? Encoding.ASCII.GetBytes(entry.Comment) : new byte[0]; + + if ( rawComment.Length > 0 ) { + baseStream_.Write(rawComment, 0, rawComment.Length); + } + + return ZipConstants.CentralHeaderBaseSize + name.Length + centralExtraData.Length + rawComment.Length; + } + #endregion + + void PostUpdateCleanup() + { + updateDataSource_ = null; + updates_ = null; + updateIndex_ = null; + + if (archiveStorage_ != null) + { + archiveStorage_.Dispose(); + archiveStorage_=null; + } + } + + string GetTransformedFileName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformFile(name) : + name; + } + + string GetTransformedDirectoryName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformDirectory(name) : + name; + } + + /// + /// Get a raw memory buffer. + /// + /// Returns a raw memory buffer. + byte[] GetBuffer() + { + if ( copyBuffer_ == null ) { + copyBuffer_ = new byte[bufferSize_]; + } + return copyBuffer_; + } + + void CopyDescriptorBytes(ZipUpdate update, Stream dest, Stream source) + { + int bytesToCopy = GetDescriptorSize(update); + + if ( bytesToCopy > 0 ) { + byte[] buffer = GetBuffer(); + + while ( bytesToCopy > 0 ) { + int readSize = Math.Min(buffer.Length, bytesToCopy); + + int bytesRead = source.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + dest.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else { + throw new ZipException("Unxpected end of stream"); + } + } + } + } + + void CopyBytes(ZipUpdate update, Stream destination, Stream source, + long bytesToCopy, bool updateCrc) + { + if ( destination == source ) { + throw new InvalidOperationException("Destination and source are the same"); + } + + // NOTE: Compressed size is updated elsewhere. + Crc32 crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do { + int readSize = buffer.Length; + + if ( bytesToCopy < readSize ) { + readSize = (int)bytesToCopy; + } + + bytesRead = source.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + if ( updateCrc ) { + crc.Update(buffer, 0, bytesRead); + } + destination.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ( (bytesRead > 0) && (bytesToCopy > 0) ); + + if ( totalBytesRead != targetBytes ) { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if ( updateCrc ) { + update.OutEntry.Crc = crc.Value; + } + } + + /// + /// Get the size of the source descriptor for a . + /// + /// The update to get the size for. + /// The descriptor size, zero if there isnt one. + int GetDescriptorSize(ZipUpdate update) + { + int result = 0; + if ( (update.Entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) { + result = ZipConstants.DataDescriptorSize - 4; + if ( update.Entry.LocalHeaderRequiresZip64 ) { + result = ZipConstants.Zip64DataDescriptorSize - 4; + } + } + return result; + } + + void CopyDescriptorBytesDirect(ZipUpdate update, Stream stream, ref long destinationPosition, long sourcePosition) + { + int bytesToCopy = GetDescriptorSize(update); + + while ( bytesToCopy > 0 ) { + int readSize = (int)bytesToCopy; + byte[] buffer = GetBuffer(); + + stream.Position = sourcePosition; + int bytesRead = stream.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + destinationPosition += bytesRead; + sourcePosition += bytesRead; + } + else { + throw new ZipException("Unxpected end of stream"); + } + } + } + + void CopyEntryDataDirect(ZipUpdate update, Stream stream, bool updateCrc, ref long destinationPosition, ref long sourcePosition) + { + long bytesToCopy = update.Entry.CompressedSize; + + // NOTE: Compressed size is updated elsewhere. + Crc32 crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do + { + int readSize = buffer.Length; + + if ( bytesToCopy < readSize ) { + readSize = (int)bytesToCopy; + } + + stream.Position = sourcePosition; + bytesRead = stream.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + if ( updateCrc ) { + crc.Update(buffer, 0, bytesRead); + } + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + + destinationPosition += bytesRead; + sourcePosition += bytesRead; + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ( (bytesRead > 0) && (bytesToCopy > 0) ); + + if ( totalBytesRead != targetBytes ) { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if ( updateCrc ) { + update.OutEntry.Crc = crc.Value; + } + } + + int FindExistingUpdate(ZipEntry entry) + { + int result = -1; + string convertedName = GetTransformedFileName(entry.Name); + + if (updateIndex_.ContainsKey(convertedName)) { + result = (int)updateIndex_[convertedName]; + } +/* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for (int index = 0; index < updates_.Count; ++index) + { + ZipUpdate zu = ( ZipUpdate )updates_[index]; + if ( (zu.Entry.ZipFileIndex == entry.ZipFileIndex) && + (string.Compare(convertedName, zu.Entry.Name, true, CultureInfo.InvariantCulture) == 0) ) { + result = index; + break; + } + } + */ + return result; + } + + int FindExistingUpdate(string fileName) + { + int result = -1; + + string convertedName = GetTransformedFileName(fileName); + + if (updateIndex_.ContainsKey(convertedName)) { + result = (int)updateIndex_[convertedName]; + } + +/* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for ( int index = 0; index < updates_.Count; ++index ) { + if ( string.Compare(convertedName, (( ZipUpdate )updates_[index]).Entry.Name, + true, CultureInfo.InvariantCulture) == 0 ) { + result = index; + break; + } + } + */ + + return result; + } + + /// + /// Get an output stream for the specified + /// + /// The entry to get an output stream for. + /// The output stream obtained for the entry. + Stream GetOutputStream(ZipEntry entry) + { + Stream result = baseStream_; + + if ( entry.IsCrypted == true ) { +#if NETCF_1_0 + throw new ZipException("Encryption not supported for Compact Framework 1.0"); +#else + result = CreateAndInitEncryptionStream(result, entry); +#endif + } + + switch ( entry.CompressionMethod ) { + case CompressionMethod.Stored: + result = new UncompressedStream(result); + break; + + case CompressionMethod.Deflated: + DeflaterOutputStream dos = new DeflaterOutputStream(result, new Deflater(9, true)); + dos.IsStreamOwner = false; + result = dos; + break; + + default: + throw new ZipException("Unknown compression method " + entry.CompressionMethod); + } + return result; + } + + void AddEntry(ZipFile workFile, ZipUpdate update) + { + Stream source = null; + + if ( update.Entry.IsFile ) { + source = update.GetSource(); + + if ( source == null ) { + source = updateDataSource_.GetSource(update.Entry, update.Filename); + } + } + + if ( source != null ) { + using ( source ) { + long sourceStreamLength = source.Length; + if ( update.OutEntry.Size < 0 ) { + update.OutEntry.Size = sourceStreamLength; + } + else { + // Check for errant entries. + if ( update.OutEntry.Size != sourceStreamLength ) { + throw new ZipException("Entry size/stream size mismatch"); + } + } + + workFile.WriteLocalEntryHeader(update); + + long dataStart = workFile.baseStream_.Position; + + using ( Stream output = workFile.GetOutputStream(update.OutEntry) ) { + CopyBytes(update, output, source, sourceStreamLength, true); + } + + long dataEnd = workFile.baseStream_.Position; + update.OutEntry.CompressedSize = dataEnd - dataStart; + + if ((update.OutEntry.Flags & (int)GeneralBitFlags.Descriptor) == (int)GeneralBitFlags.Descriptor) + { + ZipHelperStream helper = new ZipHelperStream(workFile.baseStream_); + helper.WriteDataDescriptor(update.OutEntry); + } + } + } + else { + workFile.WriteLocalEntryHeader(update); + update.OutEntry.CompressedSize = 0; + } + + } + + void ModifyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + long dataStart = workFile.baseStream_.Position; + + // TODO: This is slow if the changes don't effect the data!! + if ( update.Entry.IsFile && (update.Filename != null) ) { + using ( Stream output = workFile.GetOutputStream(update.OutEntry) ) { + using ( Stream source = this.GetInputStream(update.Entry) ) { + CopyBytes(update, output, source, source.Length, true); + } + } + } + + long dataEnd = workFile.baseStream_.Position; + update.Entry.CompressedSize = dataEnd - dataStart; + } + + void CopyEntryDirect(ZipFile workFile, ZipUpdate update, ref long destinationPosition) + { + bool skipOver = false; + if ( update.Entry.Offset == destinationPosition ) { + skipOver = true; + } + + if ( !skipOver ) { + baseStream_.Position = destinationPosition; + workFile.WriteLocalEntryHeader(update); + destinationPosition = baseStream_.Position; + } + + long sourcePosition = 0; + + const int NameLengthOffset = 26; + + // TODO: Add base for SFX friendly handling + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + // Clumsy way of handling retrieving the original name and extra data length for now. + // TODO: Stop re-reading name and data length in CopyEntryDirect. + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + sourcePosition = baseStream_.Position + nameLength + extraLength; + + if (skipOver) { + if (update.OffsetBasedSize != -1) + destinationPosition += update.OffsetBasedSize; + else + // TODO: Find out why this calculation comes up 4 bytes short on some entries in ODT (Office Document Text) archives. + // WinZip produces a warning on these entries: + // "caution: value of lrec.csize (compressed size) changed from ..." + destinationPosition += + (sourcePosition - entryDataOffset) + NameLengthOffset + // Header size + update.Entry.CompressedSize + GetDescriptorSize(update); + } + else { + if ( update.Entry.CompressedSize > 0 ) { + CopyEntryDataDirect(update, baseStream_, false, ref destinationPosition, ref sourcePosition ); + } + CopyDescriptorBytesDirect(update, baseStream_, ref destinationPosition, sourcePosition); + } + } + + void CopyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + + if ( update.Entry.CompressedSize > 0 ) { + const int NameLengthOffset = 26; + + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + // TODO: This wont work for SFX files! + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + baseStream_.Seek(nameLength + extraLength, SeekOrigin.Current); + + CopyBytes(update, workFile.baseStream_, baseStream_, update.Entry.CompressedSize, false); + } + CopyDescriptorBytes(update, workFile.baseStream_, baseStream_); + } + + void Reopen(Stream source) + { + if ( source == null ) { + throw new ZipException("Failed to reopen archive - no source"); + } + + isNewArchive_ = false; + baseStream_ = source; + ReadEntries(); + } + + void Reopen() + { + if (Name == null) { + throw new InvalidOperationException("Name is not known cannot Reopen"); + } + + Reopen(File.Open(Name, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + + void UpdateCommentOnly() + { + long baseLength = baseStream_.Length; + + ZipHelperStream updateFile = null; + + if ( archiveStorage_.UpdateMode == FileUpdateMode.Safe ) { + Stream copyStream = archiveStorage_.MakeTemporaryCopy(baseStream_); + updateFile = new ZipHelperStream(copyStream); + updateFile.IsStreamOwner = true; + + baseStream_.Close(); + baseStream_ = null; + } + else { + if (archiveStorage_.UpdateMode == FileUpdateMode.Direct) { + // TODO: archiveStorage wasnt originally intended for this use. + // Need to revisit this to tidy up handling as archive storage currently doesnt + // handle the original stream well. + // The problem is when using an existing zip archive with an in memory archive storage. + // The open stream wont support writing but the memory storage should open the same file not an in memory one. + + // Need to tidy up the archive storage interface and contract basically. + baseStream_ = archiveStorage_.OpenForDirectUpdate(baseStream_); + updateFile = new ZipHelperStream(baseStream_); + } + else { + baseStream_.Close(); + baseStream_ = null; + updateFile = new ZipHelperStream(Name); + } + } + + using ( updateFile ) { + long locatedCentralDirOffset = + updateFile.LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, + baseLength, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + if ( locatedCentralDirOffset < 0 ) { + throw new ZipException("Cannot find central directory"); + } + + const int CentralHeaderCommentSizeOffset = 16; + updateFile.Position += CentralHeaderCommentSizeOffset; + + byte[] rawComment = newComment_.RawComment; + + updateFile.WriteLEShort(rawComment.Length); + updateFile.Write(rawComment, 0, rawComment.Length); + updateFile.SetLength(updateFile.Position); + } + + if ( archiveStorage_.UpdateMode == FileUpdateMode.Safe ) { + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + else { + ReadEntries(); + } + } + + /// + /// Class used to sort updates. + /// + class UpdateComparer : IComparer + { + /// + /// Compares two objects and returns a value indicating whether one is + /// less than, equal to or greater than the other. + /// + /// First object to compare + /// Second object to compare. + /// Compare result. + public int Compare( + object x, + object y) + { + ZipUpdate zx = x as ZipUpdate; + ZipUpdate zy = y as ZipUpdate; + + int result; + + if (zx == null) { + if (zy == null) { + result = 0; + } + else { + result = -1; + } + } + else if (zy == null) { + result = 1; + } + else { + int xCmdValue = ((zx.Command == UpdateCommand.Copy) || (zx.Command == UpdateCommand.Modify)) ? 0 : 1; + int yCmdValue = ((zy.Command == UpdateCommand.Copy) || (zy.Command == UpdateCommand.Modify)) ? 0 : 1; + + result = xCmdValue - yCmdValue; + if (result == 0) { + long offsetDiff = zx.Entry.Offset - zy.Entry.Offset; + if (offsetDiff < 0) { + result = -1; + } + else if (offsetDiff == 0) { + result = 0; + } + else { + result = 1; + } + } + } + return result; + } + } + + void RunUpdates() + { + long sizeEntries = 0; + long endOfStream = 0; + bool directUpdate = false; + long destinationPosition = 0; // NOT SFX friendly + + ZipFile workFile; + + if ( IsNewArchive ) { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + } + else if ( archiveStorage_.UpdateMode == FileUpdateMode.Direct ) { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + + // Sort the updates by offset within copies/modifies, then adds. + // This ensures that data required by copies will not be overwritten. + updates_.Sort(new UpdateComparer()); + } + else { + workFile = ZipFile.Create(archiveStorage_.GetTemporaryOutput()); + workFile.UseZip64 = UseZip64; + + if (key != null) { + workFile.key = (byte[])key.Clone(); + } + } + + try { + foreach ( ZipUpdate update in updates_ ) { + if (update != null) { + switch (update.Command) { + case UpdateCommand.Copy: + if (directUpdate) { + CopyEntryDirect(workFile, update, ref destinationPosition); + } + else { + CopyEntry(workFile, update); + } + break; + + case UpdateCommand.Modify: + // TODO: Direct modifying of an entry will take some legwork. + ModifyEntry(workFile, update); + break; + + case UpdateCommand.Add: + if (!IsNewArchive && directUpdate) { + workFile.baseStream_.Position = destinationPosition; + } + + AddEntry(workFile, update); + + if (directUpdate) { + destinationPosition = workFile.baseStream_.Position; + } + break; + } + } + } + + if ( !IsNewArchive && directUpdate ) { + workFile.baseStream_.Position = destinationPosition; + } + + long centralDirOffset = workFile.baseStream_.Position; + + foreach ( ZipUpdate update in updates_ ) { + if (update != null) { + sizeEntries += workFile.WriteCentralDirectoryHeader(update.OutEntry); + } + } + + byte[] theComment = (newComment_ != null) ? newComment_.RawComment : ZipConstants.ConvertToArray(comment_); + using ( ZipHelperStream zhs = new ZipHelperStream(workFile.baseStream_) ) { + zhs.WriteEndOfCentralDirectory(updateCount_, sizeEntries, centralDirOffset, theComment); + } + + endOfStream = workFile.baseStream_.Position; + + // And now patch entries... + foreach ( ZipUpdate update in updates_ ) { + if (update != null) + { + // If the size of the entry is zero leave the crc as 0 as well. + // The calculated crc will be all bits on... + if ((update.CrcPatchOffset > 0) && (update.OutEntry.CompressedSize > 0)) { + workFile.baseStream_.Position = update.CrcPatchOffset; + workFile.WriteLEInt((int)update.OutEntry.Crc); + } + + if (update.SizePatchOffset > 0) { + workFile.baseStream_.Position = update.SizePatchOffset; + if (update.OutEntry.LocalHeaderRequiresZip64) { + workFile.WriteLeLong(update.OutEntry.Size); + workFile.WriteLeLong(update.OutEntry.CompressedSize); + } + else { + workFile.WriteLEInt((int)update.OutEntry.CompressedSize); + workFile.WriteLEInt((int)update.OutEntry.Size); + } + } + } + } + } + catch { + workFile.Close(); + if (!directUpdate && (workFile.Name != null)) { + File.Delete(workFile.Name); + } + throw; + } + + if (directUpdate) { + workFile.baseStream_.SetLength(endOfStream); + workFile.baseStream_.Flush(); + isNewArchive_ = false; + ReadEntries(); + } + else { + baseStream_.Close(); + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + } + + void CheckUpdating() + { + if ( updates_ == null ) { + throw new InvalidOperationException("BeginUpdate has not been called"); + } + } + + #endregion + + #region ZipUpdate class + /// + /// Represents a pending update to a Zip file. + /// + class ZipUpdate + { + #region Constructors + public ZipUpdate(string fileName, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName); + entry_.CompressionMethod = compressionMethod; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName) + : this(fileName, entryName, CompressionMethod.Deflated) + { + // Do nothing. + } + + [Obsolete] + public ZipUpdate(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName); + entry_.CompressionMethod = compressionMethod; + dataSource_ = dataSource; + } + + public ZipUpdate(IStaticDataSource dataSource, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + dataSource_ = dataSource; + } + + public ZipUpdate(ZipEntry original, ZipEntry updated) + { + throw new ZipException("Modify not currently supported"); + /* + command_ = UpdateCommand.Modify; + entry_ = ( ZipEntry )original.Clone(); + outEntry_ = ( ZipEntry )updated.Clone(); + */ + } + + public ZipUpdate(UpdateCommand command, ZipEntry entry) + { + command_ = command; + entry_ = ( ZipEntry )entry.Clone(); + } + + + /// + /// Copy an existing entry. + /// + /// The existing entry to copy. + public ZipUpdate(ZipEntry entry) + : this(UpdateCommand.Copy, entry) + { + // Do nothing. + } + #endregion + + /// + /// Get the for this update. + /// + /// This is the source or original entry. + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the that will be written to the updated/new file. + /// + public ZipEntry OutEntry + { + get { + if ( outEntry_ == null ) { + outEntry_ = (ZipEntry)entry_.Clone(); + } + + return outEntry_; + } + } + + /// + /// Get the command for this update. + /// + public UpdateCommand Command + { + get { return command_; } + } + + /// + /// Get the filename if any for this update. Null if none exists. + /// + public string Filename + { + get { return filename_; } + } + + /// + /// Get/set the location of the size patch for this update. + /// + public long SizePatchOffset + { + get { return sizePatchOffset_; } + set { sizePatchOffset_ = value; } + } + + /// + /// Get /set the location of the crc patch for this update. + /// + public long CrcPatchOffset + { + get { return crcPatchOffset_; } + set { crcPatchOffset_ = value; } + } + + /// + /// Get/set the size calculated by offset. + /// Specifically, the difference between this and next entry's starting offset. + /// + public long OffsetBasedSize + { + get { return _offsetBasedSize; } + set { _offsetBasedSize = value; } + } + + public Stream GetSource() + { + Stream result = null; + if ( dataSource_ != null ) { + result = dataSource_.GetSource(); + } + + return result; + } + + #region Instance Fields + ZipEntry entry_; + ZipEntry outEntry_; + UpdateCommand command_; + IStaticDataSource dataSource_; + string filename_; + long sizePatchOffset_ = -1; + long crcPatchOffset_ = -1; + long _offsetBasedSize = -1; + #endregion + } + + #endregion + #endregion + + #region Disposing + + #region IDisposable Members + void IDisposable.Dispose() + { + Close(); + } + #endregion + + void DisposeInternal(bool disposing) + { + if ( !isDisposed_ ) { + isDisposed_ = true; + entries_ = new ZipEntry[0]; + + if ( IsStreamOwner && (baseStream_ != null) ) { + lock(baseStream_) { + baseStream_.Close(); + } + } + + PostUpdateCleanup(); + } + } + + /// + /// Releases the unmanaged resources used by the this instance and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + DisposeInternal(disposing); + } + + #endregion + + #region Internal routines + #region Reading + /// + /// Read an unsigned short in little endian byte order. + /// + /// Returns the value read. + /// + /// The stream ends prematurely + /// + ushort ReadLEUshort() + { + int data1 = baseStream_.ReadByte(); + + if ( data1 < 0 ) { + throw new EndOfStreamException("End of stream"); + } + + int data2 = baseStream_.ReadByte(); + + if ( data2 < 0 ) { + throw new EndOfStreamException("End of stream"); + } + + + return unchecked((ushort)((ushort)data1 | (ushort)(data2 << 8))); + } + + /// + /// Read a uint in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + uint ReadLEUint() + { + return (uint)(ReadLEUshort() | (ReadLEUshort() << 16)); + } + + ulong ReadLEUlong() + { + return ReadLEUint() | ((ulong)ReadLEUint() << 32); + } + + #endregion + // NOTE this returns the offset of the first byte after the signature. + long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + using ( ZipHelperStream les = new ZipHelperStream(baseStream_) ) { + return les.LocateBlockWithSignature(signature, endLocation, minimumBlockSize, maximumVariableData); + } + } + + /// + /// Search for and read the central directory of a zip file filling the entries array. + /// + /// + /// An i/o error occurs. + /// + /// + /// The central directory is malformed or cannot be found + /// + void ReadEntries() + { + // Search for the End Of Central Directory. When a zip comment is + // present the directory will start earlier + // + // The search is limited to 64K which is the maximum size of a trailing comment field to aid speed. + // This should be compatible with both SFX and ZIP files but has only been tested for Zip files + // If a SFX file has the Zip data attached as a resource and there are other resources occuring later then + // this could be invalid. + // Could also speed this up by reading memory in larger blocks. + + if (baseStream_.CanSeek == false) { + throw new ZipException("ZipFile stream must be seekable"); + } + + long locatedEndOfCentralDir = LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, + baseStream_.Length, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + + if (locatedEndOfCentralDir < 0) { + throw new ZipException("Cannot find central directory"); + } + + // Read end of central directory record + ushort thisDiskNumber = ReadLEUshort(); + ushort startCentralDirDisk = ReadLEUshort(); + ulong entriesForThisDisk = ReadLEUshort(); + ulong entriesForWholeCentralDir = ReadLEUshort(); + ulong centralDirSize = ReadLEUint(); + long offsetOfCentralDir = ReadLEUint(); + uint commentSize = ReadLEUshort(); + + if ( commentSize > 0 ) { + byte[] comment = new byte[commentSize]; + + StreamUtils.ReadFully(baseStream_, comment); + comment_ = ZipConstants.ConvertToString(comment); + } + else { + comment_ = string.Empty; + } + + bool isZip64 = false; + + // Check if zip64 header information is required. + if ( (thisDiskNumber == 0xffff) || + (startCentralDirDisk == 0xffff) || + (entriesForThisDisk == 0xffff) || + (entriesForWholeCentralDir == 0xffff) || + (centralDirSize == 0xffffffff) || + (offsetOfCentralDir == 0xffffffff) ) { + isZip64 = true; + + long offset = LocateBlockWithSignature(ZipConstants.Zip64CentralDirLocatorSignature, locatedEndOfCentralDir, 0, 0x1000); + if ( offset < 0 ) { + throw new ZipException("Cannot find Zip64 locator"); + } + + // number of the disk with the start of the zip64 end of central directory 4 bytes + // relative offset of the zip64 end of central directory record 8 bytes + // total number of disks 4 bytes + ReadLEUint(); // startDisk64 is not currently used + ulong offset64 = ReadLEUlong(); + uint totalDisks = ReadLEUint(); + + baseStream_.Position = (long)offset64; + long sig64 = ReadLEUint(); + + if ( sig64 != ZipConstants.Zip64CentralFileHeaderSignature ) { + throw new ZipException(string.Format("Invalid Zip64 Central directory signature at {0:X}", offset64)); + } + + // NOTE: Record size = SizeOfFixedFields + SizeOfVariableData - 12. + ulong recordSize = ReadLEUlong(); + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + uint thisDisk = ReadLEUint(); + uint centralDirDisk = ReadLEUint(); + entriesForThisDisk = ReadLEUlong(); + entriesForWholeCentralDir = ReadLEUlong(); + centralDirSize = ReadLEUlong(); + offsetOfCentralDir = (long)ReadLEUlong(); + + // NOTE: zip64 extensible data sector (variable size) is ignored. + } + + entries_ = new ZipEntry[entriesForThisDisk]; + + // SFX/embedded support, find the offset of the first entry vis the start of the stream + // This applies to Zip files that are appended to the end of an SFX stub. + // Or are appended as a resource to an executable. + // Zip files created by some archivers have the offsets altered to reflect the true offsets + // and so dont require any adjustment here... + // TODO: Difficulty with Zip64 and SFX offset handling needs resolution - maths? + if ( !isZip64 && (offsetOfCentralDir < locatedEndOfCentralDir - (4 + (long)centralDirSize)) ) { + offsetOfFirstEntry = locatedEndOfCentralDir - (4 + (long)centralDirSize + offsetOfCentralDir); + if (offsetOfFirstEntry <= 0) { + throw new ZipException("Invalid embedded zip archive"); + } + } + + baseStream_.Seek(offsetOfFirstEntry + offsetOfCentralDir, SeekOrigin.Begin); + + for (ulong i = 0; i < entriesForThisDisk; i++) { + if (ReadLEUint() != ZipConstants.CentralHeaderSignature) { + throw new ZipException("Wrong Central Directory signature"); + } + + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + int bitFlags = ReadLEUshort(); + int method = ReadLEUshort(); + uint dostime = ReadLEUint(); + uint crc = ReadLEUint(); + long csize = (long)ReadLEUint(); + long size = (long)ReadLEUint(); + int nameLen = ReadLEUshort(); + int extraLen = ReadLEUshort(); + int commentLen = ReadLEUshort(); + + int diskStartNo = ReadLEUshort(); // Not currently used + int internalAttributes = ReadLEUshort(); // Not currently used + + uint externalAttributes = ReadLEUint(); + long offset = ReadLEUint(); + + byte[] buffer = new byte[Math.Max(nameLen, commentLen)]; + + StreamUtils.ReadFully(baseStream_, buffer, 0, nameLen); + string name = ZipConstants.ConvertToStringExt(bitFlags, buffer, nameLen); + + ZipEntry entry = new ZipEntry(name, versionToExtract, versionMadeBy, (CompressionMethod)method); + entry.Crc = crc & 0xffffffffL; + entry.Size = size & 0xffffffffL; + entry.CompressedSize = csize & 0xffffffffL; + entry.Flags = bitFlags; + entry.DosTime = (uint)dostime; + entry.ZipFileIndex = (long)i; + entry.Offset = offset; + entry.ExternalFileAttributes = (int)externalAttributes; + + if ((bitFlags & 8) == 0) { + entry.CryptoCheckValue = (byte)(crc >> 24); + } + else { + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + if (extraLen > 0) { + byte[] extra = new byte[extraLen]; + StreamUtils.ReadFully(baseStream_, extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(false); + + if (commentLen > 0) { + StreamUtils.ReadFully(baseStream_, buffer, 0, commentLen); + entry.Comment = ZipConstants.ConvertToStringExt(bitFlags, buffer, commentLen); + } + + entries_[i] = entry; + } + } + + /// + /// Locate the data for a given entry. + /// + /// + /// The start offset of the data. + /// + /// + /// The stream ends prematurely + /// + /// + /// The local header signature is invalid, the entry and central header file name lengths are different + /// or the local and entry compression methods dont match + /// + long LocateEntry(ZipEntry entry) + { + return TestLocalHeader(entry, HeaderTest.Extract); + } + +#if !NETCF_1_0 + Stream CreateAndInitDecryptionStream(Stream baseStream, ZipEntry entry) + { + CryptoStream result = null; + + if ( (entry.Version < ZipConstants.VersionStrongEncryption) + || (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0) { + PkzipClassicManaged classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for encrypted stream"); + } + + result = new CryptoStream(baseStream, classicManaged.CreateDecryptor(key, null), CryptoStreamMode.Read); + CheckClassicPassword(result, entry); + } + else { +#if !NET_1_1 && !NETCF_2_0 + if (entry.Version == ZipConstants.VERSION_AES) { + // + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for AES encrypted stream"); + } + int saltLen = entry.AESSaltLen; + byte[] saltBytes = new byte[saltLen]; + int saltIn = baseStream.Read(saltBytes, 0, saltLen); + if (saltIn != saltLen) + throw new ZipException("AES Salt expected " + saltLen + " got " + saltIn); + // + byte[] pwdVerifyRead = new byte[2]; + baseStream.Read(pwdVerifyRead, 0, 2); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + ZipAESTransform decryptor = new ZipAESTransform(rawPassword_, saltBytes, blockSize, false); + byte[] pwdVerifyCalc = decryptor.PwdVerifier; + if (pwdVerifyCalc[0] != pwdVerifyRead[0] || pwdVerifyCalc[1] != pwdVerifyRead[1]) + throw new Exception("Invalid password for AES"); + result = new ZipAESStream(baseStream, decryptor, CryptoStreamMode.Read); + } + else +#endif + { + throw new ZipException("Decryption method not supported"); + } + } + + return result; + } + + Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) + { + CryptoStream result = null; + if ( (entry.Version < ZipConstants.VersionStrongEncryption) + || (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0) { + PkzipClassicManaged classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for encrypted stream"); + } + + // Closing a CryptoStream will close the base stream as well so wrap it in an UncompressedStream + // which doesnt do this. + result = new CryptoStream(new UncompressedStream(baseStream), + classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write); + + if ( (entry.Crc < 0) || (entry.Flags & 8) != 0) { + WriteEncryptionHeader(result, entry.DosTime << 16); + } + else { + WriteEncryptionHeader(result, entry.Crc); + } + } + return result; + } + + static void CheckClassicPassword(CryptoStream classicCryptoStream, ZipEntry entry) + { + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + StreamUtils.ReadFully(classicCryptoStream, cryptbuffer); + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) { + throw new ZipException("Invalid password"); + } + } +#endif + + static void WriteEncryptionHeader(Stream stream, long crcValue) + { + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + Random rnd = new Random(); + rnd.NextBytes(cryptBuffer); + cryptBuffer[11] = (byte)(crcValue >> 24); + stream.Write(cryptBuffer, 0, cryptBuffer.Length); + } + + #endregion + + #region Instance Fields + bool isDisposed_; + string name_; + string comment_; + string rawPassword_; + Stream baseStream_; + bool isStreamOwner; + long offsetOfFirstEntry; + ZipEntry[] entries_; + byte[] key; + bool isNewArchive_; + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // Hint: Set always ZipEntry size before they are added to an archive and this setting isnt needed. + UseZip64 useZip64_ = UseZip64.Dynamic ; + + #region Zip Update Instance Fields + ArrayList updates_; + long updateCount_; // Count is managed manually as updates_ can contain nulls! + Hashtable updateIndex_; + IArchiveStorage archiveStorage_; + IDynamicDataSource updateDataSource_; + bool contentsEdited_; + int bufferSize_ = DefaultBufferSize; + byte[] copyBuffer_; + ZipString newComment_; + bool commentEdited_; + IEntryFactory updateEntryFactory_ = new ZipEntryFactory(); + #endregion + #endregion + + #region Support Classes + /// + /// Represents a string from a which is stored as an array of bytes. + /// + class ZipString + { + #region Constructors + /// + /// Initialise a with a string. + /// + /// The textual string form. + public ZipString(string comment) + { + comment_ = comment; + isSourceString_ = true; + } + + /// + /// Initialise a using a string in its binary 'raw' form. + /// + /// + public ZipString(byte[] rawString) + { + rawComment_ = rawString; + } + #endregion + + /// + /// Get a value indicating the original source of data for this instance. + /// True if the source was a string; false if the source was binary data. + /// + public bool IsSourceString + { + get { return isSourceString_; } + } + + /// + /// Get the length of the comment when represented as raw bytes. + /// + public int RawLength + { + get { + MakeBytesAvailable(); + return rawComment_.Length; + } + } + + /// + /// Get the comment in its 'raw' form as plain bytes. + /// + public byte[] RawComment + { + get { + MakeBytesAvailable(); + return (byte[])rawComment_.Clone(); + } + } + + /// + /// Reset the comment to its initial state. + /// + public void Reset() + { + if ( isSourceString_ ) { + rawComment_ = null; + } + else { + comment_ = null; + } + } + + void MakeTextAvailable() + { + if ( comment_ == null ) { + comment_ = ZipConstants.ConvertToString(rawComment_); + } + } + + void MakeBytesAvailable() + { + if ( rawComment_ == null ) { + rawComment_ = ZipConstants.ConvertToArray(comment_); + } + } + + /// + /// Implicit conversion of comment to a string. + /// + /// The to convert to a string. + /// The textual equivalent for the input value. + static public implicit operator string(ZipString zipString) + { + zipString.MakeTextAvailable(); + return zipString.comment_; + } + + #region Instance Fields + string comment_; + byte[] rawComment_; + bool isSourceString_; + #endregion + } + + /// + /// An enumerator for Zip entries + /// + class ZipEntryEnumerator : IEnumerator + { + #region Constructors + public ZipEntryEnumerator(ZipEntry[] entries) + { + array = entries; + } + + #endregion + #region IEnumerator Members + public object Current + { + get { + return array[index]; + } + } + + public void Reset() + { + index = -1; + } + + public bool MoveNext() + { + return (++index < array.Length); + } + #endregion + #region Instance Fields + ZipEntry[] array; + int index = -1; + #endregion + } + + /// + /// An is a stream that you can write uncompressed data + /// to and flush, but cannot read, seek or do anything else to. + /// + class UncompressedStream : Stream + { + #region Constructors + public UncompressedStream(Stream baseStream) + { + baseStream_ = baseStream; + } + + #endregion + + /// + /// Close this stream instance. + /// + public override void Close() + { + // Do nothing + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + public override bool CanRead + { + get { + return false; + } + } + + /// + /// Write any buffered data to underlying storage. + /// + public override void Flush() + { + baseStream_.Flush(); + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + public override bool CanWrite + { + get { + return baseStream_.CanWrite; + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + public override bool CanSeek + { + get { + return false; + } + } + + /// + /// Get the length in bytes of the stream. + /// + public override long Length + { + get { + return 0; + } + } + + /// + /// Gets or sets the position within the current stream. + /// + public override long Position + { + get { + return baseStream_.Position; + } + + set + { + } + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + return 0; + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + baseStream_.Write(buffer, offset, count); + } + + #region Instance Fields + Stream baseStream_; + #endregion + } + + /// + /// A is an + /// whose data is only a part or subsection of a file. + /// + class PartialInputStream : Stream + { + #region Constructors + /// + /// Initialise a new instance of the class. + /// + /// The containing the underlying stream to use for IO. + /// The start of the partial data. + /// The length of the partial data. + public PartialInputStream(ZipFile zipFile, long start, long length) + { + start_ = start; + length_ = length; + + // Although this is the only time the zipfile is used + // keeping a reference here prevents premature closure of + // this zip file and thus the baseStream_. + + // Code like this will cause apparently random failures depending + // on the size of the files and when garbage is collected. + // + // ZipFile z = new ZipFile (stream); + // Stream reader = z.GetInputStream(0); + // uses reader here.... + zipFile_ = zipFile; + baseStream_ = zipFile_.baseStream_; + readPos_ = start; + end_ = start + length; + } + #endregion + + /// + /// Read a byte from this stream. + /// + /// Returns the byte read or -1 on end of stream. + public override int ReadByte() + { + if (readPos_ >= end_) { + // -1 is the correct value at end of stream. + return -1; + } + + lock( baseStream_ ) { + baseStream_.Seek(readPos_++, SeekOrigin.Begin); + return baseStream_.ReadByte(); + } + } + + /// + /// Close this partial input stream. + /// + /// + /// The underlying stream is not closed. Close the parent ZipFile class to do that. + /// + public override void Close() + { + // Do nothing at all! + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + lock(baseStream_) { + if (count > end_ - readPos_) { + count = (int) (end_ - readPos_); + if (count == 0) { + return 0; + } + } + + baseStream_.Seek(readPos_, SeekOrigin.Begin); + int readCount = baseStream_.Read(buffer, offset, count); + if (readCount > 0) { + readPos_ += readCount; + } + return readCount; + } + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = readPos_; + + switch ( origin ) + { + case SeekOrigin.Begin: + newPos = start_ + offset; + break; + + case SeekOrigin.Current: + newPos = readPos_ + offset; + break; + + case SeekOrigin.End: + newPos = end_ + offset; + break; + } + + if ( newPos < start_ ) { + throw new ArgumentException("Negative position is invalid"); + } + + if ( newPos >= end_ ) { + throw new IOException("Cannot seek past end"); + } + readPos_ = newPos; + return readPos_; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + /// An I/O error occurs. + public override void Flush() + { + // Nothing to do. + } + + /// + /// Gets or sets the position within the current stream. + /// + /// + /// The current position within the stream. + /// An I/O error occurs. + /// The stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Position { + get { return readPos_ - start_; } + set { + long newPos = start_ + value; + + if ( newPos < start_ ) { + throw new ArgumentException("Negative position is invalid"); + } + + if ( newPos >= end_ ) { + throw new InvalidOperationException("Cannot seek past end"); + } + readPos_ = newPos; + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// + /// A long value representing the length of the stream in bytes. + /// A class derived from Stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Length { + get { return length_; } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// false + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return false; } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return true; } + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true. + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return true; } + } + +#if !NET_1_0 && !NET_1_1 && !NETCF_1_0 + /// + /// Gets a value that determines whether the current stream can time out. + /// + /// + /// A value that determines whether the current stream can time out. + public override bool CanTimeout { + get { return baseStream_.CanTimeout; } + } +#endif + #region Instance Fields + ZipFile zipFile_; + Stream baseStream_; + long start_; + long length_; + long readPos_; + long end_; + #endregion + } + #endregion + } + + #endregion + + #region DataSources + /// + /// Provides a static way to obtain a source of data for an entry. + /// + public interface IStaticDataSource + { + /// + /// Get a source of data by creating a new stream. + /// + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(); + } + + /// + /// Represents a source of data that can dynamically provide + /// multiple data sources based on the parameters passed. + /// + public interface IDynamicDataSource + { + /// + /// Get a data source. + /// + /// The to get a source for. + /// The name for data if known. + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(ZipEntry entry, string name); + } + + /// + /// Default implementation of a for use with files stored on disk. + /// + public class StaticDiskDataSource : IStaticDataSource + { + /// + /// Initialise a new instnace of + /// + /// The name of the file to obtain data from. + public StaticDiskDataSource(string fileName) + { + fileName_ = fileName; + } + + #region IDataSource Members + + /// + /// Get a providing data. + /// + /// Returns a provising data. + public Stream GetSource() + { + return File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + #endregion + #region Instance Fields + string fileName_; + #endregion + } + + + /// + /// Default implementation of for files stored on disk. + /// + public class DynamicDiskDataSource : IDynamicDataSource + { + /// + /// Initialise a default instance of . + /// + public DynamicDiskDataSource() + { + } + + #region IDataSource Members + /// + /// Get a providing data for an entry. + /// + /// The entry to provide data for. + /// The file name for data if known. + /// Returns a stream providing data; or null if not available + public Stream GetSource(ZipEntry entry, string name) + { + Stream result = null; + + if ( name != null ) { + result = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + return result; + } + + #endregion + } + + #endregion + + #region Archive Storage + /// + /// Defines facilities for data storage when updating Zip Archives. + /// + public interface IArchiveStorage + { + /// + /// Get the to apply during updates. + /// + FileUpdateMode UpdateMode { get; } + + /// + /// Get an empty that can be used for temporary output. + /// + /// Returns a temporary output + /// + Stream GetTemporaryOutput(); + + /// + /// Convert a temporary output stream to a final stream. + /// + /// The resulting final + /// + Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// This may be the current stream passed. + Stream OpenForDirectUpdate(Stream stream); + + /// + /// Dispose of this instance. + /// + void Dispose(); + } + + /// + /// An abstract suitable for extension by inheritance. + /// + abstract public class BaseArchiveStorage : IArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The update mode. + protected BaseArchiveStorage(FileUpdateMode updateMode) + { + updateMode_ = updateMode; + } + #endregion + + #region IArchiveStorage Members + + /// + /// Gets a temporary output + /// + /// Returns the temporary output stream. + /// + public abstract Stream GetTemporaryOutput(); + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + /// + public abstract Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of a . + /// + /// The to make a copy of. + /// Returns a temporary output that is a copy of the input. + public abstract Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The to open for direct update. + /// Returns a stream suitable for direct updating. + public abstract Stream OpenForDirectUpdate(Stream stream); + + /// + /// Disposes this instance. + /// + public abstract void Dispose(); + + /// + /// Gets the update mode applicable. + /// + /// The update mode. + public FileUpdateMode UpdateMode + { + get { + return updateMode_; + } + } + + #endregion + + #region Instance Fields + FileUpdateMode updateMode_; + #endregion + } + + /// + /// An implementation suitable for hard disks. + /// + public class DiskArchiveStorage : BaseArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The file. + /// The update mode. + public DiskArchiveStorage(ZipFile file, FileUpdateMode updateMode) + : base(updateMode) + { + if ( file.Name == null ) { + throw new ZipException("Cant handle non file archives"); + } + + fileName_ = file.Name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The file. + public DiskArchiveStorage(ZipFile file) + : this(file, FileUpdateMode.Safe) + { + } + #endregion + + #region IArchiveStorage Members + + /// + /// Gets a temporary output for performing updates on. + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + if ( temporaryName_ != null ) { + temporaryName_ = GetTempFileName(temporaryName_, true); + temporaryStream_ = File.Open(temporaryName_, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + } + else { + // Determine where to place files based on internal strategy. + // Currently this is always done in system temp directory. + temporaryName_ = Path.GetTempFileName(); + temporaryStream_ = File.Open(temporaryName_, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + } + + return temporaryStream_; + } + + /// + /// Converts a temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if ( temporaryStream_ == null ) { + throw new ZipException("No temporary stream has been created"); + } + + Stream result = null; + + string moveTempName = GetTempFileName(fileName_, false); + bool newFileCreated = false; + + try { + temporaryStream_.Close(); + File.Move(fileName_, moveTempName); + File.Move(temporaryName_, fileName_); + newFileCreated = true; + File.Delete(moveTempName); + + result = File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch(Exception) { + result = null; + + // Try to roll back changes... + if ( !newFileCreated ) { + File.Move(moveTempName, fileName_); + File.Delete(temporaryName_); + } + + throw; + } + + return result; + } + + /// + /// Make a temporary copy of a stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + stream.Close(); + + temporaryName_ = GetTempFileName(fileName_, true); + File.Copy(fileName_, temporaryName_, true); + + temporaryStream_ = new FileStream(temporaryName_, + FileMode.Open, + FileAccess.ReadWrite); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// If the stream is not null this is used as is. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) + { + if (stream != null) { + stream.Close(); + } + + result = new FileStream(fileName_, + FileMode.Open, + FileAccess.ReadWrite); + } + else + { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if ( temporaryStream_ != null ) { + temporaryStream_.Close(); + } + } + + #endregion + + #region Internal routines + static string GetTempFileName(string original, bool makeTempFile) + { + string result = null; + + if ( original == null ) { + result = Path.GetTempFileName(); + } + else { + int counter = 0; + int suffixSeed = DateTime.Now.Second; + + while ( result == null ) { + counter += 1; + string newName = string.Format("{0}.{1}{2}.tmp", original, suffixSeed, counter); + if ( !File.Exists(newName) ) { + if ( makeTempFile) { + try { + // Try and create the file. + using ( FileStream stream = File.Create(newName) ) { + } + result = newName; + } + catch { + suffixSeed = DateTime.Now.Second; + } + } + else { + result = newName; + } + } + } + } + return result; + } + #endregion + + #region Instance Fields + Stream temporaryStream_; + string fileName_; + string temporaryName_; + #endregion + } + + /// + /// An implementation suitable for in memory streams. + /// + public class MemoryArchiveStorage : BaseArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public MemoryArchiveStorage() + : base(FileUpdateMode.Direct) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use + /// This constructor is for testing as memory streams dont really require safe mode. + public MemoryArchiveStorage(FileUpdateMode updateMode) + : base(updateMode) + { + } + + #endregion + + #region Properties + /// + /// Get the stream returned by if this was in fact called. + /// + public MemoryStream FinalStream + { + get { return finalStream_; } + } + + #endregion + + #region IArchiveStorage Members + + /// + /// Gets the temporary output + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + temporaryStream_ = new MemoryStream(); + return temporaryStream_; + } + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if ( temporaryStream_ == null ) { + throw new ZipException("No temporary stream has been created"); + } + + finalStream_ = new MemoryStream(temporaryStream_.ToArray()); + return finalStream_; + } + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + temporaryStream_ = new MemoryStream(); + stream.Position = 0; + StreamUtils.Copy(stream, temporaryStream_, new byte[4096]); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The original source stream + /// Returns a stream suitable for direct updating. + /// If the passed is not null this is used; + /// otherwise a new is returned. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) { + + result = new MemoryStream(); + + if (stream != null) { + stream.Position = 0; + StreamUtils.Copy(stream, result, new byte[4096]); + + stream.Close(); + } + } + else { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if ( temporaryStream_ != null ) { + temporaryStream_.Close(); + } + } + + #endregion + + #region Instance Fields + MemoryStream temporaryStream_; + MemoryStream finalStream_; + #endregion + } + + #endregion +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs new file mode 100644 index 000000000..ed9572f05 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs @@ -0,0 +1,623 @@ +// ZipHelperStream.cs +// +// Copyright 2006, 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using System.Text; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Holds data pertinent to a data descriptor. + /// + public class DescriptorData + { + /// + /// Get /set the compressed size of data. + /// + public long CompressedSize + { + get { return compressedSize; } + set { compressedSize = value; } + } + + /// + /// Get / set the uncompressed size of data + /// + public long Size + { + get { return size; } + set { size = value; } + } + + /// + /// Get /set the crc value. + /// + public long Crc + { + get { return crc; } + set { crc = (value & 0xffffffff); } + } + + #region Instance Fields + long size; + long compressedSize; + long crc; + #endregion + } + + class EntryPatchData + { + public long SizePatchOffset + { + get { return sizePatchOffset_; } + set { sizePatchOffset_ = value; } + } + + public long CrcPatchOffset + { + get { return crcPatchOffset_; } + set { crcPatchOffset_ = value; } + } + + #region Instance Fields + long sizePatchOffset_; + long crcPatchOffset_; + #endregion + } + + /// + /// This class assists with writing/reading from Zip files. + /// + internal class ZipHelperStream : Stream + { + #region Constructors + /// + /// Initialise an instance of this class. + /// + /// The name of the file to open. + public ZipHelperStream(string name) + { + stream_ = new FileStream(name, FileMode.Open, FileAccess.ReadWrite); + isOwner_ = true; + } + + /// + /// Initialise a new instance of . + /// + /// The stream to use. + public ZipHelperStream(Stream stream) + { + stream_ = stream; + } + #endregion + + /// + /// Get / set a value indicating wether the the underlying stream is owned or not. + /// + /// If the stream is owned it is closed when this instance is closed. + public bool IsStreamOwner + { + get { return isOwner_; } + set { isOwner_ = value; } + } + + #region Base Stream Methods + public override bool CanRead + { + get { return stream_.CanRead; } + } + + public override bool CanSeek + { + get { return stream_.CanSeek; } + } + +#if !NET_1_0 && !NET_1_1 && !NETCF_1_0 + public override bool CanTimeout + { + get { return stream_.CanTimeout; } + } +#endif + + public override long Length + { + get { return stream_.Length; } + } + + public override long Position + { + get { return stream_.Position; } + set { stream_.Position = value; } + } + + public override bool CanWrite + { + get { return stream_.CanWrite; } + } + + public override void Flush() + { + stream_.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return stream_.Seek(offset, origin); + } + + public override void SetLength(long value) + { + stream_.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return stream_.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + stream_.Write(buffer, offset, count); + } + + /// + /// Close the stream. + /// + /// + /// The underlying stream is closed only if is true. + /// + override public void Close() + { + Stream toClose = stream_; + stream_ = null; + if (isOwner_ && (toClose != null)) + { + isOwner_ = false; + toClose.Close(); + } + } + #endregion + + // Write the local file header + // TODO: ZipHelperStream.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage + void WriteLocalHeader(ZipEntry entry, EntryPatchData patchData) + { + CompressionMethod method = entry.CompressionMethod; + bool headerInfoAvailable = true; // How to get this? + bool patchEntryHeader = false; + + WriteLEInt(ZipConstants.LocalHeaderSignature); + + WriteLEShort(entry.Version); + WriteLEShort(entry.Flags); + WriteLEShort((byte)method); + WriteLEInt((int)entry.DosTime); + + if (headerInfoAvailable == true) { + WriteLEInt((int)entry.Crc); + if ( entry.LocalHeaderRequiresZip64 ) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + WriteLEInt((int)entry.Size); + } + } else { + if (patchData != null) { + patchData.CrcPatchOffset = stream_.Position; + } + WriteLEInt(0); // Crc + + if ( patchData != null ) { + patchData.SizePatchOffset = stream_.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + WriteLEInt(0); // Compressed size + WriteLEInt(0); // Uncompressed size + } + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) { + ed.StartNewEntry(); + if (headerInfoAvailable) { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + } + else { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cant find extra data"); + } + + if ( patchData != null ) { + patchData.SizePatchOffset = ed.CurrentReadIndex; + } + } + else { + ed.Delete(1); + } + + byte[] extra = ed.GetEntryData(); + + WriteLEShort(name.Length); + WriteLEShort(extra.Length); + + if ( name.Length > 0 ) { + stream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + patchData.SizePatchOffset += stream_.Position; + } + + if ( extra.Length > 0 ) { + stream_.Write(extra, 0, extra.Length); + } + } + + /// + /// Locates a block with the desired . + /// + /// The signature to find. + /// Location, marking the end of block. + /// Minimum size of the block. + /// The maximum variable data. + /// Eeturns the offset of the first byte after the signature; -1 if not found + public long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + long pos = endLocation - minimumBlockSize; + if ( pos < 0 ) { + return -1; + } + + long giveUpMarker = Math.Max(pos - maximumVariableData, 0); + + // TODO: This loop could be optimised for speed. + do { + if ( pos < giveUpMarker ) { + return -1; + } + Seek(pos--, SeekOrigin.Begin); + } while ( ReadLEInt() != signature ); + + return Position; + } + + /// + /// Write Zip64 end of central directory records (File header and locator). + /// + /// The number of entries in the central directory. + /// The size of entries in the central directory. + /// The offset of the dentral directory. + public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, long centralDirOffset) + { + long centralSignatureOffset = stream_.Position; + WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature); + WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12) + WriteLEShort(ZipConstants.VersionMadeBy); // Version made by + WriteLEShort(ZipConstants.VersionZip64); // Version to extract + WriteLEInt(0); // Number of this disk + WriteLEInt(0); // number of the disk with the start of the central directory + WriteLELong(noOfEntries); // No of entries on this disk + WriteLELong(noOfEntries); // Total No of entries in central directory + WriteLELong(sizeEntries); // Size of the central directory + WriteLELong(centralDirOffset); // offset of start of central directory + // zip64 extensible data sector not catered for here (variable size) + + // Write the Zip64 end of central directory locator + WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature); + + // no of the disk with the start of the zip64 end of central directory + WriteLEInt(0); + + // relative offset of the zip64 end of central directory record + WriteLELong(centralSignatureOffset); + + // total number of disks + WriteLEInt(1); + } + + /// + /// Write the required records to end the central directory. + /// + /// The number of entries in the directory. + /// The size of the entries in the directory. + /// The start of the central directory. + /// The archive comment. (This can be null). + public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries, + long startOfCentralDirectory, byte[] comment) + { + + if ( (noOfEntries >= 0xffff) || + (startOfCentralDirectory >= 0xffffffff) || + (sizeEntries >= 0xffffffff) ) { + WriteZip64EndOfCentralDirectory(noOfEntries, sizeEntries, startOfCentralDirectory); + } + + WriteLEInt(ZipConstants.EndOfCentralDirectorySignature); + + // TODO: ZipFile Multi disk handling not done + WriteLEShort(0); // number of this disk + WriteLEShort(0); // no of disk with start of central dir + + + // Number of entries + if ( noOfEntries >= 0xffff ) { + WriteLEUshort(0xffff); // Zip64 marker + WriteLEUshort(0xffff); + } + else { + WriteLEShort(( short )noOfEntries); // entries in central dir for this disk + WriteLEShort(( short )noOfEntries); // total entries in central directory + } + + // Size of the central directory + if ( sizeEntries >= 0xffffffff ) { + WriteLEUint(0xffffffff); // Zip64 marker + } + else { + WriteLEInt(( int )sizeEntries); + } + + + // offset of start of central directory + if ( startOfCentralDirectory >= 0xffffffff ) { + WriteLEUint(0xffffffff); // Zip64 marker + } + else { + WriteLEInt(( int )startOfCentralDirectory); + } + + int commentLength = (comment != null) ? comment.Length : 0; + + if ( commentLength > 0xffff ) { + throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength)); + } + + WriteLEShort(commentLength); + + if ( commentLength > 0 ) { + Write(comment, 0, comment.Length); + } + } + + #region LE value reading/writing + /// + /// Read an unsigned short in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + public int ReadLEShort() + { + int byteValue1 = stream_.ReadByte(); + + if (byteValue1 < 0) { + throw new EndOfStreamException(); + } + + int byteValue2 = stream_.ReadByte(); + if (byteValue2 < 0) { + throw new EndOfStreamException(); + } + + return byteValue1 | (byteValue2 << 8); + } + + /// + /// Read an int in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + public int ReadLEInt() + { + return ReadLEShort() | (ReadLEShort() << 16); + } + + /// + /// Read a long in little endian byte order. + /// + /// The value read. + public long ReadLELong() + { + return (uint)ReadLEInt() | ((long)ReadLEInt() << 32); + } + + /// + /// Write an unsigned short in little endian byte order. + /// + /// The value to write. + public void WriteLEShort(int value) + { + stream_.WriteByte(( byte )(value & 0xff)); + stream_.WriteByte(( byte )((value >> 8) & 0xff)); + } + + /// + /// Write a ushort in little endian byte order. + /// + /// The value to write. + public void WriteLEUshort(ushort value) + { + stream_.WriteByte(( byte )(value & 0xff)); + stream_.WriteByte(( byte )(value >> 8)); + } + + /// + /// Write an int in little endian byte order. + /// + /// The value to write. + public void WriteLEInt(int value) + { + WriteLEShort(value); + WriteLEShort(value >> 16); + } + + /// + /// Write a uint in little endian byte order. + /// + /// The value to write. + public void WriteLEUint(uint value) + { + WriteLEUshort(( ushort )(value & 0xffff)); + WriteLEUshort(( ushort )(value >> 16)); + } + + /// + /// Write a long in little endian byte order. + /// + /// The value to write. + public void WriteLELong(long value) + { + WriteLEInt(( int )value); + WriteLEInt(( int )(value >> 32)); + } + + /// + /// Write a ulong in little endian byte order. + /// + /// The value to write. + public void WriteLEUlong(ulong value) + { + WriteLEUint(( uint )(value & 0xffffffff)); + WriteLEUint(( uint )(value >> 32)); + } + + #endregion + + /// + /// Write a data descriptor. + /// + /// The entry to write a descriptor for. + /// Returns the number of descriptor bytes written. + public int WriteDataDescriptor(ZipEntry entry) + { + if (entry == null) { + throw new ArgumentNullException("entry"); + } + + int result=0; + + // Add data descriptor if flagged as required + if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) + { + // The signature is not PKZIP originally but is now described as optional + // in the PKZIP Appnote documenting trhe format. + WriteLEInt(ZipConstants.DataDescriptorSignature); + WriteLEInt(unchecked((int)(entry.Crc))); + + result+=8; + + if (entry.LocalHeaderRequiresZip64) + { + WriteLELong(entry.CompressedSize); + WriteLELong(entry.Size); + result+=16; + } + else + { + WriteLEInt((int)entry.CompressedSize); + WriteLEInt((int)entry.Size); + result+=8; + } + } + + return result; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + /// if set to true [zip64]. + /// The data to fill in. + /// Returns the number of bytes read in the descriptor. + public void ReadDataDescriptor(bool zip64, DescriptorData data) + { + int intValue = ReadLEInt(); + + // In theory this may not be a descriptor according to PKZIP appnote. + // In practise its always there. + if (intValue != ZipConstants.DataDescriptorSignature) { + throw new ZipException("Data descriptor signature not found"); + } + + data.Crc = ReadLEInt(); + + if (zip64) { + data.CompressedSize = ReadLELong(); + data.Size = ReadLELong(); + } + else { + data.CompressedSize = ReadLEInt(); + data.Size = ReadLEInt(); + } + } + + #region Instance Fields + bool isOwner_; + Stream stream_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs new file mode 100644 index 000000000..90848d018 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs @@ -0,0 +1,675 @@ +// ZipInputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2010-05-25 Z-1663 Fixed exception when testing local header compressed size of -1 + +using System; +using System.IO; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +#if !NETCF_1_0 +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is an InflaterInputStream that reads the files baseInputStream an zip archive + /// one after another. It has a special method to get the zip entry of + /// the next file. The zip entry contains information about the file name + /// size, compressed size, Crc, etc. + /// It includes support for Stored and Deflated entries. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// This sample shows how to read a zip file + /// + /// using System; + /// using System.Text; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// using ( ZipInputStream s = new ZipInputStream(File.OpenRead(args[0]))) { + /// + /// ZipEntry theEntry; + /// const int size = 2048; + /// byte[] data = new byte[2048]; + /// + /// while ((theEntry = s.GetNextEntry()) != null) { + /// if ( entry.IsFile ) { + /// Console.Write("Show contents (y/n) ?"); + /// if (Console.ReadLine() == "y") { + /// while (true) { + /// size = s.Read(data, 0, data.Length); + /// if (size > 0) { + /// Console.Write(new ASCIIEncoding().GetString(data, 0, size)); + /// } else { + /// break; + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipInputStream : InflaterInputStream + { + #region Instance Fields + + /// + /// Delegate for reading bytes from a stream. + /// + delegate int ReadDataHandler(byte[] b, int offset, int length); + + /// + /// The current reader this instance. + /// + ReadDataHandler internalReader; + + Crc32 crc = new Crc32(); + ZipEntry entry; + + long size; + int method; + int flags; + string password; + #endregion + + #region Constructors + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + public ZipInputStream(Stream baseInputStream) + : base(baseInputStream, new Inflater(true)) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + /// Size of the buffer. + public ZipInputStream( Stream baseInputStream, int bufferSize ) + : base(baseInputStream, new Inflater(true), bufferSize) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + #endregion + + /// + /// Optional password used for encryption when non-null + /// + /// A password for all encrypted entries in this + public string Password + { + get { + return password; + } + set { + password = value; + } + } + + + /// + /// Gets a value indicating if there is a current entry and it can be decompressed + /// + /// + /// The entry can only be decompressed if the library supports the zip features required to extract it. + /// See the ZipEntry Version property for more details. + /// + public bool CanDecompressEntry { + get { + return (entry != null) && entry.CanDecompress; + } + } + + /// + /// Advances to the next entry in the archive + /// + /// + /// The next entry in the archive or null if there are no more entries. + /// + /// + /// If the previous entry is still open CloseEntry is called. + /// + /// + /// Input stream is closed + /// + /// + /// Password is not set, password is invalid, compression method is invalid, + /// version required to extract is not supported + /// + public ZipEntry GetNextEntry() + { + if (crc == null) { + throw new InvalidOperationException("Closed."); + } + + if (entry != null) { + CloseEntry(); + } + + int header = inputBuffer.ReadLeInt(); + + if (header == ZipConstants.CentralHeaderSignature || + header == ZipConstants.EndOfCentralDirectorySignature || + header == ZipConstants.CentralHeaderDigitalSignature || + header == ZipConstants.ArchiveExtraDataSignature || + header == ZipConstants.Zip64CentralFileHeaderSignature) { + // No more individual entries exist + Close(); + return null; + } + + // -jr- 07-Dec-2003 Ignore spanning temporary signatures if found + // Spanning signature is same as descriptor signature and is untested as yet. + if ( (header == ZipConstants.SpanningTempSignature) || (header == ZipConstants.SpanningSignature) ) { + header = inputBuffer.ReadLeInt(); + } + + if (header != ZipConstants.LocalHeaderSignature) { + throw new ZipException("Wrong Local header signature: 0x" + String.Format("{0:X}", header)); + } + + short versionRequiredToExtract = (short)inputBuffer.ReadLeShort(); + + flags = inputBuffer.ReadLeShort(); + method = inputBuffer.ReadLeShort(); + uint dostime = (uint)inputBuffer.ReadLeInt(); + int crc2 = inputBuffer.ReadLeInt(); + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + int nameLen = inputBuffer.ReadLeShort(); + int extraLen = inputBuffer.ReadLeShort(); + + bool isCrypted = (flags & 1) == 1; + + byte[] buffer = new byte[nameLen]; + inputBuffer.ReadRawBuffer(buffer); + + string name = ZipConstants.ConvertToStringExt(flags, buffer); + + entry = new ZipEntry(name, versionRequiredToExtract); + entry.Flags = flags; + + entry.CompressionMethod = (CompressionMethod)method; + + if ((flags & 8) == 0) { + entry.Crc = crc2 & 0xFFFFFFFFL; + entry.Size = size & 0xFFFFFFFFL; + entry.CompressedSize = csize & 0xFFFFFFFFL; + + entry.CryptoCheckValue = (byte)((crc2 >> 24) & 0xff); + + } else { + + // This allows for GNU, WinZip and possibly other archives, the PKZIP spec + // says these values are zero under these circumstances. + if (crc2 != 0) { + entry.Crc = crc2 & 0xFFFFFFFFL; + } + + if (size != 0) { + entry.Size = size & 0xFFFFFFFFL; + } + + if (csize != 0) { + entry.CompressedSize = csize & 0xFFFFFFFFL; + } + + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + entry.DosTime = dostime; + + // If local header requires Zip64 is true then the extended header should contain + // both values. + + // Handle extra data if present. This can set/alter some fields of the entry. + if (extraLen > 0) { + byte[] extra = new byte[extraLen]; + inputBuffer.ReadRawBuffer(extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(true); + if ( entry.CompressedSize >= 0 ) { + csize = entry.CompressedSize; + } + + if ( entry.Size >= 0 ) { + size = entry.Size; + } + + if (method == (int)CompressionMethod.Stored && (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size))) { + throw new ZipException("Stored, but compressed != uncompressed"); + } + + // Determine how to handle reading of data if this is attempted. + if (entry.IsCompressionMethodSupported()) { + internalReader = new ReadDataHandler(InitialRead); + } else { + internalReader = new ReadDataHandler(ReadingNotSupported); + } + + return entry; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + void ReadDataDescriptor() + { + if (inputBuffer.ReadLeInt() != ZipConstants.DataDescriptorSignature) { + throw new ZipException("Data descriptor signature not found"); + } + + entry.Crc = inputBuffer.ReadLeInt() & 0xFFFFFFFFL; + + if ( entry.LocalHeaderRequiresZip64 ) { + csize = inputBuffer.ReadLeLong(); + size = inputBuffer.ReadLeLong(); + } else { + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + } + entry.CompressedSize = csize; + entry.Size = size; + } + + /// + /// Complete cleanup as the final part of closing. + /// + /// True if the crc value should be tested + void CompleteCloseEntry(bool testCrc) + { + StopDecrypting(); + + if ((flags & 8) != 0) { + ReadDataDescriptor(); + } + + size = 0; + + if ( testCrc && + ((crc.Value & 0xFFFFFFFFL) != entry.Crc) && (entry.Crc != -1)) { + throw new ZipException("CRC mismatch"); + } + + crc.Reset(); + + if (method == (int)CompressionMethod.Deflated) { + inf.Reset(); + } + entry = null; + } + + /// + /// Closes the current zip entry and moves to the next one. + /// + /// + /// The stream is closed + /// + /// + /// The Zip stream ends early + /// + public void CloseEntry() + { + if (crc == null) { + throw new InvalidOperationException("Closed"); + } + + if (entry == null) { + return; + } + + if (method == (int)CompressionMethod.Deflated) { + if ((flags & 8) != 0) { + // We don't know how much we must skip, read until end. + byte[] tmp = new byte[4096]; + + // Read will close this entry + while (Read(tmp, 0, tmp.Length) > 0) { + } + return; + } + + csize -= inf.TotalIn; + inputBuffer.Available += inf.RemainingInput; + } + + if ( (inputBuffer.Available > csize) && (csize >= 0) ) { + inputBuffer.Available = (int)((long)inputBuffer.Available - csize); + } else { + csize -= inputBuffer.Available; + inputBuffer.Available = 0; + while (csize != 0) { + long skipped = base.Skip(csize); + + if (skipped <= 0) { + throw new ZipException("Zip archive ends early."); + } + + csize -= skipped; + } + } + + CompleteCloseEntry(false); + } + + /// + /// Returns 1 if there is an entry available + /// Otherwise returns 0. + /// + public override int Available { + get { + return entry != null ? 1 : 0; + } + } + + /// + /// Returns the current size that can be read from the current entry if available + /// + /// Thrown if the entry size is not known. + /// Thrown if no entry is currently available. + public override long Length + { + get { + if ( entry != null ) { + if ( entry.Size >= 0 ) { + return entry.Size; + } else { + throw new ZipException("Length not available for the current entry"); + } + } + else { + throw new InvalidOperationException("No current entry"); + } + } + + } + + /// + /// Reads a byte from the current zip entry. + /// + /// + /// The byte or -1 if end of stream is reached. + /// + public override int ReadByte() + { + byte[] b = new byte[1]; + if (Read(b, 0, 1) <= 0) { + return -1; + } + return b[0] & 0xff; + } + + /// + /// Handle attempts to read by throwing an . + /// + /// The destination array to store data in. + /// The offset at which data read should be stored. + /// The maximum number of bytes to read. + /// Returns the number of bytes actually read. + int ReadingNotAvailable(byte[] destination, int offset, int count) + { + throw new InvalidOperationException("Unable to read from this stream"); + } + + /// + /// Handle attempts to read from this entry by throwing an exception + /// + int ReadingNotSupported(byte[] destination, int offset, int count) + { + throw new ZipException("The compression method for this entry is not supported"); + } + + /// + /// Perform the initial read on an entry which may include + /// reading encryption headers and setting up inflation. + /// + /// The destination to fill with data read. + /// The offset to start reading at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. + int InitialRead(byte[] destination, int offset, int count) + { + if ( !CanDecompressEntry ) { + throw new ZipException("Library cannot extract this entry. Version required is (" + entry.Version.ToString() + ")"); + } + + // Handle encryption if required. + if (entry.IsCrypted) { +#if NETCF_1_0 + throw new ZipException("Encryption not supported for Compact Framework 1.0"); +#else + if (password == null) { + throw new ZipException("No password set."); + } + + // Generate and set crypto transform... + PkzipClassicManaged managed = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(password)); + + inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null); + + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize); + + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) { + throw new ZipException("Invalid password"); + } + + if (csize >= ZipConstants.CryptoHeaderSize) { + csize -= ZipConstants.CryptoHeaderSize; + } + else if ( (entry.Flags & (int)GeneralBitFlags.Descriptor) == 0 ) { + throw new ZipException(string.Format("Entry compressed size {0} too small for encryption", csize)); + } +#endif + } else { +#if !NETCF_1_0 + inputBuffer.CryptoTransform = null; +#endif + } + + if ((csize > 0) || ((flags & (int)GeneralBitFlags.Descriptor) != 0)) { + if ((method == (int)CompressionMethod.Deflated) && (inputBuffer.Available > 0)) { + inputBuffer.SetInflaterInput(inf); + } + + internalReader = new ReadDataHandler(BodyRead); + return BodyRead(destination, offset, count); + } + else { + internalReader = new ReadDataHandler(ReadingNotAvailable); + return 0; + } + } + + /// + /// Read a block of bytes from the stream. + /// + /// The destination for the bytes. + /// The index to start storing data. + /// The number of bytes to attempt to read. + /// Returns the number of bytes read. + /// Zero bytes read means end of stream. + public override int Read(byte[] buffer, int offset, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if ( (buffer.Length - offset) < count ) { + throw new ArgumentException("Invalid offset/count combination"); + } + + return internalReader(buffer, offset, count); + } + + /// + /// Reads a block of bytes from the current zip entry. + /// + /// + /// The number of bytes read (this may be less than the length requested, even before the end of stream), or 0 on end of stream. + /// + /// + /// An i/o error occured. + /// + /// + /// The deflated stream is corrupted. + /// + /// + /// The stream is not open. + /// + int BodyRead(byte[] buffer, int offset, int count) + { + if ( crc == null ) { + throw new InvalidOperationException("Closed"); + } + + if ( (entry == null) || (count <= 0) ) { + return 0; + } + + if ( offset + count > buffer.Length ) { + throw new ArgumentException("Offset + count exceeds buffer size"); + } + + bool finished = false; + + switch (method) { + case (int)CompressionMethod.Deflated: + count = base.Read(buffer, offset, count); + if (count <= 0) { + if (!inf.IsFinished) { + throw new ZipException("Inflater not finished!"); + } + inputBuffer.Available = inf.RemainingInput; + + // A csize of -1 is from an unpatched local header + if ((flags & 8) == 0 && + (inf.TotalIn != csize && csize != 0xFFFFFFFF && csize != -1 || inf.TotalOut != size)) { + throw new ZipException("Size mismatch: " + csize + ";" + size + " <-> " + inf.TotalIn + ";" + inf.TotalOut); + } + inf.Reset(); + finished = true; + } + break; + + case (int)CompressionMethod.Stored: + if ( (count > csize) && (csize >= 0) ) { + count = (int)csize; + } + + if ( count > 0 ) { + count = inputBuffer.ReadClearTextBuffer(buffer, offset, count); + if (count > 0) { + csize -= count; + size -= count; + } + } + + if (csize == 0) { + finished = true; + } else { + if (count < 0) { + throw new ZipException("EOF in stored block"); + } + } + break; + } + + if (count > 0) { + crc.Update(buffer, offset, count); + } + + if (finished) { + CompleteCloseEntry(true); + } + + return count; + } + + /// + /// Closes the zip input stream + /// + public override void Close() + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + crc = null; + entry = null; + + base.Close(); + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs new file mode 100644 index 000000000..916f4b2e7 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs @@ -0,0 +1,269 @@ +// ZipNameTransform.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + + +using System; +using System.IO; +using System.Text; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// ZipNameTransform transforms names as per the Zip file naming convention. + /// + /// The use of absolute names is supported although its use is not valid + /// according to Zip naming conventions, and should not be used if maximum compatability is desired. + public class ZipNameTransform : INameTransform + { + #region Constructors + /// + /// Initialize a new instance of + /// + public ZipNameTransform() + { + } + + /// + /// Initialize a new instance of + /// + /// The string to trim from the front of paths if found. + public ZipNameTransform(string trimPrefix) + { + TrimPrefix = trimPrefix; + } + #endregion + + /// + /// Static constructor. + /// + static ZipNameTransform() + { + char[] invalidPathChars; +#if NET_1_0 || NET_1_1 || NETCF_1_0 + invalidPathChars = Path.InvalidPathChars; +#else + invalidPathChars = Path.GetInvalidPathChars(); +#endif + int howMany = invalidPathChars.Length + 2; + + InvalidEntryCharsRelaxed = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryCharsRelaxed, 0, invalidPathChars.Length); + InvalidEntryCharsRelaxed[howMany - 1] = '*'; + InvalidEntryCharsRelaxed[howMany - 2] = '?'; + + howMany = invalidPathChars.Length + 4; + InvalidEntryChars = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryChars, 0, invalidPathChars.Length); + InvalidEntryChars[howMany - 1] = ':'; + InvalidEntryChars[howMany - 2] = '\\'; + InvalidEntryChars[howMany - 3] = '*'; + InvalidEntryChars[howMany - 4] = '?'; + } + + /// + /// Transform a windows directory name according to the Zip file naming conventions. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) { + if ( !name.EndsWith("/") ) { + name += "/"; + } + } + else { + throw new ZipException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a windows file name according to the Zip file naming conventions. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) { + string lowerName = name.ToLower(); + if ( (trimPrefix_ != null) && (lowerName.IndexOf(trimPrefix_) == 0) ) { + name = name.Substring(trimPrefix_.Length); + } + + name = name.Replace(@"\", "/"); + name = WindowsPathUtils.DropPathRoot(name); + + // Drop any leading slashes. + while ((name.Length > 0) && (name[0] == '/')) + { + name = name.Remove(0, 1); + } + + // Drop any trailing slashes. + while ((name.Length > 0) && (name[name.Length - 1] == '/')) + { + name = name.Remove(name.Length - 1, 1); + } + + // Convert consecutive // characters to / + int index = name.IndexOf("//"); + while (index >= 0) + { + name = name.Remove(index, 1); + index = name.IndexOf("//"); + } + + name = MakeValidName(name, '_'); + } + else { + name = string.Empty; + } + return name; + } + + /// + /// Get/set the path prefix to be trimmed from paths if present. + /// + /// The prefix is trimmed before any conversion from + /// a windows path is done. + public string TrimPrefix + { + get { return trimPrefix_; } + set { + trimPrefix_ = value; + if (trimPrefix_ != null) { + trimPrefix_ = trimPrefix_.ToLower(); + } + } + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to force valid + /// The replacement character to use. + /// Returns a valid name + static string MakeValidName(string name, char replacement) + { + int index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) { + StringBuilder builder = new StringBuilder(name); + + while (index >= 0 ) { + builder[index] = replacement; + + if (index >= name.Length) { + index = -1; + } + else { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + if (name.Length > 0xffff) { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// If true checking is relaxed about windows file names and absolute paths. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in Unix format, and should only contain relative paths. + /// This means that any path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid for a file where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name, bool relaxed) + { + bool result = (name != null); + + if ( result ) { + if ( relaxed ) { + result = name.IndexOfAny(InvalidEntryCharsRelaxed) < 0; + } + else { + result = + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0); + } + } + + return result; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in unix format, + /// and should only contain relative paths if a path is present. + /// This means that the path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0) + ; + return result; + } + + #region Instance Fields + string trimPrefix_; + #endregion + + #region Class Fields + static readonly char[] InvalidEntryChars; + static readonly char[] InvalidEntryCharsRelaxed; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs new file mode 100644 index 000000000..a5cb5bd20 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs @@ -0,0 +1,900 @@ +// ZipOutputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 Z-1649 Added AES support +// 22-02-2010 Z-1648 Zero byte entries would create invalid zip files + +using System; +using System.IO; +using System.Collections; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is a DeflaterOutputStream that writes the files into a zip + /// archive one after another. It has a special method to start a new + /// zip entry. The zip entries contains information about the file name + /// size, compressed size, CRC, etc. + /// + /// It includes support for Stored and Deflated entries. + /// This class is not thread safe. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// This sample shows how to create a zip file + /// + /// using System; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Core; + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// string[] filenames = Directory.GetFiles(args[0]); + /// byte[] buffer = new byte[4096]; + /// + /// using ( ZipOutputStream s = new ZipOutputStream(File.Create(args[1])) ) { + /// + /// s.SetLevel(9); // 0 - store only to 9 - means best compression + /// + /// foreach (string file in filenames) { + /// ZipEntry entry = new ZipEntry(file); + /// s.PutNextEntry(entry); + /// + /// using (FileStream fs = File.OpenRead(file)) { + /// StreamUtils.Copy(fs, s, buffer); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipOutputStream : DeflaterOutputStream + { + #region Constructors + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// + /// The output stream to which the archive contents are written. + /// + public ZipOutputStream(Stream baseOutputStream) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true)) + { + } + + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// The output stream to which the archive contents are written. + /// Size of the buffer to use. + public ZipOutputStream( Stream baseOutputStream, int bufferSize ) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true), bufferSize) + { + } + #endregion + + /// + /// Gets a flag value of true if the central header has been added for this archive; false if it has not been added. + /// + /// No further entries can be added once this has been done. + public bool IsFinished + { + get { + return entries == null; + } + } + + /// + /// Set the zip file comment. + /// + /// + /// The comment text for the entire archive. + /// + /// + /// The converted comment is longer than 0xffff bytes. + /// + public void SetComment(string comment) + { + // TODO: Its not yet clear how to handle unicode comments here. + byte[] commentBytes = ZipConstants.ConvertToArray(comment); + if (commentBytes.Length > 0xffff) { + throw new ArgumentOutOfRangeException("comment"); + } + zipComment = commentBytes; + } + + /// + /// Sets the compression level. The new level will be activated + /// immediately. + /// + /// The new compression level (1 to 9). + /// + /// Level specified is not supported. + /// + /// + public void SetLevel(int level) + { + deflater_.SetLevel(level); + defaultCompressionLevel = level; + } + + /// + /// Get the current deflater compression level + /// + /// The current compression level + public int GetLevel() + { + return deflater_.GetLevel(); + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + /// Older archivers may not understand Zip64 extensions. + /// If backwards compatability is an issue be careful when adding entries to an archive. + /// Setting this property to off is workable but less desirable as in those circumstances adding a file + /// larger then 4GB will fail. + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Write an unsigned short in little endian byte order. + /// + private void WriteLeShort(int value) + { + unchecked { + baseOutputStream_.WriteByte((byte)(value & 0xff)); + baseOutputStream_.WriteByte((byte)((value >> 8) & 0xff)); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeInt(int value) + { + unchecked { + WriteLeShort(value); + WriteLeShort(value >> 16); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeLong(long value) + { + unchecked { + WriteLeInt((int)value); + WriteLeInt((int)(value >> 32)); + } + } + + /// + /// Starts a new Zip entry. It automatically closes the previous + /// entry if present. + /// All entry elements bar name are optional, but must be correct if present. + /// If the compression method is stored and the output is not patchable + /// the compression for that entry is automatically changed to deflate level 0 + /// + /// + /// the entry. + /// + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occured. + /// + /// + /// if stream was finished + /// + /// + /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ public void PutNextEntry(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + if (entries == null) { + throw new InvalidOperationException("ZipOutputStream was finished"); + } + + if (curEntry != null) { + CloseEntry(); + } + + if (entries.Count == int.MaxValue) { + throw new ZipException("Too many entries for Zip file"); + } + + CompressionMethod method = entry.CompressionMethod; + int compressionLevel = defaultCompressionLevel; + + // Clear flags that the library manages internally + entry.Flags &= (int)GeneralBitFlags.UnicodeText; + patchEntryHeader = false; + + bool headerInfoAvailable; + + // No need to compress - definitely no data. + if (entry.Size == 0) + { + entry.CompressedSize = entry.Size; + entry.Crc = 0; + method = CompressionMethod.Stored; + headerInfoAvailable = true; + } + else + { + headerInfoAvailable = (entry.Size >= 0) && entry.HasCrc; + + // Switch to deflation if storing isnt possible. + if (method == CompressionMethod.Stored) + { + if (!headerInfoAvailable) + { + if (!CanPatchEntries) + { + // Can't patch entries so storing is not possible. + method = CompressionMethod.Deflated; + compressionLevel = 0; + } + } + else // entry.size must be > 0 + { + entry.CompressedSize = entry.Size; + headerInfoAvailable = entry.HasCrc; + } + } + } + + if (headerInfoAvailable == false) { + if (CanPatchEntries == false) { + // Only way to record size and compressed size is to append a data descriptor + // after compressed data. + + // Stored entries of this form have already been converted to deflating. + entry.Flags |= 8; + } else { + patchEntryHeader = true; + } + } + + if (Password != null) { + entry.IsCrypted = true; + if (entry.Crc < 0) { + // Need to append a data descriptor as the crc isnt available for use + // with encryption, the date is used instead. Setting the flag + // indicates this to the decompressor. + entry.Flags |= 8; + } + } + + entry.Offset = offset; + entry.CompressionMethod = (CompressionMethod)method; + + curMethod = method; + sizePatchPos = -1; + + if ( (useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic)) ) { + entry.ForceZip64(); + } + + // Write the local file header + WriteLeInt(ZipConstants.LocalHeaderSignature); + + WriteLeShort(entry.Version); + WriteLeShort(entry.Flags); + WriteLeShort((byte)entry.CompressionMethodForHeader); + WriteLeInt((int)entry.DosTime); + + // TODO: Refactor header writing. Its done in several places. + if (headerInfoAvailable == true) { + WriteLeInt((int)entry.Crc); + if ( entry.LocalHeaderRequiresZip64 ) { + WriteLeInt(-1); + WriteLeInt(-1); + } + else { + WriteLeInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + WriteLeInt((int)entry.Size); + } + } else { + if (patchEntryHeader) { + crcPatchPos = baseOutputStream_.Position; + } + WriteLeInt(0); // Crc + + if ( patchEntryHeader ) { + sizePatchPos = baseOutputStream_.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if ( entry.LocalHeaderRequiresZip64 || patchEntryHeader ) { + WriteLeInt(-1); + WriteLeInt(-1); + } + else { + WriteLeInt(0); // Compressed size + WriteLeInt(0); // Uncompressed size + } + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64) { + ed.StartNewEntry(); + if (headerInfoAvailable) { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + } + else { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cant find extra data"); + } + + if ( patchEntryHeader ) { + sizePatchPos = ed.CurrentReadIndex; + } + } + else { + ed.Delete(1); + } + +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + AddExtraDataAES(entry, ed); + } +#endif + byte[] extra = ed.GetEntryData(); + + WriteLeShort(name.Length); + WriteLeShort(extra.Length); + + if ( name.Length > 0 ) { + baseOutputStream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + sizePatchPos += baseOutputStream_.Position; + } + + if ( extra.Length > 0 ) { + baseOutputStream_.Write(extra, 0, extra.Length); + } + + offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; + // Fix offsetOfCentraldir for AES + if (entry.AESKeySize > 0) + offset += entry.AESOverheadSize; + + // Activate the entry. + curEntry = entry; + crc.Reset(); + if (method == CompressionMethod.Deflated) { + deflater_.Reset(); + deflater_.SetLevel(compressionLevel); + } + size = 0; + + if (entry.IsCrypted) { +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + WriteAESHeader(entry); + } else +#endif + { + if (entry.Crc < 0) { // so testing Zip will says its ok + WriteEncryptionHeader(entry.DosTime << 16); + } else { + WriteEncryptionHeader(entry.Crc); + } + } + } + } + + /// + /// Closes the current entry, updating header and footer information as required + /// + /// + /// An I/O error occurs. + /// + /// + /// No entry is active. + /// + public void CloseEntry() + { + if (curEntry == null) { + throw new InvalidOperationException("No open entry"); + } + + long csize = size; + + // First finish the deflater, if appropriate + if (curMethod == CompressionMethod.Deflated) { + if (size >= 0) { + base.Finish(); + csize = deflater_.TotalOut; + } + else { + deflater_.Reset(); + } + } + + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + if (curEntry.AESKeySize > 0) { + baseOutputStream_.Write(AESAuthCode, 0, 10); + } + + if (curEntry.Size < 0) { + curEntry.Size = size; + } else if (curEntry.Size != size) { + throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); + } + + if (curEntry.CompressedSize < 0) { + curEntry.CompressedSize = csize; + } else if (curEntry.CompressedSize != csize) { + throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); + } + + if (curEntry.Crc < 0) { + curEntry.Crc = crc.Value; + } else if (curEntry.Crc != crc.Value) { + throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); + } + + offset += csize; + + if (curEntry.IsCrypted) { + if (curEntry.AESKeySize > 0) { + curEntry.CompressedSize += curEntry.AESOverheadSize; + + } else { + curEntry.CompressedSize += ZipConstants.CryptoHeaderSize; + } + } + + // Patch the header if possible + if (patchEntryHeader) { + patchEntryHeader = false; + + long curPos = baseOutputStream_.Position; + baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); + WriteLeInt((int)curEntry.Crc); + + if ( curEntry.LocalHeaderRequiresZip64 ) { + + if ( sizePatchPos == -1 ) { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); + WriteLeLong(curEntry.Size); + WriteLeLong(curEntry.CompressedSize); + } + else { + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + } + baseOutputStream_.Seek(curPos, SeekOrigin.Begin); + } + + // Add data descriptor if flagged as required + if ((curEntry.Flags & 8) != 0) { + WriteLeInt(ZipConstants.DataDescriptorSignature); + WriteLeInt(unchecked((int)curEntry.Crc)); + + if ( curEntry.LocalHeaderRequiresZip64 ) { + WriteLeLong(curEntry.CompressedSize); + WriteLeLong(curEntry.Size); + offset += ZipConstants.Zip64DataDescriptorSize; + } + else { + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + offset += ZipConstants.DataDescriptorSize; + } + } + + entries.Add(curEntry); + curEntry = null; + } + + void WriteEncryptionHeader(long crcValue) + { + offset += ZipConstants.CryptoHeaderSize; + + InitializePassword(Password); + + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + Random rnd = new Random(); + rnd.NextBytes(cryptBuffer); + cryptBuffer[11] = (byte)(crcValue >> 24); + + EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); + } + +#if !NET_1_1 && !NETCF_2_0 + private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) { + + // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. + const int VENDOR_VERSION = 2; + // Vendor ID is the two ASCII characters "AE". + const int VENDOR_ID = 0x4541; //not 6965; + extraData.StartNewEntry(); + // Pack AES extra data field see http://www.winzip.com/aes_info.htm + //extraData.AddLeShort(7); // Data size (currently 7) + extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 + extraData.AddLeShort(VENDOR_ID); // "AE" + extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 + extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file + extraData.AddNewEntry(0x9901); + } + + // Replaces WriteEncryptionHeader for AES + // + private void WriteAESHeader(ZipEntry entry) { + byte[] salt; + byte[] pwdVerifier; + InitializeAESPassword(entry, Password, out salt, out pwdVerifier); + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + baseOutputStream_.Write(salt, 0, salt.Length); + baseOutputStream_.Write(pwdVerifier, 0, pwdVerifier.Length); + } +#endif + + /// + /// Writes the given buffer to the current entry. + /// + /// The buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Archive size is invalid + /// No entry is active. + public override void Write(byte[] buffer, int offset, int count) + { + if (curEntry == null) { + throw new InvalidOperationException("No open entry."); + } + + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if ( (buffer.Length - offset) < count ) { + throw new ArgumentException("Invalid offset/count combination"); + } + + crc.Update(buffer, offset, count); + size += count; + + switch (curMethod) { + case CompressionMethod.Deflated: + base.Write(buffer, offset, count); + break; + + case CompressionMethod.Stored: + if (Password != null) { + CopyAndEncrypt(buffer, offset, count); + } else { + baseOutputStream_.Write(buffer, offset, count); + } + break; + } + } + + void CopyAndEncrypt(byte[] buffer, int offset, int count) + { + const int CopyBufferSize = 4096; + byte[] localBuffer = new byte[CopyBufferSize]; + while ( count > 0 ) { + int bufferCount = (count < CopyBufferSize) ? count : CopyBufferSize; + + Array.Copy(buffer, offset, localBuffer, 0, bufferCount); + EncryptBlock(localBuffer, 0, bufferCount); + baseOutputStream_.Write(localBuffer, 0, bufferCount); + count -= bufferCount; + offset += bufferCount; + } + } + + /// + /// Finishes the stream. This will write the central directory at the + /// end of the zip file and flush the stream. + /// + /// + /// This is automatically called when the stream is closed. + /// + /// + /// An I/O error occurs. + /// + /// + /// Comment exceeds the maximum length
+ /// Entry name exceeds the maximum length + ///
+ public override void Finish() + { + if (entries == null) { + return; + } + + if (curEntry != null) { + CloseEntry(); + } + + long numEntries = entries.Count; + long sizeEntries = 0; + + foreach (ZipEntry entry in entries) { + WriteLeInt(ZipConstants.CentralHeaderSignature); + WriteLeShort(ZipConstants.VersionMadeBy); + WriteLeShort(entry.Version); + WriteLeShort(entry.Flags); + WriteLeShort((short)entry.CompressionMethodForHeader); + WriteLeInt((int)entry.DosTime); + WriteLeInt((int)entry.Crc); + + if ( entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue) ) + { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.CompressedSize); + } + + if ( entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue) ) + { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xffff) { + throw new ZipException("Name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.CentralHeaderRequiresZip64 ) { + ed.StartNewEntry(); + if ( entry.IsZip64Forced() || + (entry.Size >= 0xffffffff) ) + { + ed.AddLeLong(entry.Size); + } + + if ( entry.IsZip64Forced() || + (entry.CompressedSize >= 0xffffffff) ) + { + ed.AddLeLong(entry.CompressedSize); + } + + if ( entry.Offset >= 0xffffffff ) + { + ed.AddLeLong(entry.Offset); + } + + ed.AddNewEntry(1); + } + else { + ed.Delete(1); + } + +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + AddExtraDataAES(entry, ed); + } +#endif + byte[] extra = ed.GetEntryData(); + + byte[] entryComment = + (entry.Comment != null) ? + ZipConstants.ConvertToArray(entry.Flags, entry.Comment) : + new byte[0]; + + if (entryComment.Length > 0xffff) { + throw new ZipException("Comment too long."); + } + + WriteLeShort(name.Length); + WriteLeShort(extra.Length); + WriteLeShort(entryComment.Length); + WriteLeShort(0); // disk number + WriteLeShort(0); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) { + WriteLeInt(entry.ExternalFileAttributes); + } else { + if (entry.IsDirectory) { // mark entry as directory (from nikolam.AT.perfectinfo.com) + WriteLeInt(16); + } else { + WriteLeInt(0); + } + } + + if ( entry.Offset >= uint.MaxValue ) { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.Offset); + } + + if ( name.Length > 0 ) { + baseOutputStream_.Write(name, 0, name.Length); + } + + if ( extra.Length > 0 ) { + baseOutputStream_.Write(extra, 0, extra.Length); + } + + if ( entryComment.Length > 0 ) { + baseOutputStream_.Write(entryComment, 0, entryComment.Length); + } + + sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } + + using ( ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_) ) { + zhs.WriteEndOfCentralDirectory(numEntries, sizeEntries, offset, zipComment); + } + + entries = null; + } + + #region Instance Fields + /// + /// The entries for the archive. + /// + ArrayList entries = new ArrayList(); + + /// + /// Used to track the crc of data added to entries. + /// + Crc32 crc = new Crc32(); + + /// + /// The current entry being added. + /// + ZipEntry curEntry; + + int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION; + + CompressionMethod curMethod = CompressionMethod.Deflated; + + /// + /// Used to track the size of data for an entry during writing. + /// + long size; + + /// + /// Offset to be recorded for each entry in the central header. + /// + long offset; + + /// + /// Comment for the entire archive recorded in central header. + /// + byte[] zipComment = new byte[0]; + + /// + /// Flag indicating that header patching is required for the current entry. + /// + bool patchEntryHeader; + + /// + /// Position to patch crc + /// + long crcPatchPos = -1; + + /// + /// Position to patch size. + /// + long sizePatchPos = -1; + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // NOTE: Setting the size for entries before they are added is the best solution! + UseZip64 useZip64_ = UseZip64.Dynamic; + #endregion + } +} diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index abe89bbd9..d42a9d1ff 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -1,11 +1,286 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; namespace GitHub.Unity { - class ActionTask : TaskBase + public class TaskQueue : TPLTask + { + private TaskCompletionSource aggregateTask = new TaskCompletionSource(); + private readonly List queuedTasks = new List(); + private int finishedTaskCount; + + public TaskQueue() : base() + { + Initialize(aggregateTask.Task); + } + + public ITask Queue(ITask task) + { + // if this task fails, both OnEnd and Catch will be called + // if a task before this one on the chain fails, only Catch will be called + // so avoid calling TaskFinished twice by ignoring failed OnEnd calls + task.OnEnd += InvokeFinishOnlyOnSuccess; + task.Catch(e => TaskFinished(false, e)); + queuedTasks.Add(task); + return this; + } + + public override void RunSynchronously() + { + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + } + else + { + aggregateTask.TrySetResult(true); + } + + base.RunSynchronously(); + } + + protected override void Schedule() + { + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + } + else + { + aggregateTask.TrySetResult(true); + } + + base.Schedule(); + } + + private void InvokeFinishOnlyOnSuccess(ITask task, bool success, Exception ex) + { + if (success) + TaskFinished(true, null); + } + + private void TaskFinished(bool success, Exception ex) + { + var count = Interlocked.Increment(ref finishedTaskCount); + if (count == queuedTasks.Count) + { + var exceptions = queuedTasks.Where(x => !x.Successful).Select(x => x.Exception).ToArray(); + var isSuccessful = exceptions.Length == 0; + + if (isSuccessful) + { + aggregateTask.TrySetResult(true); + } + else + { + aggregateTask.TrySetException(new AggregateException(exceptions)); + } + } + } + } + + public class TaskQueue : TPLTask> + { + private TaskCompletionSource> aggregateTask = new TaskCompletionSource>(); + private readonly List> queuedTasks = new List>(); + private int finishedTaskCount; + private Func, TResult> resultConverter; + + /// + /// If is not assignable to , you must pass a + /// method to convert between the two. Implicit conversions don't count (so even though NPath has an implicit + /// conversion to string, you still need to pass in a converter) + /// + /// + public TaskQueue(Func, TResult> resultConverter = null) : base() + { + // this excludes implicit operators - that requires using reflection to figure out if + // the types are convertible, and I'd rather not do that + if (resultConverter == null && !typeof(TResult).IsAssignableFrom(typeof(TTaskResult))) + { + throw new ArgumentNullException(nameof(resultConverter), + String.Format(CultureInfo.InvariantCulture, "Cannot cast {0} to {1} and no {2} method was passed in to do the conversion", typeof(TTaskResult), typeof(TResult), nameof(resultConverter))); + } + this.resultConverter = resultConverter; + Initialize(aggregateTask.Task); + } + + /// + /// Queues an ITask for running, and when the task is done, is called + /// to convert the result of the task to something else + /// + /// + /// + /// + public ITask Queue(ITask task) + { + // if this task fails, both OnEnd and Catch will be called + // if a task before this one on the chain fails, only Catch will be called + // so avoid calling TaskFinished twice by ignoring failed OnEnd calls + task.OnEnd += InvokeFinishOnlyOnSuccess; + task.Catch(e => TaskFinished(default(TTaskResult), false, e)); + queuedTasks.Add(task); + return task; + } + + public override List RunSynchronously() + { + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + } + else + { + aggregateTask.TrySetResult(new List()); + } + + return base.RunSynchronously(); + } + + protected override void Schedule() + { + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + } + else + { + aggregateTask.TrySetResult(new List()); + } + + base.Schedule(); + } + + private void InvokeFinishOnlyOnSuccess(ITask task, TTaskResult result, bool success, Exception ex) + { + if (success) + TaskFinished(result, true, null); + } + + private void TaskFinished(TTaskResult result, bool success, Exception ex) + { + var count = Interlocked.Increment(ref finishedTaskCount); + if (count == queuedTasks.Count) + { + var exceptions = queuedTasks.Where(x => !x.Successful).Select(x => x.Exception).ToArray(); + var isSuccessful = exceptions.Length == 0; + + if (isSuccessful) + { + List results; + if (resultConverter != null) + results = queuedTasks.Select(x => resultConverter(x)).ToList(); + else + results = queuedTasks.Select(x => (TResult)(object)x.Result).ToList(); + aggregateTask.TrySetResult(results); + } + else + { + aggregateTask.TrySetException(new AggregateException(exceptions)); + } + } + } + } + + public class TPLTask : TaskBase + { + private Task task; + + protected TPLTask() : base() + {} + + public TPLTask(Task task) + : base() + { + Initialize(task); + } + + protected void Initialize(Task theTask) + { + this.task = theTask; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); + } + + protected override void Run(bool success) + { + base.Run(success); + + Token.ThrowIfCancellationRequested(); + try + { + if (task.Status == TaskStatus.Created && !task.IsCompleted && + ((task.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) + { + var scheduler = TaskManager.GetScheduler(Affinity); + Token.ThrowIfCancellationRequested(); + task.RunSynchronously(scheduler); + } + else + task.Wait(); + } + catch (Exception ex) + { + if (!RaiseFaultHandlers(ex)) + throw exception; + Token.ThrowIfCancellationRequested(); + } + } + } + + public class TPLTask : TaskBase + { + private Task task; + + protected TPLTask() : base() + { } + + public TPLTask(Task task) + : base() + { + Initialize(task); + } + + protected void Initialize(Task theTask) + { + this.task = theTask; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); + } + + protected override T RunWithReturn(bool success) + { + var ret = base.RunWithReturn(success); + + Token.ThrowIfCancellationRequested(); + try + { + if (task.Status == TaskStatus.Created && !task.IsCompleted && + ((task.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) + { + var scheduler = TaskManager.GetScheduler(Affinity); + Token.ThrowIfCancellationRequested(); + task.RunSynchronously(scheduler); + } + ret = task.Result; + } + catch (Exception ex) + { + if (!RaiseFaultHandlers(ex)) + throw exception; + Token.ThrowIfCancellationRequested(); + } + return ret; + } + } + + public class ActionTask : TaskBase { protected Action Callback { get; } protected Action CallbackWithException { get; } @@ -34,17 +309,9 @@ public ActionTask(CancellationToken token, Action action) Name = "ActionTask"; } - public ActionTask(Task task) - : base(task) - { - Name = "ActionTask(Task)"; - } - protected override void Run(bool success) { base.Run(success); - - RaiseOnStart(); try { Callback?.Invoke(success); @@ -56,19 +323,16 @@ protected override void Run(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); + throw exception; } } } - class ActionTask : TaskBase + public class ActionTask : TaskBase { + private readonly Func getPreviousResult; + protected Action Callback { get; } protected Action CallbackWithException { get; } @@ -83,12 +347,8 @@ public ActionTask(CancellationToken token, Action action, Func getPr { Guard.ArgumentNotNull(action, "action"); this.Callback = action; - Task = new Task(() => Run(DependsOn?.Successful ?? true, - // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task - // otherwise if there's a method to retrieve the value, call that - // otherwise use the PreviousResult property - (DependsOn?.Successful ?? false) ? ((ITask)DependsOn).Result : getPreviousResult != null ? getPreviousResult() : PreviousResult), - Token, TaskCreationOptions.None); + this.getPreviousResult = getPreviousResult; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); Name = $"ActionTask<{typeof(T)}>"; } @@ -103,27 +363,39 @@ public ActionTask(CancellationToken token, Action action, Fu { Guard.ArgumentNotNull(action, "action"); this.CallbackWithException = action; - Task = new Task(() => Run(DependsOn?.Successful ?? true, - // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task - // otherwise if there's a method to retrieve the value, call that - // otherwise use the PreviousResult property - (DependsOn?.Successful ?? false) ? ((ITask)DependsOn).Result : getPreviousResult != null ? getPreviousResult() : PreviousResult), - Token, TaskCreationOptions.None); + this.getPreviousResult = getPreviousResult; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); Name = $"ActionTask"; } - public ActionTask(Task task) - : base(task) + public override void RunSynchronously() { - Name = $"ActionTask<{typeof(T)}>(Task)"; + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task + // otherwise if there's a method to retrieve the value, call that + // otherwise use the PreviousResult property + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); + + try + { + Run(previousIsSuccessful, prevResult); + } + finally + { + RaiseOnEnd(); + } } protected virtual void Run(bool success, T previousResult) { base.Run(success); - - RaiseOnStart(); - try { Callback?.Invoke(success, previousResult); @@ -135,20 +407,15 @@ protected virtual void Run(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); + throw exception; } } public T PreviousResult { get; set; } = default(T); } - class FuncTask : TaskBase + public class FuncTask : TaskBase { protected Func Callback { get; } protected Func CallbackWithException { get; } @@ -177,18 +444,9 @@ public FuncTask(CancellationToken token, Func action) Name = $"FuncTask"; } - public FuncTask(Task task) - : base(task) - { - Name = $"FuncTask<{typeof(T)}>(Task)"; - } - protected override T RunWithReturn(bool success) { T result = base.RunWithReturn(success); - - RaiseOnStart(); - try { if (Callback != null) @@ -203,53 +461,37 @@ protected override T RunWithReturn(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(result); + throw exception; } - return result; } } - class FuncTask : TaskBase + public class FuncTask : TaskBase { protected Func Callback { get; } protected Func CallbackWithException { get; } - public FuncTask(CancellationToken token, Func action) - : base(token) + public FuncTask(CancellationToken token, Func action, Func getPreviousResult = null) + : base(token, getPreviousResult) { Guard.ArgumentNotNull(action, "action"); this.Callback = action; Name = $"FuncTask<{typeof(T)}, {typeof(TResult)}>"; } - public FuncTask(CancellationToken token, Func action) - : base(token) + public FuncTask(CancellationToken token, Func action, Func getPreviousResult = null) + : base(token, getPreviousResult) { Guard.ArgumentNotNull(action, "action"); this.CallbackWithException = action; Name = $"FuncTask<{typeof(T)}, Exception, {typeof(TResult)}>"; } - - public FuncTask(Task task) - : base(task) - { - Name = $"FuncTask<{typeof(T)}, {typeof(TResult)}>(Task)"; - } - protected override TResult RunWithData(bool success, T previousResult) { var result = base.RunWithData(success, previousResult); - - RaiseOnStart(); - try { if (Callback != null) @@ -264,20 +506,14 @@ protected override TResult RunWithData(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } - finally - { - RaiseOnEnd(result); - } - return result; } } - class FuncListTask : DataTaskBase> + public class FuncListTask : DataTaskBase> { protected Func> Callback { get; } protected Func, List> CallbackWithSelf { get; } @@ -304,16 +540,9 @@ public FuncListTask(CancellationToken token, Func, List this.CallbackWithSelf = action; } - public FuncListTask(Task> task) - : base(task) - { } - protected override List RunWithReturn(bool success) { var result = base.RunWithReturn(success); - - RaiseOnStart(); - try { if (Callback != null) @@ -330,31 +559,21 @@ protected override List RunWithReturn(bool success) result = CallbackWithException(success, thrown); } } - catch (AggregateException ex) - { - var e = ex.GetBaseException(); - Errors = e.Message; - if (!RaiseFaultHandlers(e)) - throw e; - } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } finally { if (result == null) result = new List(); - - RaiseOnEnd(result); } return result; } } - class FuncListTask : DataTaskBase> + public class FuncListTask : DataTaskBase> { protected Func> Callback { get; } protected Func> CallbackWithException { get; } @@ -373,16 +592,9 @@ public FuncListTask(CancellationToken token, Func> task) - : base(task) - { } - protected override List RunWithData(bool success, T previousResult) { var result = base.RunWithData(success, previousResult); - - RaiseOnStart(); - try { if (Callback != null) @@ -397,17 +609,10 @@ protected override List RunWithData(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(result); + throw exception; } - return result; } } - -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/BaseOutputProcessor.cs b/src/GitHub.Api/Tasks/BaseOutputProcessor.cs index b0b0342e5..51878efe4 100644 --- a/src/GitHub.Api/Tasks/BaseOutputProcessor.cs +++ b/src/GitHub.Api/Tasks/BaseOutputProcessor.cs @@ -110,7 +110,7 @@ protected override bool ProcessLine(string line, out string result) result = null; if (String.IsNullOrEmpty(line)) return false; - result = line; + result = line.Trim(); return true; } } diff --git a/src/GitHub.Api/Tasks/DownloadTask.cs b/src/GitHub.Api/Tasks/DownloadTask.cs index 0058d1c8e..d7c173c55 100644 --- a/src/GitHub.Api/Tasks/DownloadTask.cs +++ b/src/GitHub.Api/Tasks/DownloadTask.cs @@ -30,17 +30,17 @@ class DownloadTask : TaskBase protected readonly IFileSystem fileSystem; public DownloadTask(CancellationToken token, - IFileSystem fileSystem, UriString url, - NPath? targetDirectory = null, - string filename = null, + IFileSystem fileSystem, + UriString url, + NPath targetDirectory, int retryCount = 0) : base(token) { this.fileSystem = fileSystem; RetryCount = retryCount; Url = url; - Filename = filename ?? url.Filename; - TargetDirectory = targetDirectory ?? NPath.CreateTempDirectory("ghu"); + Filename = url.Filename; + TargetDirectory = targetDirectory; this.Name = $"Download {Url}"; } @@ -52,24 +52,15 @@ protected string BaseRunWithReturn(bool success) protected override NPath RunWithReturn(bool success) { var result = base.RunWithReturn(success); - - RaiseOnStart(); - try { result = RunDownload(success); } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } - finally - { - RaiseOnEnd(result); - } - return result; } @@ -77,7 +68,6 @@ protected override NPath RunWithReturn(bool success) /// The actual functionality to download with optional hash verification /// subclasses that wish to return the contents of the downloaded file /// or do something else with it can override this instead of RunWithReturn. - /// If you do, you must call RaiseOnStart()/RaiseOnEnd() ///
/// /// @@ -87,6 +77,7 @@ protected virtual NPath RunDownload(bool success) var attempts = 0; bool result = false; var partialFile = TargetDirectory.Combine(Filename + ".partial"); + TargetDirectory.EnsureDirectoryExists(); do { exception = null; @@ -116,8 +107,9 @@ protected virtual NPath RunDownload(bool success) catch (Exception ex) { exception = ex; + result = false; } - } while (attempts++ < RetryCount); + } while (!result && attempts++ < RetryCount); if (!result) { diff --git a/src/GitHub.Api/Tasks/Downloader.cs b/src/GitHub.Api/Tasks/Downloader.cs new file mode 100644 index 000000000..0a643ccc0 --- /dev/null +++ b/src/GitHub.Api/Tasks/Downloader.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Net; +using GitHub.Logging; + +namespace GitHub.Unity +{ + class DownloadData + { + public UriString Url { get; } + public NPath File { get; } + public DownloadData(UriString url, NPath file) + { + this.Url = url; + this.File = file; + } + } + + class Downloader : TaskQueue + { + public event Action OnDownloadStart; + public event Action OnDownloadComplete; + public event Action OnDownloadFailed; + + private readonly IFileSystem fileSystem; + public Downloader(IFileSystem fileSystem) + : base(t => + { + var dt = t as DownloadTask; + var destinationFile = dt.TargetDirectory.Combine(dt.Url.Filename); + return new DownloadData(dt.Url, destinationFile); + }) + { + this.fileSystem = fileSystem; + Name = "Downloader"; + Message = "Downloading..."; + } + + public void QueueDownload(UriString url, NPath targetDirectory) + { + var download = new DownloadTask(Token, fileSystem, url, targetDirectory); + download.OnStart += t => OnDownloadStart?.Invoke(((DownloadTask)t).Url); + download.OnEnd += (t, res, s, ex) => + { + if (s) + OnDownloadComplete?.Invoke(((DownloadTask)t).Url, res); + else + OnDownloadFailed?.Invoke(((DownloadTask)t).Url, ex); + }; + // queue after hooking up events so OnDownload* gets called first + Queue(download); + } + + public static bool Download(ILogging logger, UriString url, + Stream destinationStream, + Func onProgress) + { + long bytes = destinationStream.Length; + + var expectingResume = bytes > 0; + + var webRequest = (HttpWebRequest)WebRequest.Create(url); + + if (expectingResume) + { + // classlib for 3.5 doesn't take long overloads... + webRequest.AddRange((int)bytes); + } + + webRequest.Method = "GET"; + webRequest.Timeout = ApplicationConfiguration.WebTimeout; + + if (expectingResume) + logger.Trace($"Resuming download of {url}"); + else + logger.Trace($"Downloading {url}"); + + if (!onProgress(bytes, bytes * 2)) + return false; + + using (var webResponse = (HttpWebResponse)webRequest.GetResponseWithoutException()) + { + var httpStatusCode = webResponse.StatusCode; + logger.Trace($"Downloading {url} StatusCode:{(int)webResponse.StatusCode}"); + + if (expectingResume && httpStatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) + { + return !onProgress(bytes, bytes); + } + + if (!(httpStatusCode == HttpStatusCode.OK || httpStatusCode == HttpStatusCode.PartialContent)) + { + return false; + } + + if (expectingResume && httpStatusCode == HttpStatusCode.OK) + { + expectingResume = false; + destinationStream.Seek(0, SeekOrigin.Begin); + } + + var responseLength = webResponse.ContentLength; + if (expectingResume) + { + if (!onProgress(bytes, bytes + responseLength)) + return false; + } + + using (var responseStream = webResponse.GetResponseStream()) + { + return Utils.Copy(responseStream, destinationStream, responseLength, + progress: (totalRead, timeToFinish) => + { + return onProgress(totalRead, responseLength); + }); + } + } + } + } +} diff --git a/src/GitHub.Api/Tasks/ITaskManager.cs b/src/GitHub.Api/Tasks/ITaskManager.cs index 03b01469d..d5587872d 100644 --- a/src/GitHub.Api/Tasks/ITaskManager.cs +++ b/src/GitHub.Api/Tasks/ITaskManager.cs @@ -13,6 +13,8 @@ public interface ITaskManager : IDisposable T Schedule(T task) where T : ITask; Task Wait(); + ITask Run(Action action, string message = null); ITask RunInUI(Action action); + event Action OnProgress; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index f64fcc0cc..06c3c6c0c 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -54,28 +54,24 @@ class OctorunTask : ProcessTask { private readonly string clientId; private readonly string clientSecret; - private readonly string user; private readonly string userToken; private readonly NPath pathToNodeJs; private readonly NPath pathToOctorunJs; private readonly string arguments; - public OctorunTask(CancellationToken token, NPath pathToNodeJs, NPath pathToOctorunJs, string arguments, - string clientId = null, - string clientSecret = null, - string user = null, + public OctorunTask(CancellationToken token, IEnvironment environment, + string arguments, string userToken = null, IOutputProcessor processor = null) : base(token, processor ?? new OctorunResultOutputProcessor()) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.user = user; - this.userToken = userToken; - this.pathToNodeJs = pathToNodeJs; - this.pathToOctorunJs = pathToOctorunJs; + this.clientId = ApplicationInfo.ClientId; + this.clientSecret = ApplicationInfo.ClientSecret; + this.pathToNodeJs = environment.NodeJsExecutablePath; + this.pathToOctorunJs = environment.OctorunScriptPath; this.arguments = $"\"{pathToOctorunJs}\" {arguments}"; + this.userToken = userToken; } public override void Configure(ProcessStartInfo psi) @@ -85,21 +81,8 @@ public override void Configure(ProcessStartInfo psi) psi.WorkingDirectory = pathToOctorunJs.Parent.Parent.Parent; psi.EnvironmentVariables.Add("OCTOKIT_USER_AGENT", $"{ApplicationInfo.ApplicationSafeName}/{ApplicationInfo.Version}"); - - if (clientId != null) - { - psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_ID", clientId); - } - - if (clientSecret != null) - { - psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_SECRET", clientSecret); - } - - if (user != null) - { - psi.EnvironmentVariables.Add("OCTORUN_USER", user); - } + psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_ID", clientId); + psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_SECRET", clientSecret); if (userToken != null) { diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index 2debe995a..66cb5a3f2 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -6,11 +6,10 @@ using System.IO; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace GitHub.Unity { - static class ProcessTaskExtensions + public static class ProcessTaskExtensions { public static T Configure(this T task, IProcessManager processManager, bool withInput) where T : IProcess @@ -39,6 +38,7 @@ public interface IProcess { void Configure(Process existingProcess); void Configure(ProcessStartInfo psi); + void Stop(); event Action OnErrorData; StreamWriter StandardInput { get; } int ProcessId { get; } @@ -90,7 +90,9 @@ public ProcessWrapper(string taskName, Process process, IOutputProcessor outputP public void Run() { + DateTimeOffset lastOutput = DateTimeOffset.UtcNow; Exception thrownException = null; + var gotOutput = new AutoResetEvent(false); if (Process.StartInfo.RedirectStandardError) { Process.ErrorDataReceived += (s, e) => @@ -100,59 +102,74 @@ public void Run() // Logger.Trace("ErrorData \"" + (e.Data == null ? "'null'" : e.Data) + "\""); //} - string encodedData = null; + lastOutput = DateTimeOffset.UtcNow; + gotOutput.Set(); if (e.Data != null) { - encodedData = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); - errors.Add(encodedData); + var line = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(e.Data)); + errors.Add(line.TrimEnd('\r', '\n')); + Logger.Trace(line); + } + }; + } + + if (Process.StartInfo.RedirectStandardOutput) + { + Process.OutputDataReceived += (s, e) => + { + try + { + lastOutput = DateTimeOffset.UtcNow; + gotOutput.Set(); + if (e.Data != null) + { + var line = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(e.Data)); + outputProcessor.LineReceived(line.TrimEnd('\r', '\n')); + } + else + outputProcessor.LineReceived(null); + } + catch (Exception ex) + { + Logger.Error(ex); } }; } try { - Logger.Trace($"Running '{Process.StartInfo.FileName} {taskName}'"); + Logger.Trace($"Running '{Process.StartInfo.FileName} {Process.StartInfo.Arguments}'"); + token.ThrowIfCancellationRequested(); Process.Start(); if (Process.StartInfo.RedirectStandardInput) Input = new StreamWriter(Process.StandardInput.BaseStream, new UTF8Encoding(false)); if (Process.StartInfo.RedirectStandardError) Process.BeginErrorReadLine(); + if (Process.StartInfo.RedirectStandardOutput) + Process.BeginOutputReadLine(); onStart?.Invoke(); - if (Process.StartInfo.RedirectStandardOutput) + if (Process.StartInfo.CreateNoWindow) { - var outputStream = Process.StandardOutput; - var line = outputStream.ReadLine(); - while (line != null) + bool done = false; + while (!done) { - outputProcessor.LineReceived(line); - - if (token.IsCancellationRequested) + var exited = WaitForExit(500); + if (exited) { - if (!Process.HasExited) - Process.Kill(); - Process.Close(); - token.ThrowIfCancellationRequested(); + // process is done and we haven't seen output, we're done + done = !gotOutput.WaitOne(100); } - - line = outputStream.ReadLine(); - } - outputProcessor.LineReceived(null); - } - - if (Process.StartInfo.CreateNoWindow) - { - while (!WaitForExit(500)) - { - if (token.IsCancellationRequested) + else if (token.IsCancellationRequested || (taskName.Contains("git lfs") && lastOutput.AddMilliseconds(ApplicationConfiguration.DefaultGitTimeout) < DateTimeOffset.UtcNow)) + // if we're exiting or we haven't had output for a while { - Process.Kill(); - Process.Close(); + Stop(true); + token.ThrowIfCancellationRequested(); + throw new ProcessException(-2, "Process timed out"); } - token.ThrowIfCancellationRequested(); } if (Process.ExitCode != 0 && errors.Count > 0) @@ -189,6 +206,43 @@ public void Run() onEnd?.Invoke(); } + public void Stop(bool dontWait = false) + { + try + { + if (Process.StartInfo.RedirectStandardError) + Process.CancelErrorRead(); + if (Process.StartInfo.RedirectStandardOutput) + Process.CancelOutputRead(); + if (!Process.HasExited && Process.StartInfo.RedirectStandardInput) + Input.WriteLine("\x3"); + } + catch + {} + + try + { + + if (!Process.HasExited) + { + Process.Kill(); + } + + if (!dontWait) + { + bool waitSucceeded = Process.WaitForExit(500); + if (waitSucceeded) + { + Process.Close(); + } + } + } + catch(Exception ex) + { + Logger.Trace(ex); + } + } + private bool WaitForExit(int milliseconds) { //Logger.Debug("WaitForExit - time: {0}ms", milliseconds); @@ -209,7 +263,7 @@ private bool WaitForExit(int milliseconds) ///
/// The type of the results. If it's a List<> or similar, then specify the full List<> type here and the inner type of the List in /// If is a list or similar, then specify its inner type here - class ProcessTask : TaskBase, IProcessTask + public class ProcessTask : TaskBase, IProcessTask { private IOutputProcessor outputProcessor; private ProcessWrapper wrapper; @@ -278,10 +332,9 @@ public void Configure(Process existingProcess) Name = ProcessArguments; } - protected override void RaiseOnStart() + public void Stop() { - base.RaiseOnStart(); - OnStartProcess?.Invoke(this); + wrapper?.Stop(); } protected override void RaiseOnEnd() @@ -299,7 +352,7 @@ protected override T RunWithReturn(bool success) var result = base.RunWithReturn(success); wrapper = new ProcessWrapper(Name, Process, outputProcessor, - RaiseOnStart, + () => OnStartProcess?.Invoke(this), () => { try @@ -321,15 +374,8 @@ protected override T RunWithReturn(bool success) thrownException = new ProcessException(thrownException.GetExceptionMessage(), ex); } - try - { - if (thrownException != null && !RaiseFaultHandlers(thrownException)) - throw thrownException; - } - finally - { - RaiseOnEnd(result); - } + if (thrownException != null && !RaiseFaultHandlers(thrownException)) + throw thrownException; }, (ex, error) => { @@ -350,13 +396,13 @@ public override string ToString() public Process Process { get; set; } public int ProcessId { get { return Process.Id; } } - public override bool Successful { get { return !taskFailed && Task.Status == TaskStatus.RanToCompletion && Process.ExitCode == 0; } } + public override bool Successful { get { return base.Successful && Process.ExitCode == 0; } } public StreamWriter StandardInput { get { return wrapper?.Input; } } public virtual string ProcessName { get; protected set; } public virtual string ProcessArguments { get; } } - class ProcessTaskWithListOutput : DataTaskBase>, IProcessTask> + public class ProcessTaskWithListOutput : DataTaskBase>, IProcessTask> { private IOutputProcessor> outputProcessor; private Exception thrownException = null; @@ -409,10 +455,9 @@ public virtual void Configure(ProcessStartInfo psi, IOutputProcessor> ProcessName = psi.FileName; } - protected override void RaiseOnStart() + public void Stop() { - base.RaiseOnStart(); - OnStartProcess?.Invoke(this); + wrapper?.Stop(); } protected override void RaiseOnEnd() @@ -435,7 +480,7 @@ protected override List RunWithReturn(bool success) var result = base.RunWithReturn(success); wrapper = new ProcessWrapper(Name, Process, outputProcessor, - RaiseOnStart, + () => OnStartProcess?.Invoke(this), () => { try @@ -456,15 +501,8 @@ protected override List RunWithReturn(bool success) thrownException = new ProcessException(thrownException.GetExceptionMessage(), ex); } - try - { - if (thrownException != null && !RaiseFaultHandlers(thrownException)) - throw thrownException; - } - finally - { - RaiseOnEnd(result); - } + if (thrownException != null && !RaiseFaultHandlers(thrownException)) + throw thrownException; }, (ex, error) => { @@ -484,7 +522,7 @@ public override string ToString() public Process Process { get; set; } public int ProcessId { get { return Process.Id; } } - public override bool Successful { get { return Task.Status == TaskStatus.RanToCompletion && Process.ExitCode == 0; } } + public override bool Successful { get { return base.Successful && Process.ExitCode == 0; } } public StreamWriter StandardInput { get { return wrapper?.Input; } } public virtual string ProcessName { get; protected set; } public virtual string ProcessArguments { get; } @@ -492,7 +530,7 @@ public override string ToString() class FirstNonNullLineProcessTask : ProcessTask { - private readonly NPath fullPathToExecutable; + private readonly NPath? fullPathToExecutable; private readonly string arguments; public FirstNonNullLineProcessTask(CancellationToken token, NPath fullPathToExecutable, string arguments) @@ -502,11 +540,17 @@ public FirstNonNullLineProcessTask(CancellationToken token, NPath fullPathToExec this.arguments = arguments; } - public override string ProcessName => fullPathToExecutable.FileName; + public FirstNonNullLineProcessTask(CancellationToken token, string arguments) + : base(token, new FirstNonNullLineOutputProcessor()) + { + this.arguments = arguments; + } + + public override string ProcessName => fullPathToExecutable?.FileName; public override string ProcessArguments => arguments; } - class SimpleProcessTask : ProcessTask + public class SimpleProcessTask : ProcessTask { private readonly NPath? fullPathToExecutable; private readonly string arguments; @@ -528,7 +572,7 @@ public SimpleProcessTask(CancellationToken token, string arguments, IOutputProce public override string ProcessArguments => arguments; } - class SimpleListProcessTask : ProcessTaskWithListOutput + public class SimpleListProcessTask : ProcessTaskWithListOutput { private readonly NPath fullPathToExecutable; private readonly string arguments; @@ -543,4 +587,4 @@ public SimpleListProcessTask(CancellationToken token, NPath fullPathToExecutable public override string ProcessName => fullPathToExecutable; public override string ProcessArguments => arguments; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index b959c530e..768c011d5 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -32,7 +32,19 @@ public interface ITask : IAsyncResult T Finally(T taskToContinueWith) where T : ITask; ITask Start(); ITask Start(TaskScheduler scheduler); + void RunSynchronously(); + ITask Progress(Action progressHandler); + void UpdateProgress(long value, long total, string message = null); + + ITask GetTopOfChain(bool onlyCreated = true); + ITask GetEndOfChain(); + + /// + /// + /// true if any task on the chain is marked as exclusive + bool IsChainExclusive(); + bool Successful { get; } string Errors { get; } @@ -43,15 +55,8 @@ public interface ITask : IAsyncResult TaskBase DependsOn { get; } event Action OnStart; event Action OnEnd; - ITask GetTopOfChain(); - - /// - /// - /// true if any task on the chain is marked as exclusive - bool IsChainExclusive(); - - void UpdateProgress(long value, long total, string message = null); - ITask GetEndOfChain(); + string Message { get; } + Exception Exception { get; } } public interface ITask : ITask @@ -72,7 +77,9 @@ public interface ITask : ITask ITask Finally(Action continuation, TaskAffinity affinity = TaskAffinity.Concurrent); new ITask Start(); new ITask Start(TaskScheduler scheduler); + new TResult RunSynchronously(); new ITask Progress(Action progressHandler); + TResult Result { get; } new Task Task { get; } new event Action> OnStart; @@ -84,8 +91,10 @@ interface ITask : ITask event Action OnData; } - public abstract class TaskBase : ITask + public class TaskBase : ITask { + public static ITask Default = new TaskBase { Name = "Global" }; + protected const TaskContinuationOptions runAlwaysOptions = TaskContinuationOptions.None; protected const TaskContinuationOptions runOnSuccessOptions = TaskContinuationOptions.OnlyOnRanToCompletion; protected const TaskContinuationOptions runOnFaultOptions = TaskContinuationOptions.OnlyOnFaulted; @@ -93,10 +102,11 @@ public abstract class TaskBase : ITask public event Action OnStart; public event Action OnEnd; - protected bool previousSuccess = true; + protected bool? previousSuccess; protected Exception previousException; protected bool taskFailed = false; protected bool exceptionWasHandled = false; + protected bool hasRun = false; protected Exception exception; protected TaskBase continuationOnSuccess; @@ -114,41 +124,12 @@ protected TaskBase(CancellationToken token) Guard.ArgumentNotNull(token, "token"); Token = token; - Task = new Task(() => Run(DependsOn?.Successful ?? previousSuccess), Token, TaskCreationOptions.None); - } - - protected TaskBase(Task task) - : this() - { - Task = new Task(t => - { - var scheduler = TaskManager.GetScheduler(Affinity); - RaiseOnStart(); - var tk = ((Task)t); - try - { - if (tk.Status == TaskStatus.Created && !tk.IsCompleted && - ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) - { - tk.RunSynchronously(scheduler); - } - } - catch (Exception ex) - { - Errors = ex.Message; - if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); - } - }, task, Token, TaskCreationOptions.None); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); } protected TaskBase() { - this.progress = new Progress { Task = this }; + this.progress = new Progress(this); } public virtual T Then(T nextTask, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) @@ -223,11 +204,18 @@ public ITask Catch(Action handler) public ITask Catch(Func handler) { Guard.ArgumentNotNull(handler, "handler"); - catchHandler += handler; + CatchInternal(handler); DependsOn?.Catch(handler); return this; } + internal ITask CatchInternal(Func handler) + { + Guard.ArgumentNotNull(handler, "handler"); + catchHandler += handler; + return this; + } + /// /// Run a callback at the end of the task execution, on the same thread as the task that just finished, regardless of execution state /// This will always run on the same thread as the previous task @@ -246,7 +234,14 @@ public ITask Finally(Action handler) public ITask Finally(Action actionToContinueWith, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(actionToContinueWith, nameof(actionToContinueWith)); - return Finally(new ActionTask(Token, actionToContinueWith) { Affinity = affinity, Name = "Finally" }); + return Then(new ActionTask(Token, (s, ex) => + { + actionToContinueWith(s, ex); + if (!s) + throw ex; + }) + { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways) + .CatchInternal(_ => true); } /// @@ -266,7 +261,12 @@ public T Finally(T taskToContinueWith) /// internal void SetFaultHandler(TaskBase handler) { - Task.ContinueWith(t => handler.Start(t), Token, + Task.ContinueWith(t => + { + Token.ThrowIfCancellationRequested(); + handler.Start(t); + }, + Token, TaskContinuationOptions.OnlyOnFaulted, TaskManager.GetScheduler(handler.Affinity)); DependsOn?.SetFaultHandler(handler); @@ -282,14 +282,29 @@ public ITask Progress(Action handler) return this; } - public virtual ITask Start() + public ITask Start() { - var depends = GetTopMostTaskInCreatedState() ?? this; - depends.Run(); + var depends = GetTopMostStartableTask(); + depends?.Schedule(); return this; } - protected void Run() + public virtual void RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + try + { + Run(previousIsSuccessful); + } + finally + { + RaiseOnEnd(); + } + } + + protected virtual void Schedule() { if (Task.Status == TaskStatus.Created) { @@ -314,15 +329,23 @@ public virtual ITask Start(TaskScheduler scheduler) { if (Task.Status == TaskStatus.Created) { - //Logger.Trace($"Starting {Affinity} {ToString()}"); Task.Start(scheduler); } return this; } - public ITask GetTopOfChain() + public ITask GetTopOfChain(bool onlyCreated = true) { - return GetTopMostTaskInCreatedState() ?? this; + return GetTopMostTask(null, onlyCreated, false); + } + + public ITask GetEndOfChain() + { + if (continuationOnSuccess != null) + return continuationOnSuccess.GetEndOfChain(); + else if (continuationOnAlways != null) + return continuationOnAlways.GetEndOfChain(); + return this; } /// @@ -339,16 +362,21 @@ protected void SetContinuation() { if (continuationOnAlways != null) { - //Logger.Trace($"Setting ContinueWith {Affinity} {continuation}"); SetContinuation(continuationOnAlways, runAlwaysOptions); } } protected void SetContinuation(TaskBase continuation, TaskContinuationOptions runOptions) { - Task.ContinueWith(_ => ((TaskBase)(object)continuation).Run(), Token, - runOptions, - TaskManager.GetScheduler(continuation.Affinity)); + Token.ThrowIfCancellationRequested(); + Task.ContinueWith(_ => + { + Token.ThrowIfCancellationRequested(); + ((TaskBase)(object)continuation).Schedule(); + }, + Token, + runOptions, + TaskManager.GetScheduler(continuation.Affinity)); } protected ITask SetDependsOn(ITask dependsOn) @@ -357,59 +385,72 @@ protected ITask SetDependsOn(ITask dependsOn) return this; } - protected TaskBase GetTopMostTaskInCreatedState() + /// + /// Returns the first startable task on the chain. If the chain has been started + /// already, returns null + /// + protected TaskBase GetTopMostStartableTask() { - var depends = DependsOn; - if (depends == null) - return null; - return depends.GetTopMostTask(null, true); + return GetTopMostTask(null, true, true); } - protected TaskBase GetTopMostTask() + protected TaskBase GetTopMostCreatedTask() { - var depends = DependsOn; - if (depends == null) - return null; - return depends.GetTopMostTask(null, false); + return GetTopMostTask(null, true, false); } - public ITask GetEndOfChain() + protected TaskBase GetTopMostTask() { - if (continuationOnSuccess != null) - return continuationOnSuccess.GetEndOfChain(); - else if (continuationOnAlways != null) - return continuationOnAlways.GetEndOfChain(); - return this; + return GetTopMostTask(null, false, false); } - protected TaskBase GetTopMostTask(TaskBase ret, bool onlyCreatedState) + protected TaskBase GetTopMostTask(TaskBase ret, bool onlyCreated, bool onlyUnstartedChain) { - ret = (!onlyCreatedState || Task.Status == TaskStatus.Created ? this : ret); + ret = (!onlyCreated || Task.Status == TaskStatus.Created ? this : ret); var depends = DependsOn; if (depends == null) + { + // if we're at the top of the chain and the chain has already been started + // and we only care about unstarted chains, return null + if (onlyUnstartedChain && Task.Status != TaskStatus.Created) + return null; return ret; - return depends.GetTopMostTask(ret, onlyCreatedState); + } + return depends.GetTopMostTask(ret, onlyCreated, onlyUnstartedChain); } protected virtual void Run(bool success) { + taskFailed = false; + hasRun = false; + exception = null; + Token.ThrowIfCancellationRequested(); } protected virtual void RaiseOnStart() { - //Logger.Trace($"Executing {ToString()}"); + UpdateProgress(0, 100); + RaiseOnStartInternal(); + } + + protected void RaiseOnStartInternal() + { OnStart?.Invoke(this); } protected virtual bool RaiseFaultHandlers(Exception ex) { - taskFailed = true; exception = ex; + if (exception is AggregateException) + exception = exception.GetBaseException() ?? exception; + Errors = exception.Message; + taskFailed = true; if (catchHandler == null) return false; + var args = new object[] { exception }; foreach (var handler in catchHandler.GetInvocationList()) { - if ((bool)handler.DynamicInvoke(new object[] { ex })) + if ((bool)handler.DynamicInvoke(args)) { exceptionWasHandled = true; break; @@ -421,11 +462,18 @@ protected virtual bool RaiseFaultHandlers(Exception ex) protected virtual void RaiseOnEnd() { - OnEnd?.Invoke(this, !taskFailed, exception); + hasRun = true; + RaiseOnEndInternal(); SetupContinuations(); - //Logger.Trace($"Finished {ToString()}"); + UpdateProgress(100, 100); + } + + protected void RaiseOnEndInternal() + { + OnEnd?.Invoke(this, !taskFailed, exception); } + protected void SetupContinuations() { if (!taskFailed || exceptionWasHandled) @@ -461,20 +509,19 @@ protected virtual void CallFinallyHandler() protected Exception GetThrownException() { - if (DependsOn == null) - return null; - - if (DependsOn.Task.Status == TaskStatus.Faulted) + var depends = DependsOn; + while (depends != null) { - var ex = DependsOn.Task.Exception; - return ex?.InnerException ?? ex; + if (depends.taskFailed) + return depends.exception; + depends = depends.DependsOn; } - return DependsOn.GetThrownException(); + return null; } public void UpdateProgress(long value, long total, string message = null) { - progress.UpdateProgress(value, total, message); + progress.UpdateProgress(value, total, message ?? this.Message); } public override string ToString() @@ -482,10 +529,12 @@ public override string ToString() return $"{Task?.Id ?? -1} {Name} {GetType()}"; } - public virtual bool Successful { get { return Task.Status == TaskStatus.RanToCompletion && Task.Status != TaskStatus.Faulted; } } + public virtual bool Successful { get { return hasRun && !taskFailed; } } + public bool IsCompleted { get { return hasRun; } } + public Exception Exception => exception ?? GetThrownException(); + public string Errors { get; protected set; } public Task Task { get; protected set; } - public bool IsCompleted { get { return (Task as IAsyncResult).IsCompleted; } } public WaitHandle AsyncWaitHandle { get { return (Task as IAsyncResult).AsyncWaitHandle; } } public object AsyncState { get { return (Task as IAsyncResult).AsyncState; } } public bool CompletedSynchronously { get { return (Task as IAsyncResult).CompletedSynchronously; } } @@ -495,57 +544,26 @@ public override string ToString() protected ILogging Logger { get { return logger = logger ?? LogHelper.GetLogger(GetType()); } } public TaskBase DependsOn { get; private set; } public CancellationToken Token { get; } + public virtual string Message { get; set; } } - abstract class TaskBase : TaskBase, ITask + public abstract class TaskBase : TaskBase, ITask { - protected TaskCompletionSource tcs = new TaskCompletionSource(); private event Action finallyHandler; public new event Action> OnStart; public new event Action, TResult, bool, Exception> OnEnd; private TResult result; - protected TaskBase(CancellationToken token) - : base(token) + protected TaskBase() + : base() { - Task = new Task(() => - { - var ret = RunWithReturn(DependsOn?.Successful ?? previousSuccess); - tcs.SetResult(ret); - return ret; - }, Token, TaskCreationOptions.None); } - protected TaskBase(Task task) - : base() + protected TaskBase(CancellationToken token) + : base(token) { - Task = new Task(t => - { - TResult ret = default(TResult); - RaiseOnStart(); - var tk = ((Task)t); - try - { - if (tk.Status == TaskStatus.Created && !tk.IsCompleted && - ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) - { - tk.RunSynchronously(); - } - ret = tk.Result; - } - catch (Exception ex) - { - Errors = ex.Message; - if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); - } - return ret; - }, task, Token, TaskCreationOptions.None); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); } public override T Then(T continuation, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) @@ -587,7 +605,7 @@ public override T Then(T continuation, TaskRunOptions runOptions = TaskRunOpt public new ITask Catch(Func handler) { Guard.ArgumentNotNull(handler, "handler"); - catchHandler += handler; + CatchInternal(handler); DependsOn?.Catch(handler); return this; } @@ -610,9 +628,7 @@ public ITask Finally(Action handler) public ITask Finally(Func continuation, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(continuation, "continuation"); - var ret = Then(new FuncTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); - DependsOn?.SetFaultHandler(ret); - return ret; + return Then(new FuncTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); } /// @@ -621,9 +637,14 @@ public ITask Finally(Func continuati public ITask Finally(Action continuation, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(continuation, "continuation"); - var ret = Then(new ActionTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); - DependsOn?.SetFaultHandler(ret); - return ret; + return Then(new ActionTask(Token, (s, ex, res) => + { + continuation(s, ex, res); + if (!s) + throw ex; + }) + { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways) + .CatchInternal(_ => true); } public new ITask Start() @@ -653,19 +674,39 @@ protected virtual TResult RunWithReturn(bool success) return result; } + public new virtual TResult RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + TResult ret = default(TResult); + try + { + ret = RunWithReturn(previousIsSuccessful); + } + finally + { + RaiseOnEnd(ret); + } + return ret; + } + + protected override void RaiseOnStart() { - //Logger.Trace($"Executing {ToString()}"); + UpdateProgress(0, 100); OnStart?.Invoke(this); - base.RaiseOnStart(); + RaiseOnStartInternal(); } protected virtual void RaiseOnEnd(TResult data) { this.result = data; + hasRun = true; OnEnd?.Invoke(this, result, !taskFailed, exception); + RaiseOnEndInternal(); SetupContinuations(); - //Logger.Trace($"Finished {ToString()} {result}"); + UpdateProgress(100, 100); } protected override void CallFinallyHandler() @@ -679,44 +720,62 @@ protected override void CallFinallyHandler() get { return base.Task as Task; } set { base.Task = value; } } - public TResult Result { get { return Task.Result; } } + public TResult Result { get { return result; } } } - abstract class TaskBase : TaskBase + public abstract class TaskBase : TaskBase { - public TaskBase(CancellationToken token) + private readonly Func getPreviousResult; + + public TaskBase(CancellationToken token, Func getPreviousResult = null) : base(token) { - Task = new Task(() => - { - var ret = RunWithData(DependsOn?.Successful ?? previousSuccess, (DependsOn?.Successful ?? false) ? ((ITask)DependsOn).Result : default(T)); - tcs.SetResult(ret); - return ret; - }, - Token, TaskCreationOptions.None); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); + this.getPreviousResult = getPreviousResult; } - public TaskBase(Task task) - : base(task) - { } + public override TResult RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task + // otherwise if there's a method to retrieve the value, call that + // otherwise use the PreviousResult property + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); + + TResult ret = default(TResult); + try + { + ret = RunWithData(previousIsSuccessful, prevResult); + } + finally + { + RaiseOnEnd(ret); + } + return ret; + } protected virtual TResult RunWithData(bool success, T previousResult) { base.Run(success); return default(TResult); } + + public T PreviousResult { get; set; } = default(T); } - abstract class DataTaskBase : TaskBase, ITask + public abstract class DataTaskBase : TaskBase, ITask { public DataTaskBase(CancellationToken token) : base(token) {} - public DataTaskBase(Task task) - : base(task) - {} - public event Action OnData; protected void RaiseOnData(TData data) { @@ -724,16 +783,12 @@ protected void RaiseOnData(TData data) } } - abstract class DataTaskBase : TaskBase, ITask + public abstract class DataTaskBase : TaskBase, ITask { public DataTaskBase(CancellationToken token) : base(token) {} - public DataTaskBase(Task task) - : base(task) - {} - public event Action OnData; protected void RaiseOnData(TData data) { @@ -747,4 +802,4 @@ public enum TaskAffinity Exclusive, UI } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/TaskCanceledExceptions.cs b/src/GitHub.Api/Tasks/TaskCanceledExceptions.cs index 92234f0da..8ffb27fa8 100644 --- a/src/GitHub.Api/Tasks/TaskCanceledExceptions.cs +++ b/src/GitHub.Api/Tasks/TaskCanceledExceptions.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { [Serializable] - class DependentTaskFailedException : TaskCanceledException + public class DependentTaskFailedException : TaskCanceledException { protected DependentTaskFailedException() : base() { } @@ -21,7 +21,7 @@ public DependentTaskFailedException(ITask task, Exception ex) : this(ex.InnerExc } [Serializable] - class ProcessException : TaskCanceledException + public class ProcessException : TaskCanceledException { public int ErrorCode { get; } @@ -45,4 +45,4 @@ protected ProcessException(SerializationInfo info, StreamingContext context) : b public ProcessException(ITask process) : this(process.Errors) { } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/TaskExtensions.cs b/src/GitHub.Api/Tasks/TaskExtensions.cs index 265033576..fa634aaa9 100644 --- a/src/GitHub.Api/Tasks/TaskExtensions.cs +++ b/src/GitHub.Api/Tasks/TaskExtensions.cs @@ -4,7 +4,7 @@ namespace GitHub.Unity { - static class TaskExtensions + public static class TaskExtensions { public static async Task StartAwait(this ITask source, Action handler = null) { @@ -68,7 +68,7 @@ public static ITask Then(this ITask task, Func public static ITask Then(this ITask task, Task continuation, TaskAffinity affinity = TaskAffinity.Concurrent, TaskRunOptions runOptions = TaskRunOptions.OnSuccess) { - var cont = new FuncTask(continuation) { Affinity = affinity, Name = $"ThenAsync<{typeof(T)}>" }; + var cont = new TPLTask(continuation) { Affinity = affinity, Name = $"ThenAsync<{typeof(T)}>" }; return task.Then(cont, runOptions); } @@ -133,4 +133,4 @@ public static Task StartAsAsync(this ITask task) return tcs.Task; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index b89d784a7..7a3e80749 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { - class TaskManager : ITaskManager + public class TaskManager : ITaskManager { private static readonly ILogging logger = LogHelper.GetLogger(); @@ -17,7 +17,26 @@ class TaskManager : ITaskManager public CancellationToken Token { get { return cts.Token; } } private static ITaskManager instance; - public static ITaskManager Instance => instance; + public static ITaskManager Instance + { + get + { + if (instance == null) + { + instance = new TaskManager(); + } + + return instance; + } + } + + private ProgressReporter progressReporter = new ProgressReporter(); + + public event Action OnProgress + { + add { progressReporter.OnProgress += value; } + remove { progressReporter.OnProgress -= value; } + } public TaskManager() { @@ -51,6 +70,11 @@ public static TaskScheduler GetScheduler(TaskAffinity affinity) } } + public ITask Run(Action action, string message = null) + { + return new ActionTask(Token, action) { Message = message }.Start(); + } + public ITask RunInUI(Action action) { return new ActionTask(Token, action) { Affinity = TaskAffinity.UI }.Start(); @@ -107,6 +131,8 @@ private T ScheduleExclusive(T task, bool setupFaultHandler) TaskContinuationOptions.OnlyOnFaulted, ConcurrentScheduler ); } + + task.Progress(progressReporter.UpdateProgress); return (T)task.Start(manager.ExclusiveTaskScheduler); } @@ -123,6 +149,8 @@ private T ScheduleConcurrent(T task, bool setupFaultHandler) TaskContinuationOptions.OnlyOnFaulted, ConcurrentScheduler ); } + + task.Progress(progressReporter.UpdateProgress); return (T)task.Start((TaskScheduler)manager.ConcurrentTaskScheduler); } @@ -151,4 +179,4 @@ public void Dispose() Dispose(true); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Threading/ThreadingHelper.cs b/src/GitHub.Api/Threading/ThreadingHelper.cs index 13dfd9aa4..295e636f3 100644 --- a/src/GitHub.Api/Threading/ThreadingHelper.cs +++ b/src/GitHub.Api/Threading/ThreadingHelper.cs @@ -1,6 +1,4 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace GitHub.Unity @@ -19,102 +17,15 @@ public static void SetUIThread() public static bool InUIThread => InMainThread || Guard.InUnitTestRunner; - /// - /// Switch to the UI thread - /// Auto-disables switching when running in unit test mode - /// - /// - public static IAwaitable SwitchToMainThreadAsync() + public static TaskScheduler GetUIScheduler(SynchronizationContext synchronizationContext) { - return Guard.InUnitTestRunner ? - new AwaitableWrapper() : - new AwaitableWrapper(MainThreadScheduler); + // quickly swap out the sync context so we can leverage FromCurrentSynchronizationContext for our ui scheduler + var currentSyncContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(synchronizationContext); + var ret = TaskScheduler.FromCurrentSynchronizationContext(); + if (currentSyncContext != null) + SynchronizationContext.SetSynchronizationContext(currentSyncContext); + return ret; } - - - /// - /// Switch to a thread pool background thread if the current thread isn't one, otherwise does nothing - /// Auto-disables switching when running in unit test mode - /// - /// - /// - public static IAwaitable SwitchToThreadAsync(TaskScheduler scheduler = null) - { - return Guard.InUnitTestRunner ? - new AwaitableWrapper() : - new AwaitableWrapper(scheduler ?? TaskManager.Instance.ConcurrentScheduler); - } - - class AwaitableWrapper : IAwaitable - { - Func getAwaiter; - - public AwaitableWrapper() - { - getAwaiter = () => new AwaiterWrapper(); - } - - public AwaitableWrapper(TaskScheduler scheduler) - { - getAwaiter = () => new AwaiterWrapper(new TaskSchedulerAwaiter(scheduler)); - } - - public IAwaiter GetAwaiter() => getAwaiter(); - } - - class AwaiterWrapper : IAwaiter - { - Func isCompleted; - Action onCompleted; - Action getResult; - - public AwaiterWrapper() - { - isCompleted = () => true; - onCompleted = c => c(); - getResult = () => { }; - } - - public AwaiterWrapper(TaskSchedulerAwaiter awaiter) - { - isCompleted = () => awaiter.IsCompleted; - onCompleted = c => awaiter.OnCompleted(c); - getResult = () => awaiter.GetResult(); - } - - public bool IsCompleted => isCompleted(); - - public void OnCompleted(Action continuation) => onCompleted(continuation); - - public void GetResult() => getResult(); - } - - public struct TaskSchedulerAwaiter : INotifyCompletion - { - private readonly TaskScheduler scheduler; - - public bool IsCompleted - { - get - { - return (this.scheduler == TaskManager.Instance.UIScheduler && InUIThread) || (this.scheduler != TaskManager.Instance.UIScheduler && !InUIThread); - } - } - - public TaskSchedulerAwaiter(TaskScheduler scheduler) - { - this.scheduler = scheduler; - } - - public void OnCompleted(Action action) - { - Task.Factory.StartNew(action, TaskManager.Instance.Token, TaskCreationOptions.None, this.scheduler); - } - - public void GetResult() - { - } - } - } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 229296af5..57c299ed5 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -38,8 +38,6 @@ protected TreeBase() public void Load(IEnumerable treeDatas) { - //Logger.Trace("Load"); - var collapsedFolders = new HashSet(GetCollapsedFolders()); var checkedFiles = new HashSet(GetCheckedFiles()); var folders = new HashSet(); @@ -56,7 +54,7 @@ public void Load(IEnumerable treeDatas) TNode lastAddedNode = null; Clear(); - AddNode(Title, Title, -1 + displayRootLevel, true, false, false, false, isSelected, false, null, false); + AddNode(Title, Title, -1 + displayRootLevel, true, false, false, false, isSelected, false, null); foreach (var treeData in treeDatas) { @@ -74,8 +72,6 @@ public void Load(IEnumerable treeDatas) { if (PromoteNode(lastAddedNode, label)) { - //Logger.Trace("Promoting Node Label:{0}", lastAddedNode.Label); - parentIsPromoted = true; lastAddedNode.IsContainer = true; } @@ -122,13 +118,12 @@ public void Load(IEnumerable treeDatas) { isActive = treeData.IsActive; treeNodeTreeData = treeData; - isChecked = isCheckable && checkedFiles.Contains(nodePath); + isChecked = isCheckable && (checkedFiles.Contains(nodePath) || treeData.IsChecked); } isSelected = selectedNodePath != null && nodePath == selectedNodePath; - lastAddedNode = AddNode(nodePath, label, level + displayRootLevel + (parentIsPromoted ? 1 : 0), isFolder, isActive, nodeIsHidden, - nodeIsCollapsed, isSelected, isChecked, treeNodeTreeData, false); + lastAddedNode = AddNode(nodePath, label, level + displayRootLevel + (parentIsPromoted ? 1 : 0), isFolder, isActive, nodeIsHidden, nodeIsCollapsed, isSelected, isChecked, treeNodeTreeData); } } } @@ -188,29 +183,15 @@ protected bool PromoteNode(TNode previouslyAddedNode, string nextLabel) public void SetCheckStateOnAll(bool isChecked) { - var nodeCheckState = isChecked ? CheckState.Checked : CheckState.Empty; foreach (var node in Nodes) { - var wasChecked = node.CheckState == CheckState.Checked; - node.CheckState = nodeCheckState; - - if (!node.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(node); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(node); - } - } + SetCheckStateOnNode(node, isChecked); } } - protected TNode AddNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isSelected, bool isChecked, TData? treeData, bool isContainer) + protected TNode AddNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isSelected, bool isChecked, TData? treeData) { - var node = CreateTreeNode(path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData, isContainer); + var node = CreateTreeNode(path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData); SetNodeIcon(node); Nodes.Add(node); @@ -255,39 +236,32 @@ protected void ToggleNodeVisibility(int idx, TNode node) protected void ToggleNodeChecked(int idx, TNode node) { + CheckState checkState; var isChecked = false; - switch (node.CheckState) { case CheckState.Mixed: case CheckState.Empty: - node.CheckState = CheckState.Checked; + checkState = CheckState.Checked; isChecked = true; break; case CheckState.Checked: - node.CheckState = CheckState.Empty; + checkState = CheckState.Empty; break; - } - if (!node.IsFolder) - { - if (isChecked) - { - AddCheckedNode(node); - } - else - { - RemoveCheckedNode(node); - } + default: + throw new ArgumentOutOfRangeException("Unknown CheckState"); } + SetCheckStateOnNode(node, checkState); + if (node.IsFolderOrContainer) { ToggleChildrenChecked(idx, node, isChecked); } - ToggleParentFoldersChecked(idx, node, isChecked); + ToggleParentFolderAndContainersChecked(idx, node, checkState); } private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) @@ -295,20 +269,8 @@ private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) for (var i = idx + 1; i < Nodes.Count && node.Level < Nodes[i].Level; i++) { var childNode = Nodes[i]; - var wasChecked = childNode.CheckState == CheckState.Checked; - childNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - if (!childNode.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(childNode); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(childNode); - } - } + SetCheckStateOnNode(childNode, isChecked); if (childNode.IsFolderOrContainer) { @@ -329,23 +291,44 @@ private List GetLeafNodes(TNode node, int idx) for (var i = idx + 1; i < Nodes.Count && node.Level < Nodes[i].Level; i++) { var childNode = Nodes[i]; - if (childNode.IsFolder) - { - var leafNodes = GetLeafNodes(childNode, i); - results.AddRange(leafNodes); - } - else + if (!childNode.IsFolder) { results.Add(childNode); } } - return results; } + private void SetCheckStateOnNode(TNode node, bool setChecked) + { + SetCheckStateOnNode(node, setChecked ? CheckState.Checked : CheckState.Empty); + } + + private void SetCheckStateOnNode(TNode node, CheckState setCheckState) + { + var isChecked = setCheckState == CheckState.Checked + || setCheckState == CheckState.Mixed; + + var wasChecked = node.CheckState == CheckState.Checked; + + node.CheckState = setCheckState; + + if (!node.IsFolder) + { + if (isChecked && !wasChecked) + { + AddCheckedNode(node); + } + else if (!isChecked && wasChecked) + { + RemoveCheckedNode(node); + } + } + } - private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) + private void ToggleParentFolderAndContainersChecked(int idx, TNode node, CheckState checkState) { + var isChecked = checkState != CheckState.Empty; while (true) { if (node.Level > 0) @@ -395,14 +378,13 @@ private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) var parentIndex = firstSiblingIndex - 1; var parentNode = Nodes[parentIndex]; - if (siblingsInSameState) - { - parentNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - } - else - { - parentNode.CheckState = CheckState.Mixed; - } + + var parentNodeState = + siblingsInSameState + ? node.CheckState + : CheckState.Mixed; + + SetCheckStateOnNode(parentNode, parentNodeState); idx = parentIndex; node = parentNode; @@ -417,7 +399,8 @@ private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) protected abstract IEnumerable GetCollapsedFolders(); protected abstract void RemoveCheckedNode(TNode node); protected abstract void AddCheckedNode(TNode node); - protected abstract TNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TData? treeData, bool isContainer); + protected abstract TNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TData? treeData); + protected abstract void SetNodeIcon(TNode node); public string SelectedNodePath => SelectedNode?.Path; diff --git a/src/GitHub.Logging/ConsoleLogAdapter.cs b/src/GitHub.Logging/ConsoleLogAdapter.cs index 4fc945c36..a03990d13 100644 --- a/src/GitHub.Logging/ConsoleLogAdapter.cs +++ b/src/GitHub.Logging/ConsoleLogAdapter.cs @@ -3,43 +3,43 @@ namespace GitHub.Logging { - class ConsoleLogAdapter : LogAdapterBase + public class ConsoleLogAdapter : LogAdapterBase { - private string GetMessage(string level, string context, string message) + private string GetMessage(string context, string message) { var time = DateTime.Now.ToString("HH:mm:ss.fff tt"); var threadId = Thread.CurrentThread.ManagedThreadId; - return string.Format("{0} {1} [{2,2}] {3} {4}", time, level, threadId, context, message); + return string.Format("{0} [{1,2}] {2} {3}", time, threadId, context, message); } public override void Info(string context, string message) { - WriteLine("INFO", context, message); + WriteLine(context, message); } public override void Debug(string context, string message) { - WriteLine("DEBUG", context, message); + WriteLine(context, message); } public override void Trace(string context, string message) { - WriteLine("TRACE", context, message); + WriteLine(context, message); } public override void Warning(string context, string message) { - WriteLine("WARN", context, message); + WriteLine(context, message); } public override void Error(string context, string message) { - WriteLine("ERROR", context, message); + WriteLine(context, message); } - private void WriteLine(string level, string context, string message) + private void WriteLine(string context, string message) { - Console.WriteLine(GetMessage(level, context, message)); + Console.WriteLine(GetMessage(context, message)); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs index 91879ea53..9c4c0f71c 100644 --- a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs +++ b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs @@ -3,7 +3,7 @@ namespace GitHub.Logging { - static class ExceptionExtensions + public static class ExceptionExtensions { public static string GetExceptionMessage(this Exception ex) { @@ -20,5 +20,17 @@ public static string GetExceptionMessage(this Exception ex) message += Environment.NewLine + String.Join(Environment.NewLine, stack.Skip(1).SkipWhile(x => x.Contains("GitHub.Logging")).ToArray()); return message; } + + public static string GetExceptionMessageShort(this Exception ex) + { + var message = ex.ToString(); + var inner = ex.InnerException; + while (inner != null) + { + message += Environment.NewLine + inner.ToString(); + inner = inner.InnerException; + } + return message; + } } } \ No newline at end of file diff --git a/src/GitHub.Logging/FileLogAdapter.cs b/src/GitHub.Logging/FileLogAdapter.cs index 693dbc296..9c6dde452 100644 --- a/src/GitHub.Logging/FileLogAdapter.cs +++ b/src/GitHub.Logging/FileLogAdapter.cs @@ -4,7 +4,7 @@ namespace GitHub.Logging { - class FileLogAdapter : LogAdapterBase + public class FileLogAdapter : LogAdapterBase { private static readonly object lk = new object(); private readonly string filePath; diff --git a/src/GitHub.Logging/GitHub.Logging.csproj b/src/GitHub.Logging/GitHub.Logging.csproj index e33304064..5456d2117 100644 --- a/src/GitHub.Logging/GitHub.Logging.csproj +++ b/src/GitHub.Logging/GitHub.Logging.csproj @@ -21,7 +21,7 @@ false DEBUG;TRACE prompt - true + false false true @@ -32,7 +32,7 @@ TRACE prompt 4 - true + false false true @@ -53,10 +53,6 @@ - - - - diff --git a/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject b/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject index 6d9cc8a63..9e3e80dd6 100644 --- a/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject +++ b/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject @@ -1,5 +1,11 @@  + + ..\..\script\lib\UnityExtensions\Unity\TestRunner\UnityEngine.TestRunner.dll + ..\..\script\lib\UnityExtensions\Unity\TestRunner\Editor\UnityEditor.TestRunner.dll + ..\..\script\lib\Managed\UnityEditor.dll + ..\..\script\lib\Managed\UnityEngine.dll + True diff --git a/src/GitHub.Logging/LogHelper.cs b/src/GitHub.Logging/LogHelper.cs index c2709817f..9f55d56e5 100644 --- a/src/GitHub.Logging/LogHelper.cs +++ b/src/GitHub.Logging/LogHelper.cs @@ -18,7 +18,7 @@ public static bool TracingEnabled if (tracingEnabled != value) { tracingEnabled = value; - Instance?.Info("Trace Logging " + (value ? "Enabled" : "Disabled")); + Instance.Info("Trace Logging " + (value ? "Enabled" : "Disabled")); } } } @@ -45,11 +45,13 @@ public static ILogging Instance set { instance = value; } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] public static ILogging GetLogger() { return GetLogger(typeof(T)); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public static ILogging GetLogger(Type type) { return GetLogger(type.Name); diff --git a/src/GitHub.Logging/MultipleLogAdapter.cs b/src/GitHub.Logging/MultipleLogAdapter.cs index f9daf572a..df9137b8e 100644 --- a/src/GitHub.Logging/MultipleLogAdapter.cs +++ b/src/GitHub.Logging/MultipleLogAdapter.cs @@ -1,6 +1,6 @@ namespace GitHub.Logging { - class MultipleLogAdapter : LogAdapterBase + public class MultipleLogAdapter : LogAdapterBase { private readonly LogAdapterBase[] logAdapters; @@ -49,4 +49,4 @@ public override void Error(string context, string message) } } } -} \ No newline at end of file +} diff --git a/src/GitHub.Logging/NullLogAdapter.cs b/src/GitHub.Logging/NullLogAdapter.cs index 3d0e78724..7c5b672f7 100644 --- a/src/GitHub.Logging/NullLogAdapter.cs +++ b/src/GitHub.Logging/NullLogAdapter.cs @@ -1,6 +1,6 @@ namespace GitHub.Logging { - class NullLogAdapter : LogAdapterBase + public class NullLogAdapter : LogAdapterBase { public override void Info(string context, string message) { @@ -22,4 +22,4 @@ public override void Error(string context, string message) { } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/.gitignore b/src/UnityExtension/.gitignore new file mode 100644 index 000000000..848aaec94 --- /dev/null +++ b/src/UnityExtension/.gitignore @@ -0,0 +1,7 @@ +*.csproj +UnityPackageManager +JetBrains +UnityExtension.sln +Assets/**/*.zip +Assets/**/*.md5 +Assets/**/*.json diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index 352b3e2bb..272566cf7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -14,6 +14,9 @@ sealed class ApplicationCache : ScriptObjectSingleton { [SerializeField] private bool firstRun = true; [SerializeField] public string firstRunAtString; + [SerializeField] public string instanceIdString; + [SerializeField] private bool initialized = false; + [NonSerialized] private Guid? instanceId; [NonSerialized] private bool? firstRunValue; [NonSerialized] public DateTimeOffset? firstRunAtValue; @@ -34,7 +37,13 @@ public DateTimeOffset FirstRunAt if (!firstRunAtValue.HasValue) { - firstRunAtValue = DateTimeOffset.ParseExact(firstRunAtString, Constants.Iso8601Format, CultureInfo.InvariantCulture); + DateTimeOffset dt; + if (!DateTimeOffset.TryParseExact(firstRunAtString.ToEmptyIfNull(), Constants.Iso8601Formats, + CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) + { + dt = DateTimeOffset.Now; + } + FirstRunAt = dt; } return firstRunAtValue.Value; @@ -52,11 +61,46 @@ private void EnsureFirstRun() { firstRunValue = firstRun; } + } + + public Guid InstanceId + { + get + { + EnsureInstanceId(); + return instanceId.Value; + } + } - if (firstRun) + private void EnsureInstanceId() + { + if (instanceId.HasValue) { - firstRun = false; - FirstRunAt = DateTimeOffset.Now; + return; + } + + if (string.IsNullOrEmpty(instanceIdString)) + { + instanceId = Guid.NewGuid(); + instanceIdString = instanceId.ToString(); + } + else + { + instanceId = new Guid(instanceIdString); + } + } + + public bool Initialized + { + get { return initialized; } + set + { + initialized = value; + if (initialized && firstRun) + { + firstRun = false; + FirstRunAt = DateTimeOffset.Now; + } Save(true); } } @@ -103,6 +147,7 @@ public IEnvironment Environment cacheContainer.SetCacheInitializer(CacheType.GitAheadBehind, () => GitAheadBehindCache.Instance); cacheContainer.SetCacheInitializer(CacheType.GitLocks, () => GitLocksCache.Instance); cacheContainer.SetCacheInitializer(CacheType.GitLog, () => GitLogCache.Instance); + cacheContainer.SetCacheInitializer(CacheType.GitFileLog, () => GitFileLogCache.Instance); cacheContainer.SetCacheInitializer(CacheType.GitStatus, () => GitStatusCache.Instance); cacheContainer.SetCacheInitializer(CacheType.GitUser, () => GitUserCache.Instance); cacheContainer.SetCacheInitializer(CacheType.RepositoryInfo, () => RepositoryInfoCache.Instance); @@ -137,6 +182,7 @@ abstract class ManagedCacheBase : ScriptObjectSingleton where T : Scriptab [NonSerialized] private DateTimeOffset? lastUpdatedAtValue; [NonSerialized] private DateTimeOffset? initializedAtValue; [NonSerialized] private bool isInvalidating; + [NonSerialized] protected bool forcedInvalidation; public event Action CacheInvalidated; public event Action CacheUpdated; @@ -155,20 +201,27 @@ public bool ValidateData() if (needsInvalidation && !isInvalidating) { Logger.Trace("needsInvalidation isInitialized:{0} timedOut:{1}", isInitialized, timedOut); - InvalidateData(); + Invalidate(); } return !needsInvalidation; } public void InvalidateData() { - if (!isInvalidating) - { - Logger.Trace("Invalidate"); - isInvalidating = true; - LastUpdatedAt = DateTimeOffset.MinValue; - CacheInvalidated.SafeInvoke(CacheType); - } + forcedInvalidation = true; + Invalidate(); + } + + private void Invalidate() + { + isInvalidating = true; + LastUpdatedAt = DateTimeOffset.MinValue; + CacheInvalidated.SafeInvoke(CacheType); + } + + public void ResetInvalidation() + { + isInvalidating = false; } protected void SaveData(DateTimeOffset now, bool isChanged) @@ -185,7 +238,6 @@ protected void SaveData(DateTimeOffset now, bool isChanged) if (isChanged) { - Logger.Trace("Updated: {0}", now); CacheUpdated.SafeInvoke(CacheType, now); } } @@ -212,7 +264,7 @@ public DateTimeOffset LastUpdatedAt if (!lastUpdatedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(LastUpdatedAtString, Constants.Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(LastUpdatedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { lastUpdatedAtValue = result; } @@ -238,7 +290,7 @@ public DateTimeOffset InitializedAt if (!initializedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(InitializedAtString, Constants.Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(InitializedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { initializedAtValue = result; } @@ -398,6 +450,7 @@ sealed class RepositoryInfoCache : ManagedCacheBase, IRepos [SerializeField] private GitBranch currentGitBranch; [SerializeField] private ConfigBranch currentConfigBranch; [SerializeField] private ConfigRemote currentConfigRemote; + [SerializeField] private string currentHead; public RepositoryInfoCache() : base(CacheType.RepositoryInfo) { } @@ -407,30 +460,36 @@ public void UpdateData(IRepositoryInfoCacheData data) var now = DateTimeOffset.Now; var isUpdated = false; - if (!Nullable.Equals(currentGitRemote, data.CurrentGitRemote)) + if (forcedInvalidation || !Nullable.Equals(currentGitRemote, data.CurrentGitRemote)) { currentGitRemote = data.CurrentGitRemote ?? GitRemote.Default; isUpdated = true; } - if (!Nullable.Equals(currentGitBranch, data.CurrentGitBranch)) + if (forcedInvalidation || !Nullable.Equals(currentGitBranch, data.CurrentGitBranch)) { currentGitBranch = data.CurrentGitBranch ?? GitBranch.Default; isUpdated = true; } - if (!Nullable.Equals(currentConfigRemote, data.CurrentConfigRemote)) + if (forcedInvalidation || !Nullable.Equals(currentConfigRemote, data.CurrentConfigRemote)) { currentConfigRemote = data.CurrentConfigRemote ?? ConfigRemote.Default; isUpdated = true; } - if (!Nullable.Equals(currentConfigBranch, data.CurrentConfigBranch)) + if (forcedInvalidation || !Nullable.Equals(currentConfigBranch, data.CurrentConfigBranch)) { currentConfigBranch = data.CurrentConfigBranch ?? ConfigBranch.Default; isUpdated = true; } + if (forcedInvalidation || !String.Equals(currentHead, data.CurrentHead)) + { + currentHead = data.CurrentHead; + isUpdated = true; + } + SaveData(now, isUpdated); } @@ -470,6 +529,15 @@ public ConfigBranch? CurrentConfigBranch } } + public string CurrentHead + { + get + { + ValidateData(); + return currentHead; + } + } + public override TimeSpan DataTimeout { get { return TimeSpan.FromDays(1); } } } @@ -494,8 +562,6 @@ public void SetRemotes(Dictionary remoteConfigs, Dictionar remoteConfigBranches = new RemoteConfigBranchDictionary(configBranches); remotes = gitRemotes; remoteBranches = gitBranches; - - Logger.Trace("SetRemotes {0}", now); SaveData(now, true); } @@ -504,8 +570,6 @@ public void SetLocals(Dictionary configBranches, GitBranch var now = DateTimeOffset.Now; localConfigBranches = new LocalConfigBranchDictionary(configBranches); localBranches = gitBranches; - - Logger.Trace("SetLocals {0}", now); SaveData(now, true); } @@ -538,9 +602,15 @@ public List Log var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Log: current:{1} new:{2}", now, log.Count, value.Count); - - if (!log.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || log.Count > 0) + { + log.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !log.SequenceEqual(value)) { log = value; isUpdated = true; @@ -553,6 +623,46 @@ public List Log public override TimeSpan DataTimeout { get { return TimeSpan.FromMinutes(1); } } } + [Location("cache/gitfilelog.yaml", LocationAttribute.Location.LibraryFolder)] + sealed class GitFileLogCache : ManagedCacheBase, IGitFileLogCache + { + [SerializeField] private GitFileLog fileLog = GitFileLog.Default; + + public GitFileLogCache() : base(CacheType.GitFileLog) + { } + + public GitFileLog FileLog + { + get + { + ValidateData(); + return fileLog; + } + set + { + var now = DateTimeOffset.Now; + var isUpdated = false; + + var shouldUpdate = forcedInvalidation; + + if (!shouldUpdate) + { + shouldUpdate = true; + } + + if (shouldUpdate) + { + fileLog = value; + isUpdated = true; + } + + SaveData(now, isUpdated); + } + } + + public override TimeSpan DataTimeout { get { return TimeSpan.FromMinutes(1); } } + } + [Location("cache/gittrackingstatus.yaml", LocationAttribute.Location.LibraryFolder)] sealed class GitAheadBehindCache : ManagedCacheBase, IGitAheadBehindCache { @@ -574,8 +684,7 @@ public int Ahead var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Ahead: current:{1} new:{2}", now, ahead, value); - if (ahead != value) + if (forcedInvalidation || ahead != value) { ahead = value; isUpdated = true; @@ -597,9 +706,7 @@ public int Behind var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Behind: current:{1} new:{2}", now, behind, value); - - if (behind != value) + if (forcedInvalidation || behind != value) { behind = value; isUpdated = true; @@ -632,9 +739,15 @@ public List Entries var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Entries: current:{1} new:{2}", now, entries.Count, value.Count); - - if (!entries.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || entries.Count > 0) + { + entries.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !entries.SequenceEqual(value)) { entries = value; isUpdated = true; @@ -667,9 +780,15 @@ public List GitLocks var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating GitLocks: current:{1} new:{2}", now, gitLocks.Count, value.Count); - - if (!gitLocks.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || gitLocks.Count > 0) + { + gitLocks.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !gitLocks.SequenceEqual(value)) { gitLocks = value; isUpdated = true; @@ -703,9 +822,7 @@ public string Name var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Name: current:{1} new:{2}", now, gitName, value); - - if (gitName != value) + if (forcedInvalidation || gitName != value) { gitName = value; isUpdated = true; @@ -727,9 +844,7 @@ public string Email var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Email: current:{1} new:{2}", now, gitEmail, value); - - if (gitEmail != value) + if (forcedInvalidation || gitEmail != value) { gitEmail = value; isUpdated = true; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs index 4bf049cfa..e3c6a2742 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs @@ -14,18 +14,17 @@ class ApplicationManager : ApplicationManagerBase private FieldInfo quitActionField; - public ApplicationManager(IMainThreadSynchronizationContext synchronizationContext) - : base(synchronizationContext as SynchronizationContext) + public ApplicationManager(IMainThreadSynchronizationContext synchronizationContext, + IEnvironment environment) + : base(synchronizationContext as SynchronizationContext, environment) { + FirstRun = ApplicationCache.Instance.FirstRun; + InstanceId = ApplicationCache.Instance.InstanceId; + ListenToUnityExit(); Initialize(); } - protected override void SetupMetrics() - { - SetupMetrics(Environment.UnityVersion, ApplicationCache.Instance.FirstRun); - } - protected override void InitializeUI() { Logger.Trace("Restarted {0}", Environment.Repository != null ? Environment.Repository.LocalPath : "null"); @@ -33,18 +32,23 @@ protected override void InitializeUI() isBusy = false; LfsLocksModificationProcessor.Initialize(Environment, Platform); - ProjectWindowInterface.Initialize(Environment.Repository); + ProjectWindowInterface.Initialize(this); var window = Window.GetWindow(); if (window != null) window.InitializeWindow(this); + SetProjectToTextSerialization(); } - protected override void SetProjectToTextSerialization() + protected void SetProjectToTextSerialization() { - //Logger.Trace("SetProjectToTextSerialization"); EditorSettings.serializationMode = SerializationMode.ForceText; } + protected override void InitializationComplete() + { + ApplicationCache.Instance.Initialized = true; + } + private void ListenToUnityExit() { EditorApplicationQuit = (UnityAction)Delegate.Combine(EditorApplicationQuit, new UnityAction(Dispose)); @@ -97,6 +101,5 @@ protected override void Dispose(bool disposing) } public override IProcessEnvironment GitEnvironment { get { return Platform.GitEnvironment; } } - public override IEnvironment Environment { get { return EnvironmentCache.Instance.Environment; } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 51a0edc40..b1d83ca2c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -1,9 +1,6 @@ using GitHub.Logging; using System; using System.IO; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; using UnityEditor; using UnityEngine; @@ -24,8 +21,6 @@ static EntryPoint() LogHelper.LogAdapter = new FileLogAdapter(tempEnv.LogPath); - ServicePointManager.ServerCertificateValidationCallback = ServerCertificateValidationCallback; - ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; EditorApplication.update += Initialize; } @@ -35,7 +30,7 @@ private static void Initialize() EditorApplication.update -= Initialize; // this will initialize ApplicationManager and Environment if they haven't yet - var logPath = Environment.LogPath; + var logPath = ApplicationManager.Environment.LogPath; if (ApplicationCache.Instance.FirstRun) { @@ -60,21 +55,31 @@ private static void Initialize() LogHelper.Error(ex, "Error rotating log files"); } - Debug.LogFormat("Initialized GitHub for Unity version {0}{1}Log file: {2}", ApplicationInfo.Version, Environment.NewLine, logPath); + Debug.LogFormat("Initialized GitHub for Unity version {0}{1}Log file: {2}", ApplicationInfo.Version, ApplicationManager.Environment.NewLine, logPath); } LogHelper.LogAdapter = new MultipleLogAdapter(new FileLogAdapter(logPath) - //, new UnityLogAdapter() +#if DEBUG + , new UnityLogAdapter() +#endif ); - LogHelper.Info("Initializing GitHub for Unity version " + ApplicationInfo.Version); + LogHelper.Info("Initializing GitHubForUnity:'v{0}' Unity:'v{1}'", ApplicationInfo.Version, ApplicationManager.Environment.UnityVersion); + + ApplicationManager.Run(); - ApplicationManager.Run(ApplicationCache.Instance.FirstRun); + if (ApplicationCache.Instance.FirstRun) + UpdateCheckWindow.CheckForUpdates(ApplicationManager); } - private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, - X509Chain chain, SslPolicyErrors sslPolicyErrors) + internal static void Restart() { - return true; + if (appManager != null) + { + appManager.Dispose(); + appManager = null; + } + + Initialize(); } private static ApplicationManager appManager; @@ -84,14 +89,10 @@ public static IApplicationManager ApplicationManager { if (appManager == null) { - appManager = new ApplicationManager(new MainThreadSynchronizationContext()); + appManager = new ApplicationManager(new MainThreadSynchronizationContext(), EnvironmentCache.Instance.Environment); } return appManager; } } - - public static IEnvironment Environment { get { return ApplicationManager.Environment; } } - - public static IUsageTracker UsageTracker { get { return ApplicationManager.UsageTracker; } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef new file mode 100644 index 000000000..4408c5c12 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef @@ -0,0 +1,15 @@ +{ + "name": "ExtensionLoader", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "GitHub.UnityShim.dll" + ], + "autoReferenced": true +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs new file mode 100644 index 000000000..f3a7e9eef --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -0,0 +1,93 @@ +using UnityEditor; +using UnityEngine; +using System.IO; +using System; + +namespace GitHub.Unity +{ + [InitializeOnLoad] + public class ExtensionLoader : ScriptableSingleton + { + [SerializeField] private bool initialized = true; + + public bool Initialized + { + get + { + return initialized; + } + set + { + initialized = value; + Save(true); + } + } + + private static bool inSourceMode = false; + private const string sourceModePath = "Assets/Editor/build/"; + private const string realPath = "Assets/Plugins/GitHub/Editor/"; + + private static string[] assemblies20 = { "System.Threading.dll", "AsyncBridge.Net35.dll", "ReadOnlyCollectionsInterfaces.dll", "GitHub.Api.dll", "GitHub.Unity.dll" }; + private static string[] assemblies45 = { "GitHub.Api.45.dll", "GitHub.Unity.45.dll" }; + + private const string GITHUB_UNITY_DISABLE = "GITHUB_UNITY_DISABLE"; + private static bool IsDisabled { get { return Environment.GetEnvironmentVariable(GITHUB_UNITY_DISABLE) == "1"; } } + + static ExtensionLoader() + { + if (IsDisabled) + { + return; + } + EditorApplication.update += Initialize; + } + + private static void Initialize() + { + EditorApplication.update -= Initialize; + + // we're always doing this right now because if the plugin gets updated all the meta files will be disabled and we need to re-enable them + // we should probably detect if our assets change and re-run this instead of doing it every time + //if (!ExtensionLoader.instance.Initialized) + { + var scriptPath = Path.Combine(Application.dataPath, "Editor" + Path.DirectorySeparatorChar + "GitHub.Unity" + Path.DirectorySeparatorChar + "EntryPoint.cs"); + inSourceMode = File.Exists(scriptPath); + ToggleAssemblies(); + //ExtensionLoader.instance.Initialized = true; + AssetDatabase.SaveAssets(); + } + + } + + private static void ToggleAssemblies() + { + var path = inSourceMode ? sourceModePath : realPath; +#if NET_4_6 + ToggleAssemblies(path, assemblies20, false); + ToggleAssemblies(path, assemblies45, true); +#else + ToggleAssemblies(path, assemblies45, false); + ToggleAssemblies(path, assemblies20, true); +#endif + } + + private static void ToggleAssemblies(string path, string[] assemblies, bool enable) + { + foreach (var file in assemblies) + { + var filepath = path + file; + PluginImporter importer = AssetImporter.GetAtPath(filepath) as PluginImporter; + if (importer == null) + { + Debug.LogFormat("GitHub for Unity: Could not find importer for {0}. Some functionality may fail.", filepath); + continue; + } + if (importer.GetCompatibleWithEditor() != enable) + { + importer.SetCompatibleWithEditor(enable); + importer.SaveAndReimport(); + } + } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj new file mode 100644 index 000000000..29dd7e770 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj @@ -0,0 +1,82 @@ + + + + + Debug + AnyCPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115} + Library + Properties + GitHub.Unity + ExtensionLoader + v3.5 + 512 + ..\..\..\..\obj\ + ..\..\..\..\obj\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs) + prompt + 4 + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs) + prompt + 4 + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) + prompt + 4 + 4 + false + false + true + + + + + + {F94F8AE1-C171-4A83-89E8-6557CA91A188} + UnityShim + + + $(UnityDir)Managed\UnityEditor.dll + False + + + $(UnityDir)Managed\UnityEngine.dll + False + + + + + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/UnityAPIWrapper.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/UnityAPIWrapper.cs new file mode 100644 index 000000000..d034a91e3 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/UnityAPIWrapper.cs @@ -0,0 +1,20 @@ +using UnityEditor; +using UnityEngine; +using System.IO; +using System; + +namespace GitHub.Unity +{ + [InitializeOnLoad] + public class UnityAPIWrapper : ScriptableSingleton + { + static UnityAPIWrapper() + { +#if UNITY_2018_2_OR_NEWER + Editor.finishedDefaultHeaderGUI += editor => { + UnityShim.Raise_Editor_finishedDefaultHeaderGUI(editor); + }; +#endif + } + } +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj new file mode 100644 index 000000000..5945f94ea --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj @@ -0,0 +1,233 @@ + + + + + Debug + AnyCPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B} + Library + Properties + GitHub.Unity + GitHub.Unity.45 + v4.5 + 512 + $(SolutionDir)\unity\TestProject\Assets\Plugins\GitHub\Editor + ..\..\..\obj\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + false + false + true + + + + + + + + + $(UnityDir)Managed\UnityEditor.dll + False + + + $(UnityDir)Managed\UnityEngine.dll + False + + + + + {b389adaf-62cc-486e-85b4-2d8b078df76B} + GitHub.Api.45 + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + {F94F8AE1-C171-4A83-89E8-6557CA91A188} + UnityShim + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eula.txt + PreserveNewest + + + credits.txt + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject new file mode 100644 index 000000000..319cd523c --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef new file mode 100644 index 000000000..47e8a65d3 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef @@ -0,0 +1,11 @@ +{ + "name": "GitHub.Unity", + "references": [ + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 33b722ca6..1d1eceee7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -19,23 +19,23 @@ true full false - DEBUG;TRACE + DEBUG;TRACE;$(BuildDefs) prompt 4 4 - true + false false true pdbonly true - TRACE + TRACE;$(BuildDefs) prompt 4 4 Release - true + false false true @@ -43,7 +43,7 @@ true full false - DEBUG;TRACE;DEVELOPER_BUILD + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) prompt 4 4 @@ -52,26 +52,18 @@ true - - False - $(SolutionDir)\lib\Mono.Security.dll - $(SolutionDir)\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll True - - - - - $(UnityDir)UnityEditor.dll + $(UnityDir)Managed\UnityEditor.dll False - $(UnityDir)UnityEngine.dll + $(UnityDir)Managed\UnityEngine.dll False @@ -85,6 +77,10 @@ + + + + @@ -94,7 +90,6 @@ - @@ -103,6 +98,7 @@ + @@ -111,9 +107,6 @@ - - - {b389adaf-62cc-486e-85b4-2d8b078df763} @@ -123,6 +116,10 @@ {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging + + {F94F8AE1-C171-4A83-89E8-6557CA91A188} + UnityShim + @@ -174,8 +171,8 @@ - - + + @@ -193,12 +190,12 @@ - - EULA.txt + + eula.txt PreserveNewest - - CREDITS.txt + + credits.txt PreserveNewest diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light.png index 9aed80be8..7f4d5d2d6 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light.png +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:168248454e8ed2eb53df99a90ad600d4a07b08ef76a831502b80d69cf93687b0 -size 23382 +oid sha256:dc5759d0f368c9700e96596a824171b3f6f59e7039d2d676813bbed5a894bd65 +size 3996 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light@2x.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light@2x.png index 3898af4bf..939045b4a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light@2x.png +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo-light@2x.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:390ffaec27856c05e2ec7cf733a1de904c5a3fb11230c32db34bd9d80d17bc61 -size 58211 +oid sha256:21f3d167ef97cf5d83d6e57c00c357a5700e15e82ddf450b2aebe3ff49e95de9 +size 6987 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo.png index 6f0a653aa..52ed50f99 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo.png +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74095fd652f9ee3485377acae92089e69ce9de65a49b1a046a4b0f9ccd3ac88b -size 20581 +oid sha256:790f48f20d0af1c11f845930f4e9196ccf6db8929096edfc597a437e675c2c9c +size 3579 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo@2x.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo@2x.png index 4618fffc5..471c4ddf7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo@2x.png +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/big-logo@2x.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:267b005091b31403c42d9a5d4236fb08b69280c98ccc4c770e2092456983c480 -size 54108 +oid sha256:776c89e31c9ea00c27430a8a4bfdd8a2062b6619d90d1959e412b6a78f882926 +size 6381 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/moved.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/renamed.png similarity index 100% rename from src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/moved.png rename to src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/renamed.png diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/moved@2x.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/renamed@2x.png similarity index 100% rename from src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/moved@2x.png rename to src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/renamed@2x.png diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index c5d5edbcb..2fbeaf7c3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -14,6 +14,10 @@ class Styles BroadModeBranchesMinWidth = 200f, BroadModeBranchesRatio = .4f, InitialStateAreaWidth = 200f, + LocksEntryHeight = 42f, + LocksSummaryHeight = 5f, + LocksUserHeight = 5f, + LocksDateHeight = 5f, HistoryEntryHeight = 40f, HistorySummaryHeight = 16f, HistoryDetailsHeight = 16f, @@ -57,12 +61,14 @@ class Styles deletedFileLabel, longMessageStyle, headerBoxStyle, + headerStyle, headerBranchLabelStyle, headerUrlLabelStyle, headerRepoLabelStyle, + fileHistoryLogTitleStyle, headerTitleStyle, headerDescriptionStyle, - historyToolbarButtonStyle, + toolbarButtonStyle, historyLockStyle, historyEntrySummaryStyle, historyEntryDetailsStyle, @@ -81,35 +87,15 @@ class Styles genericTableBoxStyle, historyDetailsTitleStyle, historyDetailsMetaInfoStyle, - genericBoxStyle; - - private static Texture2D branchIcon, - activeBranchIcon, - trackingBranchIcon, - favoriteIconOn, - favoriteIconOff, - smallLogoIcon, - bigLogoIcon, - defaultAssetIcon, - folderIcon, - mergeIcon, - dotIcon, - localCommitIcon, - repoIcon, - lockIcon, - emptyStateInit, - dropdownListIcon, - globeIcon, - spinnerInside, - spinnerOutside, - code, - rocket, - merge, - spinnerInsideInverted, - spinnerOutsideInverted, - codeInverted, - rocketInverted, - mergeInverted; + genericBoxStyle, + hyperlinkStyle, + selectedArea, + selectedLabel, + progressAreaBackStyle, + labelNoWrap, + invisibleLabel, + locksViewLockedByStyle, + locksViewLockedBySelectedStyle; public static Texture2D GetFileStatusIcon(GitFileStatus status, bool isLocked) { @@ -198,6 +184,23 @@ public static GUIStyle HistoryFileTreeBoxStyle } } + public static GUIStyle SelectedArea + { + get + { + if (selectedArea == null) + { + selectedArea = new GUIStyle(GUI.skin.label); + selectedArea.name = "SelectedArea"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + selectedArea.normal.background = hierarchyStyle.onFocused.background; + selectedArea.focused.background = hierarchyStyle.onFocused.background; + } + return selectedArea; + } + } + public static GUIStyle Label { get @@ -212,11 +215,74 @@ public static GUIStyle Label label.onNormal.textColor = hierarchyStyle.onNormal.textColor; label.onFocused.background = hierarchyStyle.onFocused.background; label.onFocused.textColor = hierarchyStyle.onFocused.textColor; + label.wordWrap = true; } return label; } } + public static GUIStyle LabelNoWrap + { + get + { + if (labelNoWrap == null) + { + labelNoWrap = new GUIStyle(GUI.skin.label); + labelNoWrap.name = "LabelNoWrap"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + labelNoWrap.onNormal.background = hierarchyStyle.onNormal.background; + labelNoWrap.onNormal.textColor = hierarchyStyle.onNormal.textColor; + labelNoWrap.onFocused.background = hierarchyStyle.onFocused.background; + labelNoWrap.onFocused.textColor = hierarchyStyle.onFocused.textColor; + labelNoWrap.wordWrap = false; + } + return labelNoWrap; + } + } + + public static GUIStyle InvisibleLabel + { + get + { + if (invisibleLabel == null) + { + invisibleLabel = new GUIStyle(GUI.skin.label); + invisibleLabel.name = "InvisibleLabel"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + invisibleLabel.onNormal.background = hierarchyStyle.onNormal.background; + invisibleLabel.onNormal.textColor = new Color(255, 0, 0, 0); + invisibleLabel.onFocused.background = hierarchyStyle.onFocused.background; + invisibleLabel.onFocused.textColor = new Color(255, 0, 0, 0); + invisibleLabel.wordWrap = true; + } + return invisibleLabel; + } + } + + public static GUIStyle SelectedLabel + { + get + { + if (selectedLabel == null) + { + selectedLabel = new GUIStyle(GUI.skin.label); + selectedLabel.name = "SelectedLabel"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + selectedLabel.onNormal.background = hierarchyStyle.onFocused.background; + selectedLabel.onNormal.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.onFocused.background = hierarchyStyle.onFocused.background; + selectedLabel.onFocused.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.normal.background = hierarchyStyle.onFocused.background; + selectedLabel.normal.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.wordWrap = true; + } + return selectedLabel; + } + } + public static GUIStyle HeaderBranchLabelStyle { get @@ -226,6 +292,7 @@ public static GUIStyle HeaderBranchLabelStyle headerBranchLabelStyle = new GUIStyle(EditorStyles.label); headerBranchLabelStyle.name = "HeaderBranchLabelStyle"; headerBranchLabelStyle.margin = new RectOffset(0, 0, 0, 0); + headerBranchLabelStyle.wordWrap = true; } return headerBranchLabelStyle; } @@ -240,6 +307,7 @@ public static GUIStyle HeaderRepoLabelStyle headerRepoLabelStyle = new GUIStyle(EditorStyles.boldLabel); headerRepoLabelStyle.name = "HeaderRepoLabelStyle"; headerRepoLabelStyle.margin = new RectOffset(0, 0, 0, 0); + headerRepoLabelStyle.wordWrap = true; } return headerRepoLabelStyle; } @@ -281,10 +349,9 @@ public static GUIStyle HeaderDescriptionStyle { if (headerDescriptionStyle == null) { - headerDescriptionStyle = new GUIStyle(EditorStyles.label); + headerDescriptionStyle = new GUIStyle(EditorStyles.wordWrappedLabel); headerDescriptionStyle.name = "HeaderDescriptionStyle"; headerDescriptionStyle.margin = new RectOffset(0, 0, 0, 0); - headerDescriptionStyle.wordWrap = true; } return headerDescriptionStyle; } @@ -305,6 +372,21 @@ public static GUIStyle HeaderBoxStyle } } + public static GUIStyle HeaderStyle + { + get + { + if (headerStyle == null) + { + headerStyle = new GUIStyle("IN BigTitle"); + headerStyle.name = "HeaderStyle"; + headerStyle.margin = new RectOffset(0, 0, 0, 0); + headerStyle.padding = new RectOffset(0, 0, 0, 0); + } + return headerStyle; + } + } + public static GUIStyle BoldLabel { get @@ -340,9 +422,8 @@ public static GUIStyle ErrorLabel { if (errorLabel == null) { - errorLabel = new GUIStyle(EditorStyles.label); + errorLabel = new GUIStyle(EditorStyles.wordWrappedLabel); errorLabel.name = "ErrorLabel"; - errorLabel.wordWrap = true; errorLabel.normal.textColor = Color.red; } return errorLabel; @@ -355,10 +436,9 @@ public static GUIStyle CenteredErrorLabel { if (centeredErrorLabel == null) { - centeredErrorLabel = new GUIStyle(EditorStyles.label); + centeredErrorLabel = new GUIStyle(EditorStyles.wordWrappedLabel); centeredErrorLabel.alignment = TextAnchor.MiddleCenter; centeredErrorLabel.name = "CenteredErrorLabel"; - centeredErrorLabel.wordWrap = true; centeredErrorLabel.normal.textColor = Color.red; } return centeredErrorLabel; @@ -371,38 +451,36 @@ public static GUIStyle LongMessageStyle { if (longMessageStyle == null) { - longMessageStyle = new GUIStyle(EditorStyles.miniLabel); + longMessageStyle = new GUIStyle(EditorStyles.wordWrappedLabel); longMessageStyle.name = "LongMessageStyle"; longMessageStyle.richText = true; - longMessageStyle.wordWrap = true; } return longMessageStyle; } } - public static GUIStyle HistoryToolbarButtonStyle + public static GUIStyle ToolbarButtonStyle { get { - if (historyToolbarButtonStyle == null) + if (toolbarButtonStyle == null) { - historyToolbarButtonStyle = new GUIStyle(EditorStyles.toolbarButton); - historyToolbarButtonStyle.name = "HistoryToolbarButtonStyle"; - historyToolbarButtonStyle.richText = true; - historyToolbarButtonStyle.wordWrap = true; + toolbarButtonStyle = new GUIStyle(EditorStyles.toolbarButton); + toolbarButtonStyle.name = "HistoryToolbarButtonStyle"; + toolbarButtonStyle.richText = true; } - return historyToolbarButtonStyle; + return toolbarButtonStyle; } } - public static GUIStyle HistoryLockStyle + public static GUIStyle LockButtonStyle { get { if (historyLockStyle == null) { historyLockStyle = new GUIStyle(GUI.skin.FindStyle("IN LockButton")); - historyLockStyle.name = "HistoryLockStyle"; + historyLockStyle.name = "LockStyle"; } historyLockStyle.margin = new RectOffset(3, 3, 2, 2); return historyLockStyle; @@ -414,7 +492,7 @@ public static GUIStyle HistoryEntrySummaryStyle { if (historyEntrySummaryStyle == null) { - historyEntrySummaryStyle = new GUIStyle(Label); + historyEntrySummaryStyle = new GUIStyle(LabelNoWrap); historyEntrySummaryStyle.name = "HistoryEntrySummaryStyle"; historyEntrySummaryStyle.contentOffset = new Vector2(BaseSpacing * 2, 0); @@ -530,6 +608,45 @@ public static GUIStyle HistoryDetailsMetaInfoStyle } } + public static GUIStyle LocksViewLockedByStyle + { + get + { + if (locksViewLockedByStyle == null) + { + locksViewLockedByStyle = new GUIStyle(EditorStyles.miniLabel); + locksViewLockedByStyle.name = "LocksViewLockedByStyle"; + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + locksViewLockedByStyle.onNormal.background = hierarchyStyle.onNormal.background; + locksViewLockedByStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedByStyle.onFocused.background = hierarchyStyle.onFocused.background; + locksViewLockedByStyle.onFocused.textColor = hierarchyStyle.onFocused.textColor; + } + return locksViewLockedByStyle; + } + } + + public static GUIStyle LocksViewLockedBySelectedStyle + { + get + { + if (locksViewLockedBySelectedStyle == null) + { + locksViewLockedBySelectedStyle = new GUIStyle(EditorStyles.miniLabel); + locksViewLockedBySelectedStyle.name = "LocksViewLockedBySelectedStyle"; + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + locksViewLockedBySelectedStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.onNormal.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.onFocused.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.onFocused.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.normal.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.normal.textColor = hierarchyStyle.onNormal.textColor; + } + return locksViewLockedBySelectedStyle; + } + } + public static GUIStyle CommitFileAreaStyle { get @@ -539,6 +656,7 @@ public static GUIStyle CommitFileAreaStyle commitFileAreaStyle = new GUIStyle(GUI.skin.box); commitFileAreaStyle.name = "CommitFileAreaStyle"; commitFileAreaStyle.margin = new RectOffset(0, 0, 0, 0); + commitFileAreaStyle.padding = new RectOffset(0, 0, 2, 2); } return commitFileAreaStyle; } @@ -574,6 +692,22 @@ public static GUIStyle TextFieldStyle } } + public static GUIStyle ProgressAreaBackStyle + { + get + { + if (progressAreaBackStyle == null) + { + progressAreaBackStyle = new GUIStyle(GUI.skin.FindStyle("ProgressBarBack")); + progressAreaBackStyle.name = "ProgressAreaBackStyle"; + //progressAreaBackStyle.normal.background = Utility.GetTextureFromColor(new Color(194f/255f, 194f/255f, 194f/255f)); + progressAreaBackStyle.margin = new RectOffset(0, 0, 0, 0); + progressAreaBackStyle.padding = new RectOffset(0, 0, 0, 0); + } + return progressAreaBackStyle; + } + } + public static GUIStyle CenteredLabel { get @@ -601,8 +735,7 @@ public static GUIStyle BoldCenteredLabel return boldCenteredLabel; } } - - + public static GUIStyle CommitDescriptionFieldStyle { get @@ -656,53 +789,62 @@ public static GUIStyle GenericBoxStyle } } - public static Texture2D ActiveBranchIcon + public static GUIStyle HyperlinkStyle { get { - if (activeBranchIcon == null) + if (hyperlinkStyle == null) { - activeBranchIcon = Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png"); + hyperlinkStyle = new GUIStyle(EditorStyles.wordWrappedLabel); + hyperlinkStyle.normal.textColor = new Color(0, 0, 0xEE); } - return activeBranchIcon; + return hyperlinkStyle; } } - public static Texture2D BranchIcon + public static GUIStyle FileHistoryLogTitleStyle { get { - if (branchIcon == null) + if (fileHistoryLogTitleStyle == null) { - branchIcon = Utility.GetIcon("branch.png", "branch@2x.png"); + fileHistoryLogTitleStyle = new GUIStyle(EditorStyles.largeLabel); + fileHistoryLogTitleStyle.name = "FileHistoryLogTitleStyle"; + fileHistoryLogTitleStyle.margin = new RectOffset(0, 0, 0, 0); } - return branchIcon; + return fileHistoryLogTitleStyle; } } - public static Texture2D TrackingBranchIcon + public static Texture2D ActiveBranchIcon { get { - if (trackingBranchIcon == null) - { - trackingBranchIcon = Utility.GetIcon("tracked-branch-indicator.png"); - } + return Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png", Utility.IsDarkTheme); + } + } - return trackingBranchIcon; + public static Texture2D BranchIcon + { + get + { + return Utility.GetIcon("branch.png", "branch@2x.png"); } } - public static Texture2D FavoriteIconOn + public static Texture2D TrackingBranchIcon { get { - if (favoriteIconOn == null) - { - favoriteIconOn = Utility.GetIcon("favorite-branch-indicator.png"); - } + return Utility.GetIcon("tracked-branch-indicator.png"); + } + } - return favoriteIconOn; + public static Texture2D FavoriteIconOn + { + get + { + return Utility.GetIcon("favorite-branch-indicator.png"); } } @@ -710,12 +852,7 @@ public static Texture2D FavoriteIconOff { get { - if (favoriteIconOff == null) - { - favoriteIconOff = FolderIcon; - } - - return favoriteIconOff; + return FolderIcon; } } @@ -723,12 +860,7 @@ public static Texture2D SmallLogo { get { - if (smallLogoIcon == null) - { - smallLogoIcon = Utility.GetIcon("small-logo.png"); - } - - return smallLogoIcon; + return Utility.IsDarkTheme ? Utility.GetIcon("small-logo-light.png", "small-logo-light@2x.png") : Utility.GetIcon("small-logo.png", "small-logo@2x.png"); } } @@ -736,12 +868,7 @@ public static Texture2D BigLogo { get { - if (bigLogoIcon == null) - { - bigLogoIcon = Utility.GetIcon("big-logo.png"); - } - - return bigLogoIcon; + return Utility.IsDarkTheme ? Utility.GetIcon("big-logo-light.png", "big-logo-light@2x.png") : Utility.GetIcon("big-logo.png", "big-logo@2x.png"); } } @@ -749,12 +876,7 @@ public static Texture2D MergeIcon { get { - if (mergeIcon == null) - { - mergeIcon = Utility.GetIcon("git-merge.png", "git-merge@2x.png"); - } - - return mergeIcon; + return Utility.GetIcon("git-merge.png", "git-merge@2x.png"); } } @@ -762,12 +884,7 @@ public static Texture2D DotIcon { get { - if (dotIcon == null) - { - dotIcon = Utility.GetIcon("dot.png", "dot@2x.png"); - } - - return dotIcon; + return Utility.GetIcon("dot.png", "dot@2x.png", Utility.IsDarkTheme); } } @@ -775,25 +892,7 @@ public static Texture2D LocalCommitIcon { get { - if (localCommitIcon == null) - { - localCommitIcon = Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png"); - } - - return localCommitIcon; - } - } - - public static Texture2D DefaultAssetIcon - { - get - { - if (defaultAssetIcon == null) - { - defaultAssetIcon = EditorGUIUtility.FindTexture("DefaultAsset Icon"); - } - - return defaultAssetIcon; + return Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png", Utility.IsDarkTheme); } } @@ -801,12 +900,7 @@ public static Texture2D FolderIcon { get { - if (folderIcon == null) - { - folderIcon = EditorGUIUtility.FindTexture("Folder Icon"); - } - - return folderIcon; + return EditorGUIUtility.FindTexture("Folder Icon"); } } @@ -814,11 +908,7 @@ public static Texture2D RepoIcon { get { - if (repoIcon == null) - { - repoIcon = Utility.GetIcon("repo.png", "repo@2x.png"); - } - return repoIcon; + return Utility.GetIcon("repo.png", "repo@2x.png", Utility.IsDarkTheme); } } @@ -826,36 +916,23 @@ public static Texture2D LockIcon { get { - if (lockIcon == null) - { - lockIcon = Utility.GetIcon("lock.png", "lock@2x.png"); - } - return lockIcon; + return Utility.GetIcon("lock.png", "lock@2x.png"); } } public static Texture2D EmptyStateInit { - get - { - if (emptyStateInit == null) + get { - emptyStateInit = Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); + return Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); } - return emptyStateInit; - } - } public static Texture2D DropdownListIcon { get { - if (dropdownListIcon == null) - { - dropdownListIcon = Utility.GetIcon("dropdown-list-icon.png", "dropdown-list-icon@2x.png"); - } - return dropdownListIcon; + return Utility.GetIcon("dropdown-list-icon.png", "dropdown-list-icon@2x.png"); } } @@ -863,11 +940,7 @@ public static Texture2D GlobeIcon { get { - if (globeIcon == null) - { - globeIcon = Utility.GetIcon("globe.png", "globe@2x.png"); - } - return globeIcon; + return Utility.GetIcon("globe.png", "globe@2x.png", Utility.IsDarkTheme); } } @@ -875,11 +948,7 @@ public static Texture2D SpinnerInside { get { - if (spinnerInside == null) - { - spinnerInside = Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); - } - return spinnerInside; + return Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); } } @@ -887,11 +956,7 @@ public static Texture2D SpinnerOutside { get { - if (spinnerOutside == null) - { - spinnerOutside = Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); - } - return spinnerOutside; + return Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); } } @@ -899,11 +964,7 @@ public static Texture2D Code { get { - if (code == null) - { - code = Utility.GetIcon("code.png", "code@2x.png"); - } - return code; + return Utility.GetIcon("code.png", "code@2x.png"); } } @@ -911,11 +972,7 @@ public static Texture2D Rocket { get { - if (rocket == null) - { - rocket = Utility.GetIcon("rocket.png", "rocket@2x.png"); - } - return rocket; + return Utility.GetIcon("rocket.png", "rocket@2x.png"); } } @@ -923,11 +980,7 @@ public static Texture2D Merge { get { - if (merge == null) - { - merge = Utility.GetIcon("merge.png", "merge@2x.png"); - } - return merge; + return Utility.GetIcon("merge.png", "merge@2x.png"); } } @@ -935,12 +988,7 @@ public static Texture2D SpinnerInsideInverted { get { - if (spinnerInsideInverted == null) - { - spinnerInsideInverted = Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); - spinnerInsideInverted.InvertColors(); - } - return spinnerInsideInverted; + return Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png", true); } } @@ -948,12 +996,7 @@ public static Texture2D SpinnerOutsideInverted { get { - if (spinnerOutsideInverted == null) - { - spinnerOutsideInverted = Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); - spinnerOutsideInverted.InvertColors(); - } - return spinnerOutsideInverted; + return Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png", true); } } @@ -961,12 +1004,7 @@ public static Texture2D CodeInverted { get { - if (codeInverted == null) - { - codeInverted = Utility.GetIcon("code.png", "code@2x.png"); - codeInverted.InvertColors(); - } - return codeInverted; + return Utility.GetIcon("code.png", "code@2x.png", true); } } @@ -974,12 +1012,7 @@ public static Texture2D RocketInverted { get { - if (rocketInverted == null) - { - rocketInverted = Utility.GetIcon("rocket.png", "rocket@2x.png"); - rocketInverted.InvertColors(); - } - return rocketInverted; + return Utility.GetIcon("rocket.png", "rocket@2x.png", true); } } @@ -987,12 +1020,7 @@ public static Texture2D MergeInverted { get { - if (mergeInverted == null) - { - mergeInverted = Utility.GetIcon("merge.png", "merge@2x.png"); - mergeInverted.InvertColors(); - } - return mergeInverted; + return Utility.GetIcon("merge.png", "merge@2x.png", true); } } private static GUIStyle foldout; @@ -1088,5 +1116,39 @@ public static GUIStyle FocusedActiveTreeNode return focusedActiveTreeNode; } } + + private static GUIStyle lockPathStyle; + public static GUIStyle LockPathStyle + { + get + { + if (lockPathStyle == null) + { + lockPathStyle = new GUIStyle(GUI.skin.label); + lockPathStyle.name = "Custom LockPathStyle"; + + lockPathStyle.fontSize = 11; + } + + return lockPathStyle; + } + } + + private static GUIStyle lockMetaDataStyle; + public static GUIStyle LockMetaDataStyle + { + get + { + if (lockMetaDataStyle == null) + { + lockMetaDataStyle = new GUIStyle(GUI.skin.label); + lockMetaDataStyle.name = "Custom LockMetaDataStyle"; + + lockMetaDataStyle.fontSize = 10; + } + + return lockMetaDataStyle; + } + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs index e590d1d4f..c43c76893 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs @@ -5,34 +5,53 @@ using System.Reflection; using UnityEditor; using UnityEngine; +using System.Collections.Generic; namespace GitHub.Unity { class Utility : ScriptableObject { - public static Texture2D GetIcon(string filename, string filename2x = "") + private static Dictionary iconCache = new Dictionary(); + + public static bool IsDarkTheme { + get { + var defaultTextColor = Styles.Label.normal.textColor; + return defaultTextColor.r > 0.5f && defaultTextColor.g > 0.5f && defaultTextColor.b > 0.5f; + } + } + + public static Texture2D GetIcon(string filename, string filename2x = "", bool invertColors = false) { if (EditorGUIUtility.pixelsPerPoint > 1f && !string.IsNullOrEmpty(filename2x)) { filename = filename2x; } - Texture2D texture2D = null; + var key = invertColors ? "dark_" + filename : "light_" + filename; - var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("GitHub.Unity.IconsAndLogos." + filename); - if (stream != null) + if (iconCache.ContainsKey(key)) { - texture2D = stream.ToTexture2D(); + return iconCache[key]; } - else + + Texture2D texture2D = null; + + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("GitHub.Unity.IconsAndLogos." + filename); + if (stream == null) { - var iconPath = EntryPoint.Environment.ExtensionInstallPath.Combine("IconsAndLogos", filename).ToString(SlashMode.Forward); - texture2D = AssetDatabase.LoadAssetAtPath(iconPath); + stream = new MemoryStream(Application.dataPath.ToNPath().Combine("Editor/GitHub.Unity/IconsAndLogos/", filename).ReadAllBytes()); } + texture2D = stream.ToTexture2D(); + stream.Dispose(); + if (texture2D != null) { texture2D.hideFlags = HideFlags.HideAndDontSave; + if (invertColors) { + texture2D.InvertColors(); + } + iconCache.Add(key, texture2D); } return texture2D; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs index effeac2f0..a320c4438 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle("GitHub.Api")] -[assembly: AssemblyDescription("GitHub Api")] +[assembly: AssemblyTitle("GitHub.Unity")] +[assembly: AssemblyDescription("GitHub for Unity")] [assembly: Guid("add7a18b-dd2a-4c22-a2c1-488964eff30a")] diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs index 04f1c0026..42452077c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs @@ -10,20 +10,34 @@ namespace GitHub.Unity sealed class LocationAttribute : Attribute { public enum Location { PreferencesFolder, ProjectFolder, LibraryFolder, UserFolder } - public string filepath { get; set; } + + private string relativePath; + private Location location; + + private string filePath; + public string FilePath { + get { + if (filePath != null) return filePath; + + if (relativePath[0] == '/') + relativePath = relativePath.Substring(1); + + if (location == Location.PreferencesFolder) + filePath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath; + else if (location == Location.UserFolder) + filePath = EntryPoint.ApplicationManager.Environment.UserCachePath.Combine(relativePath).ToString(SlashMode.Forward); + else if (location == Location.LibraryFolder) + filePath = EntryPoint.ApplicationManager.Environment.UnityProjectPath.Combine("Library", "gfu", relativePath); + + return filePath; + } + } + public LocationAttribute(string relativePath, Location location) { Guard.ArgumentNotNullOrWhiteSpace(relativePath, "relativePath"); - - if (relativePath[0] == '/') - relativePath = relativePath.Substring(1); - - if (location == Location.PreferencesFolder) - filepath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath; - else if (location == Location.UserFolder) - filepath = EntryPoint.Environment.UserCachePath.Combine(relativePath).ToString(SlashMode.Forward); - else if (location == Location.LibraryFolder) - filepath = EntryPoint.Environment.UnityProjectPath.Combine("Library", "gfu", relativePath); + this.relativePath = relativePath; + this.location = location; } } @@ -99,7 +113,7 @@ protected virtual void Save(bool saveAsText) if (attr == null) return null; - return attr.filepath.ToNPath(); + return attr.FilePath.ToNPath(); } } } \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index cf6f12222..b756bfa94 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -1,4 +1,7 @@ using System; +using System.Text; +using System.Threading; +using GitHub.Logging; namespace GitHub.Unity { @@ -8,14 +11,18 @@ class AuthenticationService private LoginResult loginResultData; - public AuthenticationService(UriString host, IKeychain keychain, NPath nodeJsExecutablePath, NPath octorunExecutablePath) + public AuthenticationService(UriString host, IKeychain keychain, + IProcessManager processManager, ITaskManager taskManager, + IEnvironment environment + ) { - client = new ApiClient(host, keychain, EntryPoint.ApplicationManager.ProcessManager, EntryPoint.ApplicationManager.TaskManager, nodeJsExecutablePath, octorunExecutablePath); + client = new ApiClient(keychain, processManager, taskManager, environment, host); } + public HostAddress HostAddress { get { return client.HostAddress; } } + public void Login(string username, string password, Action twofaRequired, Action authResult) { - loginResultData = null; client.Login(username, password, r => { @@ -24,11 +31,53 @@ public void Login(string username, string password, Action twofaRequired }, authResult); } + public void LoginWithToken(string token, Action authResult) + { + client.LoginWithToken(token, authResult); + } + public void LoginWith2fa(string code) { if (loginResultData == null) throw new InvalidOperationException("Call Login() first"); client.ContinueLogin(loginResultData, code); } + + public void GetServerMeta(Action serverMeta, Action error) + { + loginResultData = null; + client.GetEnterpriseServerMeta(data => + { + serverMeta(data); + }, exception => { + error(exception.Message); + }); + } + + public Uri GetLoginUrl(string state) + { + var query = new StringBuilder(); + + query.Append("client_id="); + query.Append(Uri.EscapeDataString(ApplicationInfo.ClientId)); + query.Append("&redirect_uri="); + query.Append(Uri.EscapeDataString(OAuthCallbackManager.CallbackUrl.ToString())); + query.Append("&scope="); + query.Append(Uri.EscapeDataString("user,repo")); + query.Append("&state="); + query.Append(Uri.EscapeDataString(state)); + + var uri = new Uri(HostAddress.WebUri, "login/oauth/authorize"); + var uriBuilder = new UriBuilder(uri) + { + Query = query.ToString() + }; + return uriBuilder.Uri; + } + + public void LoginWithOAuthCode(string code, Action result) + { + client.CreateOAuthToken(code, result); + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Tools/MozRoots.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Tools/MozRoots.cs deleted file mode 100644 index b68d685b9..000000000 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Tools/MozRoots.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography; -using System.Text; -using Mono.Security.X509; - -namespace GitHub.Unity -{ - static class MozRoots - { - // this URL is recommended by https://bugzilla.mozilla.org/show_bug.cgi?id=1279952#c8 and is also used as basis for curl's https://curl.haxx.se/ca/cacert.pem bundle - private const string url = "https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt"; - private static string inputFile = null; // if we want to load from a file instead in the future, we can use this - - public static void Run() - { - Process(); - } - - private static byte[] DecodeOctalString(string s) - { - var pieces = s.Split('\\'); - var data = new byte[pieces.Length - 1]; - for (var i = 1; i < pieces.Length; i++) - { - data[i - 1] = (byte)((pieces[i][0] - '0' << 6) + (pieces[i][1] - '0' << 3) + (pieces[i][2] - '0')); - } - - return data; - } - - private static X509Certificate DecodeCertificate(string s) - { - var rawdata = DecodeOctalString(s); - return new X509Certificate(rawdata); - } - - private static Stream GetFile() - { - try - { - if (inputFile != null) - { - return File.OpenRead(inputFile); - } - else - { - var req = (HttpWebRequest)WebRequest.Create(url); - req.Timeout = 10000; - return req.GetResponse().GetResponseStream(); - } - } - catch - { - return null; - } - } - - private static X509CertificateCollection DecodeCollection() - { - var roots = new X509CertificateCollection(); - var sb = new StringBuilder(); - var processing = false; - - using (var s = GetFile()) - { - if (s == null) - { - return null; - } - - var sr = new StreamReader(s); - while (true) - { - var line = sr.ReadLine(); - if (line == null) - break; - - if (processing) - { - if (line.StartsWith("END")) - { - processing = false; - var root = DecodeCertificate(sb.ToString()); - roots.Add(root); - - sb = new StringBuilder(); - continue; - } - - sb.Append(line); - } - else - { - processing = line.StartsWith("CKA_VALUE MULTILINE_OCTAL"); - } - } - - return roots; - } - } - - private static int Process() - { - ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - if (sslPolicyErrors != SslPolicyErrors.None) - { - //Console.WriteLine("WARNING: Downloading the trusted certificate list couldn't be done securely (error: {0}), continuing anyway. If you're using mozroots to bootstrap Mono's trust store on a clean system this might be OK, otherwise it could indicate a network intrusion. Please ensure you're using a trusted network or move to cert-sync.", sslPolicyErrors); - } - - // this is very bad, but on a clean system without an existing trust store we don't really have a better option - return true; - }; - - var roots = DecodeCollection(); - if (roots == null) - { - return 1; - } - else if (roots.Count == 0) - { - return 0; - } - - var stores = X509StoreManager.CurrentUser; - var trusted = stores.TrustedRoot.Certificates; - var additions = 0; - foreach (var root in roots) - { - if (!trusted.Contains(root)) - { - stores.TrustedRoot.Import(root); - additions++; - } - } - - if (additions > 0) - { - //WriteLine("{0} new root certificates were added to your trust store.", additions); - } - - var removed = new X509CertificateCollection(); - foreach (var trust in trusted) - { - if (!roots.Contains(trust)) - { - removed.Add(trust); - } - } - - if (removed.Count > 0) - { - //WriteLine("{0} previously trusted certificates were removed.", removed.Count); - - foreach (var old in removed) - { - stores.TrustedRoot.Remove(old); - } - } - - //WriteLine("Import process completed.{0}", Environment.NewLine); - return 0; - } - } -} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index fd3dc96e7..dbd51c4c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Threading; using UnityEngine; using UnityEditor; @@ -9,266 +11,156 @@ class AuthenticationView : Subview { private static readonly Vector2 viewSize = new Vector2(290, 290); - private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; - private const string NeedAuthenticationMessage = "We need you to authenticate first"; private const string WindowTitle = "Authenticate"; - private const string UsernameLabel = "Username"; - private const string PasswordLabel = "Password"; - private const string TwofaLabel = "2FA Code"; - private const string LoginButton = "Sign in"; - private const string BackButton = "Back"; - private const string AuthTitle = "Sign in to GitHub"; - private const string TwofaTitle = "Two-Factor Authentication"; - private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; - private const string TwofaButton = "Verify"; - [SerializeField] private Vector2 scroll; - [SerializeField] private string username = string.Empty; - [SerializeField] private string two2fa = string.Empty; - [SerializeField] private string message; - [SerializeField] private string errorMessage; - [SerializeField] private bool need2fa; - - [NonSerialized] private bool isBusy; - [NonSerialized] private bool enterPressed; - [NonSerialized] private string password = string.Empty; - [NonSerialized] private AuthenticationService authenticationService; + [SerializeField] private SubTab changeTab = SubTab.GitHub; + [SerializeField] private SubTab activeTab = SubTab.GitHub; + [SerializeField] private GitHubAuthenticationView gitHubAuthenticationView; + [SerializeField] private GitHubEnterpriseAuthenticationView gitHubEnterpriseAuthenticationView; + [SerializeField] private bool hasGitHubDotComConnection; + [SerializeField] private bool hasGitHubEnterpriseConnection; public override void InitializeView(IView parent) { base.InitializeView(parent); - need2fa = isBusy = false; - message = errorMessage = null; Title = WindowTitle; Size = viewSize; - } - public void Initialize(Exception exception) - { - var usernameMismatchException = exception as TokenUsernameMismatchException; - if (usernameMismatchException != null) - { - message = CredentialsNeedRefreshMessage; - username = usernameMismatchException.CachedUsername; - } + gitHubAuthenticationView = gitHubAuthenticationView ?? new GitHubAuthenticationView(); + gitHubEnterpriseAuthenticationView = gitHubEnterpriseAuthenticationView ?? new GitHubEnterpriseAuthenticationView(); - var keychainEmptyException = exception as KeychainEmptyException; - if (keychainEmptyException != null) + try { - message = NeedAuthenticationMessage; + OAuthCallbackManager.Start(); } - - if (usernameMismatchException == null && keychainEmptyException == null) + catch (Exception ex) { - message = exception.Message; + Logger.Trace(ex, "Error Starting OAuthCallbackManager"); } - } - public override void OnGUI() - { - HandleEnterPressed(); + gitHubAuthenticationView.InitializeView(this); + gitHubEnterpriseAuthenticationView.InitializeView(this); - EditorGUIUtility.labelWidth = 90f; + hasGitHubDotComConnection = Platform.Keychain.Connections.Any(HostAddress.IsGitHubDotCom); + hasGitHubEnterpriseConnection = Platform.Keychain.Connections.Any(connection => !HostAddress.IsGitHubDotCom(connection)); - scroll = GUILayout.BeginScrollView(scroll); + if (hasGitHubDotComConnection) { - GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); - { - GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); - } - GUILayout.EndHorizontal(); - - GUILayout.BeginVertical(); - { - if (!need2fa) - { - OnGUILogin(); - } - else - { - OnGUI2FA(); - } - } - - GUILayout.EndVertical(); + changeTab = SubTab.GitHubEnterprise; + UpdateActiveTab(); } - GUILayout.EndScrollView(); } - - private void HandleEnterPressed() - { - if (Event.current.type != EventType.KeyDown) - return; - enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; - if (enterPressed) - Event.current.Use(); + public void Initialize(Exception exception) + { + } - private void OnGUILogin() + public override void OnGUI() { - EditorGUI.BeginDisabledGroup(isBusy); - { - ShowMessage(); - - EditorGUILayout.Space(); - - GUILayout.BeginHorizontal(); - { - username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); - } - GUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - - GUILayout.BeginHorizontal(); - { - password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); - } - GUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - - ShowErrorMessage(); - - GUILayout.Space(Styles.BaseSpacing + 3); - GUILayout.BeginHorizontal(); - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) - { - GUI.FocusControl(null); - isBusy = true; - AuthenticationService.Login(username, password, DoRequire2fa, DoResult); - } - } - GUILayout.EndHorizontal(); - } - EditorGUI.EndDisabledGroup(); + DoToolbarGUI(); + ActiveView.OnGUI(); } - - private void OnGUI2FA() + + public override bool IsBusy { - GUILayout.BeginVertical(); - { - GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); - GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); - - EditorGUI.BeginDisabledGroup(isBusy); - { - EditorGUILayout.Space(); - two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); - EditorGUILayout.Space(); - ShowErrorMessage(); - - GUILayout.BeginHorizontal(); - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button(BackButton)) - { - GUI.FocusControl(null); - Clear(); - } - - if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) - { - GUI.FocusControl(null); - isBusy = true; - AuthenticationService.LoginWith2fa(two2fa); - } - } - GUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - } - EditorGUI.EndDisabledGroup(); - } - GUILayout.EndVertical(); + get { return (gitHubAuthenticationView != null && gitHubAuthenticationView.IsBusy) || (gitHubEnterpriseAuthenticationView != null && gitHubEnterpriseAuthenticationView.IsBusy); } } - private void DoRequire2fa(string msg) + public override void OnDataUpdate() { - Logger.Trace("Starting 2FA - Message:\"{0}\"", msg); - - need2fa = true; - errorMessage = msg; - isBusy = false; - Redraw(); + base.OnDataUpdate(); + MaybeUpdateData(); } - private void Clear() + public override void Finish(bool result) { - need2fa = false; - errorMessage = null; - isBusy = false; - Redraw(); + OAuthCallbackManager.Stop(); + base.Finish(result); } - private void DoResult(bool success, string msg) + private void MaybeUpdateData() { - Logger.Trace("DoResult - Success:{0} Message:\"{1}\"", success, msg); - - isBusy = false; - - if (success == true) - { - Clear(); - Finish(true); - } - else - { - errorMessage = msg; - Redraw(); - } } - private void ShowMessage() + private static SubTab TabButton(SubTab tab, string title, SubTab currentTab) { - if (message != null) - { - EditorGUILayout.HelpBox(message, MessageType.Warning); - } + return GUILayout.Toggle(currentTab == tab, title, EditorStyles.toolbarButton) ? tab : currentTab; } - private void ShowErrorMessage() + private enum SubTab { - if (errorMessage != null) - { - EditorGUILayout.HelpBox(errorMessage, MessageType.Error); - } + None, + GitHub, + GitHubEnterprise } - private AuthenticationService AuthenticationService + private void DoToolbarGUI() { - get + GUILayout.BeginHorizontal(EditorStyles.toolbar); { - if (authenticationService == null) + EditorGUI.BeginChangeCheck(); { - UriString host; - if (Repository != null && Repository.CloneUrl != null && Repository.CloneUrl.IsValidUri) + EditorGUI.BeginDisabledGroup(hasGitHubDotComConnection || IsBusy); { - host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + changeTab = TabButton(SubTab.GitHub, "GitHub", changeTab); } - else + EditorGUI.EndDisabledGroup(); + + EditorGUI.BeginDisabledGroup(hasGitHubEnterpriseConnection || IsBusy); { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); + changeTab = TabButton(SubTab.GitHubEnterprise, "GitHub Enterprise", changeTab); } + EditorGUI.EndDisabledGroup(); + } - AuthenticationService = new AuthenticationService(host, Platform.Keychain, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); + if (EditorGUI.EndChangeCheck()) + { + UpdateActiveTab(); } - return authenticationService; + + GUILayout.FlexibleSpace(); } - set + EditorGUILayout.EndHorizontal(); + } + + private void UpdateActiveTab() + { + if (changeTab != activeTab) { - authenticationService = value; + var fromView = ActiveView; + activeTab = changeTab; + var toView = ActiveView; + SwitchView(fromView, toView); } } + private void SwitchView(Subview fromView, Subview toView) + { + GUI.FocusControl(null); - public override bool IsBusy + if (fromView != null) + fromView.OnDisable(); + toView.OnEnable(); + + // this triggers a repaint + Parent.Redraw(); + } + + private Subview ActiveView { - get { return isBusy; } + get + { + switch (activeTab) + { + case SubTab.GitHub: + return gitHubAuthenticationView; + case SubTab.GitHubEnterprise: + return gitHubEnterpriseAuthenticationView; + default: + throw new NotImplementedException(); + } + } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index 6089232a9..e0fe85c84 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -1,11 +1,13 @@ using GitHub.Logging; using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; namespace GitHub.Unity { - abstract class BaseWindow : EditorWindow, IView + public abstract class BaseWindow : EditorWindow, IView { [NonSerialized] private bool initialized = false; [NonSerialized] private IUser cachedUser; @@ -13,9 +15,13 @@ abstract class BaseWindow : EditorWindow, IView [NonSerialized] private bool initializeWasCalled; [NonSerialized] protected bool inLayout; + public BaseWindow() + { + RefreshEvents = new Dictionary(); + } + public virtual void Initialize(IApplicationManager applicationManager) { - //Logger.Trace("Initialize ApplicationManager:{0} Initialized:{1}", applicationManager, initialized); } public void InitializeWindow(IApplicationManager applicationManager, bool requiresRedraw = true) @@ -37,7 +43,6 @@ public virtual void Redraw() public virtual void Refresh() { - //Logger.Debug("Refresh"); } public virtual void Finish(bool result) @@ -45,14 +50,12 @@ public virtual void Finish(bool result) public virtual void Awake() { - //Logger.Trace("Awake Initialized:{0}", initialized); if (!initialized) InitializeWindow(EntryPoint.ApplicationManager, false); } public virtual void OnEnable() { - //Logger.Trace("OnEnable Initialized:{0}", initialized); if (!initialized) InitializeWindow(EntryPoint.ApplicationManager, false); } @@ -116,9 +119,46 @@ public virtual void OnDestroy() public virtual void OnSelectionChange() {} + public virtual void DoneRefreshing() + { + IsRefreshing = false; + } + + public void Refresh(CacheType type) + { + if (Repository == null) + return; + + IsRefreshing = true; + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + RefreshEvents[type]++; + Repository.Refresh(type); + } + + public void ReceivedEvent(CacheType type) + { + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + var val = RefreshEvents[type] - 1; + RefreshEvents[type] = val > -1 ? val : 0; + if (IsRefreshing && !RefreshEvents.Values.Any(x => x > 0)) + { + DoneRefreshing(); + } + } + + public virtual void DoEmptyGUI() + {} + public virtual void DoProgressGUI() + {} + public virtual void UpdateProgress(IProgress progress) + {} + public Rect Position { get { return position; } } public IApplicationManager Manager { get; private set; } public abstract bool IsBusy { get; } + public bool IsRefreshing { get; private set; } public bool HasFocus { get; private set; } public IRepository Repository { get { return inLayout ? cachedRepository : Environment.Repository; } } public bool HasRepository { get { return Repository != null; } } @@ -129,6 +169,7 @@ public virtual void OnSelectionChange() protected IGitClient GitClient { get { return Manager.GitClient; } } protected IEnvironment Environment { get { return Manager.Environment; } } protected IPlatform Platform { get { return Manager.Platform; } } + public Dictionary RefreshEvents { get; set; } private ILogging logger; protected ILogging Logger { @@ -140,4 +181,4 @@ protected ILogging Logger } } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 6d03f5290..70d1d0c56 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -92,6 +92,13 @@ public override void OnDisable() DetachHandlers(Repository); } + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.Branches); + Refresh(CacheType.RepositoryInfo); + } + public override void OnDataUpdate() { base.OnDataUpdate(); @@ -119,6 +126,7 @@ private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpd { if (!lastCurrentBranchAndRemoteChange.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastCurrentBranchAndRemoteChange = cacheUpdateEvent; currentBranchAndRemoteChangeHasUpdate = true; Redraw(); @@ -130,6 +138,7 @@ private void RepositoryOnLocalAndRemoteBranchListChanged(CacheUpdateEvent cacheU { if (!lastLocalAndRemoteBranchListChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastLocalAndRemoteBranchListChangedEvent = cacheUpdateEvent; localAndRemoteBranchListHasUpdate = true; Redraw(); @@ -212,6 +221,7 @@ private void Render() Redraw(); } } + DoProgressGUI(); } private void BuildTree() @@ -304,11 +314,12 @@ private void OnButtonBarGUI() // Effectuate create if (createBranch) { - GitClient.CreateBranch(newBranchName, treeLocals.SelectedNode.Path) + Repository.CreateBranch(newBranchName, treeLocals.SelectedNode.Path) .FinallyInUI((success, e) => { if (success) { + UsageTracker.IncrementBranchesViewButtonCreateBranch(); Redraw(); } else @@ -473,17 +484,20 @@ private void CheckoutRemoteBranch(string branch) if (confirmCheckout) { - GitClient.CreateBranch(branchName, branch).FinallyInUI((success, e) => { - if (success) - { - Redraw(); - } - else + Repository.CreateBranch(branchName, branch) + .FinallyInUI((success, e) => { - EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); - } - }).Start(); + if (success) + { + UsageTracker.IncrementBranchesViewButtonCheckoutRemoteBranch(); + Redraw(); + } + else + { + EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, + String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); + } + }).Start(); } } } @@ -493,17 +507,21 @@ private void SwitchBranch(string branch) if (EditorUtility.DisplayDialog(ConfirmSwitchTitle, String.Format(ConfirmSwitchMessage, branch), ConfirmSwitchOK, ConfirmSwitchCancel)) { - GitClient.SwitchBranch(branch).FinallyInUI((success, e) => { - if (success) - { - Redraw(); - } - else + Repository.SwitchBranch(branch) + .FinallyInUI((success, e) => { - EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); - } - }).Start(); + if (success) + { + UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch(); + Redraw(); + AssetDatabase.Refresh(); + } + else + { + EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, + String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); + } + }).Start(); } } @@ -512,7 +530,9 @@ private void DeleteLocalBranch(string branch) var dialogMessage = string.Format(DeleteBranchMessageFormatString, branch); if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { - GitClient.DeleteBranch(branch, true).Start(); + Repository.DeleteBranch(branch, true) + .Finally(s => { if (s) UsageTracker.IncrementBranchesViewButtonDeleteBranch(); } ) + .Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs index 36e818c5c..9554e2b55 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs @@ -29,7 +29,7 @@ public GitStatusEntry GitStatusEntry } public string ProjectPath { get { return GitStatusEntry.projectPath; } } - public GitFileStatus GitFileStatus { get { return GitStatusEntry.status; } } + public GitFileStatus GitFileStatus { get { return GitStatusEntry.Status; } } } [Serializable] @@ -163,17 +163,13 @@ protected Texture GetNodeIcon(ChangesTreeNode node) { if (!string.IsNullOrEmpty(node.ProjectPath)) { - nodeIcon = AssetDatabase.GetCachedIcon(node.ProjectPath); + nodeIcon = UnityEditorInternal.InternalEditorUtility.GetIconForFile(node.ProjectPath); } if (nodeIcon != null) { nodeIcon.hideFlags = HideFlags.HideAndDontSave; } - else - { - nodeIcon = Styles.DefaultAssetIcon; - } } return nodeIcon; @@ -190,7 +186,7 @@ protected Texture GetNodeIconBadge(ChangesTreeNode node) return Styles.GetFileStatusIcon(gitFileStatus, node.IsLocked); } - protected override ChangesTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitStatusEntryTreeData? treeData, bool isContainer) + protected override ChangesTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitStatusEntryTreeData? treeData) { var gitStatusEntry = GitStatusEntry.Default; var isLocked = false; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 9f89aeb67..0d6235b42 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -18,12 +18,12 @@ class ChangesView : Subview private const string OneChangedFileLabel = "1 changed file"; private const string NoChangedFilesLabel = "No changed files"; - [NonSerialized] private bool currentBranchHasUpdate; - [NonSerialized] private bool currentStatusEntriesHasUpdate; - [NonSerialized] private bool currentLocksHasUpdate; - [NonSerialized] private bool isBusy; + [SerializeField] private bool currentBranchHasUpdate; + [SerializeField] private bool currentStatusEntriesHasUpdate; + [SerializeField] private bool currentLocksHasUpdate; [NonSerialized] private GUIContent discardGuiContent; + [NonSerialized] private bool isBusy; [SerializeField] private string commitBody = ""; [SerializeField] private string commitMessage = ""; @@ -32,8 +32,8 @@ class ChangesView : Subview [SerializeField] private Vector2 treeScroll; [SerializeField] private ChangesTree treeChanges = new ChangesTree { DisplayRootNode = false, IsCheckable = true, IsUsingGlobalSelection = true }; - [SerializeField] private HashSet gitLocks; - [SerializeField] private List gitStatusEntries; + [SerializeField] private HashSet gitLocks = new HashSet(); + [SerializeField] private List gitStatusEntries = new List(); [SerializeField] private string changedFilesText = NoChangedFilesLabel; @@ -61,6 +61,14 @@ public override void OnDisable() DetachHandlers(Repository); } + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.GitStatus); + Refresh(CacheType.RepositoryInfo); + Refresh(CacheType.GitLocks); + } + public override void OnDataUpdate() { base.OnDataUpdate(); @@ -69,43 +77,26 @@ public override void OnDataUpdate() public override void OnGUI() { - GUILayout.BeginHorizontal(); + DoButtonBarGUI(); + if (gitStatusEntries.Count == 0) { - EditorGUI.BeginDisabledGroup(gitStatusEntries == null || !gitStatusEntries.Any()); - { - if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft)) - { - SelectAll(); - } - - if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight)) - { - SelectNone(); - } - } - EditorGUI.EndDisabledGroup(); - - GUILayout.FlexibleSpace(); - - GUILayout.Label(changedFilesText, EditorStyles.miniLabel); + GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + DoEmptyGUI(); + GUILayout.EndVertical(); } - GUILayout.EndHorizontal(); - - var rect = GUILayoutUtility.GetLastRect(); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + else { - treeScroll = GUILayout.BeginScrollView(treeScroll); - { - OnTreeGUI(new Rect(0f, 0f, Position.width, Position.height - rect.height + Styles.CommitAreaPadding)); - } - GUILayout.EndScrollView(); + EditorGUI.BeginDisabledGroup(isBusy); + DoChangesTreeGUI(); + EditorGUI.EndDisabledGroup(); } - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); + EditorGUI.BeginDisabledGroup(isBusy); + + DoProgressGUI(); // Do the commit details area - OnCommitDetailsAreaGUI(); + DoCommitGUI(); + EditorGUI.EndDisabledGroup(); } public override void OnSelectionChange() @@ -119,8 +110,6 @@ public override void OnSelectionChange() public override void OnFocusChanged() { - Logger.Debug("OnFocusChanged: {0}", HasFocus); - base.OnFocusChanged(); var hasFocus = HasFocus; if (treeChanges.ViewHasFocus != hasFocus) @@ -130,6 +119,47 @@ public override void OnFocusChanged() } } + private void DoChangesTreeGUI() + { + var rect = GUILayoutUtility.GetLastRect(); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + { + treeScroll = GUILayout.BeginScrollView(treeScroll); + { + OnTreeGUI(new Rect(0f, 0f, Position.width, Position.height - rect.height + Styles.CommitAreaPadding)); + } + GUILayout.EndScrollView(); + } + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + private void DoButtonBarGUI() + { + GUILayout.BeginHorizontal(); + { + EditorGUI.BeginDisabledGroup(gitStatusEntries == null || gitStatusEntries.Count == 0); + { + if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft)) + { + SelectAll(); + } + + if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight)) + { + SelectNone(); + } + } + EditorGUI.EndDisabledGroup(); + + GUILayout.FlexibleSpace(); + + GUILayout.Label(changedFilesText, EditorStyles.miniLabel); + } + GUILayout.EndHorizontal(); + } + private void OnTreeGUI(Rect rect) { if (treeChanges != null) @@ -159,12 +189,47 @@ private GenericMenu CreateContextMenu(ChangesTreeNode node) { var genericMenu = new GenericMenu(); + genericMenu.AddItem(new GUIContent("Show Diff"), false, () => + { + ITask calculateDiff = null; + if (node.IsFolder) + { + calculateDiff = CalculateFolderDiff(node); + } + else + { + calculateDiff = CalculateFileDiff(node); + } + calculateDiff.FinallyInUI((s, ex, leftRight) => + { + if (s) + EditorUtility.InvokeDiffTool( + leftRight[0].IsInitialized ? leftRight[0].FileName : null, + leftRight[0].IsInitialized ? leftRight[0].MakeAbsolute().ToString() : null, + leftRight[1].IsInitialized ? leftRight[1].FileName : null, + leftRight[1].IsInitialized ? leftRight[1].MakeAbsolute().ToString() : null, + null, null); + else + throw ex; + }) + .Start(); + }); + + genericMenu.AddSeparator(""); + if (discardGuiContent == null) { discardGuiContent = new GUIContent("Discard"); } - genericMenu.AddItem(discardGuiContent, false, () => { + genericMenu.AddItem(discardGuiContent, false, () => + { + if (!EditorUtility.DisplayDialog(Localization.DiscardConfirmTitle, + Localization.DiscardConfirmDescription, + Localization.DiscardConfirmYes, + Localization.Cancel)) + return; + GitStatusEntry[] discardEntries; if (node.isFolder) { @@ -179,16 +244,76 @@ private GenericMenu CreateContextMenu(ChangesTreeNode node) } Repository.DiscardChanges(discardEntries) + .ThenInUI(AssetDatabase.Refresh) .Start(); }); return genericMenu; } + private ITask CalculateFolderDiff(ChangesTreeNode node) + { + var rightFile = node.Path.ToNPath(); + var tmpDir = Manager.Environment.UnityProjectPath.Combine("Temp").CreateTempDirectory(); + var changedFiles = treeChanges.GetLeafNodes(node).Select(x => x.Path.ToNPath()).ToList(); + return new FuncTask, NPath[]>(TaskManager.Token, (s, files) => + { + var leftFolder = tmpDir.Combine("left", rightFile.FileName); + var rightFolder = tmpDir.Combine("right", rightFile.FileName); + foreach (var file in files) + { + var txt = new SimpleProcessTask(TaskManager.Token, "show HEAD:\"" + file.ToString(SlashMode.Forward) + "\"") + .Configure(Manager.ProcessManager, false) + .Catch(_ => true) + .RunSynchronously(); + if (txt != null) + leftFolder.Combine(file.RelativeTo(rightFile)).WriteAllText(txt); + if (file.FileExists()) + rightFolder.Combine(file.RelativeTo(rightFile)).WriteAllText(file.ReadAllText()); + } + return new NPath[] { leftFolder, rightFolder }; + }, () => changedFiles) { Message = "Calculating diff..." }; + } + + private ITask CalculateFileDiff(ChangesTreeNode node) + { + var rightFile = node.Path.ToNPath(); + var tmpDir = Manager.Environment.UnityProjectPath.Combine("Temp", "ghu-diffs").EnsureDirectoryExists(); + var leftFile = tmpDir.Combine(rightFile.FileName + "_" + Repository.CurrentHead + rightFile.ExtensionWithDot); + return new SimpleProcessTask(TaskManager.Token, "show HEAD:\"" + rightFile.ToString(SlashMode.Forward) + "\"") + .Configure(Manager.ProcessManager, false) + .Catch(_ => true) + .Then((success, txt) => + { + // both files exist, just compare them + if (success && rightFile.FileExists()) + { + leftFile.WriteAllText(txt); + return new NPath[] { leftFile, rightFile }; + } + + var leftFolder = tmpDir.Combine("left", leftFile.FileName).EnsureDirectoryExists(); + var rightFolder = tmpDir.Combine("right", leftFile.FileName).EnsureDirectoryExists(); + // file was deleted + if (!rightFile.FileExists()) + { + leftFolder.Combine(rightFile).WriteAllText(txt); + } + + // file was created + if (!success) + { + rightFolder.Combine(rightFile).WriteAllText(rightFile.ReadAllText()); + } + return new NPath[] { leftFolder, rightFolder }; + }); + } + private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastStatusEntriesChangedEvent = cacheUpdateEvent; currentStatusEntriesHasUpdate = true; Redraw(); @@ -199,6 +324,7 @@ private void RepositoryOnCurrentBranchChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastCurrentBranchChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastCurrentBranchChangedEvent = cacheUpdateEvent; currentBranchHasUpdate = true; Redraw(); @@ -209,6 +335,7 @@ private void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastLocksChangedEvent = cacheUpdateEvent; currentLocksHasUpdate = true; Redraw(); @@ -254,12 +381,13 @@ private void MaybeUpdateData() currentBranch = string.Format("[{0}]", Repository.CurrentBranchName); } - if (currentStatusEntriesHasUpdate || currentLocksHasUpdate) + if (currentLocksHasUpdate) { - currentStatusEntriesHasUpdate = false; - currentLocksHasUpdate = false; + gitLocks = new HashSet(Repository.CurrentLocks.Select(gitLock => gitLock.Path)); + } - gitLocks = new HashSet(Repository.CurrentLocks.Select(gitLock => gitLock.Path)); + if (currentStatusEntriesHasUpdate) + { gitStatusEntries = Repository.CurrentChanges.Where(x => x.Status != GitFileStatus.Ignored).ToList(); changedFilesText = gitStatusEntries.Count == 0 @@ -267,6 +395,12 @@ private void MaybeUpdateData() : gitStatusEntries.Count == 1 ? OneChangedFileLabel : String.Format(ChangedFilesLabel, gitStatusEntries.Count); + } + + if (currentStatusEntriesHasUpdate || currentLocksHasUpdate) + { + currentStatusEntriesHasUpdate = false; + currentLocksHasUpdate = false; BuildTree(); } @@ -275,11 +409,11 @@ private void MaybeUpdateData() private void BuildTree() { treeChanges.PathSeparator = Environment.FileSystem.DirectorySeparatorChar.ToString(); - treeChanges.Load(gitStatusEntries.Select(entry => new GitStatusEntryTreeData(entry, gitLocks.Contains(entry.Path)))); + treeChanges.Load(gitStatusEntries.Select(entry => new GitStatusEntryTreeData(entry, gitLocks.Contains(entry.Path.ToNPath())))); Redraw(); } - private void OnCommitDetailsAreaGUI() + private void DoCommitGUI() { GUILayout.BeginHorizontal(); { @@ -304,6 +438,8 @@ private void OnCommitDetailsAreaGUI() GUILayout.Space(Styles.CommitAreaPadding); // Disable committing when already committing or if we don't have all the data needed + //Debug.LogFormat("IsBusy:{0} string.IsNullOrEmpty(commitMessage): {1} treeChanges.GetCheckedFiles().Any(): {2}", + // IsBusy, string.IsNullOrEmpty(commitMessage), treeChanges.GetCheckedFiles().Any()); EditorGUI.BeginDisabledGroup(IsBusy || string.IsNullOrEmpty(commitMessage) || !treeChanges.GetCheckedFiles().Any()); { GUILayout.BeginHorizontal(); @@ -340,8 +476,9 @@ private void SelectNone() private void Commit() { + isBusy = true; var files = treeChanges.GetCheckedFiles().ToList(); - ITask addTask; + ITask addTask = null; if (files.Count == gitStatusEntries.Count) { @@ -349,15 +486,33 @@ private void Commit() } else { - addTask = Repository.CommitFiles(files, commitMessage, commitBody); + ITask commit = Repository.CommitFiles(files, commitMessage, commitBody); + + // if there are files that have been staged outside of Unity, but they aren't selected for commit, remove them + // from the index before commiting, otherwise the commit will take them along. + var filesStagedButNotChecked = gitStatusEntries.Where(x => x.Staged).Select(x => x.Path).Except(files).ToList(); + if (filesStagedButNotChecked.Count > 0) + addTask = GitClient.Remove(filesStagedButNotChecked); + addTask = addTask == null ? commit : addTask.Then(commit); } addTask - .FinallyInUI((b, exception) => + .FinallyInUI((success, exception) => { - commitMessage = ""; - commitBody = ""; + if (success) + { + UsageTracker.IncrementChangesViewButtonCommit(); + + commitMessage = ""; + commitBody = ""; + } + isBusy = false; }).Start(); } + + public override bool IsBusy + { + get { return isBusy || base.IsBusy; } + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs new file mode 100644 index 000000000..43e32161c --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs @@ -0,0 +1,241 @@ +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace GitHub.Unity +{ + public class FileHistoryWindow : BaseWindow + { + [MenuItem("Assets/Git/History", false)] + private static void GitFileHistory() + { + if (Selection.assetGUIDs != null) + { + var assetPath = + AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs.First()) + .ToNPath(); + + var windowType = typeof(Window); + var fileHistoryWindow = GetWindow(windowType); + fileHistoryWindow.InitializeWindow(EntryPoint.ApplicationManager); + fileHistoryWindow.SetSelectedPath(assetPath); + fileHistoryWindow.Show(); + } + } + + [MenuItem("Assets/Git/History", true)] + private static bool GitFileHistoryValidation() + { + return Selection.assetGUIDs != null && Selection.assetGUIDs.Length > 0; + } + + private const string Title = "File History"; + + [NonSerialized] private bool firstOnGUI = true; + [NonSerialized] private Texture selectedIcon; + + [SerializeField] private bool locked; + [SerializeField] private FileHistoryView fileHistoryView = new FileHistoryView(); + [SerializeField] private UnityEngine.Object selectedObject; + [SerializeField] private string selectedObjectAssetPath; + [SerializeField] private string selectedObjectRepositoryPath; + + public void SetSelectedPath(string assetPath) + { + selectedObject = null; + selectedObjectAssetPath = null; + selectedObjectRepositoryPath = null; + + if (selectedObjectAssetPath != NPath.Default) + { + selectedObjectAssetPath = assetPath; + selectedObject = AssetDatabase.LoadMainAssetAtPath(selectedObjectAssetPath); + + selectedObjectRepositoryPath = + Environment.GetRepositoryPath(assetPath.ToNPath()) + .ToString(SlashMode.Forward); + } + + LoadSelectedIcon(); + + Repository.UpdateFileLog(selectedObjectRepositoryPath) + .Start(); + } + + public override void Initialize(IApplicationManager applicationManager) + { + base.Initialize(applicationManager); + + fileHistoryView.InitializeView(this); + } + + public override bool IsBusy + { + get { return false; } + } + + public override void OnEnable() + { + base.OnEnable(); + + LoadSelectedIcon(); + + if (fileHistoryView != null) + fileHistoryView.OnEnable(); + } + + public override void OnDisable() + { + base.OnDisable(); + if (fileHistoryView != null) + fileHistoryView.OnDisable(); + } + + public override void OnDataUpdate() + { + base.OnDataUpdate(); + MaybeUpdateData(); + + if (fileHistoryView != null) + fileHistoryView.OnDataUpdate(); + } + + public override void OnRepositoryChanged(IRepository oldRepository) + { + base.OnRepositoryChanged(oldRepository); + + DetachHandlers(oldRepository); + AttachHandlers(Repository); + } + + public override void OnSelectionChange() + { + base.OnSelectionChange(); + if (fileHistoryView != null) + fileHistoryView.OnSelectionChange(); + + if (!locked) + { + selectedObject = Selection.activeObject; + + string assetPath = null; + if (selectedObject != null) + { + assetPath = AssetDatabase.GetAssetPath(selectedObject); + } + + SetSelectedPath(assetPath); + } + } + + public override void Refresh() + { + base.Refresh(); + if (fileHistoryView != null) + fileHistoryView.Refresh(); + Refresh(CacheType.GitFileLog); + Redraw(); + } + + public override void OnUI() + { + base.OnUI(); + + if (selectedObject != null) + { + GUILayout.BeginVertical(Styles.HeaderStyle); + { + DoHeaderGUI(); + + fileHistoryView.OnGUI(); + } + GUILayout.EndVertical(); + } + } + + private void MaybeUpdateData() + { + if (firstOnGUI) + { + titleContent = new GUIContent(Title, Styles.SmallLogo); + } + firstOnGUI = false; + } + + private void AttachHandlers(IRepository repository) + { + if (repository == null) + return; + } + + private void DetachHandlers(IRepository repository) + { + if (repository == null) + return; + } + + private void LoadSelectedIcon() + { + Texture nodeIcon = null; + + if (!string.IsNullOrEmpty(selectedObjectAssetPath)) + { + if (selectedObjectAssetPath.ToNPath().DirectoryExists()) + { + nodeIcon = Styles.FolderIcon; + } + else + { + nodeIcon = UnityEditorInternal.InternalEditorUtility.GetIconForFile(selectedObjectAssetPath); + } + + nodeIcon.hideFlags = HideFlags.HideAndDontSave; + } + + selectedIcon = nodeIcon; + } + + private void ShowButton(Rect rect) + { + EditorGUI.BeginChangeCheck(); + + locked = GUI.Toggle(rect, locked, GUIContent.none, Styles.LockButtonStyle); + + if (!EditorGUI.EndChangeCheck()) + return; + + this.OnSelectionChange(); + } + + private void DoHeaderGUI() + { + GUILayout.BeginHorizontal(Styles.HeaderBoxStyle); + { + var iconWidth = 32; + var iconHeight = 32; + + GUILayout.Label(selectedIcon, GUILayout.Height(iconWidth), GUILayout.Width(iconHeight)); + GUILayout.Space(16); + + GUILayout.BeginVertical(); + { + GUILayout.Label(selectedObjectAssetPath, Styles.FileHistoryLogTitleStyle); + + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Show in Project")) + { + EditorGUIUtility.PingObject(selectedObject); + } + } + GUILayout.EndHorizontal(); + } + GUILayout.EndVertical(); + } + GUILayout.EndHorizontal(); + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs new file mode 100644 index 000000000..921d190b0 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -0,0 +1,310 @@ +using System; +using System.Threading; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + [Serializable] + class GitHubAuthenticationView : Subview + { + private static readonly Vector2 viewSize = new Vector2(290, 290); + + private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; + private const string NeedAuthenticationMessage = "We need you to authenticate first"; + private const string WindowTitle = "Authenticate"; + private const string UsernameLabel = "Username"; + private const string PasswordLabel = "Password"; + private const string TwofaLabel = "2FA Code"; + private const string LoginButton = "Sign in"; + private const string BackButton = "Back"; + private const string AuthTitle = "Sign in to GitHub"; + private const string TwofaTitle = "Two-Factor Authentication"; + private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; + private const string TwofaButton = "Verify"; + + [SerializeField] private Vector2 scroll; + [SerializeField] private string username = string.Empty; + [SerializeField] private string two2fa = string.Empty; + [SerializeField] private string message; + [SerializeField] private string errorMessage; + [SerializeField] private bool need2fa; + + [NonSerialized] private bool isBusy; + [NonSerialized] private bool enterPressed; + [NonSerialized] private string password = string.Empty; + [NonSerialized] private AuthenticationService authenticationService; + [NonSerialized] private string oAuthState; + [NonSerialized] private string oAuthOpenUrl; + + public override void InitializeView(IView parent) + { + Logger.Trace("InitializeView"); + + base.InitializeView(parent); + need2fa = isBusy = false; + message = errorMessage = null; + Title = WindowTitle; + Size = viewSize; + + oAuthState = Guid.NewGuid().ToString(); + oAuthOpenUrl = AuthenticationService.GetLoginUrl(oAuthState).ToString(); + + OAuthCallbackManager.OnCallback += OnOAuthCallback; + } + + public void Initialize(Exception exception) + { + var usernameMismatchException = exception as TokenUsernameMismatchException; + if (usernameMismatchException != null) + { + message = CredentialsNeedRefreshMessage; + username = usernameMismatchException.CachedUsername; + } + + var keychainEmptyException = exception as KeychainEmptyException; + if (keychainEmptyException != null) + { + message = NeedAuthenticationMessage; + } + + if (usernameMismatchException == null && keychainEmptyException == null) + { + message = exception.Message; + } + } + + public override void OnGUI() + { + HandleEnterPressed(); + + EditorGUIUtility.labelWidth = 90f; + + scroll = GUILayout.BeginScrollView(scroll); + { + GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); + { + GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginVertical(); + { + if (!need2fa) + { + OnGUILogin(); + } + else + { + OnGUI2FA(); + } + } + + GUILayout.EndVertical(); + } + GUILayout.EndScrollView(); + } + + private void HandleEnterPressed() + { + if (Event.current.type != EventType.KeyDown) + return; + + enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; + if (enterPressed) + Event.current.Use(); + } + + private void OnGUILogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + username = EditorGUILayout.TextField(UsernameLabel, username, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + AuthenticationService.Login(username, password, DoRequire2fa, DoResult); + } + } + GUILayout.EndHorizontal(); + + if (OAuthCallbackManager.IsRunning) + { + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Sign in with your browser", Styles.HyperlinkStyle)) + { + GUI.FocusControl(null); + Application.OpenURL(oAuthOpenUrl); + } + } + GUILayout.EndHorizontal(); + } + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUI2FA() + { + GUILayout.BeginVertical(); + { + GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); + GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); + + EditorGUI.BeginDisabledGroup(isBusy); + { + EditorGUILayout.Space(); + two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); + EditorGUILayout.Space(); + ShowErrorMessage(); + + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(BackButton)) + { + GUI.FocusControl(null); + Clear(); + } + + if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + AuthenticationService.LoginWith2fa(two2fa); + } + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + } + EditorGUI.EndDisabledGroup(); + } + GUILayout.EndVertical(); + } + + private void OnOAuthCallback(string state, string code) + { + if (state.Equals(oAuthState)) + { + isBusy = true; + authenticationService.LoginWithOAuthCode(code, (b, s) => TaskManager.RunInUI(() => DoOAuthCodeResult(b, s))); + } + } + + private void DoRequire2fa(string msg) + { + need2fa = true; + errorMessage = msg; + isBusy = false; + Redraw(); + } + + private void Clear() + { + need2fa = false; + errorMessage = null; + isBusy = false; + Redraw(); + } + + private void DoResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void DoOAuthCodeResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void ShowMessage() + { + if (message != null) + { + EditorGUILayout.HelpBox(message, MessageType.Warning); + } + } + + private void ShowErrorMessage() + { + if (errorMessage != null) + { + EditorGUILayout.HelpBox(errorMessage, MessageType.Error); + } + } + + private AuthenticationService AuthenticationService + { + get + { + if (authenticationService == null) + { + AuthenticationService = new AuthenticationService(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + } + return authenticationService; + } + set + { + authenticationService = value; + } + } + + public override bool IsBusy + { + get { return isBusy; } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs new file mode 100644 index 000000000..29f2e93e7 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -0,0 +1,467 @@ +using System; +using System.Threading; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + [Serializable] + class GitHubEnterpriseAuthenticationView : Subview + { + private static readonly Vector2 viewSize = new Vector2(290, 290); + + private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; + private const string NeedAuthenticationMessage = "We need you to authenticate first"; + private const string WindowTitle = "Authenticate"; + private const string ServerAddressLabel = "Server Address"; + private const string TokenLabel = "Token"; + private const string UsernameLabel = "Username"; + private const string PasswordLabel = "Password"; + private const string TwofaLabel = "2FA Code"; + private const string LoginButton = "Sign in"; + private const string BackButton = "Back"; + private const string AuthTitle = "Sign in to GitHub Enterprise"; + private const string TwofaTitle = "Two-Factor Authentication"; + private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; + private const string TwofaButton = "Verify"; + + [SerializeField] private Vector2 scroll; + [SerializeField] private string serverAddress = string.Empty; + [SerializeField] private string username = string.Empty; + [SerializeField] private string two2fa = string.Empty; + [SerializeField] private string message; + [SerializeField] private string errorMessage; + [SerializeField] private bool need2fa; + [SerializeField] private bool hasServerMeta; + [SerializeField] private bool verifiablePasswordAuthentication; + + [NonSerialized] private bool isBusy; + [NonSerialized] private bool enterPressed; + [NonSerialized] private string password = string.Empty; + [NonSerialized] private string token = string.Empty; + [NonSerialized] private AuthenticationService authenticationService; + [NonSerialized] private string oAuthState; + [NonSerialized] private string oAuthOpenUrl; + + public override void InitializeView(IView parent) + { + base.InitializeView(parent); + need2fa = isBusy = false; + message = errorMessage = null; + Title = WindowTitle; + Size = viewSize; + + OAuthCallbackManager.OnCallback += OnOAuthCallback; + } + + public void Initialize(Exception exception) + { + var usernameMismatchException = exception as TokenUsernameMismatchException; + if (usernameMismatchException != null) + { + message = CredentialsNeedRefreshMessage; + username = usernameMismatchException.CachedUsername; + } + + var keychainEmptyException = exception as KeychainEmptyException; + if (keychainEmptyException != null) + { + message = NeedAuthenticationMessage; + } + + if (usernameMismatchException == null && keychainEmptyException == null) + { + message = exception.Message; + } + } + + public override void OnGUI() + { + HandleEnterPressed(); + + EditorGUIUtility.labelWidth = 90f; + + scroll = GUILayout.BeginScrollView(scroll); + { + GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); + { + GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginVertical(); + { + if (!hasServerMeta) + { + OnGUIHost(); + } + else + { + EditorGUILayout.Space(); + + EditorGUI.BeginDisabledGroup(true); + { + GUILayout.BeginHorizontal(); + { + serverAddress = EditorGUILayout.TextField(ServerAddressLabel, serverAddress, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + + if (!need2fa) + { + if (verifiablePasswordAuthentication) + { + OnGUIUserPasswordLogin(); + } + else + { + OnGUITokenLogin(); + } + + if (OAuthCallbackManager.IsRunning) + { + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Sign in with your browser", Styles.HyperlinkStyle)) + { + GUI.FocusControl(null); + Application.OpenURL(oAuthOpenUrl); + } + } + GUILayout.EndHorizontal(); + } + } + else + { + OnGUI2FA(); + } + } + } + + GUILayout.EndVertical(); + } + GUILayout.EndScrollView(); + } + + private void HandleEnterPressed() + { + if (Event.current.type != EventType.KeyDown) + return; + + enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; + if (enterPressed) + Event.current.Use(); + } + + private void OnGUIHost() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + serverAddress = EditorGUILayout.TextField(ServerAddressLabel, serverAddress, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + errorMessage = null; + isBusy = true; + + GetAuthenticationService(serverAddress) + .GetServerMeta(DoServerMetaResult, DoServerMetaError); + + Redraw(); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUIUserPasswordLogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Back")) + { + GUI.FocusControl(null); + BackToGetServerMeta(); + } + + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .Login(username, password, DoRequire2fa, DoResult); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void BackToGetServerMeta() + { + hasServerMeta = false; + oAuthOpenUrl = null; + oAuthState = null; + Redraw(); + } + + private void OnGUITokenLogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + token = EditorGUILayout.TextField(TokenLabel, token, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Back")) + { + GUI.FocusControl(null); + BackToGetServerMeta(); + } + + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .LoginWithToken(token, DoTokenResult); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUI2FA() + { + GUILayout.BeginVertical(); + { + GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); + GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); + + EditorGUI.BeginDisabledGroup(isBusy); + { + EditorGUILayout.Space(); + two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); + + ShowErrorMessage(); + + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(BackButton)) + { + GUI.FocusControl(null); + Clear(); + } + + if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .LoginWith2fa(two2fa); + } + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + } + EditorGUI.EndDisabledGroup(); + } + GUILayout.EndVertical(); + } + + private void OnOAuthCallback(string state, string code) + { + TaskManager.RunInUI(() => { + if (state.Equals(oAuthState)) + { + isBusy = true; + authenticationService.LoginWithOAuthCode(code, (b, s) => TaskManager.RunInUI(() => DoOAuthCodeResult(b, s))); + } + }); + } + + private void DoServerMetaResult(GitHubHostMeta gitHubHostMeta) + { + hasServerMeta = true; + verifiablePasswordAuthentication = gitHubHostMeta.VerifiablePasswordAuthentication; + isBusy = false; + Redraw(); + } + + private void DoServerMetaError(string message) + { + errorMessage = message; + hasServerMeta = false; + isBusy = false; + Redraw(); + } + + private void DoRequire2fa(string msg) + { + need2fa = true; + errorMessage = msg; + isBusy = false; + Redraw(); + } + + private void Clear() + { + need2fa = false; + errorMessage = null; + isBusy = false; + Redraw(); + } + + private void DoResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void DoTokenResult(bool success) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = "Error validating token."; + Redraw(); + } + } + + private void DoOAuthCodeResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void ShowMessage() + { + if (message != null) + { + EditorGUILayout.HelpBox(message, MessageType.Warning); + } + } + + private void ShowErrorMessage() + { + if (errorMessage != null) + { + EditorGUILayout.Space(); + + EditorGUILayout.HelpBox(errorMessage, MessageType.Error); + } + } + + private AuthenticationService GetAuthenticationService(string host) + { + if (authenticationService == null || authenticationService.HostAddress.WebUri.Host != host) + { + authenticationService + = new AuthenticationService( + host, + Platform.Keychain, + Manager.ProcessManager, + Manager.TaskManager, + Environment); + + oAuthState = Guid.NewGuid().ToString(); + oAuthOpenUrl = authenticationService.GetLoginUrl(oAuthState).ToString(); + } + + return authenticationService; + } + + public override bool IsBusy + { + get { return isBusy; } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs index 07c766e71..72f774b13 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs @@ -10,11 +10,12 @@ class GitPathView : Subview { private const string GitInstallTitle = "Git installation"; private const string PathToGit = "Path to Git"; - private const string GitPathSaveButton = "Save Path"; - private const string UseInternalGitButton = "Use internal git"; + private const string PathToGitLfs = "Path to Git LFS"; + private const string GitPathSaveButton = "Save"; + private const string SetToBundledGitButton = "Set to bundled git"; private const string FindSystemGitButton = "Find system git"; private const string BrowseButton = "..."; - private const string GitInstallBrowseTitle = "Select git binary"; + private const string GitInstallBrowseTitle = "Select executable"; private const string ErrorInvalidPathMessage = "Invalid Path."; private const string ErrorInstallingInternalGit = "Error installing portable git."; private const string ErrorValidatingGitPath = "Error validating Git Path."; @@ -23,33 +24,27 @@ class GitPathView : Subview private const string ErrorMinimumGitVersionMessageFormat = "Git version {0} found. Git version {1} is required."; private const string ErrorMinimumGitLfsVersionMessageFormat = "Git LFS version {0} found. Git LFS version {1} is required."; - [SerializeField] private string gitExec; - [SerializeField] private string gitExecParent; - [SerializeField] private string gitExecExtension; - [SerializeField] private string newGitExec; - [SerializeField] private bool isValueChanged; - [SerializeField] private bool isValueChangedAndFileExists; - [SerializeField] private string gitFileErrorMessage; - [SerializeField] private string gitVersionErrorMessage; + [SerializeField] private string gitPath; + [SerializeField] private string gitLfsPath; + [SerializeField] private string errorMessage; + [SerializeField] private bool resetToBundled; + [SerializeField] private bool resetToSystem; + [SerializeField] private bool changingManually; [NonSerialized] private bool isBusy; - [NonSerialized] private bool gitExecHasChanged; - [NonSerialized] private bool gitExecutableIsSet; - [NonSerialized] private string portableGitPath; + [NonSerialized] private bool refresh; + [NonSerialized] private GitInstaller.GitInstallationState installationState; + [NonSerialized] private GitInstaller.GitInstallDetails installDetails; public override void InitializeView(IView parent) { base.InitializeView(parent); - gitExecutableIsSet = Environment.GitExecutablePath.IsInitialized; - - var gitInstallDetails = new GitInstaller.GitInstallDetails(Environment.UserCachePath, Environment.IsWindows); - portableGitPath = gitInstallDetails.GitExecutablePath; } public override void OnEnable() { base.OnEnable(); - gitExecHasChanged = true; + refresh = true; } public override void OnDataUpdate() @@ -58,76 +53,118 @@ public override void OnDataUpdate() MaybeUpdateData(); } + private void MaybeUpdateData() + { + if (refresh) + { + installationState = Environment.GitInstallationState; + gitPath = installationState.GitExecutablePath; + gitLfsPath = installationState.GitLfsExecutablePath; + installDetails = new GitInstaller.GitInstallDetails(Environment.UserCachePath, Environment.IsWindows); + refresh = false; + } + } + public override void OnGUI() { // Install path GUILayout.Label(GitInstallTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(!gitExecutableIsSet || IsBusy || Parent.IsBusy); + EditorGUI.BeginDisabledGroup(IsBusy || Parent.IsBusy); { - // Install path field - GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); { - EditorGUI.BeginChangeCheck(); + GUILayout.BeginHorizontal(); { - newGitExec = EditorGUILayout.TextField(PathToGit, newGitExec); - } + EditorGUI.BeginChangeCheck(); + { + gitPath = EditorGUILayout.TextField(PathToGit, gitPath); + gitPath = gitPath != null ? gitPath.Trim() : gitPath; + if (GUILayout.Button(BrowseButton, EditorStyles.miniButton, GUILayout.Width(Styles.BrowseButtonWidth))) + { + GUI.FocusControl(null); - if (EditorGUI.EndChangeCheck()) - { - CheckEnteredGitPath(); + var newPath = EditorUtility.OpenFilePanel(GitInstallBrowseTitle, + !String.IsNullOrEmpty(gitPath) ? gitPath.ToNPath().Parent : "", + Environment.ExecutableExtension.TrimStart('.')); + + if (!string.IsNullOrEmpty(newPath)) + { + gitPath = newPath.ToNPath().ToString(); + } + } + } + if (EditorGUI.EndChangeCheck()) + { + changingManually = ViewHasChanges; + } } + GUILayout.EndHorizontal(); - if (GUILayout.Button(BrowseButton, EditorStyles.miniButton, GUILayout.Width(Styles.BrowseButtonWidth))) + GUILayout.BeginHorizontal(); { - GUI.FocusControl(null); - - var newValue = EditorUtility.OpenFilePanel(GitInstallBrowseTitle, - gitExecParent, - gitExecExtension); - - if (!string.IsNullOrEmpty(newValue)) + EditorGUI.BeginChangeCheck(); { - newGitExec = newValue; - - if (Environment.IsWindows) + gitLfsPath = EditorGUILayout.TextField(PathToGitLfs, gitLfsPath); + gitLfsPath = gitLfsPath != null ? gitLfsPath.Trim() : gitLfsPath; + if (GUILayout.Button(BrowseButton, EditorStyles.miniButton, GUILayout.Width(Styles.BrowseButtonWidth))) { - //Normalizing the path separator in windows - newGitExec = newGitExec.ToNPath().ToString(); - } + GUI.FocusControl(null); - CheckEnteredGitPath(); + var newPath = EditorUtility.OpenFilePanel(GitInstallBrowseTitle, + !String.IsNullOrEmpty(gitLfsPath) ? gitLfsPath.ToNPath().Parent : "", + Environment.ExecutableExtension.TrimStart('.')); + + if (!string.IsNullOrEmpty(newPath)) + { + gitLfsPath = newPath.ToNPath().ToString(); + } + } + } + if (EditorGUI.EndChangeCheck()) + { + changingManually = ViewHasChanges; + errorMessage = ""; } } + GUILayout.EndHorizontal(); + } - GUILayout.EndHorizontal(); + GUILayout.EndVertical(); GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); GUILayout.BeginHorizontal(); { - EditorGUI.BeginDisabledGroup(!isValueChangedAndFileExists); + EditorGUI.BeginDisabledGroup(!changingManually && !resetToBundled && !resetToSystem); { if (GUILayout.Button(GitPathSaveButton, GUILayout.ExpandWidth(false))) { GUI.FocusControl(null); isBusy = true; - - ValidateAndSetGitInstallPath(newGitExec); + ValidateAndSetGitInstallPath(); } } EditorGUI.EndDisabledGroup(); - // disable if we are not on windows - // disable if the newPath == portableGitPath - EditorGUI.BeginDisabledGroup(!Environment.IsWindows || Environment.IsWindows && newGitExec == portableGitPath); - if (GUILayout.Button(UseInternalGitButton, GUILayout.ExpandWidth(false))) + // disable the button if the paths are already pointing to the bundled git + // both on windows, only lfs on mac + EditorGUI.BeginDisabledGroup( + (!Environment.IsWindows || gitPath == installDetails.GitExecutablePath) && + gitLfsPath == installDetails.GitLfsExecutablePath); { - GUI.FocusControl(null); + if (GUILayout.Button(SetToBundledGitButton, GUILayout.ExpandWidth(false))) + { + GUI.FocusControl(null); - Logger.Trace("Expected portableGitPath: {0}", portableGitPath); - newGitExec = portableGitPath; - CheckEnteredGitPath(); + if (Environment.IsWindows) + gitPath = installDetails.GitExecutablePath; + gitLfsPath = installDetails.GitLfsExecutablePath; + resetToBundled = ViewHasChanges; + resetToSystem = false; + changingManually = false; + errorMessage = ""; + } } EditorGUI.EndDisabledGroup(); @@ -136,52 +173,46 @@ public override void OnGUI() { GUI.FocusControl(null); isBusy = true; - - newGitExec = gitExec; - CheckEnteredGitPath(); - - new FindExecTask("git", Manager.CancellationToken) - .Configure(Manager.ProcessManager, dontSetupGit: true) - .Catch(ex => true) - .FinallyInUI((success, ex, path) => { + new FuncTask(TaskManager.Token, () => + { + var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); + return gitInstaller.FindSystemGit(new GitInstaller.GitInstallationState()); + }) + { Message = "Locating git..." } + .FinallyInUI((success, ex, state) => + { if (success) { - Logger.Trace("FindGit Path:{0}", path); - newGitExec = path; - CheckEnteredGitPath(); - } - else - { - if (ex != null) + if (state.GitIsValid) { - Logger.Error(ex, "FindGit Error Path:{0}", path); + gitPath = state.GitExecutablePath; } - else + if (state.GitLfsIsValid) { - Logger.Error("FindGit Failed Path:{0}", path); + gitLfsPath = state.GitLfsExecutablePath; } } - + else + { + Logger.Error(ex); + } isBusy = false; - }).Start(); + resetToBundled = false; + resetToSystem = ViewHasChanges; + changingManually = false; + errorMessage = ""; + Redraw(); + }) + .Start(); } } GUILayout.EndHorizontal(); - if (gitFileErrorMessage != null) - { - GUILayout.BeginHorizontal(); - { - GUILayout.Label(gitFileErrorMessage, Styles.ErrorLabel); - } - GUILayout.EndHorizontal(); - } - - if (gitVersionErrorMessage != null) + if (!String.IsNullOrEmpty(errorMessage)) { GUILayout.BeginHorizontal(); { - GUILayout.Label(gitVersionErrorMessage, Styles.ErrorLabel); + GUILayout.Label(errorMessage, Styles.ErrorLabel); } GUILayout.EndHorizontal(); } @@ -189,122 +220,103 @@ public override void OnGUI() EditorGUI.EndDisabledGroup(); } - private void MaybeUpdateData() + private void ValidateAndSetGitInstallPath() { - if (gitExecHasChanged) + if (resetToBundled) { - if (gitExecExtension == null) - { - gitExecExtension = Environment.ExecutableExtension; - - if (Environment.IsWindows) + new FuncTask(TaskManager.Token, () => { - gitExecExtension = gitExecExtension.TrimStart('.'); - } - } - - if (Environment.GitExecutablePath.IsInitialized) - { - newGitExec = gitExec = Environment.GitExecutablePath.ToString(); - gitExecParent = Environment.GitExecutablePath.Parent.ToString(); - - CheckEnteredGitPath(); - } - - if (gitExecParent == null) - { - gitExecParent = Environment.GitInstallPath; - } - - gitExecHasChanged = false; - } - } - - private void CheckEnteredGitPath() - { - isValueChanged = !string.IsNullOrEmpty(newGitExec) && newGitExec != gitExec; - - isValueChangedAndFileExists = isValueChanged && newGitExec.ToNPath().FileExists(); - - gitFileErrorMessage = isValueChanged && !isValueChangedAndFileExists ? ErrorInvalidPathMessage : null; - - gitVersionErrorMessage = null; - } - - private void ValidateAndSetGitInstallPath(string value) - { - value = value.Trim(); - - if (value == portableGitPath) - { - Logger.Trace("Attempting to restore portable Git Path:{0}", value); - - var gitInstaller = new GitInstaller(Environment, EntryPoint.ApplicationManager.ProcessManager, - EntryPoint.ApplicationManager.TaskManager); - - gitInstaller.SetupGitIfNeeded() - .FinallyInUI((success, exception, installationState) => + var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); + var state = new GitInstaller.GitInstallationState(); + state = gitInstaller.SetDefaultPaths(state); + // on non-windows we only bundle git-lfs + if (!Environment.IsWindows) + { + state.GitExecutablePath = installationState.GitExecutablePath; + state.GitInstallationPath = installationState.GitInstallationPath; + } + state = gitInstaller.SetupGitIfNeeded(state); + if (state.GitIsValid && state.GitLfsIsValid) + { + Manager.SetupGit(state); + Manager.RestartRepository(); + } + return state; + }) + { Message = "Setting up git... " } + .FinallyInUI((success, exception, state) => { - Logger.Trace("Setup Git Using the installer:{0}", success); - if (!success) { Logger.Error(exception, ErrorInstallingInternalGit); - gitVersionErrorMessage = ErrorValidatingGitPath; + errorMessage = ErrorValidatingGitPath; } else { - Manager.SystemSettings.Unset(Constants.GitInstallPathKey); - Environment.GitExecutablePath = installationState.GitExecutablePath; - Environment.GitLfsExecutablePath = installationState.GitLfsExecutablePath; - Environment.IsCustomGitExecutable = false; - - gitExecHasChanged = true; + refresh = true; } - isBusy = false; + resetToBundled = false; + resetToSystem = false; + changingManually = false; + Redraw(); }).Start(); } else { - //Logger.Trace("Validating Git Path:{0}", value); + var newState = new GitInstaller.GitInstallationState(); + newState.GitExecutablePath = gitPath.ToNPath(); + newState.GitLfsExecutablePath = gitLfsPath.ToNPath(); + var installer = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); + installer.Progress.OnProgress += UpdateProgress; - gitVersionErrorMessage = null; - - GitClient.ValidateGitInstall(value.ToNPath(), true) - .ThenInUI((success, result) => + new FuncTask(TaskManager.Token, () => + { + return installer.SetupGitIfNeeded(newState); + }) + .Then((success, state) => + { + if (state.GitIsValid && state.GitLfsIsValid) + { + Manager.SetupGit(state); + Manager.RestartRepository(); + } + return state; + }) + .FinallyInUI((success, ex, state) => { + installer.Progress.OnProgress -= UpdateProgress; if (!success) { - Logger.Trace(ErrorValidatingGitPath); - gitVersionErrorMessage = ErrorValidatingGitPath; + Logger.Error(ex, ErrorValidatingGitPath); + return; } - else if (!result.IsValid) + + if (!state.GitIsValid || !state.GitLfsIsValid) { + var errorMessageStringBuilder = new StringBuilder(); Logger.Warning( "Software versions do not meet minimums Git:{0} (Minimum:{1}) GitLfs:{2} (Minimum:{3})", - result.GitVersion, Constants.MinimumGitVersion, result.GitLfsVersion, + state.GitVersion, Constants.MinimumGitVersion, state.GitLfsVersion, Constants.MinimumGitLfsVersion); - var errorMessageStringBuilder = new StringBuilder(); - - if (result.GitVersion == null) + if (state.GitVersion == TheVersion.Default) { errorMessageStringBuilder.Append(ErrorGitNotFoundMessage); } - else if (result.GitLfsVersion == null) + else if (state.GitLfsVersion == TheVersion.Default) { errorMessageStringBuilder.Append(ErrorGitLfsNotFoundMessage); } else { - if (result.GitVersion < Constants.MinimumGitVersion) + if (state.GitVersion < Constants.MinimumGitVersion) { errorMessageStringBuilder.AppendFormat(ErrorMinimumGitVersionMessageFormat, - result.GitVersion, Constants.MinimumGitVersion); + state.GitVersion, Constants.MinimumGitVersion); } - if (result.GitLfsVersion < Constants.MinimumGitLfsVersion) + if (state.GitLfsVersion < Constants.MinimumGitLfsVersion) { if (errorMessageStringBuilder.Length > 0) { @@ -312,31 +324,38 @@ private void ValidateAndSetGitInstallPath(string value) } errorMessageStringBuilder.AppendFormat(ErrorMinimumGitLfsVersionMessageFormat, - result.GitLfsVersion, Constants.MinimumGitLfsVersion); + state.GitLfsVersion, Constants.MinimumGitLfsVersion); } } - gitVersionErrorMessage = errorMessageStringBuilder.ToString(); + errorMessage = errorMessageStringBuilder.ToString(); } else { Logger.Trace("Software versions meet minimums Git:{0} GitLfs:{1}", - result.GitVersion, - result.GitLfsVersion); - - Manager.SystemSettings.Set(Constants.GitInstallPathKey, value); - Environment.GitExecutablePath = value.ToNPath(); - Environment.IsCustomGitExecutable = true; - - gitExecHasChanged = true; + state.GitVersion, + state.GitLfsVersion); + + refresh = true; } - isBusy = false; + resetToBundled = false; + resetToSystem = false; + changingManually = false; + Redraw(); }).Start(); } } + public bool ViewHasChanges + { + get + { + return gitPath != installationState.GitExecutablePath || gitLfsPath != installationState.GitLfsExecutablePath; + } + } + public override bool IsBusy { get { return isBusy; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 31cb30ab0..c4b326c1e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEditor; @@ -7,7 +7,7 @@ namespace GitHub.Unity { [Serializable] - class HistoryControl + public class HistoryControl { private const string HistoryEntryDetailFormat = "{0} {1}"; @@ -302,191 +302,121 @@ public void ScrollTo(int index, float offset = 0f) } } - [Serializable] - class HistoryView : Subview + abstract class HistoryBase : Subview { - private const string PullButton = "Pull"; - private const string PullButtonCount = "Pull ({0})"; - private const string PushButton = "Push"; - private const string PushButtonCount = "Push ({0})"; - private const string PullConfirmTitle = "Pull Changes?"; - private const string PullConfirmDescription = "Would you like to pull changes from remote '{0}'?"; - private const string PullConfirmYes = "Pull"; - private const string PullConfirmCancel = "Cancel"; - private const string PushConfirmTitle = "Push Changes?"; - private const string PushConfirmDescription = "Would you like to push changes to remote '{0}'?"; - private const string PushConfirmYes = "Push"; - private const string PushConfirmCancel = "Cancel"; - private const string CommitDetailsTitle = "Commit details"; - private const string ClearSelectionButton = "×"; - private const string PublishButton = "Publish"; - private const string FetchActionTitle = "Fetch Changes"; - private const string FetchButtonText = "Fetch"; - private const string FetchFailureDescription = "Could not fetch changes"; - - [NonSerialized] private bool currentLogHasUpdate; - [NonSerialized] private bool currentRemoteHasUpdate; - [NonSerialized] private bool currentTrackingStatusHasUpdate; - - [SerializeField] private bool hasItemsToCommit; - [SerializeField] private bool hasRemote; - [SerializeField] private string currentRemoteName; - - [SerializeField] private HistoryControl historyControl; - [SerializeField] private GitLogEntry selectedEntry = GitLogEntry.Default; + protected const string CommitDetailsTitle = "Commit details"; + protected const string ClearSelectionButton = "×"; - [SerializeField] private Vector2 detailsScroll; - - [SerializeField] private List logEntries = new List(); + protected abstract HistoryControl HistoryControl { get; set; } + protected abstract GitLogEntry SelectedEntry { get; set; } + protected abstract ChangesTree TreeChanges { get; set; } + protected abstract Vector2 DetailsScroll { get; set; } - [SerializeField] private int statusAhead; - [SerializeField] private int statusBehind; - - [SerializeField] private ChangesTree treeChanges = new ChangesTree { IsSelectable = false, DisplayRootNode = false }; - - [SerializeField] private CacheUpdateEvent lastCurrentRemoteChangedEvent; - [SerializeField] private CacheUpdateEvent lastLogChangedEvent; - [SerializeField] private CacheUpdateEvent lastAheadBehindChangedEvent; - - public override void OnEnable() + protected void BuildHistoryControl(int loadAhead, List gitLogEntries) { - base.OnEnable(); - - if (treeChanges != null) + if (HistoryControl == null) { - treeChanges.ViewHasFocus = HasFocus; - treeChanges.UpdateIcons(Styles.FolderIcon); + HistoryControl = new HistoryControl(); } - AttachHandlers(Repository); - ValidateCachedData(Repository); + HistoryControl.Load(loadAhead, gitLogEntries); + if (!SelectedEntry.Equals(GitLogEntry.Default) + && SelectedEntry.CommitID != HistoryControl.SelectedGitLogEntry.CommitID) + { + SelectedEntry = GitLogEntry.Default; + } } - public override void OnDisable() + protected void BuildTreeChanges() { - base.OnDisable(); - DetachHandlers(Repository); + TreeChanges.PathSeparator = Environment.FileSystem.DirectorySeparatorChar.ToString(); + TreeChanges.Load(SelectedEntry.changes.Select(entry => new GitStatusEntryTreeData(entry))); + Redraw(); } - public override void OnDataUpdate() + protected void RevertCommit() { - base.OnDataUpdate(); - MaybeUpdateData(); - } + var dialogTitle = "Revert commit"; + var dialogBody = string.Format(@"Are you sure you want to revert the following commit:""{0}""", SelectedEntry.Summary); - public override void OnFocusChanged() - { - base.OnFocusChanged(); - var hasFocus = HasFocus; - if (treeChanges.ViewHasFocus != hasFocus) + if (EditorUtility.DisplayDialog(dialogTitle, dialogBody, "Revert", "Cancel")) { - treeChanges.ViewHasFocus = hasFocus; - Redraw(); + Repository + .Revert(SelectedEntry.CommitID) + .FinallyInUI((success, e) => { + if (!success) + { + EditorUtility.DisplayDialog(dialogTitle, + "Error reverting commit: " + e.Message, Localization.Cancel); + } + AssetDatabase.Refresh(); + }) + .Start(); } } - public override void OnGUI() + protected void HistoryDetailsEntry(GitLogEntry entry) { - // History toolbar - GUILayout.BeginHorizontal(EditorStyles.toolbar); - { - GUILayout.FlexibleSpace(); - - if (hasRemote) - { - EditorGUI.BeginDisabledGroup(currentRemoteName == null); - { - // Fetch button - var fetchClicked = GUILayout.Button(FetchButtonText, Styles.HistoryToolbarButtonStyle); - if (fetchClicked) - { - Fetch(); - } - - // Pull button - var pullButtonText = statusBehind > 0 ? String.Format(PullButtonCount, statusBehind) : PullButton; - var pullClicked = GUILayout.Button(pullButtonText, Styles.HistoryToolbarButtonStyle); + GUILayout.BeginVertical(Styles.HeaderBoxStyle); + GUILayout.Label(entry.Summary, Styles.HistoryDetailsTitleStyle); - if (pullClicked && - EditorUtility.DisplayDialog(PullConfirmTitle, - String.Format(PullConfirmDescription, currentRemoteName), - PullConfirmYes, - PullConfirmCancel) - ) - { - Pull(); - } - } - EditorGUI.EndDisabledGroup(); + GUILayout.Space(-5); - // Push button - EditorGUI.BeginDisabledGroup(currentRemoteName == null || statusBehind != 0); - { - var pushButtonText = statusAhead > 0 ? String.Format(PushButtonCount, statusAhead) : PushButton; - var pushClicked = GUILayout.Button(pushButtonText, Styles.HistoryToolbarButtonStyle); - - if (pushClicked && - EditorUtility.DisplayDialog(PushConfirmTitle, - String.Format(PushConfirmDescription, currentRemoteName), - PushConfirmYes, - PushConfirmCancel) - ) - { - Push(); - } - } - EditorGUI.EndDisabledGroup(); - } - else - { - // Publishing a repo - var publishedClicked = GUILayout.Button(PublishButton, Styles.HistoryToolbarButtonStyle); - if (publishedClicked) - { - PopupWindow.OpenWindow(PopupWindow.PopupViewType.PublishView); - } - } - } + GUILayout.BeginHorizontal(); + GUILayout.Label(entry.PrettyTimeString, Styles.HistoryDetailsMetaInfoStyle); + GUILayout.Label(entry.AuthorName, Styles.HistoryDetailsMetaInfoStyle); + GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); - var rect = GUILayoutUtility.GetLastRect(); - if (historyControl != null) + GUILayout.Space(3); + GUILayout.EndVertical(); + } + + protected void DoHistoryGui(Rect rect, Action historyControlRightClick = null, + Action changesTreeRightClick = null) + { + if (HistoryControl != null) { var historyControlRect = new Rect(0f, 0f, Position.width, Position.height - rect.height); - var requiresRepaint = historyControl.Render(historyControlRect, - entry => { - selectedEntry = entry; - BuildTree(); + var requiresRepaint = HistoryControl.Render(historyControlRect, + singleClick: entry => { + SelectedEntry = entry; + BuildTreeChanges(); }, - entry => { }, - entry => { }); + doubleClick: entry => { + + }, + rightClick: historyControlRightClick); if (requiresRepaint) Redraw(); } - if (!selectedEntry.Equals(GitLogEntry.Default)) + DoProgressGUI(); + + if (!SelectedEntry.Equals(GitLogEntry.Default)) { // Top bar for scrolling to selection or clearing it GUILayout.BeginHorizontal(EditorStyles.toolbar); { - if (GUILayout.Button(CommitDetailsTitle, Styles.HistoryToolbarButtonStyle)) + if (GUILayout.Button(CommitDetailsTitle, Styles.ToolbarButtonStyle)) { - historyControl.ScrollTo(historyControl.SelectedIndex); + HistoryControl.ScrollTo(HistoryControl.SelectedIndex); } - if (GUILayout.Button(ClearSelectionButton, Styles.HistoryToolbarButtonStyle, GUILayout.ExpandWidth(false))) + + if (GUILayout.Button(ClearSelectionButton, Styles.ToolbarButtonStyle, GUILayout.ExpandWidth(false))) { - selectedEntry = GitLogEntry.Default; - historyControl.SelectedIndex = -1; + SelectedEntry = GitLogEntry.Default; + HistoryControl.SelectedIndex = -1; } } GUILayout.EndHorizontal(); // Log entry details - including changeset tree (if any changes are found) - detailsScroll = GUILayout.BeginScrollView(detailsScroll, GUILayout.Height(250)); + DetailsScroll = GUILayout.BeginScrollView(DetailsScroll, GUILayout.Height(250)); { - HistoryDetailsEntry(selectedEntry); + HistoryDetailsEntry(SelectedEntry); GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); GUILayout.Label("Files changed", EditorStyles.boldLabel); @@ -497,24 +427,23 @@ public override void OnGUI() GUILayout.BeginVertical(); { var borderLeft = Styles.Label.margin.left; - var treeControlRect = new Rect(rect.x + borderLeft, rect.y, Position.width - borderLeft * 2, Position.height - rect.height + Styles.CommitAreaPadding); + var treeControlRect = new Rect(rect.x + borderLeft, rect.y, Position.width - borderLeft * 2, + Position.height - rect.height + Styles.CommitAreaPadding); var treeRect = new Rect(0f, 0f, 0f, 0f); - if (treeChanges != null) + if (TreeChanges != null) { - treeChanges.FolderStyle = Styles.Foldout; - treeChanges.TreeNodeStyle = Styles.TreeNode; - treeChanges.ActiveTreeNodeStyle = Styles.ActiveTreeNode; - treeChanges.FocusedTreeNodeStyle = Styles.FocusedTreeNode; - treeChanges.FocusedActiveTreeNodeStyle = Styles.FocusedActiveTreeNode; - - treeRect = treeChanges.Render(treeControlRect, detailsScroll, - node => { }, - node => { - }, - node => { - }); - - if (treeChanges.RequiresRepaint) + TreeChanges.FolderStyle = Styles.Foldout; + TreeChanges.TreeNodeStyle = Styles.TreeNode; + TreeChanges.ActiveTreeNodeStyle = Styles.ActiveTreeNode; + TreeChanges.FocusedTreeNodeStyle = Styles.FocusedTreeNode; + TreeChanges.FocusedActiveTreeNodeStyle = Styles.FocusedActiveTreeNode; + + treeRect = TreeChanges.Render(treeControlRect, DetailsScroll, + singleClick: node => { }, + doubleClick: node => { }, + rightClick: changesTreeRightClick); + + if (TreeChanges.RequiresRepaint) Redraw(); } @@ -529,110 +458,136 @@ public override void OnGUI() } } - private void HistoryDetailsEntry(GitLogEntry entry) + public override void OnEnable() { - GUILayout.BeginVertical(Styles.HeaderBoxStyle); - GUILayout.Label(entry.Summary, Styles.HistoryDetailsTitleStyle); + base.OnEnable(); - GUILayout.Space(-5); + if (TreeChanges != null) + { + TreeChanges.ViewHasFocus = HasFocus; + TreeChanges.UpdateIcons(Styles.FolderIcon); + } - GUILayout.BeginHorizontal(); - GUILayout.Label(entry.PrettyTimeString, Styles.HistoryDetailsMetaInfoStyle); - GUILayout.Label(entry.AuthorName, Styles.HistoryDetailsMetaInfoStyle); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); + AttachHandlers(Repository); + ValidateCachedData(Repository); + } - GUILayout.Space(3); - GUILayout.EndVertical(); + public override void OnDisable() + { + base.OnDisable(); + DetachHandlers(Repository); } - private void RepositoryTrackingOnStatusChanged(CacheUpdateEvent cacheUpdateEvent) + public override void OnDataUpdate() { - if (!lastAheadBehindChangedEvent.Equals(cacheUpdateEvent)) + base.OnDataUpdate(); + MaybeUpdateData(); + } + + public override void OnFocusChanged() + { + base.OnFocusChanged(); + var hasFocus = HasFocus; + if (TreeChanges.ViewHasFocus != hasFocus) { - lastAheadBehindChangedEvent = cacheUpdateEvent; - currentTrackingStatusHasUpdate = true; + TreeChanges.ViewHasFocus = hasFocus; Redraw(); } } - private void RepositoryOnLogChanged(CacheUpdateEvent cacheUpdateEvent) + protected abstract void AttachHandlers(IRepository repository); + protected abstract void DetachHandlers(IRepository repository); + protected abstract void ValidateCachedData(IRepository repository); + protected abstract void MaybeUpdateData(); + } + + [Serializable] + class HistoryView : HistoryBase + { + [SerializeField] private bool currentLogHasUpdate; + [SerializeField] private bool currentTrackingStatusHasUpdate; + + [SerializeField] private List logEntries = new List(); + + [SerializeField] private int statusAhead; + + [SerializeField] private CacheUpdateEvent lastLogChangedEvent; + [SerializeField] private CacheUpdateEvent lastTrackingStatusChangedEvent; + + [SerializeField] private HistoryControl historyControl; + [SerializeField] private GitLogEntry selectedEntry = GitLogEntry.Default; + [SerializeField] private ChangesTree treeChanges = new ChangesTree { DisplayRootNode = false }; + [SerializeField] private Vector2 detailsScroll; + + public override void Refresh() { - if (!lastLogChangedEvent.Equals(cacheUpdateEvent)) + base.Refresh(); + Refresh(CacheType.GitLog); + Refresh(CacheType.GitAheadBehind); + } + + private void RepositoryOnTrackingStatusChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastTrackingStatusChangedEvent.Equals(cacheUpdateEvent)) { - lastLogChangedEvent = cacheUpdateEvent; - currentLogHasUpdate = true; + ReceivedEvent(cacheUpdateEvent.cacheType); + lastTrackingStatusChangedEvent = cacheUpdateEvent; + currentTrackingStatusHasUpdate = true; Redraw(); } } - private void RepositoryOnCurrentRemoteChanged(CacheUpdateEvent cacheUpdateEvent) + private void RepositoryOnLogChanged(CacheUpdateEvent cacheUpdateEvent) { - if (!lastCurrentRemoteChangedEvent.Equals(cacheUpdateEvent)) + if (!lastLogChangedEvent.Equals(cacheUpdateEvent)) { - lastCurrentRemoteChangedEvent = cacheUpdateEvent; - currentRemoteHasUpdate = true; + ReceivedEvent(cacheUpdateEvent.cacheType); + lastLogChangedEvent = cacheUpdateEvent; + currentLogHasUpdate = true; Redraw(); } } - private void AttachHandlers(IRepository repository) + protected override void AttachHandlers(IRepository repository) { if (repository == null) { return; } - repository.TrackingStatusChanged += RepositoryTrackingOnStatusChanged; + repository.TrackingStatusChanged += RepositoryOnTrackingStatusChanged; repository.LogChanged += RepositoryOnLogChanged; - repository.CurrentRemoteChanged += RepositoryOnCurrentRemoteChanged; } - private void DetachHandlers(IRepository repository) + protected override void DetachHandlers(IRepository repository) { if (repository == null) { return; } - repository.TrackingStatusChanged -= RepositoryTrackingOnStatusChanged; + repository.TrackingStatusChanged -= RepositoryOnTrackingStatusChanged; repository.LogChanged -= RepositoryOnLogChanged; - repository.CurrentRemoteChanged -= RepositoryOnCurrentRemoteChanged; } - private void ValidateCachedData(IRepository repository) + protected override void ValidateCachedData(IRepository repository) { repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLog, lastLogChangedEvent); - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitAheadBehind, lastAheadBehindChangedEvent); - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentRemoteChangedEvent); + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitAheadBehind, lastTrackingStatusChangedEvent); } - private void MaybeUpdateData() + protected override void MaybeUpdateData() { if (Repository == null) { return; } - if (currentRemoteHasUpdate) - { - currentRemoteHasUpdate = false; - - var currentRemote = Repository.CurrentRemote; - hasRemote = currentRemote.HasValue; - currentRemoteName = hasRemote ? currentRemote.Value.Name : "placeholder"; - } - if (currentTrackingStatusHasUpdate) { currentTrackingStatusHasUpdate = false; statusAhead = Repository.CurrentAhead; - statusBehind = Repository.CurrentBehind; - - var currentChanges = Repository.CurrentChanges; - hasItemsToCommit = currentChanges != null - && currentChanges.Any(entry => entry.Status != GitFileStatus.Ignored && !entry.Staged); } if (currentLogHasUpdate) @@ -641,102 +596,205 @@ private void MaybeUpdateData() logEntries = Repository.CurrentLog; - BuildHistoryControl(); + BuildHistoryControl(statusAhead, logEntries); } } - private void BuildHistoryControl() + public override void OnGUI() + { + var lastRect = GUILayoutUtility.GetLastRect(); + DoHistoryGui(lastRect, entry => { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Revert"), false, RevertCommit); + menu.ShowAsContext(); + }, node => { + var menu = CreateChangesTreeContextMenu(node); + menu.ShowAsContext(); + }); + } + + protected override HistoryControl HistoryControl + { + get { return historyControl; } + set { historyControl = value; } + } + + protected override GitLogEntry SelectedEntry + { + get { return selectedEntry; } + set { selectedEntry = value; } + } + + protected override ChangesTree TreeChanges + { + get { return treeChanges; } + set { treeChanges = value; } + } + + protected override Vector2 DetailsScroll + { + get { return detailsScroll; } + set { detailsScroll = value; } + } + + private GenericMenu CreateChangesTreeContextMenu(ChangesTreeNode node) + { + var genericMenu = new GenericMenu(); + + genericMenu.AddItem(new GUIContent("Show History"), false, () => { }); + + return genericMenu; + } + } + + [Serializable] + class FileHistoryView : HistoryBase + { + [SerializeField] private bool currentFileLogHasUpdate; + [SerializeField] private bool currentStatusEntriesHasUpdate; + + [SerializeField] private GitFileLog gitFileLog; + + [SerializeField] private HistoryControl historyControl; + [SerializeField] private GitLogEntry selectedEntry = GitLogEntry.Default; + [SerializeField] private ChangesTree treeChanges = new ChangesTree { DisplayRootNode = false }; + [SerializeField] private Vector2 detailsScroll; + [SerializeField] private List gitStatusEntries = new List(); + + [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; + [SerializeField] private CacheUpdateEvent lastFileLogChangedEvent; + + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.GitLog); + Refresh(CacheType.GitAheadBehind); + } + + private void RepositoryOnFileLogChanged(CacheUpdateEvent cacheUpdateEvent) { - if (historyControl == null) + if (!lastFileLogChangedEvent.Equals(cacheUpdateEvent)) { - historyControl = new HistoryControl(); + ReceivedEvent(cacheUpdateEvent.cacheType); + lastFileLogChangedEvent = cacheUpdateEvent; + currentFileLogHasUpdate = true; + Redraw(); } + } - historyControl.Load(statusAhead, logEntries); - if (!selectedEntry.Equals(GitLogEntry.Default) - && selectedEntry.CommitID != historyControl.SelectedGitLogEntry.CommitID) + private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) { - selectedEntry = GitLogEntry.Default; + ReceivedEvent(cacheUpdateEvent.cacheType); + lastStatusEntriesChangedEvent = cacheUpdateEvent; + currentStatusEntriesHasUpdate = true; + Redraw(); } } - private void Pull() + protected override void AttachHandlers(IRepository repository) { - if (hasItemsToCommit) + if (repository == null) { - EditorUtility.DisplayDialog("Pull", "You need to commit your changes before pulling.", "Cancel"); + return; } - else + + repository.FileLogChanged += RepositoryOnFileLogChanged; + repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + } + + protected override void DetachHandlers(IRepository repository) + { + if (repository == null) { - Repository - .Pull() - // we need the error propagated from the original git command to handle things appropriately - .Then(success => { - if (!success) - { - // if Pull fails we need to parse the output of the command, figure out - // whether pull triggered a merge or a rebase, and abort the operation accordingly - // (either git rebase --abort or git merge --abort) - } - }, runOptions: TaskRunOptions.OnAlways) - .FinallyInUI((success, e) => { - if (success) - { - EditorUtility.DisplayDialog(Localization.PullActionTitle, - String.Format(Localization.PullSuccessDescription, currentRemoteName), - Localization.Ok); - } - else - { - EditorUtility.DisplayDialog(Localization.PullActionTitle, - Localization.PullFailureDescription, - Localization.Ok); - } - }) - .Start(); + return; } + + repository.FileLogChanged -= RepositoryOnFileLogChanged; + repository.FileLogChanged -= RepositoryOnStatusEntriesChanged; } - private void Push() + protected override void ValidateCachedData(IRepository repository) { - Repository - .Push() - .FinallyInUI((success, e) => { - if (success) - { - EditorUtility.DisplayDialog(Localization.PushActionTitle, - String.Format(Localization.PushSuccessDescription, currentRemoteName), - Localization.Ok); - } - else - { - EditorUtility.DisplayDialog(Localization.PushActionTitle, - Localization.PushFailureDescription, - Localization.Ok); - } - }) - .Start(); + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitFileLog, lastFileLogChangedEvent); } - private void Fetch() + protected override void MaybeUpdateData() { - Repository - .Fetch() - .FinallyInUI((success, e) => { - if (!success) - { - EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, - Localization.Ok); - } - }) - .Start(); + if (Repository == null) + { + return; + } + + if (currentFileLogHasUpdate) + { + currentFileLogHasUpdate = false; + + gitFileLog = Repository.CurrentFileLog; + + BuildHistoryControl(0, gitFileLog.LogEntries); + } + + if (currentStatusEntriesHasUpdate) + { + currentStatusEntriesHasUpdate = false; + + gitStatusEntries = Repository.CurrentChanges; + } } - private void BuildTree() + public override void OnGUI() { - treeChanges.PathSeparator = Environment.FileSystem.DirectorySeparatorChar.ToString(); - treeChanges.Load(selectedEntry.changes.Select(entry => new GitStatusEntryTreeData(entry))); - Redraw(); + var lastRect = GUILayoutUtility.GetLastRect(); + DoHistoryGui(lastRect, entry => { + GenericMenu menu = new GenericMenu(); + string checkoutPrompt = string.Format("Checkout revision {0}", entry.ShortID); + menu.AddItem(new GUIContent(checkoutPrompt), false, () => Checkout(entry.commitID)); + menu.ShowAsContext(); + }, node => { + }); + } + + protected override HistoryControl HistoryControl + { + get { return historyControl; } + set { historyControl = value; } + } + + protected override GitLogEntry SelectedEntry + { + get { return selectedEntry; } + set { selectedEntry = value; } + } + + protected override ChangesTree TreeChanges + { + get { return treeChanges; } + set { treeChanges = value; } + } + + protected override Vector2 DetailsScroll + { + get { return detailsScroll; } + set { detailsScroll = value; } + } + + private const string ConfirmCheckoutTitle = "Discard Changes?"; + private const string ConfirmCheckoutMessage = "You've made changes to file '{0}'. Overwrite these changes with the historical version?"; + private const string ConfirmCheckoutOK = "Overwrite"; + private const string ConfirmCheckoutCancel = "Cancel"; + + protected void Checkout(string commitId) + { + var promptUser = gitStatusEntries.Count > 0 && gitStatusEntries.Any(statusEntry => gitFileLog.Path.Equals(statusEntry.Path.ToNPath())); + + if (!promptUser || EditorUtility.DisplayDialog(ConfirmCheckoutTitle, string.Format(ConfirmCheckoutMessage, gitFileLog.Path), ConfirmCheckoutOK, ConfirmCheckoutCancel)) + { + Repository.CheckoutVersion(commitId, new string[] { gitFileLog.Path }) + .ThenInUI(AssetDatabase.Refresh) + .Start(); + } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs index dae2d62c6..4b284f519 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs @@ -1,14 +1,18 @@ using System; +using System.Collections.Generic; using UnityEngine; namespace GitHub.Unity { - interface IView + interface IView : IUIEmpty, IUIProgress { void OnEnable(); void OnDisable(); void Refresh(); void Redraw(); + void Refresh(CacheType type); + void ReceivedEvent(CacheType type); + void DoneRefreshing(); Rect Position { get; } void Finish(bool result); @@ -18,6 +22,19 @@ interface IView bool HasUser { get; } IApplicationManager Manager { get; } bool IsBusy { get; } + bool IsRefreshing { get; } bool HasFocus { get; } + Dictionary RefreshEvents { get; } + } + + interface IUIEmpty + { + void DoEmptyGUI(); + } + + interface IUIProgress + { + void DoProgressGUI(); + void UpdateProgress(IProgress progress); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs index adeb21a44..1a6b8a693 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs @@ -30,7 +30,7 @@ public override void OnEnable() base.OnEnable(); AttachHandlers(); - User.CheckUserChangedEvent(lastCheckUserChangedEvent); + User.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, lastCheckUserChangedEvent); } public override void OnDisable() @@ -48,9 +48,9 @@ public override void OnGUI() GUILayout.BeginHorizontal(); { - GUILayout.FlexibleSpace(); - GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); - GUILayout.FlexibleSpace(); + GUILayout.FlexibleSpace(); + GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); + GUILayout.FlexibleSpace(); } GUILayout.EndHorizontal(); @@ -65,7 +65,7 @@ public override void OnGUI() { if (GUILayout.Button(Localization.InitializeRepositoryButtonText, "Button")) { - Manager.InitializeRepository().Start(); + Manager.InitializeRepository(); } } EditorGUI.EndDisabledGroup(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs index 07bfe1535..cbdec46e8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs @@ -2,6 +2,7 @@ using System.Linq; using GitHub.Logging; using UnityEditor; +using UnityEngine; namespace GitHub.Unity { @@ -12,18 +13,21 @@ class LfsLocksModificationProcessor : UnityEditor.AssetModificationProcessor private static IPlatform platform; private static IEnvironment environment; - private static Dictionary locks = new Dictionary(); + private static Dictionary locks = new Dictionary(); private static CacheUpdateEvent lastLocksChangedEvent; private static string loggedInUser; public static void Initialize(IEnvironment env, IPlatform plat) { - //Logger.Trace("Initialize HasRepository:{0}", repo != null); environment = env; platform = plat; platform.Keychain.ConnectionsChanged += UserMayHaveChanged; + // we need to do this to get the initial user information up front + UserMayHaveChanged(); repository = environment.Repository; + UnityShim.Editor_finishedDefaultHeaderGUI += InspectorHeaderFinished; + if (repository != null) { repository.LocksChanged += RepositoryOnLocksChanged; @@ -33,28 +37,32 @@ public static void Initialize(IEnvironment env, IPlatform plat) public static string[] OnWillSaveAssets(string[] paths) { - //Logger.Trace("OnWillSaveAssets: [{0}]", string.Join(", ", paths)); return paths; } public static AssetMoveResult OnWillMoveAsset(string oldPath, string newPath) { - //Logger.Trace("OnWillMoveAsset:{0}->{1}", oldPath, newPath); - return IsLocked(oldPath) || IsLocked(newPath) ? AssetMoveResult.FailedMove : AssetMoveResult.DidNotMove; + return IsLockedBySomeoneElse(oldPath) || IsLockedBySomeoneElse(newPath) ? AssetMoveResult.FailedMove : AssetMoveResult.DidNotMove; } public static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions option) { - //Logger.Trace("OnWillDeleteAsset:{0}", assetPath); - return IsLocked(assetPath) ? AssetDeleteResult.FailedDelete : AssetDeleteResult.DidNotDelete; + return IsLockedBySomeoneElse(assetPath) ? AssetDeleteResult.FailedDelete : AssetDeleteResult.DidNotDelete; } + // Returns true if this file can be edited by this user public static bool IsOpenForEdit(string assetPath, out string message) { - //Logger.Trace("IsOpenForEdit:{0}", assetPath); var lck = GetLock(assetPath); - message = lck.HasValue ? "File is locked for editing by " + lck.Value.User : null; - return !lck.HasValue; + bool canEdit = true; + if (assetPath.EndsWith(".meta")) + { + canEdit &= !IsLockedBySomeoneElse(lck); + assetPath = assetPath.TrimEnd(".meta"); + } + canEdit &= !IsLockedBySomeoneElse(lck); + message = !canEdit ? string.Format("File is locked for editing by {0}", lck.Value.Owner.Name) : null; + return canEdit; } private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) @@ -71,9 +79,14 @@ private static void UserMayHaveChanged() loggedInUser = platform.Keychain.Connections.Select(x => x.Username).FirstOrDefault(); } - private static bool IsLocked(string assetPath) + private static bool IsLockedBySomeoneElse(GitLock? lck) { - return GetLock(assetPath).HasValue; + return lck.HasValue && !lck.Value.Owner.Name.Equals(loggedInUser); + } + + private static bool IsLockedBySomeoneElse(string assetPath) + { + return IsLockedBySomeoneElse(GetLock(assetPath)); } private static GitLock? GetLock(string assetPath) @@ -83,9 +96,42 @@ private static bool IsLocked(string assetPath) GitLock lck; var repositoryPath = environment.GetRepositoryPath(assetPath.ToNPath()); - if (!locks.TryGetValue(repositoryPath, out lck) || lck.User.Equals(loggedInUser)) - return null; - return lck; + if (locks.TryGetValue(repositoryPath, out lck)) + return lck; + return null; + } + + private static void InspectorHeaderFinished(Editor editor) + { + string message = ""; + if (!IsOpenForEdit(AssetDatabase.GetAssetPath(editor.target), out message)) + { + var enabled = GUI.enabled; + GUI.enabled = true; + GUILayout.BeginVertical(); + { + GUILayout.Space(9); + GUILayout.BeginHorizontal(); + { + GUILayout.BeginVertical(GUILayout.Width(32)); + { + GUILayout.Label(Utility.GetIcon("big-logo.png", "big-logo@2x.png", Utility.IsDarkTheme), GUILayout.Width(32), GUILayout.Height(32)); + } + GUILayout.EndVertical(); + + GUILayout.BeginVertical(); + { + GUILayout.Space(9); + GUILayout.Label(message, Styles.HeaderBranchLabelStyle); + } + GUILayout.EndVertical(); + } + GUILayout.EndHorizontal(); + } + GUILayout.EndVertical(); + GUI.enabled = enabled; + } + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs new file mode 100644 index 000000000..0b22ee253 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -0,0 +1,670 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace GitHub.Unity +{ + [Serializable] + public class GitLockEntryDictionary : SerializableDictionary { } + + [Serializable] + public class GitStatusDictionary : SerializableDictionary { } + + [Serializable] + public class GitLockEntry + { + public static GitLockEntry Default = new GitLockEntry(GitLock.Default, GitFileStatus.None); + + [NonSerialized] public Texture Icon; + [NonSerialized] public Texture IconBadge; + + [SerializeField] private GitLock gitLock; + [SerializeField] private GitFileStatus gitFileStatus; + [SerializeField] private string lockedAt; + + public GitLockEntry(GitLock gitLock, GitFileStatus gitFileStatus) + { + this.gitLock = gitLock; + this.gitFileStatus = gitFileStatus; + this.lockedAt = gitLock.LockedAt.ToLocalTime().CreateRelativeTime(DateTimeOffset.Now); + } + + public GitLock GitLock + { + get { return gitLock; } + } + + public GitFileStatus GitFileStatus + { + get { return gitFileStatus; } + } + + public string LockedAt { get { return lockedAt; } } + } + + [Serializable] + class LocksControl + { + [NonSerialized] private Action rightClickNextRender; + [NonSerialized] private GitLockEntry rightClickNextRenderEntry; + [NonSerialized] private int controlId; + [NonSerialized] private UnityEngine.Object lastActivatedObject; + [NonSerialized] private Dictionary visibleItems = new Dictionary(); + + [SerializeField] private Vector2 scroll; + [SerializeField] private List gitLockEntries = new List(); + [SerializeField] public GitLockEntryDictionary assets = new GitLockEntryDictionary(); + [SerializeField] public GitStatusDictionary gitStatusDictionary = new GitStatusDictionary(); + [SerializeField] private GitLockEntry selectedEntry; + [SerializeField] public NPath projectPath; + + public bool IsEmpty { get { return gitLockEntries.Count == 0; } } + + public GitLockEntry SelectedEntry + { + get + { + return selectedEntry; + } + set + { + selectedEntry = value; + + var activeObject = selectedEntry != null && selectedEntry.GitLock != GitLock.Default && projectPath.IsInitialized + ? AssetDatabase.LoadMainAssetAtPath(selectedEntry.GitLock.Path.MakeAbsolute().RelativeTo(projectPath)) + : null; + + lastActivatedObject = activeObject; + + if (LocksControlHasFocus) + { + Selection.activeObject = activeObject; + } + } + } + + public bool Render(Rect containingRect, Action singleClick = null, + Action doubleClick = null, Action rightClick = null) + { + var requiresRepaint = false; + scroll = GUILayout.BeginScrollView(scroll); + { + controlId = GUIUtility.GetControlID(FocusType.Keyboard); + + if (Event.current.type != EventType.Repaint) + { + if (rightClickNextRender != null) + { + rightClickNextRender.Invoke(rightClickNextRenderEntry.GitLock); + rightClickNextRender = null; + rightClickNextRenderEntry = GitLockEntry.Default; + } + } + + var startDisplay = scroll.y; + var endDisplay = scroll.y + containingRect.height; + + var rect = new Rect(containingRect.x, containingRect.y, containingRect.width, 0); + for (var index = 0; index < gitLockEntries.Count; index++) + { + var entry = gitLockEntries[index]; + + var entryRect = new Rect(rect.x, rect.y, rect.width, Styles.LocksEntryHeight); + + if (Event.current.type == EventType.Layout) + { + var shouldRenderEntry = !(entryRect.y > endDisplay || entryRect.yMax < startDisplay); + visibleItems[entry.GitLock.ID] = shouldRenderEntry; + } + + if (visibleItems.ContainsKey(entry.GitLock.ID) && visibleItems[entry.GitLock.ID]) + { + entryRect = RenderEntry(entryRect, entry); + } + + var entryRequiresRepaint = + HandleInput(entryRect, entry, index, singleClick, doubleClick, rightClick); + requiresRepaint = requiresRepaint || entryRequiresRepaint; + + rect.y += entryRect.height; + } + + GUILayout.Space(rect.y - containingRect.y); + } + GUILayout.EndScrollView(); + + return requiresRepaint; + } + + private Rect RenderEntry(Rect entryRect, GitLockEntry entry) + { + var isSelected = entry == SelectedEntry; + var iconWidth = 32; + var iconHeight = 32; + var iconBadgeWidth = 16; + var iconBadgeHeight = 16; + var hasKeyboardFocus = GUIUtility.keyboardControl == controlId; + + GUILayout.BeginHorizontal(isSelected ? Styles.SelectedArea : Styles.Label); + GUILayout.Label(entry.Icon, GUILayout.Height(iconWidth), GUILayout.Width(iconHeight)); + if (Event.current.type == EventType.Repaint) + { + var iconRect = GUILayoutUtility.GetLastRect(); + var iconBadgeRect = new Rect(iconRect.x + iconBadgeWidth, iconRect.y + iconBadgeHeight, iconBadgeWidth, iconBadgeHeight); + Styles.Label.Draw(iconBadgeRect, entry.IconBadge, false, false, false, hasKeyboardFocus); + } + GUILayout.BeginVertical(); + GUILayout.Label(entry.GitLock.Path, isSelected ? Styles.SelectedLabel : Styles.Label); + GUILayout.Label(string.Format("Locked {0} by {1}", entry.LockedAt, entry.GitLock.Owner.Name), isSelected ? Styles.LocksViewLockedBySelectedStyle : Styles.LocksViewLockedByStyle); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + var itemRect = GUILayoutUtility.GetLastRect(); + return itemRect; + } + + private bool HandleInput(Rect rect, GitLockEntry entry, int index, Action singleClick = null, + Action doubleClick = null, Action rightClick = null) + { + var requiresRepaint = false; + var clickRect = new Rect(0f, rect.y, rect.width, rect.height); + if (Event.current.type == EventType.MouseDown && clickRect.Contains(Event.current.mousePosition)) + { + Event.current.Use(); + GUIUtility.keyboardControl = controlId; + + SelectedEntry = entry; + requiresRepaint = true; + var clickCount = Event.current.clickCount; + var mouseButton = Event.current.button; + + if (mouseButton == 0 && clickCount == 1 && singleClick != null) + { + singleClick(entry.GitLock); + } + if (mouseButton == 0 && clickCount > 1 && doubleClick != null) + { + doubleClick(entry.GitLock); + } + if (mouseButton == 1 && clickCount == 1 && rightClick != null) + { + rightClickNextRender = rightClick; + rightClickNextRenderEntry = entry; + } + } + + // Keyboard navigation if this child is the current selection + if (GUIUtility.keyboardControl == controlId && entry == SelectedEntry && Event.current.type == EventType.KeyDown) + { + var directionY = Event.current.keyCode == KeyCode.UpArrow ? -1 : Event.current.keyCode == KeyCode.DownArrow ? 1 : 0; + if (directionY != 0) + { + Event.current.Use(); + + if (directionY > 0) + { + requiresRepaint = SelectNext(index); + } + else + { + requiresRepaint = SelectPrevious(index); + } + } + } + + return requiresRepaint; + } + + public void Load(List locks, List gitStatusEntries) + { + var statusEntries = new Dictionary(); + for (int i = 0; i < gitStatusEntries.Count; i++) + statusEntries.Add(gitStatusEntries[i].Path.ToNPath().ToString(SlashMode.Forward), i); + var selectedLockId = SelectedEntry != null && SelectedEntry.GitLock != GitLock.Default + ? SelectedEntry.GitLock.ID + : null; + + var scrollValue = scroll.y; + var previousCount = gitLockEntries.Count; + var scrollIndex = (int)(scrollValue / Styles.LocksEntryHeight); + + assets.Clear(); + visibleItems.Clear(); + + gitLockEntries = locks.Select(gitLock => + { + int index = -1; + GitFileStatus gitFileStatus = GitFileStatus.None; + if (statusEntries.TryGetValue(gitLock.Path.ToString(SlashMode.Forward), out index)) + { + gitFileStatus = gitStatusEntries[index].Status; + } + + var gitLockEntry = new GitLockEntry(gitLock, gitFileStatus); + LoadIcon(gitLockEntry, true); + var path = gitLock.Path.MakeAbsolute().RelativeTo(projectPath); + var assetGuid = AssetDatabase.AssetPathToGUID(path); + if (!string.IsNullOrEmpty(assetGuid)) + { + assets.Add(assetGuid, gitLockEntry); + } + + visibleItems.Add(gitLockEntry.GitLock.ID, false); + return gitLockEntry; + }).ToList(); + + var selectionPresent = false; + for (var index = 0; index < gitLockEntries.Count; index++) + { + var gitLockEntry = gitLockEntries[index]; + if (selectedLockId == gitLockEntry.GitLock.ID) + { + selectedEntry = gitLockEntry; + selectionPresent = true; + break; + } + } + + if (!selectionPresent) + { + selectedEntry = GitLockEntry.Default; + } + + if (scrollIndex > gitLockEntries.Count) + { + ScrollTo(0); + } + else + { + var scrollOffset = scrollValue % Styles.LocksEntryHeight; + + var scrollIndexFromBottom = previousCount - scrollIndex; + var newScrollIndex = gitLockEntries.Count - scrollIndexFromBottom; + + ScrollTo(newScrollIndex, scrollOffset); + } + } + + public void LoadIcons() + { + foreach (var gitLockEntry in gitLockEntries) + { + LoadIcon(gitLockEntry); + } + } + + private void LoadIcon(GitLockEntry gitLockEntry, bool force = false) + { + if (force || gitLockEntry.Icon == null) + { + gitLockEntry.Icon = GetNodeIcon(gitLockEntry.GitLock); + } + + if (force || gitLockEntry.IconBadge == null) + { + gitLockEntry.IconBadge = Styles.GetFileStatusIcon(gitLockEntry.GitFileStatus, true); + } + } + + protected Texture GetNodeIcon(GitLock node) + { + Texture nodeIcon = null; + + if (!string.IsNullOrEmpty(node.Path)) + { + nodeIcon = UnityEditorInternal.InternalEditorUtility.GetIconForFile(node.Path); + } + + if (nodeIcon != null) + { + nodeIcon.hideFlags = HideFlags.HideAndDontSave; + } + + return nodeIcon; + } + + protected bool LocksControlHasFocus + { + get { return GUIUtility.keyboardControl == controlId; } + } + + private bool SelectNext(int index) + { + index++; + + if (index < gitLockEntries.Count) + { + SelectedEntry = gitLockEntries[index]; + return true; + } + + return false; + } + + private bool SelectPrevious(int index) + { + index--; + + if (index >= 0) + { + SelectedEntry = gitLockEntries[index]; + return true; + } + + return false; + } + + public void ScrollTo(int index, float offset = 0f) + { + scroll.Set(scroll.x, Styles.LocksEntryHeight * index + offset); + } + + public bool OnSelectionChange() + { + if (!LocksControlHasFocus) + { + GitLockEntry gitLockEntry = GitLockEntry.Default; + if (Selection.activeObject != lastActivatedObject) + { + var activeAssetPath = AssetDatabase.GetAssetPath(Selection.activeObject); + var activeAssetGuid = AssetDatabase.AssetPathToGUID(activeAssetPath); + assets.TryGetValue(activeAssetGuid, out gitLockEntry); + } + SelectedEntry = gitLockEntry; + return true; + } + return false; + } + } + + [Serializable] + class LocksView : Subview + { + [NonSerialized] private bool isBusy; + + [SerializeField] private bool currentRemoteHasUpdate; + [SerializeField] private bool currentStatusEntriesHasUpdate; + [SerializeField] private bool currentLocksHasUpdate; + [SerializeField] private bool keychainHasUpdate; + [SerializeField] private LocksControl locksControl; + [SerializeField] private CacheUpdateEvent lastCurrentRemoteChangedEvent; + [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; + [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; + [SerializeField] private List lockedFiles = new List(); + [SerializeField] private List gitStatusEntries = new List(); + [SerializeField] private string currentUsername; + [SerializeField] private GUIContent unlockFileMenuContent = new GUIContent(Localization.UnlockFileMenuItem); + [SerializeField] private GUIContent forceUnlockFileMenuContent = new GUIContent(Localization.ForceUnlockFileMenuItem); + + public override void OnEnable() + { + base.OnEnable(); + + if (locksControl != null) + { + locksControl.LoadIcons(); + } + + AttachHandlers(Repository); + ValidateCachedData(Repository); + KeychainConnectionsChanged(); + } + + public override void OnDisable() + { + base.OnDisable(); + DetachHandlers(Repository); + } + + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.GitStatus); + Refresh(CacheType.GitLocks); + } + + public override void OnDataUpdate() + { + base.OnDataUpdate(); + MaybeUpdateData(); + } + + public override void OnGUI() + { + var rect = GUILayoutUtility.GetLastRect(); + + EditorGUI.BeginDisabledGroup(IsBusy); + + if (locksControl != null && !locksControl.IsEmpty) + { + var lockControlRect = new Rect(rect.x, rect.y, Position.width, Position.height - rect.height); + + var requiresRepaint = locksControl.Render(lockControlRect, + entry => {}, + entry => {}, + entry => + { + var menu = new GenericMenu(); + if (entry.Owner.Name == currentUsername) + { + menu.AddItem(unlockFileMenuContent, false, UnlockSelectedEntry); + } + menu.AddItem(forceUnlockFileMenuContent, false, ForceUnlockSelectedEntry); + menu.ShowAsContext(); + }); + + if (requiresRepaint) + Redraw(); + } + else + { + DoEmptyGUI(); + } + + EditorGUI.EndDisabledGroup(); + DoProgressGUI(); + } + + private void UnlockSelectedEntry() + { + isBusy = true; + Repository + .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, false) + .FinallyInUI((success, ex) => + { + if (success) + { + Manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); + } + else + { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, + error, + Localization.Ok); + } + + isBusy = false; + }) + .Start(); + } + + private void ForceUnlockSelectedEntry() + { + isBusy = true; + Repository + .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, true) + .FinallyInUI((success, ex) => + { + if (success) + { + Manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); + } + else + { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, + error, + Localization.Ok); + } + + isBusy = false; + }) + .Start(); + } + + private void AttachHandlers(IRepository repository) + { + if (repository == null) + { + return; + } + + Platform.Keychain.ConnectionsChanged += KeychainConnectionsChanged; + repository.CurrentRemoteChanged += RepositoryOnCurrentRemoteChanged; + repository.LocksChanged += RepositoryOnLocksChanged; + repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + } + + private void DetachHandlers(IRepository repository) + { + if (repository == null) + { + return; + } + + Platform.Keychain.ConnectionsChanged -= KeychainConnectionsChanged; + repository.CurrentRemoteChanged -= RepositoryOnCurrentRemoteChanged; + repository.LocksChanged -= RepositoryOnLocksChanged; + repository.StatusEntriesChanged -= RepositoryOnStatusEntriesChanged; + } + + private void RepositoryOnCurrentRemoteChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastCurrentRemoteChangedEvent.Equals(cacheUpdateEvent)) + { + lastCurrentRemoteChangedEvent = cacheUpdateEvent; + currentRemoteHasUpdate = true; + Redraw(); + } + } + + private void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) + { + lastLocksChangedEvent = cacheUpdateEvent; + currentLocksHasUpdate = true; + Redraw(); + } + } + + private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) + { + lastStatusEntriesChangedEvent = cacheUpdateEvent; + currentStatusEntriesHasUpdate = true; + Redraw(); + } + } + + private void KeychainConnectionsChanged() + { + keychainHasUpdate = true; + Redraw(); + } + + private void ValidateCachedData(IRepository repository) + { + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentRemoteChangedEvent); + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastStatusEntriesChangedEvent); + } + + private void MaybeUpdateData() + { + if (Repository == null) + { + return; + } + + if (keychainHasUpdate || currentRemoteHasUpdate) + { + var username = String.Empty; + if (Repository != null) + { + Connection connection; + if (!string.IsNullOrEmpty(Repository.CloneUrl)) + { + var host = Repository.CloneUrl + .ToRepositoryUri() + .GetComponents(UriComponents.Host, UriFormat.SafeUnescaped); + + connection = Platform.Keychain.Connections.FirstOrDefault(x => x.Host == host); + } + else + { + connection = Platform.Keychain.Connections.FirstOrDefault(HostAddress.IsGitHubDotCom); + } + + if (connection != null) + { + username = connection.Username; + } + } + + currentUsername = username; + + keychainHasUpdate = false; + currentRemoteHasUpdate = false; + } + + if (currentLocksHasUpdate) + { + lockedFiles = Repository.CurrentLocks; + } + + if (currentStatusEntriesHasUpdate) + { + gitStatusEntries = Repository.CurrentChanges.Where(x => x.Status != GitFileStatus.Ignored).ToList(); + } + + if (currentStatusEntriesHasUpdate || currentLocksHasUpdate) + { + currentStatusEntriesHasUpdate = false; + currentLocksHasUpdate = false; + BuildLocksControl(); + } + } + + private void BuildLocksControl() + { + if (locksControl == null) + { + locksControl = new LocksControl(); + } + + locksControl.projectPath = Environment.UnityProjectPath; + locksControl.Load(lockedFiles, gitStatusEntries); + } + public override void OnSelectionChange() + { + base.OnSelectionChange(); + if (locksControl.OnSelectionChange()) + { + Redraw(); + } + } + + public override bool IsBusy + { + get { return isBusy || base.IsBusy; } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index 2f421b7a6..406a5e178 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; namespace GitHub.Unity { [Serializable] - class PopupWindow : BaseWindow + public class PopupWindow : BaseWindow { public enum PopupViewType { @@ -14,8 +16,6 @@ public enum PopupViewType AuthenticationView, } - [NonSerialized] private IApiClient client; - [SerializeField] private PopupViewType activeViewType; [SerializeField] private AuthenticationView authenticationView; [SerializeField] private LoadingView loadingView; @@ -113,34 +113,46 @@ private void Open(PopupViewType popupViewType, Action onClose) OnClose.SafeInvoke(false); OnClose = null; - //Logger.Trace("OpenView: {0}", popupViewType.ToString()); - var viewNeedsAuthentication = popupViewType == PopupViewType.PublishView; + if (viewNeedsAuthentication) { - //Logger.Trace("Validating to open view"); - - Client.GetCurrentUser(user => { + var userHasAuthentication = false; + foreach (var keychainConnection in Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom)) + { + var apiClient = new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, + Environment, keychainConnection.Host); - //Logger.Trace("User validated opening view"); + try + { + apiClient.EnsureValidCredentials(); + userHasAuthentication = true; + break; + } + catch (Exception ex) + { + Logger.Trace(ex, "Exception validating host {0}", keychainConnection.Host); + } + } + if (userHasAuthentication) + { OpenInternal(popupViewType, onClose); shouldCloseOnFinish = true; - - }, exception => { - //Logger.Trace("User required validation opening AuthenticationView"); - authenticationView.Initialize(exception); - OpenInternal(PopupViewType.AuthenticationView, completedAuthentication => { + } + else + { + authenticationView.Initialize(null); + OpenInternal(PopupViewType.AuthenticationView, completedAuthentication => + { if (completedAuthentication) { - //Logger.Trace("User completed validation opening view: {0}", popupViewType.ToString()); - Open(popupViewType, onClose); } }); shouldCloseOnFinish = false; - }); + } } else { @@ -175,30 +187,6 @@ private void SwitchView(Subview fromView, Subview toView) Repaint(); } - public IApiClient Client - { - get - { - if (client == null) - { - var repository = Environment.Repository; - UriString host; - if (repository != null && !string.IsNullOrEmpty(repository.CloneUrl)) - { - host = repository.CloneUrl.ToRepositoryUrl(); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); - } - - return client; - } - } - private Subview ActiveView { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index c342f93b3..9e9a3547e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -1,48 +1,72 @@ -using GitHub.Logging; using System; +using GitHub.Logging; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using UnityEditor; using UnityEngine; +using Object = UnityEngine.Object; namespace GitHub.Unity { class ProjectWindowInterface : AssetPostprocessor { - private static readonly List entries = new List(); + private const string AssetsMenuRequestLock = "Assets/Request Lock"; + private const string AssetsMenuReleaseLock = "Assets/Release Lock"; + private const string AssetsMenuReleaseLockForced = "Assets/Release Lock (forced)"; + + private static List entries = new List(); private static List locks = new List(); + private static List guids = new List(); + private static List guidsLocks = new List(); + private static string currentUsername; - private static readonly List guids = new List(); - private static readonly List guidsLocks = new List(); - private static IRepository repository; + private static IApplicationManager manager; private static bool isBusy = false; private static ILogging logger; private static ILogging Logger { get { return logger = logger ?? LogHelper.GetLogger(); } } private static CacheUpdateEvent lastRepositoryStatusChangedEvent; private static CacheUpdateEvent lastLocksChangedEvent; + private static CacheUpdateEvent lastCurrentRemoteChangedEvent; + private static IRepository Repository { get { return manager != null ? manager.Environment.Repository : null; } } + private static IPlatform Platform { get { return manager != null ? manager.Platform : null; } } + private static bool IsInitialized { get { return Repository != null; } } - public static void Initialize(IRepository repo) + public static void Initialize(IApplicationManager theManager) { - //Logger.Trace("Initialize HasRepository:{0}", repo != null); - EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemGUI; EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI; - repository = repo; + manager = theManager; - if (repository != null) + Platform.Keychain.ConnectionsChanged += UpdateCurrentUsername; + UpdateCurrentUsername(); + + if (IsInitialized) { - repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; - repository.LocksChanged += RepositoryOnLocksChanged; + Repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + Repository.LocksChanged += RepositoryOnLocksChanged; + Repository.CurrentRemoteChanged += RepositoryOnCurrentRemoteChanged; + ValidateCachedData(); } } - private static void ValidateCachedData(IRepository repository) + private static bool EnsureInitialized() + { + if (locks == null) + locks = new List(); + if (entries == null) + entries = new List(); + if (guids == null) + guids = new List(); + if (guidsLocks == null) + guidsLocks = new List(); + return IsInitialized; + } + + private static void ValidateCachedData() { - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastRepositoryStatusChangedEvent); - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); + Repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastRepositoryStatusChangedEvent); + Repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); } private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) @@ -51,7 +75,7 @@ private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdat { lastRepositoryStatusChangedEvent = cacheUpdateEvent; entries.Clear(); - entries.AddRange(repository.CurrentChanges); + entries.AddRange(Repository.CurrentChanges); OnStatusUpdate(); } } @@ -61,121 +85,200 @@ private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) { lastLocksChangedEvent = cacheUpdateEvent; - locks = repository.CurrentLocks; + locks = Repository.CurrentLocks; OnLocksUpdate(); } } - [MenuItem("Assets/Request Lock", true)] + private static void RepositoryOnCurrentRemoteChanged(CacheUpdateEvent cacheUpdateEvent) + { + if (!lastCurrentRemoteChangedEvent.Equals(cacheUpdateEvent)) + { + lastCurrentRemoteChangedEvent = cacheUpdateEvent; + } + } + + private static void UpdateCurrentUsername() + { + var username = String.Empty; + if (Repository != null) + { + Connection connection; + if (!string.IsNullOrEmpty(Repository.CloneUrl)) + { + var host = Repository.CloneUrl + .ToRepositoryUri() + .GetComponents(UriComponents.Host, UriFormat.SafeUnescaped); + + connection = Platform.Keychain.Connections.FirstOrDefault(x => x.Host == host); + } + else + { + connection = Platform.Keychain.Connections.FirstOrDefault(HostAddress.IsGitHubDotCom); + } + + if (connection != null) + { + username = connection.Username; + } + } + + currentUsername = username; + } + + [MenuItem(AssetsMenuRequestLock, true, 10000)] private static bool ContextMenu_CanLock() { + if (!EnsureInitialized()) + return false; + if (!Repository.CurrentRemote.HasValue) + return false; if (isBusy) return false; - if (repository == null || !repository.CurrentRemote.HasValue) + return Selection.objects.Any(IsObjectUnlocked); + } + + [MenuItem(AssetsMenuReleaseLock, true, 10001)] + private static bool ContextMenu_CanUnlock() + { + if (!EnsureInitialized()) + return false; + if (!Repository.CurrentRemote.HasValue) + return false; + if (isBusy) return false; + return Selection.objects.Any(f => IsObjectLocked(f , true)); + } - var selected = Selection.activeObject; - if (selected == null) + [MenuItem(AssetsMenuReleaseLockForced, true, 10002)] + private static bool ContextMenu_CanUnlockForce() + { + if (!EnsureInitialized()) return false; - if (locks == null) + if (!Repository.CurrentRemote.HasValue) return false; + if (isBusy) + return false; + return Selection.objects.Any(IsObjectLocked); + } - NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + [MenuItem(AssetsMenuRequestLock, false, 10000)] + private static void ContextMenu_Lock() + { + RunLockUnlock(IsObjectUnlocked, CreateLockObjectTask, Localization.RequestLockActionTitle, "Failed to lock: no permissions"); + } - var alreadyLocked = locks.Any(x => - { - return repositoryPath == x.Path.ToNPath(); + [MenuItem(AssetsMenuReleaseLock, false, 10001)] + private static void ContextMenu_Unlock() + { + RunLockUnlock(IsObjectLocked, x => CreateUnlockObjectTask(x, false), Localization.ReleaseLockActionTitle, "Failed to unlock: no permissions"); + } - }); - GitFileStatus status = GitFileStatus.None; - if (entries != null) + [MenuItem(AssetsMenuReleaseLockForced, false, 10002)] + private static void ContextMenu_UnlockForce() + { + RunLockUnlock(IsObjectLocked, x => CreateUnlockObjectTask(x, true), Localization.ReleaseLockActionTitle, "Failed to unlock: no permissions"); + } + + private static void RunLockUnlock(Func selector, Func creator, string title, string errorMessage) + { + isBusy = true; + var taskQueue = new TaskQueue(); + foreach (var lockedObject in Selection.objects.Where(selector)) { - status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; + taskQueue.Queue(creator(lockedObject)); } - return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; + taskQueue.FinallyInUI((success, exception) => + { + if (!success) + { + var error = exception.Message; + if (error.Contains("exit status 255")) + error = errorMessage; + EditorUtility.DisplayDialog(title, error, Localization.Ok); + } + isBusy = false; + }); + taskQueue.Start(); } - [MenuItem("Assets/Request Lock")] - private static void ContextMenu_Lock() + private static bool IsObjectUnlocked(Object selected) { - isBusy = true; - var selected = Selection.activeObject; + if (selected == null) + return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - repository - .RequestLock(repositoryPath) - .ThenInUI(_ => - { - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }) - .Start(); + var alreadyLocked = locks.Any(x => repositoryPath == x.Path); + if (alreadyLocked) + return false; + + GitFileStatus status = GitFileStatus.None; + if (entries != null) + { + status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; + } + return status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; } - [MenuItem("Assets/Release lock", true, 1000)] - private static bool ContextMenu_CanUnlock() + private static bool IsObjectLocked(Object selected) { - if (isBusy) - return false; - if (repository == null || !repository.CurrentRemote.HasValue) - return false; + return IsObjectLocked(selected, false); + } - var selected = Selection.activeObject; + private static bool IsObjectLocked(Object selected, bool isLockedByCurrentUser) + { if (selected == null) return false; - if (locks == null || locks.Count == 0) - return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - var isLocked = locks.Any(x => repositoryPath == x.Path.ToNPath()); - return isLocked; + return locks.Any(x => repositoryPath == x.Path && (!isLockedByCurrentUser || x.Owner.Name == currentUsername)); } - [MenuItem("Assets/Release lock", false, 1000)] - private static void ContextMenu_Unlock() + private static ITask CreateUnlockObjectTask(Object selected, bool force) { - isBusy = true; - var selected = Selection.activeObject; + NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); + + var task = Repository.ReleaseLock(repositoryPath, force); + task.OnEnd += (_, s, __) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); }; + return task; + } + private static ITask CreateLockObjectTask(Object selected) + { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - repository - .ReleaseLock(repositoryPath, false) - .ThenInUI(_ => - { - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }) - .Start(); + var task = Repository.RequestLock(repositoryPath); + task.OnEnd += (_, s, ___) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); }; + return task; } private static void OnLocksUpdate() { - if (locks == null) - { - return; - } - locks = locks.ToList(); - guidsLocks.Clear(); foreach (var lck in locks) { - NPath repositoryPath = lck.Path.ToNPath(); - NPath assetPath = EntryPoint.Environment.GetAssetPath(repositoryPath); + NPath repositoryPath = lck.Path; + NPath assetPath = manager.Environment.GetAssetPath(repositoryPath); var g = AssetDatabase.AssetPathToGUID(assetPath); guidsLocks.Add(g); } - EditorApplication.RepaintProjectWindow(); + // https://github.com/github-for-unity/Unity/pull/959#discussion_r236694800 + // We need to repaint not only the project window, but also the inspector. + // so that we can show the "this thing is locked by X" and that the IsOpenForEdit call happens + // and the inspector is disabled. There's no way to refresh the editor directly + // (well, there is, but it's an internal api), so this just causes Unity to repaint everything. + // Nail, meet bazooka, unfortunately, but that's the only way to do it with public APIs ¯_(ツ)_/¯ + + //EditorApplication.RepaintProjectWindow(); + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } private static void OnStatusUpdate() @@ -183,33 +286,19 @@ private static void OnStatusUpdate() guids.Clear(); for (var index = 0; index < entries.Count; ++index) { - var gitStatusEntry = entries[index]; - - var path = gitStatusEntry.ProjectPath; - if (gitStatusEntry.Status == GitFileStatus.Ignored) - { - continue; - } - - if (!path.StartsWith("Assets", StringComparison.CurrentCultureIgnoreCase)) - { - continue; - } - - if (path.EndsWith(".meta", StringComparison.CurrentCultureIgnoreCase)) - { - continue; - } - + var path = entries[index].ProjectPath; var guid = AssetDatabase.AssetPathToGUID(path); guids.Add(guid); } - EditorApplication.RepaintProjectWindow(); + AssetDatabase.Refresh(); } private static void OnProjectWindowItemGUI(string guid, Rect itemRect) { + if (!EnsureInitialized()) + return; + if (Event.current.type != EventType.Repaint || string.IsNullOrEmpty(guid)) { return; @@ -256,6 +345,7 @@ private static void OnProjectWindowItemGUI(string guid, Rect itemRect) { var scale = itemRect.height / 90f; var size = new Vector2(texture.width * scale, texture.height * scale); + size = size / EditorGUIUtility.pixelsPerPoint; var offset = new Vector2(itemRect.width * Mathf.Min(.4f * scale, .2f), itemRect.height * Mathf.Min(.2f * scale, .2f)); rect = new Rect(itemRect.center.x - size.x * .5f + offset.x, itemRect.center.y - size.y * .5f + offset.y, size.x, size.y); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index 077f54266..832cfa7c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -22,7 +22,10 @@ class PublishView : Subview private const string PublishLimitPrivateRepositoriesError = "You are currently at your limit of private repositories"; private const string PublishToGithubLabel = "Publish to GitHub"; - [SerializeField] private string username; + [SerializeField] private Connection[] connections; + [SerializeField] private string[] connectionLabels; + [SerializeField] private int selectedConnection; + [SerializeField] private string[] owners = { OwnersDefaultText }; [SerializeField] private string[] publishOwners; [SerializeField] private int selectedOwner; @@ -30,39 +33,18 @@ class PublishView : Subview [SerializeField] private string repoDescription = ""; [SerializeField] private bool togglePrivate; - [NonSerialized] private IApiClient client; + [NonSerialized] private Dictionary clients = new Dictionary(); + [NonSerialized] private IApiClient selectedClient; [NonSerialized] private bool isBusy; [NonSerialized] private string error; + [NonSerialized] private bool connectionsNeedLoading; [NonSerialized] private bool ownersNeedLoading; - public IApiClient Client - { - get - { - if (client == null) - { - var repository = Environment.Repository; - UriString host; - if (repository != null && !string.IsNullOrEmpty(repository.CloneUrl)) - { - host = repository.CloneUrl.ToRepositoryUrl(); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); - } - - return client; - } - } - public override void OnEnable() { base.OnEnable(); ownersNeedLoading = publishOwners == null && !isBusy; + connectionsNeedLoading = connections == null && !isBusy; } public override void OnDataUpdate() @@ -73,6 +55,17 @@ public override void OnDataUpdate() private void MaybeUpdateData() { + if (connectionsNeedLoading) + { + connectionsNeedLoading = false; + connections = Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom).ToArray(); + connectionLabels = connections.Select(c => HostAddress.IsGitHubDotCom(c) ? "GitHub" : c.Host.ToUriString().Host).ToArray(); + + var connection = connections.First(); + selectedConnection = 0; + selectedClient = GetApiClient(connection); + } + if (ownersNeedLoading) { ownersNeedLoading = false; @@ -80,6 +73,20 @@ private void MaybeUpdateData() } } + private IApiClient GetApiClient(Connection connection) + { + IApiClient client; + + if (!clients.TryGetValue(connection.Host, out client)) + { + client = new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, Environment, connection.Host); + + clients.Add(connection.Host, client); + } + + return client; + } + public override void InitializeView(IView parent) { base.InitializeView(parent); @@ -89,38 +96,28 @@ public override void InitializeView(IView parent) private void LoadOwners() { - var keychainConnections = Platform.Keychain.Connections; - //TODO: ONE_USER_LOGIN This assumes only ever one user can login - isBusy = true; - //TODO: ONE_USER_LOGIN This assumes only ever one user can login - username = keychainConnections.First().Username; - - //Logger.Trace("Loading Owners"); - - Client.GetOrganizations(orgs => + selectedClient.GetOrganizations(orgs => { - //Logger.Trace("Loaded {0} Owners", orgs.Length); - publishOwners = orgs .OrderBy(organization => organization.Login) .Select(organization => organization.Login) .ToArray(); - owners = new[] { OwnersDefaultText, username }.Union(publishOwners).ToArray(); + owners = new[] { OwnersDefaultText, connections[selectedConnection].Username }.Union(publishOwners).ToArray(); isBusy = false; Redraw(); - }, exception => + }, + exception => { isBusy = false; var keychainEmptyException = exception as KeychainEmptyException; if (keychainEmptyException != null) { - //Logger.Trace("Keychain empty"); PopupWindow.OpenWindow(PopupWindow.PopupViewType.AuthenticationView); return; } @@ -133,12 +130,26 @@ public override void OnGUI() { GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); { - GUILayout.Label(PublishToGithubLabel, EditorStyles.boldLabel); + GUILayout.Label(PublishToGithubLabel, EditorStyles.boldLabel); } GUILayout.EndHorizontal(); EditorGUI.BeginDisabledGroup(isBusy); { + if (connections.Length > 1) + { + EditorGUI.BeginChangeCheck(); + { + selectedConnection = EditorGUILayout.Popup("Connections:", selectedConnection, connectionLabels); + } + if (EditorGUI.EndChangeCheck()) + { + selectedClient = GetApiClient(connections[selectedConnection]); + ownersNeedLoading = true; + Redraw(); + } + } + selectedOwner = EditorGUILayout.Popup(SelectedOwnerLabel, selectedOwner, owners); repoName = EditorGUILayout.TextField(RepositoryNameLabel, repoName); repoDescription = EditorGUILayout.TextField(DescriptionLabel, repoDescription); @@ -157,31 +168,29 @@ public override void OnGUI() GUI.FocusControl(null); isBusy = true; - var organization = owners[selectedOwner] == username ? null : owners[selectedOwner]; + var organization = owners[selectedOwner] == connections[selectedConnection].Username ? null : owners[selectedOwner]; var cleanRepoDescription = repoDescription.Trim(); cleanRepoDescription = string.IsNullOrEmpty(cleanRepoDescription) ? null : cleanRepoDescription; - Client.CreateRepository(repoName, cleanRepoDescription, togglePrivate, (repository, ex) => + selectedClient.CreateRepository(repoName, cleanRepoDescription, togglePrivate, (repository, ex) => { if (ex != null) { Logger.Error(ex, "Repository Create Error Type:{0}", ex.GetType().ToString()); - error = GetPublishErrorMessage(ex); isBusy = false; return; } + UsageTracker.IncrementPublishViewButtonPublish(); + if (repository == null) { Logger.Warning("Returned Repository is null"); isBusy = false; return; } - - Logger.Trace("Repository Created"); - Repository.RemoteAdd("origin", repository.CloneUrl) .Then(Repository.Push("origin")) .ThenInUI(Finish) @@ -208,7 +217,7 @@ private string GetPublishErrorMessage(Exception ex) { return PublishLimitPrivateRepositoriesError; } - + return ex.Message; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs index f5f509bd8..890fb09c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs @@ -18,11 +18,11 @@ class SettingsView : Subview private const string DebugSettingsTitle = "Debug"; private const string PrivacyTitle = "Privacy"; private const string WebTimeoutLabel = "Timeout of web requests"; + private const string GitTimeoutLabel = "Timeout of git commands"; private const string EnableTraceLoggingLabel = "Enable Trace Logging"; private const string MetricsOptInLabel = "Help us improve by sending anonymous usage data"; private const string DefaultRepositoryRemoteName = "origin"; - [NonSerialized] private bool currentLocksHasUpdate; [NonSerialized] private bool currentRemoteHasUpdate; [NonSerialized] private bool isBusy; [NonSerialized] private bool metricsHasChanged; @@ -30,10 +30,6 @@ class SettingsView : Subview [SerializeField] private GitPathView gitPathView = new GitPathView(); [SerializeField] private bool hasRemote; [SerializeField] private CacheUpdateEvent lastCurrentRemoteChangedEvent; - [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; - [SerializeField] private List lockedFiles = new List(); - [SerializeField] private int lockedFileSelection = -1; - [SerializeField] private Vector2 lockScrollPos; [SerializeField] private bool metricsEnabled; [SerializeField] private string newRepositoryRemoteUrl; [SerializeField] private string repositoryRemoteName; @@ -41,6 +37,7 @@ class SettingsView : Subview [SerializeField] private Vector2 scroll; [SerializeField] private UserSettingsView userSettingsView = new UserSettingsView(); [SerializeField] private int webTimeout; + [SerializeField] private int gitTimeout; public override void InitializeView(IView parent) { @@ -64,7 +61,6 @@ public override void OnEnable() metricsHasChanged = true; } - public override void OnDisable() { base.OnDisable(); @@ -87,6 +83,7 @@ public override void Refresh() base.Refresh(); gitPathView.Refresh(); userSettingsView.Refresh(); + Refresh(CacheType.RepositoryInfo); } public override void OnGUI() @@ -100,11 +97,6 @@ public override void OnGUI() if (Repository != null) { OnRepositorySettingsGUI(); - - GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); - - OnGitLfsLocksGUI(); - GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); } @@ -115,6 +107,8 @@ public override void OnGUI() } GUILayout.EndScrollView(); + + DoProgressGUI(); } private void AttachHandlers(IRepository repository) @@ -125,17 +119,6 @@ private void AttachHandlers(IRepository repository) } repository.CurrentRemoteChanged += RepositoryOnCurrentRemoteChanged; - repository.LocksChanged += RepositoryOnLocksChanged; - } - - private void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) - { - if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) - { - lastLocksChangedEvent = cacheUpdateEvent; - currentLocksHasUpdate = true; - Redraw(); - } } private void RepositoryOnCurrentRemoteChanged(CacheUpdateEvent cacheUpdateEvent) @@ -154,12 +137,13 @@ private void DetachHandlers(IRepository repository) { return; } + + repository.CurrentRemoteChanged -= RepositoryOnCurrentRemoteChanged; } private void ValidateCachedData(IRepository repository) { repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentRemoteChangedEvent); - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); } private void MaybeUpdateData() @@ -170,9 +154,6 @@ private void MaybeUpdateData() metricsHasChanged = false; } - if (lockedFiles == null) - lockedFiles = new List(); - if (Repository == null) return; @@ -192,16 +173,6 @@ private void MaybeUpdateData() newRepositoryRemoteUrl = repositoryRemoteUrl = currentRemote.Value.Url; } } - - if (currentLocksHasUpdate) - { - currentLocksHasUpdate = false; - var repositoryCurrentLocks = Repository.CurrentLocks; - lockedFileSelection = -1; - lockedFiles = repositoryCurrentLocks != null - ? repositoryCurrentLocks.ToList() - : new List(); - } } private void OnRepositorySettingsGUI() @@ -238,91 +209,18 @@ private void OnRepositorySettingsGUI() EditorGUI.EndDisabledGroup(); } - private void OnGitLfsLocksGUI() - { - EditorGUI.BeginDisabledGroup(IsBusy || Repository == null); - { - GUILayout.BeginVertical(); - { - GUILayout.Label("Locked files", EditorStyles.boldLabel); - - lockScrollPos = EditorGUILayout.BeginScrollView(lockScrollPos, Styles.GenericTableBoxStyle, - GUILayout.Height(125)); - { - GUILayout.BeginVertical(); - { - var lockedFilesCount = lockedFiles.Count; - for (var index = 0; index < lockedFilesCount; ++index) - { - GUIStyle rowStyle = (lockedFileSelection == index) - ? Styles.LockedFileRowSelectedStyle - : Styles.LockedFileRowStyle; - GUILayout.Box(lockedFiles[index].Path, rowStyle); - - if (Event.current.type == EventType.MouseDown && - GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition)) - { - var currentEvent = Event.current; - - if (currentEvent.button == 0) - { - lockedFileSelection = index; - } - - Event.current.Use(); - } - } - } - - GUILayout.EndVertical(); - } - - EditorGUILayout.EndScrollView(); - - if (lockedFileSelection > -1) - { - GUILayout.BeginVertical(); - { - var lck = lockedFiles[lockedFileSelection]; - GUILayout.Label(lck.Path, EditorStyles.boldLabel); - - GUILayout.BeginHorizontal(); - { - GUILayout.Label("Locked by " + lck.User); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Unlock")) - { - Repository.ReleaseLock(lck.Path, false).Start(); - } - } - GUILayout.EndHorizontal(); - } - GUILayout.EndVertical(); - } - } - - GUILayout.EndVertical(); - - } - EditorGUI.EndDisabledGroup(); - } - private void OnPrivacyGui() { GUILayout.Label(PrivacyTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy && Manager.UsageTracker != null); + EditorGUI.BeginChangeCheck(); { - - EditorGUI.BeginChangeCheck(); - { - metricsEnabled = GUILayout.Toggle(metricsEnabled, MetricsOptInLabel); - } - if (EditorGUI.EndChangeCheck()) - { - if (Manager.UsageTracker != null) - Manager.UsageTracker.Enabled = metricsEnabled; - } + metricsEnabled = GUILayout.Toggle(metricsEnabled, MetricsOptInLabel); + } + if (EditorGUI.EndChangeCheck()) + { + if (Manager.UsageTracker != null) + Manager.UsageTracker.Enabled = metricsEnabled; } EditorGUI.EndDisabledGroup(); } @@ -331,43 +229,45 @@ private void OnLoggingSettingsGui() { GUILayout.Label(DebugSettingsTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy); - { - var traceLogging = LogHelper.TracingEnabled; + var traceLogging = LogHelper.TracingEnabled; - EditorGUI.BeginChangeCheck(); - { - traceLogging = GUILayout.Toggle(traceLogging, EnableTraceLoggingLabel); - } - if (EditorGUI.EndChangeCheck()) - { - LogHelper.TracingEnabled = traceLogging; - Manager.UserSettings.Set(Constants.TraceLoggingKey, traceLogging); - } + EditorGUI.BeginChangeCheck(); + { + traceLogging = GUILayout.Toggle(traceLogging, EnableTraceLoggingLabel); + } + if (EditorGUI.EndChangeCheck()) + { + LogHelper.TracingEnabled = traceLogging; + Manager.UserSettings.Set(Constants.TraceLoggingKey, traceLogging); } - EditorGUI.EndDisabledGroup(); } private void OnGeneralSettingsGui() { GUILayout.Label(GeneralSettingsTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy); + webTimeout = ApplicationConfiguration.WebTimeout; + EditorGUI.BeginChangeCheck(); { - webTimeout = ApplicationConfiguration.WebTimeout; - EditorGUI.BeginChangeCheck(); - { - webTimeout = EditorGUILayout.IntField(WebTimeoutLabel, webTimeout); - } - if (EditorGUI.EndChangeCheck()) - { - ApplicationConfiguration.WebTimeout = webTimeout; - Manager.UserSettings.Set(Constants.WebTimeoutKey, webTimeout); - } + webTimeout = EditorGUILayout.IntField(WebTimeoutLabel, webTimeout); + } + if (EditorGUI.EndChangeCheck()) + { + ApplicationConfiguration.WebTimeout = webTimeout; + Manager.UserSettings.Set(Constants.WebTimeoutKey, webTimeout); } - EditorGUI.EndDisabledGroup(); - } + gitTimeout = ApplicationConfiguration.GitTimeout; + EditorGUI.BeginChangeCheck(); + { + gitTimeout = EditorGUILayout.IntField(GitTimeoutLabel, gitTimeout); + } + if (EditorGUI.EndChangeCheck()) + { + ApplicationConfiguration.GitTimeout = gitTimeout; + Manager.UserSettings.Set(Constants.GitTimeoutKey, gitTimeout); + } + } public override bool IsBusy { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index 35af289e7..4fbfb81e8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -1,5 +1,6 @@ using GitHub.Logging; -using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace GitHub.Unity @@ -8,39 +9,36 @@ abstract class Subview : IView { private const string NullParentError = "Subview parent is null"; + public Subview() + { + } + public virtual void InitializeView(IView parent) { Debug.Assert(parent != null, NullParentError); - //Logger.Trace("InitializeView"); Parent = parent; } public virtual void OnEnable() - { - //Logger.Trace("OnEnable"); - } + {} public virtual void OnDisable() - { - //Logger.Trace("OnDisable"); - } + {} public virtual void OnDataUpdate() {} public virtual void OnGUI() - { } + {} public virtual void OnSelectionChange() - { } + {} public virtual void OnFocusChanged() - { } + {} public virtual void Refresh() - { - Logger.Trace("Refresh"); - } + {} public virtual void Redraw() { @@ -52,25 +50,61 @@ public virtual void Finish(bool result) Parent.Finish(result); } + public void DoEmptyGUI() + { + Parent.DoEmptyGUI(); + } + + public void DoProgressGUI() + { + Parent.DoProgressGUI(); + } + + public void UpdateProgress(IProgress progress) + { + Parent.UpdateProgress(progress); + } + + public void Refresh(CacheType type) + { + Parent.Refresh(type); + } + + public void ReceivedEvent(CacheType type) + { + Parent.ReceivedEvent(type); + } + + public virtual void DoneRefreshing() + { + Parent.DoneRefreshing(); + } + protected IView Parent { get; private set; } + public IApplicationManager Manager { get { return Parent.Manager; } } public IRepository Repository { get { return Parent.Repository; } } public bool HasRepository { get { return Parent.HasRepository; } } public IUser User { get { return Parent.User; } } public bool HasUser { get { return Parent.HasUser; } } + protected ITaskManager TaskManager { get { return Manager.TaskManager; } } + protected IGitClient GitClient { get { return Manager.GitClient; } } + protected IEnvironment Environment { get { return Manager.Environment; } } + protected IPlatform Platform { get { return Manager.Platform; } } + protected IUsageTracker UsageTracker { get { return Manager.UsageTracker; } } + protected IOAuthCallbackManager OAuthCallbackManager { get { return Manager.OAuthCallbackManager; } } + public bool HasFocus { get { return Parent != null && Parent.HasFocus; } } public virtual bool IsBusy { get { return (Manager != null && Manager.IsBusy) || (Repository != null && Repository.IsBusy); } } - protected ITaskManager TaskManager { get { return Manager.TaskManager; } } - protected IGitClient GitClient { get { return Manager.GitClient; } } - protected IEnvironment Environment { get { return Manager.Environment; } } - protected IPlatform Platform { get { return Manager.Platform; } } public Rect Position { get { return Parent.Position; } } public string Title { get; protected set; } public Vector2 Size { get; protected set; } + public Dictionary RefreshEvents { get { return Parent.RefreshEvents; } } + public bool IsRefreshing { get { return Parent.IsRefreshing; } } private ILogging logger; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs index 206f7edbb..3dc82e14c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs @@ -660,14 +660,13 @@ protected Texture GetNodeIcon(TreeNode node) return nodeIcon; } - protected override TreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitBranchTreeData? treeData, bool isContainer) + protected override TreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitBranchTreeData? treeData) { var node = new TreeNode { Path = path, Label = label, Level = level, IsFolder = isFolder, - IsContainer = isContainer, IsActive = isActive, IsHidden = isHidden, IsCollapsed = isCollapsed, diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs index c860c8ac9..cbb7ba3cd 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs @@ -29,6 +29,12 @@ public override void InitializeView(IView parent) gitExecutableIsSet = Environment.GitExecutablePath.IsInitialized; } + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.GitUser); + } + public override void OnDataUpdate() { base.OnDataUpdate(); @@ -73,7 +79,7 @@ public override void OnEnable() base.OnEnable(); AttachHandlers(); - User.CheckUserChangedEvent(lastCheckUserChangedEvent); + User.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, lastCheckUserChangedEvent); } public override void OnDisable() @@ -89,7 +95,6 @@ private void AttachHandlers() private void UserOnChanged(CacheUpdateEvent cacheUpdateEvent) { - //Logger.Trace("UserOnChanged"); lastCheckUserChangedEvent = cacheUpdateEvent; userHasChanges = true; isBusy = false; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index bddedb97b..bfe6b5d70 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -8,31 +8,24 @@ namespace GitHub.Unity [Serializable] class Window : BaseWindow { - private const float DefaultNotificationTimeout = 4f; + private const float DefaultNotificationTimeout = 2f; private const string Title = "GitHub"; private const string Menu_Window_GitHub = "Window/GitHub"; private const string Menu_Window_GitHub_Command_Line = "Window/GitHub Command Line"; - private const string BadNotificationDelayError = "A delay of {0} is shorter than the default delay and thus would get pre-empted."; - private const string InitializeTitle = "Initialize"; - private const string HistoryTitle = "History"; - private const string ChangesTitle = "Changes"; - private const string BranchesTitle = "Branches"; - private const string SettingsTitle = "Settings"; - private const string DefaultRepoUrl = "No remote configured"; - private const string Window_RepoUrlTooltip = "Url of the {0} remote"; - private const string Window_RepoNoUrlTooltip = "Add a remote in the Settings tab"; - private const string Window_RepoBranchTooltip = "Active branch"; - private const float SpinnerAnimationDuration = 4f; - - [NonSerialized] private double notificationClearTime = -1; - [NonSerialized] private double timeSinceLastRotation = -1f; - [NonSerialized] private bool currentBranchAndRemoteHasUpdate; - [NonSerialized] private bool gitExecutableIsSet; - [NonSerialized] private Spinner spinner; - [NonSerialized] private IProgress progress; - [NonSerialized] private float progressValue; - [NonSerialized] private string progressMessage; + [NonSerialized] private Spinner spinner; + [NonSerialized] private IProgress repositoryProgress; + [NonSerialized] private IProgress appManagerProgress; + [NonSerialized] private bool firstOnGUI = true; + + [SerializeField] private double progressMessageClearTime = -1; + [SerializeField] private double notificationClearTime = -1; + [SerializeField] private double timeSinceLastRotation = -1f; + [SerializeField] private bool currentBranchAndRemoteHasUpdate; + [SerializeField] private bool currentTrackingStatusHasUpdate; + [SerializeField] private bool currentStatusEntriesHasUpdate; + [SerializeField] private bool repositoryProgressHasUpdate; + [SerializeField] private bool appManagerProgressHasUpdate; [SerializeField] private SubTab changeTab = SubTab.InitProject; [SerializeField] private SubTab activeTab = SubTab.InitProject; [SerializeField] private InitProjectView initProjectView = new InitProjectView(); @@ -40,13 +33,31 @@ class Window : BaseWindow [SerializeField] private ChangesView changesView = new ChangesView(); [SerializeField] private HistoryView historyView = new HistoryView(); [SerializeField] private SettingsView settingsView = new SettingsView(); - - [SerializeField] private string repoRemote; - [SerializeField] private string repoBranch; - [SerializeField] private string repoUrl; - [SerializeField] private GUIContent repoBranchContent; - [SerializeField] private GUIContent repoUrlContent; + [SerializeField] private LocksView locksView = new LocksView(); + [SerializeField] private bool hasRemote; + [SerializeField] private string currentRemoteName; + [SerializeField] private string currentBranch; + [SerializeField] private string currentRemoteUrl; + [SerializeField] private int statusAhead; + [SerializeField] private int statusBehind; + [SerializeField] private bool hasItemsToCommit; + [SerializeField] private bool isTrackingRemoteBranch; + [SerializeField] private GUIContent currentBranchContent; + [SerializeField] private GUIContent currentRemoteUrlContent; [SerializeField] private CacheUpdateEvent lastCurrentBranchAndRemoteChangedEvent; + [SerializeField] private CacheUpdateEvent lastTrackingStatusChangedEvent; + [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; + + [SerializeField] private GUIContent pullButtonContent = new GUIContent(Localization.PullButton); + [SerializeField] private GUIContent pushButtonContent = new GUIContent(Localization.PushButton); + [SerializeField] private GUIContent refreshButtonContent = new GUIContent(Localization.RefreshButton); + [SerializeField] private GUIContent fetchButtonContent = new GUIContent(Localization.FetchButton); + [SerializeField] private float repositoryProgressValue; + [SerializeField] private string repositoryProgressMessage; + [SerializeField] private float appManagerProgressValue; + [SerializeField] private string appManagerProgressMessage; + [SerializeField] private Connection[] connections; + [SerializeField] private string primaryConnectionUsername; [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() @@ -58,16 +69,24 @@ public static void Window_GitHub() public static void GitHub_CommandLine() { EntryPoint.ApplicationManager.ProcessManager.RunCommandLineWindow(NPath.CurrentDirectory); + EntryPoint.ApplicationManager.UsageTracker.IncrementApplicationMenuMenuItemCommandLine(); } -#if DEBUG - [MenuItem("GitHub/Select Window")] - public static void GitHub_SelectWindow() - { - var window = Resources.FindObjectsOfTypeAll(typeof(Window)).FirstOrDefault() as Window; - Selection.activeObject = window; - } -#endif +#if DEBUG + + [MenuItem("GitHub/Select Window")] + public static void GitHub_SelectWindow() + { + var window = Resources.FindObjectsOfTypeAll(typeof(Window)).FirstOrDefault() as Window; + Selection.activeObject = window; + } + + [MenuItem("GitHub/Restart")] + public static void GitHub_Restart() + { + EntryPoint.Restart(); + } +#endif public static void ShowWindow(IApplicationManager applicationManager) { @@ -86,19 +105,17 @@ public override void Initialize(IApplicationManager applicationManager) { base.Initialize(applicationManager); - applicationManager.OnProgress += OnProgress; + applicationManager.OnProgress += ApplicationManagerOnProgress; HistoryView.InitializeView(this); ChangesView.InitializeView(this); BranchesView.InitializeView(this); SettingsView.InitializeView(this); + LocksView.InitializeView(this); InitProjectView.InitializeView(this); - titleContent = new GUIContent(Title, Styles.SmallLogo); - if (!HasRepository) { - //Logger.Trace("Initialize set all tabs to InitProject"); changeTab = activeTab = SubTab.InitProject; } } @@ -107,9 +124,6 @@ public override void OnEnable() { base.OnEnable(); -#if DEVELOPER_BUILD - Selection.activeObject = this; -#endif if (Repository != null) ValidateCachedData(Repository); @@ -118,6 +132,8 @@ public override void OnEnable() if (spinner == null) spinner = new Spinner(); + + ClearProgressMessage(); } public override void OnDisable() @@ -153,8 +169,6 @@ public override void OnRepositoryChanged(IRepository oldRepository) { if (activeTab == SubTab.InitProject) { - //Logger.Trace("OnRepositoryChanged set changeTab to History"); - changeTab = SubTab.History; UpdateActiveTab(); } @@ -163,8 +177,6 @@ public override void OnRepositoryChanged(IRepository oldRepository) { if (activeTab != SubTab.InitProject) { - //Logger.Trace("OnRepositoryChanged set changeTab to InitProject"); - changeTab = SubTab.InitProject; UpdateActiveTab(); } @@ -180,109 +192,152 @@ public override void OnSelectionChange() public override void Refresh() { + SetProgressMessage(Localization.MessageRefreshing, 0); base.Refresh(); if (ActiveView != null) ActiveView.Refresh(); - Repaint(); + Refresh(CacheType.GitLocks); + Redraw(); } + public override void DoneRefreshing() + { + base.DoneRefreshing(); + SetProgressMessage(Localization.MessageRefreshed, 100); + } - public override void OnUI() + private void ValidateCachedData(IRepository repository) { - base.OnUI(); + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentBranchAndRemoteChangedEvent); + } - if (HasRepository) - { - DoHeaderGUI(); + private void MaybeUpdateData() + { + if (firstOnGUI) + { + titleContent = new GUIContent(Title, Styles.SmallLogo); } + firstOnGUI = false; - DoToolbarGUI(); + if (HasRepository && !string.IsNullOrEmpty(Repository.CloneUrl)) + { + var host = Repository.CloneUrl + .ToRepositoryUri() + .GetComponents(UriComponents.Host, UriFormat.SafeUnescaped); - var rect = GUILayoutUtility.GetLastRect(); - // GUI for the active tab - if (ActiveView != null) + connections = Platform.Keychain.Connections.OrderByDescending(x => x.Host == host).ToArray(); + } + else { - ActiveView.OnGUI(); + connections = Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom).ToArray(); } - if (IsBusy && activeTab != SubTab.Settings && Event.current.type == EventType.Repaint) + var connectionCount = connections.Length; + if (connectionCount > 1) { - if (timeSinceLastRotation < 0) + var connection = connections.First(); + var isGitHubDotCom = HostAddress.IsGitHubDotCom(connection); + + if (isGitHubDotCom) { - timeSinceLastRotation = EditorApplication.timeSinceStartup; + primaryConnectionUsername = "GitHub: " + connection.Username; } else { - var elapsedTime = (float)(EditorApplication.timeSinceStartup - timeSinceLastRotation); - if (spinner == null) - spinner = new Spinner(); - spinner.Start(elapsedTime); - spinner.Rotate(elapsedTime); - - spinner.Render(); - - rect = new Rect(0f, rect.y + rect.height, Position.width, Position.height - (rect.height + rect.y)); - rect = spinner.Layout(rect); - rect.y += rect.height + 30; - rect.height = 20; - if (!String.IsNullOrEmpty(progressMessage)) - EditorGUI.ProgressBar(rect, progressValue / 100, progressMessage); + primaryConnectionUsername = connection.Host + ": " + connection.Username; } } - } - - public override void Update() - { - base.Update(); - - // Notification auto-clear timer override - if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime) - { - notificationClearTime = -1f; - RemoveNotification(); - Redraw(); - } - - if (IsBusy && activeTab != SubTab.Settings) + else if(connectionCount == 1) { - Redraw(); + primaryConnectionUsername = connections.First().Username; } else { - timeSinceLastRotation = -1f; - spinner.Stop(); + primaryConnectionUsername = null; } - } - private void ValidateCachedData(IRepository repository) - { - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentBranchAndRemoteChangedEvent); - } - private void MaybeUpdateData() - { - if (progress != null) + if (repositoryProgressHasUpdate) { - progressValue = progress.Value; - progressMessage = progress.Message; + if (repositoryProgress != null) + { + repositoryProgressMessage = repositoryProgress.Message; + repositoryProgressValue = repositoryProgress.Percentage; + if (progressMessageClearTime == -1f || progressMessageClearTime < EditorApplication.timeSinceStartup + DefaultNotificationTimeout) + progressMessageClearTime = EditorApplication.timeSinceStartup + DefaultNotificationTimeout; + } + else + { + repositoryProgressMessage = ""; + repositoryProgressValue = 0; + progressMessageClearTime = -1f; + } + repositoryProgressHasUpdate = false; + } + + if (appManagerProgressHasUpdate) + { + if (appManagerProgress != null) + { + appManagerProgressValue = appManagerProgress.Percentage; + appManagerProgressMessage = appManagerProgress.Message; + } + else + { + appManagerProgressValue = 0; + appManagerProgressMessage = ""; + } + appManagerProgressHasUpdate = false; } - gitExecutableIsSet = !String.IsNullOrEmpty(Environment.GitExecutablePath); string updatedRepoRemote = null; - string updatedRepoUrl = DefaultRepoUrl; + string updatedRepoUrl = Localization.DefaultRepoUrl; var shouldUpdateContentFields = false; + if (currentTrackingStatusHasUpdate) + { + currentTrackingStatusHasUpdate = false; + statusAhead = Repository.CurrentAhead; + statusBehind = Repository.CurrentBehind; + } + + if (currentStatusEntriesHasUpdate) + { + currentStatusEntriesHasUpdate = false; + var currentChanges = Repository.CurrentChanges; + hasItemsToCommit = currentChanges != null && + currentChanges.Any(entry => entry.Status != GitFileStatus.Ignored && !entry.Staged); + } + + if (currentBranchAndRemoteHasUpdate) + { + hasRemote = false; + } + if (Repository != null) { - if (repoBranch == null || repoRemote == null || currentBranchAndRemoteHasUpdate) + if (currentBranch == null || currentRemoteName == null || currentBranchAndRemoteHasUpdate) { + currentBranchAndRemoteHasUpdate = false; + var repositoryCurrentBranch = Repository.CurrentBranch; - var updatedRepoBranch = repositoryCurrentBranch.HasValue ? repositoryCurrentBranch.Value.Name : null; + string updatedRepoBranch; + if (repositoryCurrentBranch.HasValue) + { + updatedRepoBranch = repositoryCurrentBranch.Value.Name; + isTrackingRemoteBranch = !string.IsNullOrEmpty(repositoryCurrentBranch.Value.Tracking); + } + else + { + updatedRepoBranch = null; + isTrackingRemoteBranch = false; + } var repositoryCurrentRemote = Repository.CurrentRemote; if (repositoryCurrentRemote.HasValue) { + hasRemote = true; updatedRepoRemote = repositoryCurrentRemote.Value.Name; if (!string.IsNullOrEmpty(repositoryCurrentRemote.Value.Url)) { @@ -290,57 +345,59 @@ private void MaybeUpdateData() } } - if (repoRemote != updatedRepoRemote) + if (currentRemoteName != updatedRepoRemote) { - repoRemote = updatedRepoBranch; + currentRemoteName = updatedRepoRemote; shouldUpdateContentFields = true; } - if (repoBranch != updatedRepoBranch) + if (currentBranch != updatedRepoBranch) { - repoBranch = updatedRepoBranch; + currentBranch = updatedRepoBranch; shouldUpdateContentFields = true; } - if (repoUrl != updatedRepoUrl) + if (currentRemoteUrl != updatedRepoUrl) { - repoUrl = updatedRepoUrl; + currentRemoteUrl = updatedRepoUrl; shouldUpdateContentFields = true; } } } else { - if (repoRemote != null) + isTrackingRemoteBranch = false; + + if (currentRemoteName != null) { - repoRemote = null; + currentRemoteName = null; shouldUpdateContentFields = true; } - if (repoBranch != null) + if (currentBranch != null) { - repoBranch = null; + currentBranch = null; shouldUpdateContentFields = true; } - if (repoUrl != DefaultRepoUrl) + if (currentRemoteUrl != Localization.DefaultRepoUrl) { - repoUrl = DefaultRepoUrl; + currentRemoteUrl = Localization.DefaultRepoUrl; shouldUpdateContentFields = true; } } - if (shouldUpdateContentFields || repoBranchContent == null || repoUrlContent == null) + if (shouldUpdateContentFields || currentBranchContent == null || currentRemoteUrlContent == null) { - repoBranchContent = new GUIContent(repoBranch, Window_RepoBranchTooltip); + currentBranchContent = new GUIContent(currentBranch, Localization.Window_RepoBranchTooltip); - if (repoRemote != null) + if (currentRemoteName != null) { - repoUrlContent = new GUIContent(repoUrl, string.Format(Window_RepoUrlTooltip, repoRemote)); + currentRemoteUrlContent = new GUIContent(currentRemoteUrl, string.Format(Localization.Window_RepoUrlTooltip, currentRemoteName)); } else { - repoUrlContent = new GUIContent(repoUrl, Window_RepoNoUrlTooltip); + currentRemoteUrlContent = new GUIContent(currentRemoteUrl, Localization.Window_RepoNoUrlTooltip); } } } @@ -350,6 +407,22 @@ private void AttachHandlers(IRepository repository) if (repository == null) return; repository.CurrentBranchAndRemoteChanged += RepositoryOnCurrentBranchAndRemoteChanged; + repository.TrackingStatusChanged += RepositoryOnTrackingStatusChanged; + repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + repository.OnProgress += UpdateProgress; + Platform.Keychain.ConnectionsChanged += ConnectionsChanged; + } + + private void DetachHandlers(IRepository repository) + { + if (repository == null) + return; + repository.CurrentBranchAndRemoteChanged -= RepositoryOnCurrentBranchAndRemoteChanged; + repository.TrackingStatusChanged -= RepositoryOnTrackingStatusChanged; + repository.StatusEntriesChanged -= RepositoryOnStatusEntriesChanged; + repository.OnProgress -= UpdateProgress; + Manager.OnProgress -= ApplicationManagerOnProgress; + Platform.Keychain.ConnectionsChanged -= ConnectionsChanged; } private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpdateEvent) @@ -362,16 +435,127 @@ private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpd } } - private void OnProgress(IProgress progr) + private void RepositoryOnTrackingStatusChanged(CacheUpdateEvent cacheUpdateEvent) { - progress = progr; + if (!lastTrackingStatusChangedEvent.Equals(cacheUpdateEvent)) + { + lastTrackingStatusChangedEvent = cacheUpdateEvent; + currentTrackingStatusHasUpdate = true; + Redraw(); + } } - private void DetachHandlers(IRepository repository) + private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) { - if (repository == null) - return; - repository.CurrentBranchAndRemoteChanged -= RepositoryOnCurrentBranchAndRemoteChanged; + if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) + { + lastStatusEntriesChangedEvent = cacheUpdateEvent; + currentStatusEntriesHasUpdate = true; + Redraw(); + } + } + + private static object lck = new object(); + public override void UpdateProgress(IProgress progress) + { + lock (lck) + { + repositoryProgress = progress; + if (repositoryProgress != null && progress != null) + { + repositoryProgress.UpdateProgress(progress.Value, progress.Total, progress.Message); + } + repositoryProgressHasUpdate = true; + } + + if (!ThreadingHelper.InUIThread) + TaskManager.RunInUI(Redraw); + else + Redraw(); + } + + private void ApplicationManagerOnProgress(IProgress progress) + { + appManagerProgress = progress; + appManagerProgressHasUpdate = true; + } + + private void ConnectionsChanged() + { + if (!ThreadingHelper.InUIThread) + TaskManager.RunInUI(Redraw); + else + Redraw(); + } + + public override void OnUI() + { + base.OnUI(); + + GUILayout.BeginVertical(Styles.HeaderStyle); + + if (HasRepository) + { + DoActionbarGUI(); + DoHeaderGUI(); + } + + DoToolbarGUI(); + DoActiveViewGUI(); + + GUILayout.EndVertical(); + } + + public override void Update() + { + base.Update(); + + // Notification auto-clear timer override + if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime) + { + notificationClearTime = -1f; + RemoveNotification(); + Redraw(); + } + + // Notification auto-clear timer override + if (progressMessageClearTime > 0f && EditorApplication.timeSinceStartup > progressMessageClearTime) + { + repositoryProgressHasUpdate = true; + ClearProgressMessage(); + } + else if (EditorApplication.timeSinceStartup < progressMessageClearTime) + { + Redraw(); + } + + if (IsBusy && activeTab != SubTab.Settings) + { + Redraw(); + } + else + { + timeSinceLastRotation = -1f; + spinner.Stop(); + } + } + + public override void DoProgressGUI() + { + Rect rect1 = GUILayoutUtility.GetRect(position.width, 20); + if (Event.current.GetTypeForControl(GUIUtility.GetControlID("ghu_ProgressBar".GetHashCode(), FocusType.Keyboard, position)) == EventType.Repaint) + { + var style = Styles.ProgressAreaBackStyle; + style.Draw(rect1, false, false, false, false); + Rect rect2 = new Rect(rect1.x, rect1.y, position.width * repositoryProgressValue, rect1.height); + style = GUI.skin.FindStyle("ProgressBarBar"); + style.Draw(rect2, false, false, false, false); + style = GUI.skin.FindStyle("ProgressBarText"); + style.Draw(rect1, repositoryProgressMessage, false, false, false, false); + } + + if (repositoryProgressValue == 1f) + Redraw(); } private void DoHeaderGUI() @@ -390,9 +574,9 @@ private void DoHeaderGUI() { GUILayout.Space(3); - GUILayout.Label(repoUrlContent, Styles.HeaderRepoLabelStyle); + GUILayout.Label(currentRemoteUrlContent, Styles.HeaderRepoLabelStyle); GUILayout.Space(-2); - GUILayout.Label(repoBranchContent, Styles.HeaderBranchLabelStyle); + GUILayout.Label(currentBranchContent, Styles.HeaderBranchLabelStyle); } GUILayout.EndVertical(); } @@ -408,15 +592,16 @@ private void DoToolbarGUI() { if (HasRepository) { - changeTab = TabButton(SubTab.Changes, ChangesTitle, changeTab); - changeTab = TabButton(SubTab.History, HistoryTitle, changeTab); - changeTab = TabButton(SubTab.Branches, BranchesTitle, changeTab); + changeTab = TabButton(SubTab.Changes, Localization.ChangesTitle, changeTab); + changeTab = TabButton(SubTab.Locks, Localization.LocksTitle, changeTab); + changeTab = TabButton(SubTab.History, Localization.HistoryTitle, changeTab); + changeTab = TabButton(SubTab.Branches, Localization.BranchesTitle, changeTab); } else if (!HasRepository) { - changeTab = TabButton(SubTab.InitProject, InitializeTitle, changeTab); + changeTab = TabButton(SubTab.InitProject, Localization.InitializeTitle, changeTab); } - changeTab = TabButton(SubTab.Settings, SettingsTitle, changeTab); + changeTab = TabButton(SubTab.Settings, Localization.SettingsTitle, changeTab); } if (EditorGUI.EndChangeCheck()) @@ -426,12 +611,243 @@ private void DoToolbarGUI() GUILayout.FlexibleSpace(); - if (GUILayout.Button("Account", EditorStyles.toolbarDropDown)) - DoAccountDropdown(); + if (!HasRepository) + { + GUILayout.FlexibleSpace(); + + if (!connections.Any()) + { + if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) + SignIn(null); + } + else + { + if (GUILayout.Button(primaryConnectionUsername, EditorStyles.toolbarDropDown)) + { + DoAccountDropdown(); + } + } + } } EditorGUILayout.EndHorizontal(); } + private void DoActionbarGUI() + { + GUILayout.BeginHorizontal(EditorStyles.toolbar); + { + if (hasRemote) + { + EditorGUI.BeginDisabledGroup(currentRemoteName == null); + { + // Fetch button + var fetchClicked = GUILayout.Button(fetchButtonContent, Styles.ToolbarButtonStyle); + if (fetchClicked) + { + Fetch(); + } + + // Pull button + var pullButtonText = statusBehind > 0 ? new GUIContent(String.Format(Localization.PullButtonCount, statusBehind)) : pullButtonContent; + var pullClicked = GUILayout.Button(pullButtonText, Styles.ToolbarButtonStyle); + + if (pullClicked && + EditorUtility.DisplayDialog(Localization.PullConfirmTitle, + String.Format(Localization.PullConfirmDescription, currentRemoteName), + Localization.PullConfirmYes, + Localization.Cancel) + ) + { + Pull(); + } + } + EditorGUI.EndDisabledGroup(); + + // Push button + EditorGUI.BeginDisabledGroup(currentRemoteName == null || isTrackingRemoteBranch && statusAhead == 0); + { + var pushButtonText = statusAhead > 0 ? new GUIContent(String.Format(Localization.PushButtonCount, statusAhead)) : pushButtonContent; + var pushClicked = GUILayout.Button(pushButtonText, Styles.ToolbarButtonStyle); + + if (pushClicked && + EditorUtility.DisplayDialog(Localization.PushConfirmTitle, + String.Format(Localization.PushConfirmDescription, currentRemoteName), + Localization.PushConfirmYes, + Localization.Cancel) + ) + { + Push(); + } + } + EditorGUI.EndDisabledGroup(); + } + else + { + // Publishing a repo + if (GUILayout.Button(Localization.PublishButton, Styles.ToolbarButtonStyle)) + { + PopupWindow.OpenWindow(PopupWindow.PopupViewType.PublishView); + } + } + + if (GUILayout.Button(refreshButtonContent, Styles.ToolbarButtonStyle)) + { + Refresh(); + } + + GUILayout.FlexibleSpace(); + + if (!connections.Any()) + { + if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) + SignIn(null); + } + else + { + var connection = connections.First(); + if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) + { + DoAccountDropdown(); + } + } + } + EditorGUILayout.EndHorizontal(); + } + + private void DoActiveViewGUI() + { + var rect = GUILayoutUtility.GetLastRect(); + // GUI for the active tab + if (ActiveView != null) + { + ActiveView.OnGUI(); + } + + if (IsBusy && activeTab != SubTab.Settings && Event.current.type == EventType.Repaint) + { + if (timeSinceLastRotation < 0) + { + timeSinceLastRotation = EditorApplication.timeSinceStartup; + } + else + { + var elapsedTime = (float)(EditorApplication.timeSinceStartup - timeSinceLastRotation); + if (spinner == null) + spinner = new Spinner(); + spinner.Start(elapsedTime); + spinner.Rotate(elapsedTime); + + spinner.Render(); + + rect = new Rect(0f, rect.y + rect.height, Position.width, Position.height - (rect.height + rect.y)); + rect = spinner.Layout(rect); + rect.y += rect.height + 30; + rect.height = 20; + if (!String.IsNullOrEmpty(appManagerProgressMessage)) + EditorGUI.ProgressBar(rect, appManagerProgressValue, appManagerProgressMessage); + } + } + } + + public override void DoEmptyGUI() + { + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + } + + private void Pull() + { + if (hasItemsToCommit) + { + EditorUtility.DisplayDialog("Pull", "You need to commit your changes before pulling.", "Cancel"); + } + else + { + SetProgressMessage(Localization.MessagePulling, 0, 60f); + Repository + .Pull() + .FinallyInUI((success, e) => + { + if (success) + { + SetProgressMessage(Localization.MessagePulled, 100); + Manager.UsageTracker.IncrementHistoryViewToolbarPull(); + + EditorUtility.DisplayDialog(Localization.PullActionTitle, + String.Format(Localization.PullSuccessDescription, currentRemoteName), + Localization.Ok); + + AssetDatabase.Refresh(); + } + else + { + SetProgressMessage(Localization.MessagePullFailed, 100); + EditorUtility.DisplayDialog(Localization.PullActionTitle, + e.Message, + Localization.Ok); + } + }) + .Start(); + } + } + + private void Push() + { + SetProgressMessage(Localization.MessagePushing, 0, 60f); + Repository + .Push() + .FinallyInUI((success, e) => + { + if (success) + { + SetProgressMessage(Localization.MessagePushed, 100); + Manager.UsageTracker.IncrementHistoryViewToolbarPush(); + + EditorUtility.DisplayDialog(Localization.PushActionTitle, + String.Format(Localization.PushSuccessDescription, currentRemoteName), + Localization.Ok); + } + else + { + SetProgressMessage(Localization.MessagePushFailed, 100); + EditorUtility.DisplayDialog(Localization.PushActionTitle, + e.Message, + Localization.Ok); + } + }) + .Start(); + } + + private void Fetch() + { + SetProgressMessage(Localization.MessageFetching, 0, 60f); + Repository + .Fetch() + .FinallyInUI((success, e) => + { + if (success) + { + SetProgressMessage(Localization.MessageFetched, 100); + Manager.UsageTracker.IncrementHistoryViewToolbarFetch(); + } + else + { + SetProgressMessage(Localization.MessageFetchFailed, 100); + EditorUtility.DisplayDialog(Localization.FetchActionTitle, e.Message, Localization.Ok); + } + }) + .Start(); + } + private void UpdateActiveTab() { if (changeTab != activeTab) @@ -454,23 +870,44 @@ private void SwitchView(Subview fromView, Subview toView) toView.OnDataUpdate(); // this triggers a repaint - Repaint(); + Redraw(); } private void DoAccountDropdown() { GenericMenu accountMenu = new GenericMenu(); - if (!Platform.Keychain.HasKeys) + if (connections.Length == 1) { - accountMenu.AddItem(new GUIContent("Sign in"), false, SignIn, "sign in"); + var connection = connections.First(); + accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, connection); + accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, connection); + accountMenu.AddSeparator(""); + accountMenu.AddItem(new GUIContent("Sign In"), false, SignIn, "sign in"); } else { - accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, "profile"); - accountMenu.AddSeparator(""); - accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, "sign out"); + for (var index = 0; index < connections.Length; index++) + { + var connection = connections[index]; + var isGitHubDotCom = HostAddress.IsGitHubDotCom(connection); + + string rootPath; + if (isGitHubDotCom) + { + rootPath = "GitHub/"; + } + else + { + var uriString = connection.Host.ToUriString(); + rootPath = uriString.Host + "/"; + } + + accountMenu.AddItem(new GUIContent(rootPath + "Go to Profile"), false, GoToProfile, connection); + accountMenu.AddItem(new GUIContent(rootPath + "Sign out"), false, SignOut, connection); + } } + accountMenu.ShowAsContext(); } @@ -481,27 +918,16 @@ private void SignIn(object obj) private void GoToProfile(object obj) { - //TODO: ONE_USER_LOGIN This assumes only ever one user can login - var keychainConnection = Platform.Keychain.Connections.First(); - var uriString = new UriString(keychainConnection.Host).Combine(keychainConnection.Username); + var connection = (Connection) obj; + var uriString = new UriString(connection.Host).Combine(connection.Username); Application.OpenURL(uriString); } private void SignOut(object obj) { - UriString host; - if (Repository != null && Repository.CloneUrl != null && Repository.CloneUrl.IsValidUri) - { - host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - var apiClient = new ApiClient(host, Platform.Keychain, null, null, NPath.Default, NPath.Default); - apiClient.Logout(host); + var connection = (Connection)obj; + var loginManager = new LoginManager(Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + loginManager.Logout(connection.Host).FinallyInUI((s, e) => Redraw()); } public new void ShowNotification(GUIContent content) @@ -511,12 +937,32 @@ private void SignOut(object obj) public void ShowNotification(GUIContent content, float timeout) { - Debug.Assert(timeout <= DefaultNotificationTimeout, String.Format(BadNotificationDelayError, timeout)); - notificationClearTime = timeout < DefaultNotificationTimeout ? EditorApplication.timeSinceStartup + timeout : -1f; base.ShowNotification(content); } + private void SetProgressMessage(string message, long value) + { + SetProgressMessage(message, value, DefaultNotificationTimeout); + } + + private void SetProgressMessage(string message, long value, float timeout) + { + progressMessageClearTime = EditorApplication.timeSinceStartup + timeout; + if (repositoryProgress == null) + repositoryProgress = new Progress(TaskBase.Default); + repositoryProgress.UpdateProgress(value, repositoryProgress.Total, message); + UpdateProgress(repositoryProgress); + Redraw(); + } + + private void ClearProgressMessage() + { + progressMessageClearTime = -1f; + UpdateProgress(null); + Redraw(); + } + private static SubTab TabButton(SubTab tab, string title, SubTab currentTab) { return GUILayout.Toggle(currentTab == tab, title, EditorStyles.toolbarButton) ? tab : currentTab; @@ -536,6 +982,8 @@ private Subview ToView(SubTab tab) return branchesView; case SubTab.Settings: return settingsView; + case SubTab.Locks: + return locksView; default: throw new ArgumentOutOfRangeException("tab"); } @@ -561,6 +1009,11 @@ public SettingsView SettingsView get { return settingsView; } } + public LocksView LocksView + { + get { return locksView; } + } + public InitProjectView InitProjectView { get { return initProjectView; } @@ -583,7 +1036,8 @@ private enum SubTab History, Changes, Branches, - Settings + Settings, + Locks } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs new file mode 100644 index 000000000..d3afe1306 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -0,0 +1,235 @@ +using System; +using UnityEngine; +using UnityEditor; +using System.Linq; +using GitHub.Logging; + +namespace GitHub.Unity +{ + [Serializable] + class GUIPackage + { + [SerializeField] private string version; + [SerializeField] private string url; + [SerializeField] private string releaseNotes; + [SerializeField] private string releaseNotesUrl; + [SerializeField] private string message; + + [NonSerialized] private Package package; + public Package Package + { + get + { + if (package == null) + { + package = new Package + { + Version = TheVersion.Parse(version), + Url = url, + ReleaseNotes = releaseNotes, + ReleaseNotesUrl = releaseNotesUrl, + Message = message + }; + } + return package; + } + } + + public GUIPackage() + {} + + public GUIPackage(Package package) + { + version = package.Version.ToString(); + url = package.Url; + releaseNotes = package.ReleaseNotes; + releaseNotesUrl = package.ReleaseNotesUrl; + message = package.Message; + } + } + + class UpdateCheckWindow : BaseWindow + { + public const string UpdateFeedUrl = +#if DEBUG + "http://localhost:50000/unity/latest.json" +#else + "http://github-vs.s3.amazonaws.com/unity/latest.json" +#endif + ; + + public static void CheckForUpdates(IApplicationManager manager) + { + var download = new DownloadTask(manager.TaskManager.Token, manager.Environment.FileSystem, UpdateFeedUrl, manager.Environment.UserCachePath) + .Catch(ex => + { + LogHelper.Warning(@"Error downloading update check:{0} ""{1}""", UpdateFeedUrl, ex.GetExceptionMessageShort()); + return true; + }); + download.OnEnd += (thisTask, result, success, exception) => + { + if (success) + { + try + { + Package package = result.ReadAllText().FromJson(lowerCase: true, onlyPublic: false); + TheVersion current = TheVersion.Parse(ApplicationInfo.Version); + TheVersion newVersion = package.Version; + + var versionToSkip = manager.UserSettings.Get(Constants.SkipVersionKey); + if (versionToSkip == newVersion) + { + LogHelper.Info("Skipping GitHub for Unity update v" + newVersion); + return; + } + + if (newVersion <= current) + { + LogHelper.Trace("Skipping GitHub for Unity update v" + newVersion + ", we already have it"); + return; + } + + manager.TaskManager.RunInUI(() => + { + NotifyOfNewUpdate(manager, current, package); + }); + } + catch(Exception ex) + { + LogHelper.GetLogger().Error(ex); + } + } + }; + download.Start(); + } + + private static void NotifyOfNewUpdate(IApplicationManager manager, TheVersion currentVersion, Package package) + { + var window = GetWindowWithRect(new Rect(100, 100, 580, 400), true, windowTitle); + window.Initialize(manager, currentVersion, package); + window.Show(); + } + + private const string windowTitle = "GitHub for Unity Update Check"; + private const string newUpdateMessage = "There is a new version of GitHub for Unity available.\n\nCurrent version is {0}\nNew version is {1}"; + private const string skipThisVersionMessage = "Skip new version"; + private const string downloadNewVersionMessage = "Download new version"; + private const string browseReleaseNotes = "Browse the release notes"; + + private static GUIContent guiLogo; + private static GUIContent guiNewUpdate; + private static GUIContent guiPackageReleaseNotes; + private static GUIContent guiPackageMessage; + private static GUIContent guiSkipThisVersion; + private static GUIContent guiDownloadNewVersion; + private static GUIContent guiBrowseReleaseNotes; + + [SerializeField] private GUIPackage package; + [SerializeField] private string currentVersion; + [SerializeField] private Vector2 scrollPos; + [SerializeField] private bool hasReleaseNotes; + [SerializeField] private bool hasReleaseNotesUrl; + [SerializeField] private bool hasMessage; + + private void Initialize(IApplicationManager manager, TheVersion current, Package newPackage) + { + package = new GUIPackage(newPackage); + currentVersion = current.ToString(); + var requiresRedraw = guiLogo != null; + guiLogo = null; + this.InitializeWindow(manager, requiresRedraw); + } + + public override void OnDataUpdate() + { + base.OnDataUpdate(); + LoadContents(); + } + + public override void OnUI() + { + GUILayout.BeginVertical(); + + GUILayout.Space(10); + GUI.Box(new Rect(13, 8, guiLogo.image.width, guiLogo.image.height), guiLogo, GUIStyle.none); + + GUILayout.Space(15); + EditorGUILayout.BeginHorizontal(); + + GUILayout.Space(150); + EditorGUILayout.BeginVertical(); + + EditorGUILayout.LabelField(guiNewUpdate, "WordWrappedLabel", GUILayout.Width(300)); + + if (hasReleaseNotesUrl) + { + if (GUILayout.Button(guiBrowseReleaseNotes, Styles.HyperlinkStyle)) + { + Help.BrowseURL(package.Package.ReleaseNotesUrl); + } + } + + if (hasMessage || hasReleaseNotes) + { + GUILayout.Space(20); + + scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Width(405), GUILayout.Height(200)); + if (hasMessage) + { + EditorGUILayout.LabelField(guiPackageMessage, "WordWrappedLabel"); + } + + if (hasReleaseNotes) + { + EditorGUILayout.LabelField(guiPackageReleaseNotes, "WordWrappedLabel"); + } + EditorGUILayout.EndScrollView(); + } + + GUILayout.Space(20); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button(guiDownloadNewVersion, GUILayout.Width(200))) + { + Help.BrowseURL(package.Package.Url); + Close(); + } + + if (GUILayout.Button(guiSkipThisVersion, GUILayout.Width(200))) + { + var settings = EntryPoint.ApplicationManager.UserSettings; + settings.Set(Constants.SkipVersionKey, package.Package.Version); + Close(); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + GUILayout.Space(8); + + GUILayout.Space(10); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private void LoadContents() + { + if (guiLogo != null) + return; + + guiLogo = new GUIContent(Styles.BigLogo); + guiNewUpdate = new GUIContent(String.Format(newUpdateMessage, currentVersion, package.Package.Version.ToString())); + guiSkipThisVersion = new GUIContent(skipThisVersionMessage); + guiDownloadNewVersion = new GUIContent(downloadNewVersionMessage); + guiBrowseReleaseNotes = new GUIContent(browseReleaseNotes); + hasMessage = !String.IsNullOrEmpty(package.Package.Message); + hasReleaseNotes = !String.IsNullOrEmpty(package.Package.ReleaseNotes); + hasReleaseNotesUrl = !String.IsNullOrEmpty(package.Package.ReleaseNotesUrl); + if (hasMessage) + guiPackageMessage = new GUIContent(package.Package.Message); + if (hasReleaseNotes) + guiPackageReleaseNotes = new GUIContent(package.Package.ReleaseNotes); + } + + public override bool IsBusy { get { return false; } } + } +} diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef new file mode 100644 index 000000000..d49630a1a --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef @@ -0,0 +1,18 @@ +{ + "name": "UnityTests", + "references": [ + "GitHub.Unity" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj new file mode 100644 index 000000000..63cceb65d --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj @@ -0,0 +1,88 @@ + + + + 4 + + + Debug + AnyCPU + 10.0.20506 + 2.0 + + + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5} + Library + Properties + UnityTests + v4.5 + 512 + ..\..\..\obj\ + $(SolutionDir)build\$(AssemblyName) + + + + true + full + false + DEBUG;TRACE + prompt + 4 + 0169 + true + + + pdbonly + true + prompt + 4 + 0169 + true + + + + + $(UnityDir)Managed\UnityEditor.dll + + + $(UnityDir)Managed\UnityEngine.dll + + + $(UnityDir)UnityExtensions\Unity\TestRunner\Editor\UnityEditor.TestRunner.dll + + + $(UnityDir)UnityExtensions\Unity\TestRunner\UnityEngine.TestRunner.dll + + + $(SolutionDir)packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + + + + + + {B389ADAF-62CC-486E-85B4-2D8B078DF763} + GitHub.Api + + + {3dd3451c-30fa-4294-a3a9-1e080342f867} + TestWebServer + + + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A} + GitHub.Unity + + + + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject new file mode 100644 index 000000000..319cd523c --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs b/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs new file mode 100644 index 000000000..53e71ec45 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs @@ -0,0 +1,218 @@ +using UnityEngine; +using UnityEditor; +using UnityEngine.TestTools; +using NUnit.Framework; +using System.Collections; +using GitHub.Unity; + +public class VersionTests +{ + [Test] + public void OnePart_IsValid() + { + var version = TheVersion.Parse("2"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void TwoParts_IsValid() + { + var version = TheVersion.Parse("2.1"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void ThreeParts_IsValid() + { + var version = TheVersion.Parse("2.1.32"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void FourParts_IsValid() + { + var version = TheVersion.Parse("2.1.32.5"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(5, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void TwoPartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1alpha1"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual("1alpha1", version.Special); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void ThreePartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1.3beta"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual("3beta", version.Special); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void FourPartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1.32.delta"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual("delta", version.Special); + } + + [Test] + public void ParsingStopsAtAlpha() + { + var version = TheVersion.Parse("2.1.1beta2.3"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual("1beta2", version.Special); + + version = TheVersion.Parse("2.3beta2.3alpha.4"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual("3beta2", version.Special); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void EqualsWorks() + { + var version1 = TheVersion.Parse("2.1"); + var version2 = TheVersion.Parse("2.1.0.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2"); + version2 = TheVersion.Parse("2.0.0.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.3"); + version2 = TheVersion.Parse("2.1.3.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.alpha1.1.2"); + version2 = TheVersion.Parse("2.alpha1"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.alpha1.1"); + version2 = TheVersion.Parse("2.1.alpha1"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.3.alpha1"); + version2 = TheVersion.Parse("2.1.3.alpha1"); + Assert.AreEqual(version1, version2); + } + + [Test] + public void ComparisonWorks() + { + var version1 = TheVersion.Parse("2"); + var version2 = TheVersion.Parse("1"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("1"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version2 > version1); + + version1 = TheVersion.Parse("1.0"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version1 < version2); + + version1 = TheVersion.Parse("1.1"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("1.1"); + version2 = TheVersion.Parse("1.2.3alpha1"); + Assert.IsTrue(version1 <= version2); + + version1 = TheVersion.Parse("1.2.3"); + version2 = TheVersion.Parse("1.2.3alpha1"); + Assert.IsTrue(version1 >= version2); + + version1 = TheVersion.Parse("0.33.0-beta"); + version2 = TheVersion.Parse("0.32.0"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("0.33.2"); + version2 = TheVersion.Parse("0.33.3-beta"); + Assert.IsTrue(version1 < version2); + + version1 = TheVersion.Parse("0.33.3-alpha"); + version2 = TheVersion.Parse("0.33.3-beta"); + Assert.IsTrue(version1 < version2); + + version1 = TheVersion.Parse("0.33.3"); + version2 = TheVersion.Parse("0.33.3-beta"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("git version 2.11.1.windows.1"); + version2 = TheVersion.Parse("2.17.0.windows.1"); + Assert.IsTrue(version1 < version2); + } + + [Test] + public void DetectingUnstableVersionsWorks() + { + var version = TheVersion.Parse("2"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2.3"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2.3.4"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2alpha1"); + Assert.IsTrue(version.IsUnstable); + + version = TheVersion.Parse("1.2.3stuff"); + Assert.IsTrue(version.IsUnstable); + + version = TheVersion.Parse("1.2.3.4whatever"); + Assert.IsTrue(version.IsUnstable); + } + + [Test] + public void ParsingInvalidVersionStringsWorks() + { + var ret = TheVersion.Parse(null); + Assert.AreEqual(TheVersion.Default, ret); + ret = TheVersion.Parse(""); + Assert.AreEqual(TheVersion.Default, ret); + ret = TheVersion.Parse("bla"); + Assert.AreEqual(TheVersion.Default, ret); + ret = TheVersion.Parse("git version 2.11.1.windows.1"); + Assert.AreEqual(TheVersion.Default, ret); + } +} diff --git a/src/UnityExtension/ProjectSettings/ProjectVersion.txt b/src/UnityExtension/ProjectSettings/ProjectVersion.txt new file mode 100644 index 000000000..78fa54032 --- /dev/null +++ b/src/UnityExtension/ProjectSettings/ProjectVersion.txt @@ -0,0 +1 @@ +m_EditorVersion: 2018.3.2f1 diff --git a/src/UnityShim/UnityShim.cs b/src/UnityShim/UnityShim.cs new file mode 100644 index 000000000..3cee6665a --- /dev/null +++ b/src/UnityShim/UnityShim.cs @@ -0,0 +1,14 @@ +using System; +using UnityEditor; +namespace GitHub.Unity +{ + public static class UnityShim + { + public static event Action Editor_finishedDefaultHeaderGUI; + public static void Raise_Editor_finishedDefaultHeaderGUI(Editor editor) + { + if (Editor_finishedDefaultHeaderGUI != null) + Editor_finishedDefaultHeaderGUI(editor); + } + } +} \ No newline at end of file diff --git a/src/UnityShim/UnityShim.csproj b/src/UnityShim/UnityShim.csproj new file mode 100644 index 000000000..6f10a7734 --- /dev/null +++ b/src/UnityShim/UnityShim.csproj @@ -0,0 +1,74 @@ + + + + + Debug + AnyCPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188} + Library + Properties + GitHub.Unity + GitHub.UnityShim + v3.5 + 512 + + 6 + + + ..\UnityExtension\Assets\Editor\build\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs) + prompt + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs) + prompt + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) + prompt + 4 + false + false + true + ..\..\common\codeanalysis-debug.ruleset + + + Debug + + + + + $(UnityDir)Managed\UnityEditor.dll + False + + + $(UnityDir)Managed\UnityEngine.dll + False + + + + + + + + \ No newline at end of file diff --git a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj index c6f82ed7f..40067b0aa 100644 --- a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj +++ b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj @@ -9,9 +9,10 @@ Properties deleteme deleteme - v3.5 + v4.5 512 $(SolutionDir)src\UnityExtension\Assets\Editor\build + true @@ -20,6 +21,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -27,25 +29,28 @@ TRACE prompt 4 + false + + + {b389adaf-62cc-486e-85b4-2d8b078df763} GitHub.Api + + {b389adaf-62cc-486e-85b4-2d8b078df76B} + GitHub.Api.45 + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging - - False - $(SolutionDir)\lib\Mono.Security.dll - $(SolutionDir)\lib\sfw\sfw.net.dll True - diff --git a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj index 05105c57e..4f839eeae 100644 --- a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj +++ b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj @@ -9,9 +9,10 @@ Properties deleteme deleteme - v3.5 + v4.5 512 build + Release @@ -20,6 +21,7 @@ TRACE prompt 4 + false Debug @@ -28,55 +30,77 @@ TRACE prompt 4 + false {b389adaf-62cc-486e-85b4-2d8b078df763} GitHub.Api + + {b389adaf-62cc-486e-85b4-2d8b078df76b} + GitHub.Api.45 + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging + + {add7a18b-dd2a-4c22-a2c1-488964eff30b} + GitHub.Unity.45 + {add7a18b-dd2a-4c22-a2c1-488964eff30a} GitHub.Unity True - - $(SolutionDir)\packages\AsyncBridge.Net35.0.2.3333.0\lib\net35-Client\AsyncBridge.Net35.dll - True - - - $(SolutionDir)\packages\ReadOnlyCollectionInterfaces.1.0.0\lib\NET20\ReadOnlyCollectionsInterfaces.dll + + {add7a18b-dd2a-4c22-a2c1-488964eff30b} + GitHub.Unity True - + $(SolutionDir)\lib\sfw\sfw.net.dll True - - $(SolutionDir)\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll - True - + + ExtensionLoader.cs + PreserveNewest + + + UnityAPIWrapper.cs + PreserveNewest + + + ReadOnlyCollectionsInterfaces.dll + PreserveNewest + + + AsyncBridge.Net35.dll + PreserveNewest + + + System.Threading.dll + PreserveNewest + - - EULA.txt + + eula.txt PreserveNewest - - CREDITS.txt + + credits.txt PreserveNewest diff --git a/src/tests/TestApp/App.config b/src/tests/CommandLine/App.config similarity index 84% rename from src/tests/TestApp/App.config rename to src/tests/CommandLine/App.config index 343984d02..258fc34a2 100644 --- a/src/tests/TestApp/App.config +++ b/src/tests/CommandLine/App.config @@ -1,6 +1,6 @@ - - + + diff --git a/src/tests/TestApp/TestApp.csproj b/src/tests/CommandLine/CommandLine.csproj similarity index 74% rename from src/tests/TestApp/TestApp.csproj rename to src/tests/CommandLine/CommandLine.csproj index ff3b3c2bb..2b00d6b80 100644 --- a/src/tests/TestApp/TestApp.csproj +++ b/src/tests/CommandLine/CommandLine.csproj @@ -7,19 +7,19 @@ {08B87D2A-8CF1-4211-B7AA-5209F00F72F8} Exe Properties - TestApp - TestApp + Test.CommandLine + CommandLine v3.5 512 true + $(SolutionDir)build\$(AssemblyName) AnyCPU true full false - bin\Debug\ DEBUG;TRACE prompt 4 @@ -29,7 +29,6 @@ AnyCPU pdbonly true - bin\Release\ TRACE prompt 4 @@ -47,8 +46,22 @@ + + + {b389adaf-62cc-486e-85b4-2d8b078df763} + GitHub.Api + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + {3dd3451c-30fa-4294-a3a9-1e080342f867} + TestWebServer + + - + PreserveNewest - files\git\windows\git-lfs.zip + files\unity\git\windows\git-lfs.zip PreserveNewest - - files\git\windows\git-lfs.zip.md5 + PreserveNewest - + - files\git\mac\git-lfs.zip + files\unity\git\mac\git-lfs.zip + PreserveNewest + + + PreserveNewest + + PreserveNewest - - files\git\mac\git-lfs.zip.md5 + PreserveNewest + + + {B389ADAF-62CC-486E-85B4-2D8B078DF763} + GitHub.Api + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging + + +