diff options
author | Mike Krüger <mikkrg@microsoft.com> | 2019-08-26 09:38:01 +0300 |
---|---|---|
committer | Mike Krüger <mikkrg@microsoft.com> | 2019-09-20 11:56:37 +0300 |
commit | a813dd32130af3759488cc74391c148133f8845a (patch) | |
tree | 87adc524be548418c465121816aee39344470957 /main/src/addins | |
parent | fda4e5644b56ced4c713d16e3ed4ea28bfeaa2c7 (diff) |
[VersionControl] Implemented git client library.
This library uses the git command line client instead of libgit2 to perform git tasks.
Diffstat (limited to 'main/src/addins')
39 files changed, 2731 insertions, 131 deletions
diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/ArgumentHandler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/ArgumentHandler.cs new file mode 100644 index 0000000000..4bc116434e --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/ArgumentHandler.cs @@ -0,0 +1,114 @@ +// +// ArgumentHandler.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.IO; +using System.Text.RegularExpressions; + +namespace GitAskPass +{ + class ArgumentHandler + { + static Regex passPhraseRegex = new Regex("Enter passphrase for key \\'(.*)\\':\\s?", RegexOptions.Compiled); + static Regex passwordPrompt = new Regex("(.*)\\'s password:\\s?", RegexOptions.Compiled); + + internal static bool Handle (string command, StreamStringReadWriter writer) => + HandleUsernameRequest (command, writer) || + HandlePasswordRequest (command, writer) || + HandleConnecting (command, writer) || + HandleSSHPassphrase (command, writer) || + HandleSSHPassword (command, writer) || + Error (command, writer); + + static bool Error (string command, StreamStringReadWriter writer) + { + writer.WriteLine ("Error"); + writer.WriteLine (command); + return false; + } + + static bool HandleSSHPassword (string command, StreamStringReadWriter writer) + { + var match = passwordPrompt.Match (command); + if (match.Success) { + writer.WriteLine ("SSHPassword"); + writer.WriteLine (match.Groups [1].Value); + return true; + } + return false; + } + + static bool HandleSSHPassphrase (string command, StreamStringReadWriter writer) + { + var match = passPhraseRegex.Match (command); + if (match.Success) { + writer.WriteLine ("SSHPassPhrase"); + writer.WriteLine (match.Groups [1].Value); + return true; + } + return false; + } + + static bool HandleConnecting (string command, StreamStringReadWriter writer) + { + if (command.Contains ("Are you sure you want to continue connecting")) { + writer.WriteLine ("Continue connecting"); + return true; + } + return false; + } + + static bool HandleUsernameRequest (string command, StreamStringReadWriter writer) + { + if (command.StartsWith ("Username", StringComparison.InvariantCultureIgnoreCase)) { + var url = ParseUrl (command); + writer.WriteLine ("Username"); + writer.WriteLine (url); + return true; + } + return false; + } + + static bool HandlePasswordRequest (string command, StreamStringReadWriter writer) + { + if (command.StartsWith ("Password", StringComparison.InvariantCultureIgnoreCase)) { + var url = ParseUrl (command); + writer.WriteLine ("Password"); + writer.WriteLine (url); + return true; + } + return false; + } + + static string ParseUrl (string command) + { + var idx1 = command.IndexOf ('\''); + var idx2 = command.LastIndexOf ('\''); + var url = idx1 >= 0 && idx2 >= 0 ? command.Substring (idx1 + 1, idx2 - idx1 - 1) : "unknown"; + return url; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/GitAskPass.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/GitAskPass.cs new file mode 100644 index 0000000000..422aa5b69c --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/GitAskPass.cs @@ -0,0 +1,65 @@ +// +// GitAskPass.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.IO; +using System.IO.Pipes; +using System.Security.Principal; + +namespace GitAskPass +{ + class GitAskPass + { + public static int Main (string [] args) + { + if (args.Length < 1) { + Console.WriteLine ("No arguments specified."); + return 1; + } + + string pipe = Environment.GetEnvironmentVariable ("MONODEVELOP_GIT_ASKPASS_PIPE"); + if (string.IsNullOrEmpty (pipe)) { + Console.WriteLine ("No arguments specified."); + return 1; + } + try { + using (var client = new NamedPipeClientStream (".", pipe, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation)) { + client.Connect (); + var writer = new StreamStringReadWriter (client); + if (!ArgumentHandler.Handle (args [0], writer)) { + Console.WriteLine ("can't handle : " + args [0]); + return 2; + } + Console.WriteLine (writer.ReadLine ()); + } + } catch (Exception e) { + Console.WriteLine (e.Message); + return -1; + } + return 0; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/MonoDevelop.VersionControl.Git.AskPass.csproj b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/MonoDevelop.VersionControl.Git.AskPass.csproj new file mode 100644 index 0000000000..05b402d08d --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/MonoDevelop.VersionControl.Git.AskPass.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>netcoreapp3.0</TargetFramework> + <AssemblyName>GitAskPass</AssemblyName> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <OutputPath>..\..\..\..\build\AddIns\VersionControl</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <OutputPath>..\..\..\..\build\AddIns\VersionControl</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> + </PropertyGroup> +</Project> diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/StreamStringReadWriter.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/StreamStringReadWriter.cs new file mode 100644 index 0000000000..f92763ce23 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.AskPass/StreamStringReadWriter.cs @@ -0,0 +1,70 @@ +// +// StreamStringReadWriter.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.IO; +using System.Text; + +namespace GitAskPass +{ + sealed class StreamStringReadWriter + { + readonly Stream baseStream; + + public StreamStringReadWriter (Stream baseStream) + { + this.baseStream = baseStream ?? throw new ArgumentNullException (nameof (baseStream)); + } + + public string ReadLine () + { + int len = 0; + for (int i = 0; i < 4; i++) + len |= (int)(baseStream.ReadByte () << (8 * i)); + + var stringBuffer = new byte [len]; + baseStream.Read (stringBuffer, 0, len); + + return Encoding.UTF8.GetString (stringBuffer); + } + + public void WriteLine (string value) + { + if (value is null) + throw new ArgumentNullException (nameof (value)); + var buffer = Encoding.UTF8.GetBytes (value); + if (buffer.LongLength > int.MaxValue) + throw new InvalidOperationException ("string too long : " + buffer.LongLength); + int len = buffer.Length; + for (int i = 0; i < 4; i++) { + baseStream.WriteByte ((byte)(len & 255)); + len >>= 8; + } + baseStream.Write (buffer, 0, buffer.Length); + baseStream.Flush (); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.AskPass/GitAskPassTests.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.AskPass/GitAskPassTests.cs new file mode 100644 index 0000000000..f0211cd6ee --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.AskPass/GitAskPassTests.cs @@ -0,0 +1,131 @@ +// +// GitAskPassTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using NUnit.Framework; +using GitAskPass; +using System.IO; +using System.Diagnostics; +using MonoDevelop.VersionControl.Git.ClientLibrary; +using System.Threading; +using System.IO.Pipes; + +namespace MonoDevelop.VersionControl.Git.AskPass +{ + [TestFixture] + public class GitAskPassTests + { + string Start (string argument, Action<StreamStringReadWriter> callback, int exitCode = 0) + { + var passApp = Path.Combine (Path.GetDirectoryName (typeof (GitProcess).Assembly.Location), "../AddIns/VersionControl/GitAskPass"); + var startInfo = new ProcessStartInfo (passApp); + startInfo.RedirectStandardOutput = true; + startInfo.UseShellExecute = false; + startInfo.Arguments = "\"" + argument + "\""; + Console.WriteLine (startInfo.Arguments); + var pipe = "testPipe" + Thread.CurrentThread.ManagedThreadId; + startInfo.EnvironmentVariables.Add ("MONODEVELOP_GIT_ASKPASS_PIPE", pipe); + using (var server = new NamedPipeServerStream (pipe)) { + server.BeginWaitForConnection (stream => { + callback (new StreamStringReadWriter (server)); + }, server); + if (!File.Exists (passApp)) + Assert.Fail ("GitAskPass not found " + passApp); + var process = Process.Start (startInfo); + process.WaitForExit (5000); + server.Close (); + + Assert.AreEqual (exitCode, process.ExitCode); + return process.StandardOutput.ReadToEnd ().TrimEnd (); + } + } + + [Test] + public void TestUsernameRequest () + { + var result = Start ("Username for 'fooBar':", stream => { + Assert.AreEqual ("Username", stream.ReadLine ()); + Assert.AreEqual ("fooBar", stream.ReadLine ()); + stream.WriteLine ("test_user"); + }); + Assert.AreEqual ("test_user", result); + } + + [Test] + public void TestPasswordRequest () + { + var result = Start ("Password for 'fooBar':", stream => { + Assert.AreEqual ("Password", stream.ReadLine ()); + Assert.AreEqual ("fooBar", stream.ReadLine ()); + stream.WriteLine ("test_pass"); + }); + Assert.AreEqual ("test_pass", result); + } + + [Test] + public void TestContinueConnectingMessage () + { + var result = Start ("Are you sure you want to continue connecting (yes/no)?", stream => { + Assert.AreEqual ("Continue connecting", stream.ReadLine ()); + stream.WriteLine ("yes"); + + }); + Assert.AreEqual ("yes", result); + } + + [Test] + public void TestSshPassword () + { + var result = Start ("Foo Bar's password:", stream => { + Assert.AreEqual ("SSHPassword", stream.ReadLine ()); + stream.WriteLine ("Foo Bar"); + + }); + Assert.AreEqual ("Foo Bar", result); + } + + [Test] + public void TestKeyPassphrase () + { + var result = Start ("Enter passphrase for key 'FooBar':", stream => { + Assert.AreEqual ("SSHPassPhrase", stream.ReadLine ()); + stream.WriteLine ("FooBar"); + + }); + Assert.AreEqual ("FooBar", result); + } + + [Test] + public void TestError () + { + var result = Start ("Some gibberish text.", stream => { + Assert.AreEqual ("Error", stream.ReadLine ()); + stream.WriteLine ("Some gibberish text."); + }, exitCode: 2); + } + } +} + + diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests.csproj b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests.csproj new file mode 100644 index 0000000000..b949740733 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests.csproj @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="..\..\..\..\packages\NUnit.3.12.0\build\NUnit.props" Condition="Exists('..\..\..\..\packages\NUnit.3.12.0\build\NUnit.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{72811761-AD0E-475E-8B53-AF24F449AF8D}</ProjectGuid> + <OutputType>Library</OutputType> + <RootNamespace>MonoDevelop.VersionControl.Git.ClientLibrary.Tests</RootNamespace> + <AssemblyName>MonoDevelop.VersionControl.Git.ClientLibrary.Tests</AssemblyName> + <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\..\..\build\tests</OutputPath> + <DefineConstants>DEBUG;</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <ConsolePause>false</ConsolePause> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <Optimize>true</Optimize> + <OutputPath>..\..\..\..\build\tests</OutputPath> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <ConsolePause>false</ConsolePause> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="nunit.framework"> + <HintPath>..\..\..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll</HintPath> + </Reference> + </ItemGroup> + <ItemGroup> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary.Tests\GitProcessTests.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.AskPass\GitAskPassTests.cs" /> + <Compile Include="..\MonoDevelop.VersionControl.Git.AskPass\ArgumentHandler.cs"> + <Link>MonoDevelop.VersionControl.Git.AskPass\ArgumentHandler.cs</Link> + </Compile> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary.Tests\GitVersionTests.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary.Tests\Git_v1_StatusTests.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary.Tests\Git_v2_StatusTests.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\MonoDevelop.VersionControl.Git.ClientLibrary\MonoDevelop.VersionControl.Git.ClientLibrary.csproj"> + <Project>{7FBB8B40-2B59-4C11-8C51-47C09B67A2B3}</Project> + <Name>MonoDevelop.VersionControl.Git.ClientLibrary</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <Folder Include="MonoDevelop.VersionControl.Git.ClientLibrary.Tests\" /> + <Folder Include="MonoDevelop.VersionControl.Git.AskPass\" /> + </ItemGroup> + <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> +</Project>
\ No newline at end of file diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitProcessTests.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitProcessTests.cs new file mode 100644 index 0000000000..b5998c7a1e --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitProcessTests.cs @@ -0,0 +1,86 @@ +// +// GitProcessTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using NUnit.Framework; +using System.Threading.Tasks; +using NUnit.Framework.Internal; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary.Tests +{ + [TestFixture] + public class GitProcessTests + { + [TestCase ("error: Error")] + [TestCase (" error: Error")] + public void IsErrorTests (string errorLine) + { + Assert.IsTrue (GitProcess.IsError (errorLine)); + } + + [TestCase ("")] + [TestCase (" ")] + [TestCase ("some line…")] + public void IsNoErrorTests (string errorLine) + { + Assert.IsFalse (GitProcess.IsError (errorLine)); + } + + [TestCase] + public void TestStartRootPathNullCheck () + { + try { + new GitProcess ().StartAsync (null, new GitCallbackHandler (), false); + } catch (ArgumentNullException e) { + Assert.AreEqual ("arguments", e.ParamName); + return; + } + Assert.Fail ("No exception thrown."); + } + + [TestCase] + public void TestGitCallbackHandlerNullCheck () + { + try { + var args = new GitArguments ("."); + args.AddArgument ("Foo"); + new GitProcess ().StartAsync (args, null, false); + } catch (ArgumentNullException e) { + Assert.AreEqual ("callbackHandler", e.ParamName); + return; + } + Assert.Fail ("No exception thrown."); + } + + [TestCase] + public async Task TestGitStartupFail () + { + var args = new GitArguments ("."); + args.AddArgument ("needs to fail"); + var result = await new GitProcess ().StartAsync (args, new GitCallbackHandler (), false); + Assert.AreEqual (1, result.ExitCode); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitVersionTests.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitVersionTests.cs new file mode 100644 index 0000000000..69ae34e7b8 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/GitVersionTests.cs @@ -0,0 +1,53 @@ +// +// GitVersionTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using NUnit.Framework; +using System.Threading.Tasks; +using NUnit.Framework.Internal; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary.Tests +{ + [TestFixture] + class GitVersionTests + { + [TestCase] + public async Task TestGitVersion () + { + var version = await GitVersion.GetVersionAsync (); + Assert.IsTrue (version.Major != 0); + } + + [TestCase] + public void TestGitVersionParse () + { + var version = GitVersion.ParseVersionString ("git version 1.2.3"); + Assert.AreEqual (new Version (1, 2, 3, 0), version); + + version = GitVersion.ParseVersionString ("git version 1.2.3.4"); + Assert.AreEqual (new Version (1, 2, 3, 4), version); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v1_StatusTests.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v1_StatusTests.cs new file mode 100644 index 0000000000..6a6a1bdefd --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v1_StatusTests.cs @@ -0,0 +1,73 @@ +// +// Git_v1_StatusTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using NUnit.Framework; +using System.Threading.Tasks; +using NUnit.Framework.Internal; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary.Tests +{ + public class Git_v1_StatusTests + { + [TestCase] + public void ParsePathTests () + { + string test = "AM test/path\0AM test2/path2\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v1); + handler.OnOutput (test); + var dict = handler.FileList; + Assert.IsTrue (dict.ContainsKey ("test/path")); + Assert.IsTrue (dict.ContainsKey ("test2/path2")); + } + + [TestCase] + public void ParsePathTests_noPaths () + { + var test = "AM \0AM test2/path2\0"; + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v1); + handler.OnOutput (test); + var dict = handler.FileList; + Assert.IsTrue (dict.ContainsKey ("")); + Assert.IsTrue (dict.ContainsKey ("test2/path2")); + } + + [TestCase] + public void ParseOrigPathTests () + { + string test = "AM test/path\0AM test3/path3 -> test2/path2\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v1); + handler.OnOutput (test); + var dict = handler.FileList; + Assert.IsTrue (dict.ContainsKey ("test/path")); + Assert.IsTrue (dict.ContainsKey ("test2/path2")); + var state = dict ["test2/path2"]; + Assert.AreEqual ("test3/path3", state.OriginalPath); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v2_StatusTests.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v2_StatusTests.cs new file mode 100644 index 0000000000..2c211b340e --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Git_v2_StatusTests.cs @@ -0,0 +1,128 @@ +// +// Git_v2_StatusTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using NUnit.Framework; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary.Tests +{ + public class Git_v2_StatusTests + { + [TestCase] + public void ParseUntrackedItems () + { + string test = "? /foo/bar.cs\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["/foo/bar.cs"]; + Assert.AreEqual (GitStatusCode.Untracked, state.StageState); + } + + [TestCase] + public void ParseIgnoredItems () + { + string test = "! /foo/bar.cs\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["/foo/bar.cs"]; + Assert.AreEqual (GitStatusCode.Ignored, state.StageState); + } + + [TestCase] + public void ParseOrdinaryChanges () + { + string test = "1 .M N... 100644 100700 100777 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 README.md\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["README.md"]; + Assert.AreEqual (GitStatusCode.WorktreeModified, state.StageState); + Assert.AreEqual (GitSubmoduleState.NoSubmodule, state.SubmoduleState); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Head); + Assert.AreEqual (Convert.ToInt32 ("100700", 8), state.FileMode_Index); + Assert.AreEqual (Convert.ToInt32 ("100777", 8), state.FileMode_Worktree); + } + + [TestCase] + public void ParseRenamedChanges () + { + string test = "2 R. N... 100644 100644 100644 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 R100 README2.md\0README.md\01 .M N... 100644 100700 100777 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 foo.cs\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["README2.md"]; + Assert.AreEqual (GitStatusCode.IndexRenamed, state.StageState); + Assert.AreEqual (GitSubmoduleState.NoSubmodule, state.SubmoduleState); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Head); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Index); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Worktree); + Assert.IsTrue (state.RenameOrCopyScore.IsRename); + Assert.AreEqual (100, state.RenameOrCopyScore.Score); + Assert.AreEqual ("README.md", state.OriginalPath); + Assert.IsTrue (dict.ContainsKey ("foo.cs")); + } + + [TestCase] + public void ParseUnmergedEntries () + { + string test = "u UU N... 100644 100645 100646 100647 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 599bc5aca51701a55c1c8f8586bcbdb8fd56c568 dc8f30622338a15b432458a52d39a9784dafc44f README.md\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["README.md"]; + Assert.AreEqual (GitStatusCode.IndexUnmerged | GitStatusCode.WorktreeUnmerged, state.StageState); + Assert.AreEqual (GitSubmoduleState.NoSubmodule, state.SubmoduleState); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Stage1); + Assert.AreEqual (Convert.ToInt32 ("100645", 8), state.FileMode_Stage2); + Assert.AreEqual (Convert.ToInt32 ("100646", 8), state.FileMode_Stage3); + Assert.AreEqual (Convert.ToInt32 ("100647", 8), state.FileMode_Worktree); + } + + [TestCase] + public void TestIgnoreUnknownHeaders () + { + string test = "#Doesn't make sense\01 DT N... 100644 100700 100777 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 119b2af8dc31e93206df6b8c8a7f58b930ac0ea8 README.md\0"; + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (StatusVersion.v2); + handler.OnOutput (test); + var dict = handler.FileList; + var state = dict ["README.md"]; + Assert.AreEqual (GitStatusCode.IndexDeleted | GitStatusCode.WorktreeTypeChanged, state.StageState); + Assert.AreEqual (GitSubmoduleState.NoSubmodule, state.SubmoduleState); + Assert.AreEqual (Convert.ToInt32 ("100644", 8), state.FileMode_Head); + Assert.AreEqual (Convert.ToInt32 ("100700", 8), state.FileMode_Index); + Assert.AreEqual (Convert.ToInt32 ("100777", 8), state.FileMode_Worktree); + } + + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Properties/AssemblyInfo.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d89a08a0d7 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,51 @@ +// +// AssemblyInfo.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle ("MonoDevelop.VersionControl.Git.ClientLibrary.Tests")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion ("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/packages.config b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/packages.config new file mode 100644 index 0000000000..41573816f2 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary.Tests/packages.config @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="NUnit" version="3.12.0" targetFramework="net472" /> +</packages>
\ No newline at end of file diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary.csproj b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary.csproj new file mode 100644 index 0000000000..25de0877ad --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary.csproj @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{7FBB8B40-2B59-4C11-8C51-47C09B67A2B3}</ProjectGuid> + <OutputType>Library</OutputType> + <RootNamespace>MonoDevelop.VersionControl.Git.ClientLibrary</RootNamespace> + <AssemblyName>MonoDevelop.VersionControl.Git.ClientLibrary</AssemblyName> + <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\..\..\build\AddIns\VersionControl</OutputPath> + <DefineConstants>DEBUG;</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <ConsolePause>false</ConsolePause> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <Optimize>true</Optimize> + <OutputPath>..\..\..\..\build\AddIns\VersionControl</OutputPath> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <ConsolePause>false</ConsolePause> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + </ItemGroup> + <ItemGroup> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitCallbackHandler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitCloneOptions.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitCredentials.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitProcess.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitProgressEventArgs.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitResult.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\ProgressParser.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\AbstractGitCallbackHandler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitCallbackHandlerDecorator.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\GitException.cs" /> + <Compile Include="..\MonoDevelop.VersionControl.Git.AskPass\StreamStringReadWriter.cs"> + <Link>MonoDevelop.VersionControl.Git.ClientLibrary\StreamStringReadWriter.cs</Link> + </Compile> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Clone\GitClone.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Clone\CloneCallbackHandler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Version\GitVersion.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitStatus.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitStatusCallbackHandler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitFileState.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitStatusCallbackHandler.V1_Handler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitStatusCallbackHandler.V2_Handler.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitStatusCode.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitSubmoduleState.cs" /> + <Compile Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\GitRenameOrCopyScore.cs" /> + </ItemGroup> + <ItemGroup> + <Folder Include="MonoDevelop.VersionControl.Git.ClientLibrary\" /> + <Folder Include="MonoDevelop.VersionControl.Git.ClientLibrary\Clone\" /> + <Folder Include="MonoDevelop.VersionControl.Git.ClientLibrary\Version\" /> + <Folder Include="MonoDevelop.VersionControl.Git.ClientLibrary\Status\" /> + </ItemGroup> + <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> +</Project>
\ No newline at end of file diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/AbstractGitCallbackHandler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/AbstractGitCallbackHandler.cs new file mode 100644 index 0000000000..3638771e73 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/AbstractGitCallbackHandler.cs @@ -0,0 +1,42 @@ +// +// GitProcess.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public abstract class AbstractGitCallbackHandler + { + public abstract void OnOutput (string line); + public abstract void OnReportProgress (string operation, int percentage); + + public abstract GitCredentials OnGetCredentials (string url); + + public abstract string OnGetSSHPassword (string userName); + public abstract string OnGetSSHPassphrase (string key); + public abstract bool OnGetContinueConnecting (); + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/CloneCallbackHandler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/CloneCallbackHandler.cs new file mode 100644 index 0000000000..cf3e7b6026 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/CloneCallbackHandler.cs @@ -0,0 +1,51 @@ +// +// CloneCallbackHandler.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + class CloneCallbackHandler : GitCallbackHandlerDecorator + { + public string CloneTarget { get; set; } + + public CloneCallbackHandler (AbstractGitCallbackHandler callbackHandler) : base (callbackHandler) + { + } + + public override void OnOutput (string line) + { + if (CloneTarget == null && line.StartsWith ("Cloning into", StringComparison.InvariantCultureIgnoreCase)) { + var idx1 = line.IndexOf ('\''); + var idx2 = line.LastIndexOf ('\''); + if (idx1 >= 0 && idx2 >= 0) { + CloneTarget = line.Substring (idx1 + 1, idx2 - idx1); + } + } + + base.OnOutput (line); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/GitClone.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/GitClone.cs new file mode 100644 index 0000000000..8ef7d8de3f --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Clone/GitClone.cs @@ -0,0 +1,71 @@ +// +// GitClone.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.Threading.Tasks; +using System.Text; +using System.Threading; +using System.Text.RegularExpressions; +using System.IO; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public static class GitClone + { + public static Task<GitResult> CloneAsync (string sourceUrl, string outputDir, AbstractGitCallbackHandler callbacks, CancellationToken cancellationToken = default) => CloneAsync (sourceUrl, outputDir, callbacks, GitCloneOptions.Default, cancellationToken); + + public static async Task<GitResult> CloneAsync (string sourceUrl, string outputDir, AbstractGitCallbackHandler callbacks, GitCloneOptions options, CancellationToken cancellationToken = default) + { + if (sourceUrl is null) + throw new ArgumentNullException (nameof (sourceUrl)); + if (outputDir is null) + throw new ArgumentNullException (nameof (outputDir)); + if (options is null) + throw new ArgumentNullException (nameof (options)); + var arg = new GitArguments (outputDir); + arg.AddArgument ("clone"); + arg.AddArgument ("-c credential.helper="); + arg.AddArgument ("-c core.quotepath=false"); + arg.AddArgument ("-c log.showSignature=false "); + arg.AddArgument ("--progress"); + if (options.RecurseSubmodules) + arg.AddArgument ("--recurse-submodules"); + if (options.IsBare) + arg.AddArgument ("--bare"); + if (!string.IsNullOrEmpty (options.BranchName)) { + arg.AddArgument ("-- branch"); + arg.AddArgument (options.BranchName); + } + arg.EndOptions (); + arg.AddArgument (sourceUrl); + arg.AddArgument (outputDir); + var handler = new CloneCallbackHandler (callbacks); + var result = await new GitProcess ().StartAsync (arg, handler, true, cancellationToken); + result.Properties[GitCloneOptions.CloneTargetProperty] = handler.CloneTarget; + return result; + } + + } +}
\ No newline at end of file diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandler.cs new file mode 100644 index 0000000000..72a62000d7 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandler.cs @@ -0,0 +1,55 @@ +// +// GitCallbackHandler.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public class GitCallbackHandler : AbstractGitCallbackHandler + { + public Func<string, GitCredentials> GetCredentialsHandler; + + public override GitCredentials OnGetCredentials (string url) => GetCredentialsHandler (url); + + public event EventHandler<string> Output; + + public override void OnOutput (string line) => Output?.Invoke (this, line); + + public event EventHandler<GitProgressEventArgs> Progress; + + public override void OnReportProgress (string operation, int percentage) => Progress?.Invoke (this, new GitProgressEventArgs (operation, percentage)); + + public Func<string, string> GetSSHPassword; + public override string OnGetSSHPassword (string userName) => GetSSHPassword (userName); + + public Func<string, string> GetSSHPassphrase; + public override string OnGetSSHPassphrase (string key) => GetSSHPassphrase (key); + + public Func<bool> GetContinueConnecting; + + public override bool OnGetContinueConnecting () => GetContinueConnecting (); + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandlerDecorator.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandlerDecorator.cs new file mode 100644 index 0000000000..9315f22409 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCallbackHandlerDecorator.cs @@ -0,0 +1,71 @@ +// +// GitCallbackHandler.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Text; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + class GitCallbackHandlerDecorator : AbstractGitCallbackHandler + { + readonly AbstractGitCallbackHandler callbackHandler; + + public GitCallbackHandlerDecorator (AbstractGitCallbackHandler callbackHandler) + { + this.callbackHandler = callbackHandler; + } + + public override bool OnGetContinueConnecting () => callbackHandler?.OnGetContinueConnecting () == true; + + public override GitCredentials OnGetCredentials (string url) => callbackHandler?.OnGetCredentials (url); + + public override string OnGetSSHPassphrase (string key) => callbackHandler?.OnGetSSHPassword (key); + + public override string OnGetSSHPassword (string userName) => callbackHandler?.OnGetSSHPassword (userName); + + public override void OnOutput (string line) => callbackHandler?.OnOutput (line); + + public override void OnReportProgress (string operation, int percentage) => callbackHandler?.OnReportProgress (operation, percentage); + + } + + class GitOutputTrackerCallbackHandler : GitCallbackHandlerDecorator + { + StringBuilder output = new StringBuilder (); + + public string Output { get => output.ToString (); } + + public GitOutputTrackerCallbackHandler (AbstractGitCallbackHandler callbackHandler = null) : base (callbackHandler) + { + } + + public override void OnOutput (string line) + { + output.AppendLine (line); + base.OnOutput (line); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCloneOptions.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCloneOptions.cs new file mode 100644 index 0000000000..4ea63fa94b --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCloneOptions.cs @@ -0,0 +1,41 @@ +// +// GitCloneOptions.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public sealed class GitCloneOptions + { + public static readonly string CloneTargetProperty = "CloneTarget"; + + public static readonly GitCloneOptions Default = new GitCloneOptions (); + + public bool IsBare { get; set; } + + public string BranchName { get; set; } + + public bool RecurseSubmodules { get; set; } = true; + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCredentials.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCredentials.cs new file mode 100644 index 0000000000..fae0075928 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitCredentials.cs @@ -0,0 +1,41 @@ +// +// GitCredentials.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public sealed class GitCredentials + { + public string Username { get; set; } + + public string Password { get; set; } + + public GitCredentials (string username, string password) + { + Username = username; + Password = password; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitException.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitException.cs new file mode 100644 index 0000000000..4b13586c05 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitException.cs @@ -0,0 +1,42 @@ +// +// GitException.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public class GitException : Exception + { + public GitException (string message) : base (message) + { + } + } + + public class GitExternalException : Exception + { + public GitExternalException (string message) : base (message) + { + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProcess.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProcess.cs new file mode 100644 index 0000000000..aec694bccb --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProcess.cs @@ -0,0 +1,266 @@ +// +// GitProcess.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Text; +using System.IO; +using System.Text.RegularExpressions; +using System.Net; +using System.Net.Sockets; +using System.IO.Pipes; +using System.Linq; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + sealed class GitArguments + { + public string GitRootPath { get; } + + StringBuilder arguments = new StringBuilder (); + + public string Arguments { get => arguments.ToString (); } + + public bool TrackProgress { get; set; } = true; + + public GitArguments (string rootPath) + { + if (rootPath is null) + throw new ArgumentNullException (nameof (rootPath)); + if (!Directory.Exists (rootPath)) + throw new DirectoryNotFoundException (rootPath); + this.GitRootPath = rootPath; + } + + public void AddArgument (string argument) + { + if (arguments.Length > 0) + arguments.Append (' '); + arguments.Append (argument); + } + + public void EndOptions () + { + AddArgument ("--"); + } + } + + sealed class GitProcess + { + public readonly static string [] errorIndicatorTags = { + "warning:", + "error:", + "fatal:", + "remote: error", + "Cannot", + "Could not", + "Interactive rebase already started", + "refusing to pull", + "cannot rebase:", + "conflict", + "unable", + "The file will have its original", + "runnerw:" + }; + + static TaskFactory exclusiveOperationFactory; + static TaskFactory concurrentOperationFactory; + + static GitProcess () + { + var scheduler = new ConcurrentExclusiveSchedulerPair (); + exclusiveOperationFactory = new TaskFactory (scheduler.ExclusiveScheduler); + concurrentOperationFactory = new TaskFactory (scheduler.ConcurrentScheduler); + } + + Process process; + GitCredentials credentials = null; + AbstractGitCallbackHandler callbacks; + NamedPipeServerStream server; + + StringBuilder errorText = new StringBuilder (); + string pipe; + + public GitProcess () + { + pipe = "MD_GIT_Pipe_" + Process.GetCurrentProcess ().Id; + } + + public bool IsRunning { get => process != null && !process.HasExited; } + + public Task<GitResult> StartAsync (GitArguments arguments, AbstractGitCallbackHandler callbackHandler, bool askPass, CancellationToken cancellationToken = default) + { + if (arguments is null) + throw new ArgumentNullException (nameof (arguments)); + if (callbackHandler is null) + throw new ArgumentNullException (nameof (callbackHandler)); + + + return (askPass ? exclusiveOperationFactory : concurrentOperationFactory).StartNew (delegate { + try { + this.callbacks = callbackHandler; + var startInfo = new ProcessStartInfo ("git"); + startInfo.WorkingDirectory = arguments.GitRootPath; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + startInfo.UseShellExecute = false; + startInfo.Arguments = arguments.Arguments; + + if (askPass) { + var passApp = Path.Combine (Path.GetDirectoryName (typeof (GitProcess).Assembly.Location), "GitAskPass"); + startInfo.EnvironmentVariables.Add ("GIT_ASKPASS", passApp); + startInfo.EnvironmentVariables.Add ("SSH_ASKPASS", passApp); + startInfo.EnvironmentVariables.Add ("MONODEVELOP_GIT_ASKPASS_PIPE", pipe); + server = new NamedPipeServerStream (pipe); + } + + process = Process.Start (startInfo); + process.OutputDataReceived += (sender, e) => { + if (cancellationToken.IsCancellationRequested) { + SafeKillProcess (); + cancellationToken.ThrowIfCancellationRequested (); + } + if (e?.Data == null) + return; + if (arguments.TrackProgress && ProgressParser.ParseProgress (e.Data, callbackHandler)) + return; + callbackHandler.OnOutput (e.Data); + }; + process.ErrorDataReceived += (sender, e) => { + if (cancellationToken.IsCancellationRequested) { + SafeKillProcess (); + cancellationToken.ThrowIfCancellationRequested (); + } + if (e?.Data == null) + return; + if (IsError (e.Data)) { + errorText.AppendLine (e.Data); + } else { + if (arguments.TrackProgress && ProgressParser.ParseProgress (e.Data, callbackHandler)) + return; + callbackHandler.OnOutput (e.Data); + } + }; + if (!process.Start ()) + throw new InvalidOperationException ("Can't start git process."); + process.BeginOutputReadLine (); + process.BeginErrorReadLine (); + cancellationToken.Register (SafeKillProcess); + if (askPass) + server.BeginWaitForConnection (HandleAsyncCallback, server); + process.WaitForExit (); + } finally { + SafeKillProcess (); + } + return new GitResult { Success = process.ExitCode == 0, ExitCode = process.ExitCode, ErrorMessage = errorText.ToString () }; + }, cancellationToken); + } + + void SafeKillProcess () + { + try { + if (!process.HasExited) + process.Kill (); + if (server != null) { + server.Dispose (); + server = null; + } + } catch { } + } + + void HandleAsyncCallback (IAsyncResult result) + { + var ns = server; + + var reader = new GitAskPass.StreamStringReadWriter (ns); + var request = reader.ReadLine (); + switch (request) { + case "Username": + var url = reader.ReadLine (); + if (GetCredentials (url)) { + reader.WriteLine (credentials.Username); + } + break; + case "Password": + url = reader.ReadLine (); + if (GetCredentials (url)) { + reader.WriteLine (credentials.Password); + } + break; + case "Continue connecting": + reader.WriteLine (callbacks.OnGetContinueConnecting () ? "yes" : "no"); + break; + case "SSHPassPhrase": + string key = reader.ReadLine (); + reader.WriteLine (callbacks.OnGetSSHPassphrase (key)); + break; + case "SSHPassword": + string userName = reader.ReadLine (); + reader.WriteLine (callbacks.OnGetSSHPassword (userName)); + break; + case "Error": + throw new GitExternalException (reader.ReadLine ()); + } + server.Close (); + + server = new NamedPipeServerStream (pipe); + server.BeginWaitForConnection (HandleAsyncCallback, server); + } + + bool GetCredentials (string url) + { + if (credentials == null) { + try { + credentials = callbacks.OnGetCredentials (url); + if (credentials == null) + SafeKillProcess (); + } catch { + credentials = null; + SafeKillProcess (); + } + } + return credentials != null; + } + + public static bool IsError (string line) + { + int start = 0; + while (start + 1 < line.Length && char.IsWhiteSpace (line [start])) + start++; + if (start >= line.Length) + return false; + foreach (var tag in errorIndicatorTags) { + if (start + tag.Length >= line.Length) + continue; + if (line.IndexOf (tag, start, tag.Length, StringComparison.InvariantCultureIgnoreCase) == start) + return true; + } + return false; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProgressEventArgs.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProgressEventArgs.cs new file mode 100644 index 0000000000..64dbbf82c2 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitProgressEventArgs.cs @@ -0,0 +1,42 @@ +// +// GitProgressEventArgs.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public sealed class GitProgressEventArgs : EventArgs + { + public string Operation { get; } + public int Percentage { get; } + + public GitProgressEventArgs (string operation, int percentage) + { + Operation = operation; + Percentage = percentage; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitResult.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitResult.cs new file mode 100644 index 0000000000..27140d0b13 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/GitResult.cs @@ -0,0 +1,41 @@ +// +// GitResult.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.Collections.Generic; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public sealed class GitResult + { + public bool Success { get; internal set; } + + public int ExitCode { get; internal set; } + + public string ErrorMessage { get; internal set; } + + public Dictionary<string, string> Properties { get; } = new Dictionary<string, string> (); + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/ProgressParser.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/ProgressParser.cs new file mode 100644 index 0000000000..278927d0d6 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/ProgressParser.cs @@ -0,0 +1,52 @@ +// +// ProgressParser.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Text.RegularExpressions; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + static class ProgressParser + { + static readonly Regex progressRegex = new Regex (".*((?:Receiving|Writing|Compressing|Counting) objects|Resolving deltas|Checking out files): +(\\d{1,3})%j*", RegexOptions.Compiled); + + public static bool ParseProgress (string line, AbstractGitCallbackHandler callbackHandler) + { + if (line is null) + throw new ArgumentNullException (nameof (line)); + if (callbackHandler is null) + throw new ArgumentNullException (nameof (callbackHandler)); + + var match = progressRegex.Match (line); + if (match.Success) { + callbackHandler.OnReportProgress (match.Groups [1].Value, int.Parse(match.Groups [2].Value)); + return true; + } + + return false; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitFileState.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitFileState.cs new file mode 100644 index 0000000000..f568036437 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitFileState.cs @@ -0,0 +1,82 @@ +// +// GitStatus.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + + public readonly struct GitFileState + { + // data + readonly uint stateFlags; // 20 bit data contains: RenameOrCopyScore (8 bit) GitSubmoduleState (4 bit) GitStageState (16 bit) + readonly ulong fileMode; // contains 64 bit data : 4 FileModes = [WorkTree] [Stage3] [Stage2/Index] [Stage1/Head] + readonly string originalPath; + + // all entries: + public GitStatusCode StageState { get => (GitStatusCode)(stateFlags & 0xFFFF); } + public GitSubmoduleState SubmoduleState { get => (GitSubmoduleState)((stateFlags >> 16) & 0xF); } + + // for ordinary entries: + public ushort FileMode_Head { get => (ushort)fileMode; } + public ushort FileMode_Index { get => (ushort)(fileMode >> 16); } + public ushort FileMode_Worktree { get => (ushort)(fileMode >> 48); } + + // For renamed/copied Entries: + public string OriginalPath { get => originalPath; } + public GitRenameOrCopyScore RenameOrCopyScore { get => new GitRenameOrCopyScore((byte)(stateFlags >> 20)); } + + // For unmerged entries: + public ushort FileMode_Stage1 { get => FileMode_Head; } + public ushort FileMode_Stage2 { get => FileMode_Index; } + public ushort FileMode_Stage3 { get => (ushort)(fileMode >> 32); } + + GitFileState (GitStatusCode stageState, GitSubmoduleState submoduleState, ushort fileMode_Stage1, ushort fileMode_Stage2, ushort fileMode_Stage3, ushort fileMode_Worktree, string originalPath, byte renameOrCopyScore) + { + stateFlags = (uint)stageState | ((uint)((byte)submoduleState & 0xF) << 16) | ((uint)renameOrCopyScore << 20); + fileMode = fileMode_Stage1 | (ulong)fileMode_Stage2 << 16 | (ulong)fileMode_Stage3 << 32 | (ulong)fileMode_Worktree << 48; + this.originalPath = originalPath; + } + + internal static GitFileState CreateSimpleState (GitStatusCode stageState) + { + return new GitFileState (stageState, GitSubmoduleState.NoSubmodule, 0, 0, 0, 0, null, 0); + } + + internal static GitFileState CreateOrdinaryState (GitStatusCode stageState, GitSubmoduleState submoduleState, ushort fileMode_Head, ushort fileMode_Index, ushort fileMode_Worktree) + { + return new GitFileState (stageState, submoduleState, fileMode_Head, fileMode_Index, 0, fileMode_Worktree, null, 0); + } + + internal static GitFileState CreateRenamedState (GitStatusCode stageState, GitSubmoduleState submoduleState, ushort fileMode_Head, ushort fileMode_Index, ushort fileMode_Worktree, string originalPath, byte renameOrCopyScore) + { + return new GitFileState (stageState, submoduleState, fileMode_Head, fileMode_Index, 0, fileMode_Worktree, originalPath, renameOrCopyScore); + } + + internal static GitFileState CreateUnmergedState (GitStatusCode stageState, GitSubmoduleState submoduleState, ushort fileMode_Stage1, ushort fileMode_Stage2, ushort fileMode_Stage3, ushort fileMode_Worktree) + { + return new GitFileState (stageState, submoduleState, fileMode_Stage1, fileMode_Stage2, fileMode_Stage3, fileMode_Worktree, null, 0); + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitRenameOrCopyScore.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitRenameOrCopyScore.cs new file mode 100644 index 0000000000..4ea356e0e1 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitRenameOrCopyScore.cs @@ -0,0 +1,42 @@ +// +// GitRenameOrCopyScore.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public readonly struct GitRenameOrCopyScore + { + readonly byte data; + + public bool IsRename { get => (data & 0b1000_0000) != 0; } + + public int Score { get => data & 0b0111_1111; } + + internal GitRenameOrCopyScore (byte data) + { + this.data = data; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatus.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatus.cs new file mode 100644 index 0000000000..1c6f0e9379 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatus.cs @@ -0,0 +1,67 @@ +// +// GitStatus.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.Threading.Tasks; +using System.Text; +using System.Threading; +using System.Text.RegularExpressions; +using System.IO; +using System.Collections.Generic; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + + public static class GitStatus + { + public static async Task<Dictionary<string, GitFileState>> GetStatusAsync (string repository, IEnumerable<string> paths = null, StatusVersion version = StatusVersion.v2, CancellationToken cancellationToken = default) + { + if (repository is null) + throw new ArgumentNullException (nameof (repository)); + + var handler = GitStatusCallbackHandler.CreateCallbackHandler (version); + + var args = new GitArguments (repository); + args.AddArgument ("status"); + args.AddArgument ("--ignored"); + if (version == StatusVersion.v2) { + args.AddArgument ("--porcelain=v2"); + } else { + args.AddArgument ("--porcelain"); + } + args.AddArgument ("-z"); + args.AddArgument ("--untracked-files"); + args.EndOptions (); + args.TrackProgress = false; // not required + if (paths != null) { + foreach (var path in paths) + args.AddArgument (path); + } + var result = await new GitProcess ().StartAsync (args, handler, false, cancellationToken); + + return handler.FileList; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V1_Handler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V1_Handler.cs new file mode 100644 index 0000000000..35ad7fedda --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V1_Handler.cs @@ -0,0 +1,69 @@ +// +// GitStatus.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + partial class GitStatusCallbackHandler + { + class V1_Handler : StatusCallbackHandler + { + protected override bool TryParseState (string data, int start, ref int end, out GitFileState state, out string path) + { + var (sx, sy, origPath, path2) = ParsePath (data, start, end); + path = path2; + if (origPath == null) { + state = GitFileState.CreateSimpleState (GetState (sx, sy)); + } else { + state = GitFileState.CreateRenamedState (GetState (sx, sy), GitSubmoduleState.NoSubmodule, 0, 0, 0, origPath, 0); + } + return true; + } + + internal static (char sx, char sy, string origPath, string path) ParsePath (string text, int from, int to) + { + // format to parse is: + // XY PATH + // XY ORIG_PATH -> PATH + + char sx = text [from]; + char sy = text [from + 1]; + string origPath = null; + string path = null; + const int headerLength = 3; + var arrowIdx = text.IndexOf (" -> ", from + headerLength, to - from - headerLength, StringComparison.Ordinal); + if (arrowIdx >= 0) { + origPath = text.Substring (from + headerLength, arrowIdx - from - headerLength); + const int arrowLength = 4; // " -> ".Length; + path = text.Substring (arrowIdx + arrowLength, to - arrowIdx - arrowLength); + } else { + path = text.Substring (from + headerLength, to - from - headerLength); + } + return (sx, sy, origPath, path); + } + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V2_Handler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V2_Handler.cs new file mode 100644 index 0000000000..1503034102 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.V2_Handler.cs @@ -0,0 +1,214 @@ +// +// GitStatus.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.IO; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + partial class GitStatusCallbackHandler + { + class V2_Handler : StatusCallbackHandler + { + protected override bool TryParseState (string data, int start, ref int end, out GitFileState state, out string path) + { + /* +Ordinary changed entries have the following format: + 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path> +Renamed or copied entries have the following format: + 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath> +Unmerged entries have the following format; the first character is a "u" to distinguish from ordinary changed entries. + u <xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path> +Untracked items have the following format: + ? <path> +Ignored items have the following format: + ! <path> + + * */ + switch (data [start]) { + case '1': return TryParseOrdinaryEntry (data, start + 2, end, out state, out path); + case '2': return TryParseRenamedEntry (data, start + 2, ref end, out state, out path); + case 'u': return TryParseUnmergedEntry (data, start + 2, end, out state, out path); + case '?': + state = UntrackedState; + path = data.Substring (start + 2, end - 2 - start); + return true; + + case '!': + state = IgnoredState; + path = data.Substring (start + 2, end - 2 - start); + return true; + case '#': // Header: Ignore for now + state = UntrackedState; + path = null; + return false; + } + + state = UntrackedState; + path = null; + + return false; + } + + bool TryParseOrdinaryEntry (string data, int start, int end, out GitFileState state, out string path) + { + //<XY> <sub> <mH> <mI> <mW> <hH> <hI> <path> + var k = start; + char x = data [k++]; // <XY> + char y = data [k++]; + k++; // skip space + char subIndicator = data [k++]; // <sub> A 4 character field describing the submodule state. + char subHasCommitChanged = data [k++]; + char subHasTrackedChanges = data [k++]; + char subHasUntrackedChanges = data [k++]; + k++; // skip space + var fileModeHead = ParseOctalNumber (data, ref k); // <mH> + var fileModeIndex = ParseOctalNumber (data, ref k); // <mI> + var fileModeWorkTree = ParseOctalNumber (data, ref k); // <mW> + + while (data [k] != ' ') k++; // <hH> skip octal file mode in WorkTree + k++; // skip space + + while (data [k] != ' ') k++; // <hI> skip octal file mode in WorkTree + k++; // skip space + + path = data.Substring (k, end - k); + state = GitFileState.CreateOrdinaryState (GetState (x, y), GetSubmoduleState (subIndicator, subHasCommitChanged, subHasTrackedChanges, subHasUntrackedChanges), fileModeHead, fileModeIndex, fileModeWorkTree); + return true; + } + + bool TryParseRenamedEntry (string data, int start, ref int end, out GitFileState state, out string path) + { // 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath> + var k = start; + char x = data [k++]; // <XY> + char y = data [k++]; + k++; // skip space + char subIndicator = data [k++]; // <sub> A 4 character field describing the submodule state. + char subHasCommitChanged = data [k++]; + char subHasTrackedChanges = data [k++]; + char subHasUntrackedChanges = data [k++]; + k++; // skip space + var fileModeHead = ParseOctalNumber (data, ref k); // <mH> + var fileModeIndex = ParseOctalNumber (data, ref k); // <mI> + var fileModeWorkTree = ParseOctalNumber (data, ref k); // <mW> + + while (k < end && data [k] != ' ') k++; // <hH> skip octal file mode in WorkTree + k++; // skip space + + while (k < end && data [k] != ' ') k++; // <hI> skip octal file mode in WorkTree + k++; // skip space + + byte renameOrCopyScore = (byte)(data [k++] == 'R' ? 0x80 : 0); // <X> + int percentage = 0; + while (k < end) { // <score> + char ch = data [k]; + if (ch == ' ') + break; + percentage = percentage * 10 + (ch - '0'); + k++; + } + k++; // skip space + + path = data.Substring (k, end - k); + end++; + int next = data.IndexOf ('\u0000', end); + + state = GitFileState.CreateRenamedState ( + GetState (x, y), + GetSubmoduleState (subIndicator, subHasCommitChanged, subHasTrackedChanges, subHasUntrackedChanges), + fileModeHead, fileModeIndex, fileModeWorkTree, + data.Substring (end, next - end), + (byte)(renameOrCopyScore | (percentage & 0x7F)) + ); + end = next; + return true; + } + + + bool TryParseUnmergedEntry (string data, int start, int end, out GitFileState state, out string path) + { + //<xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path> + var k = start; + char x = data [k++]; // <XY> + char y = data [k++]; + k++; // skip space + char subIndicator = data [k++]; // <sub> A 4 character field describing the submodule state. + char subHasCommitChanged = data [k++]; + char subHasTrackedChanges = data [k++]; + char subHasUntrackedChanges = data [k++]; + k++; // skip space + var fileModeStage1 = ParseOctalNumber (data, ref k); // <m1> + var fileModeStage2 = ParseOctalNumber (data, ref k); // <m2> + var fileModeStage3 = ParseOctalNumber (data, ref k); // <m3> + var fileModeWorkTree = ParseOctalNumber (data, ref k); // <mW> + + while (data [k] != ' ') k++; // <h1> skip object name in stage 1. + k++; // skip space + + while (data [k] != ' ') k++; // <h2> skip object name in stage 2. + k++; // skip space + + while (data [k] != ' ') k++; // <h3> skip object name in stage 3. + k++; // skip space + + path = data.Substring (k, end - k); + state = GitFileState.CreateUnmergedState (GetState (x, y), GetSubmoduleState (subIndicator, subHasCommitChanged, subHasTrackedChanges, subHasUntrackedChanges), fileModeStage1, fileModeStage2, fileModeStage3, fileModeWorkTree); + return true; + } + + static ushort ParseOctalNumber (string data, ref int k) + { + int result = 0; + while (k < data.Length) { + char ch = data [k]; + if (ch == ' ') { + k++; // skip space + return (ushort)result; + } + result = result * 8 + (ch - '0'); + k++; + } + return (ushort)result; + } + + static GitSubmoduleState GetSubmoduleState (char subIndicator, char subHasCommitChanged, char subHasTrackedChanges, char subHasUntrackedChanges) + { + if (subIndicator == 'N') + return GitSubmoduleState.NoSubmodule; + if (subIndicator != 'S') + throw new InvalidDataException ("Sub module indicator " + subIndicator + " unknown."); + var result = GitSubmoduleState.IsSubmodule; + if (subHasCommitChanged == 'C') + result |= GitSubmoduleState.CommitChanged; + if (subHasTrackedChanges == 'M') + result |= GitSubmoduleState.HasTrackedChanged; + if (subHasUntrackedChanges == 'U') + result |= GitSubmoduleState.HasUntrackedChanged; + return result; + } + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.cs new file mode 100644 index 0000000000..db55b9abb0 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCallbackHandler.cs @@ -0,0 +1,167 @@ +// +// GitStatus.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.Collections.Generic; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public enum StatusVersion + { + v1, + v2 + } + + abstract class StatusCallbackHandler : GitCallbackHandler + { + protected readonly static GitFileState IgnoredState = GitFileState.CreateSimpleState (GitStatusCode.Ignored); + protected readonly static GitFileState UntrackedState = GitFileState.CreateSimpleState (GitStatusCode.Untracked); + + + Dictionary<string, GitFileState> fileList = new Dictionary<string, GitFileState> (); + + public Dictionary<string, GitFileState> FileList { + get { + return fileList; + } + } + + public sealed override void OnOutput (string line) + { + int i = 0; + while (i < line.Length) { + int next = line.IndexOf ('\u0000', i); + if (next < 0 || next >= line.Length) + break; + if (TryParseState (line, i, ref next, out var state, out var path)) { + fileList [path] = state; + } + i = next + 1; + } + } + + protected abstract bool TryParseState (string data, int start, ref int end, out GitFileState state, out string path); + + + protected static GitStatusCode GetState (char x, char y) + { + var result = GitStatusCode.Unmodified; + + switch (x) { + case ' ': + case '.': break; + case 'M': + result |= GitStatusCode.IndexModified; + break; + case 'A': + result |= GitStatusCode.IndexAdded; + break; + case 'D': + result |= GitStatusCode.IndexDeleted; + break; + case 'R': + result |= GitStatusCode.IndexRenamed; + break; + case 'C': + result |= GitStatusCode.IndexCopied; + break; + case 'U': + result |= GitStatusCode.IndexUnmerged; + break; + case 'T': + result |= GitStatusCode.IndexTypeChanged; + break; + case 'X': + result |= GitStatusCode.IndexUnknown; + break; + case '?': + switch (y) { + case '?': return GitStatusCode.Untracked; + default: + throw new InvalidOperationException ("Unknown y state: " + y + " x state: " + x); + } + case '!': + switch (y) { + case '!': return GitStatusCode.Ignored; + default: + throw new InvalidOperationException ("Unknown y state: " + y + " x state: " + x); + } + default: + throw new InvalidOperationException ("Unknown x state: " + x + " (y state: " + y + ")"); + } + + switch (y) { + case ' ': + case '.': break; + case 'M': + result |= GitStatusCode.WorktreeModified; + break; + case 'A': + result |= GitStatusCode.WorktreeAdded; + break; + case 'D': + result |= GitStatusCode.WorktreeDeleted; + break; + case 'R': + result |= GitStatusCode.WorktreeRenamed; + break; + case 'C': + result |= GitStatusCode.WorktreeCopied; + break; + case 'U': + result |= GitStatusCode.WorktreeUnmerged; + break; + case 'T': + result |= GitStatusCode.WorktreeTypeChanged; + break; + case 'X': + result |= GitStatusCode.WorktreeUnknown; + break; + case '?': // should've been handled above. + case '!': + throw new InvalidOperationException ("Invalid y state: " + y + " (x state: " + x +")"); + default: + throw new InvalidOperationException ("Unknown y state: " + y + " (x state: " + x + ")"); + } + + return result; + } + } + + partial class GitStatusCallbackHandler + { + public static StatusCallbackHandler CreateCallbackHandler (StatusVersion version) + { + switch (version) { + case StatusVersion.v1: + return new V1_Handler (); + case StatusVersion.v2: + return new V2_Handler (); + default: + throw new InvalidOperationException ("Git status :" + version + " unknown."); + } + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCode.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCode.cs new file mode 100644 index 0000000000..5f0832a07c --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitStatusCode.cs @@ -0,0 +1,58 @@ +// +// GitStatusCode.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + [Flags] + public enum GitStatusCode : ushort + {// X Y + Unmodified = 0b_0000_0000_0000_0000, + + INDEX_MASK = 0b_1111_1111_0000_0000, + IndexModified = 0b_0000_0001_0000_0000, // M + IndexAdded = 0b_0000_0010_0000_0000, // A + IndexDeleted = 0b_0000_0011_0000_0000, // D + IndexRenamed = 0b_0000_0100_0000_0000, // R + IndexCopied = 0b_0000_0101_0000_0000, // C + IndexUnmerged = 0b_0000_0110_0000_0000, // U + IndexTypeChanged = 0b_0000_0111_0000_0000, // T + IndexUnknown = 0b_0000_1111_0000_0000, // X (should never happen) + + WORKTREE_MASK = 0b_0000_0000_1111_1111, + WorktreeModified = 0b_0000_0000_0000_0001, + WorktreeAdded = 0b_0000_0000_0000_0010, + WorktreeDeleted = 0b_0000_0000_0000_0011, + WorktreeRenamed = 0b_0000_0000_0000_0100, + WorktreeCopied = 0b_0000_0000_0000_0101, + WorktreeUnmerged = 0b_0000_0000_0000_0110, + WorktreeTypeChanged = 0b_0000_0000_0000_0111, + WorktreeUnknown = 0b_0000_0000_0000_1111, + + Untracked = 0b_1111_1110_1111_1110, + Ignored = 0b_1111_1111_1111_1111 + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitSubmoduleState.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitSubmoduleState.cs new file mode 100644 index 0000000000..07b311a79c --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Status/GitSubmoduleState.cs @@ -0,0 +1,39 @@ +// +// GitSubmoduleState.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + [Flags] + public enum GitSubmoduleState : byte + { + NoSubmodule = 0b0000, + IsSubmodule = 0b0001, + CommitChanged = 0b0010, + HasTrackedChanged = 0b0100, + HasUntrackedChanged = 0b1000, + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Version/GitVersion.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Version/GitVersion.cs new file mode 100644 index 0000000000..21110cc590 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/MonoDevelop.VersionControl.Git.ClientLibrary/Version/GitVersion.cs @@ -0,0 +1,70 @@ +// +// GitVersion.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Threading.Tasks; +using System.Text; +using System.Threading; +using System.Text.RegularExpressions; +using System.IO; + +namespace MonoDevelop.VersionControl.Git.ClientLibrary +{ + public static class GitVersion + { + public static async Task<Version> GetVersionAsync (CancellationToken cancellationToken = default) + { + var handler = new GitOutputTrackerCallbackHandler (); + var arguments = new GitArguments ("."); + arguments.AddArgument ("--version"); + + var result = await new GitProcess ().StartAsync (arguments, handler, false, cancellationToken); + + return ParseVersionString (handler.Output); + } + + static readonly Regex versionRegex = new Regex ("git version (\\d+)\\.(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(.*)"); + + internal static Version ParseVersionString (string output) + {
+ var match = versionRegex.Match (output); + if (!match.Success) + throw new InvalidOperationException ("Can't parse : " + output); + + int major = ParseGroup (match, 1); + int minor = ParseGroup (match, 2); + int build = ParseGroup (match, 3); + int revision = ParseGroup (match, 4); + + return new Version (major, minor, build, revision); + } + + static int ParseGroup (Match match, int grp) + { + return !string.IsNullOrEmpty (match.Groups[grp].Value) ? int.Parse (match.Groups[grp].Value) : 0; + } + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/Properties/AssemblyInfo.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1ae98fecbd --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git.ClientLibrary/Properties/AssemblyInfo.cs @@ -0,0 +1,51 @@ +// +// AssemblyInfo.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle ("MonoDevelop.VersionControl.Git.ClientLibrary")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion ("1.0.*")] +[assembly: InternalsVisibleTo("MonoDevelop.VersionControl.Git.ClientLibrary.Tests")] +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git.csproj b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git.csproj index 5b156373db..b83670b966 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git.csproj +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git.csproj @@ -107,6 +107,11 @@ <Name>MonoDevelop.SourceEditor</Name> <Private>False</Private> </ProjectReference> + <ProjectReference Include="..\MonoDevelop.VersionControl.Git.ClientLibrary\MonoDevelop.VersionControl.Git.ClientLibrary.csproj"> + <Project>{7FBB8B40-2B59-4C11-8C51-47C09B67A2B3}</Project> + <Name>MonoDevelop.VersionControl.Git.ClientLibrary</Name> + <Private>False</Private> + </ProjectReference> </ItemGroup> <ItemGroup> <EmbeddedResource Include="MonoDevelop.VersionControl.Git.addin.xml" /> diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git/GitRepository.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git/GitRepository.cs index 21bea9dd5f..f9d1aeb657 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git/GitRepository.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl.Git/MonoDevelop.VersionControl.Git/GitRepository.cs @@ -38,6 +38,7 @@ using LibGit2Sharp; using MonoDevelop.Core; using MonoDevelop.Core.Text; using MonoDevelop.Ide; +using MonoDevelop.VersionControl.Git.ClientLibrary; using ProgressMonitor = MonoDevelop.Core.ProgressMonitor; namespace MonoDevelop.VersionControl.Git @@ -496,9 +497,13 @@ namespace MonoDevelop.VersionControl.Git } public override bool TryGetFileUpdateEventInfo (Repository rep, FilePath file, out FileUpdateEventInfo eventInfo) { - if (file.FileName == "index" && file.ParentDirectory.FileName == ".git") { - eventInfo = FileUpdateEventInfo.UpdateRepository (rep); - return true; + if (file.ParentDirectory.FileName == ".git") { + if (file.FileName == "index") { + eventInfo = FileUpdateEventInfo.UpdateRepository (rep); + return true; + } + eventInfo = null; + return false; } return base.TryGetFileUpdateEventInfo (rep, file, out eventInfo); } @@ -1014,43 +1019,22 @@ namespace MonoDevelop.VersionControl.Git return versions.ToArray (); } - - void GetFilesVersionInfoCore (LibGit2Sharp.Repository repo, GitRevision rev, List<FilePath> localPaths, List<VersionInfo> versions) + + static async Task GetFilesVersionInfoCoreAsync (LibGit2Sharp.Repository repo, GitRevision rev, List<FilePath> localPaths, List<VersionInfo> versions) { - if (IsDisposed) - return; - AssertIsGitThread (); - foreach (var localPath in localPaths) { - if (!localPath.IsDirectory) { - var file = repo.ToGitPath (localPath); - var status = repo.RetrieveStatus (file); - AddStatus (repo, rev, file, versions, status, null); - } + var paths = localPaths.Where (f => !f.IsDirectory).Select (f => repo.ToGitPath (f)).ToArray (); + var result = await GitStatus.GetStatusAsync (repo.Info.WorkingDirectory, paths, version: StatusVersion.v1); + foreach (var file in paths) { + if (!result.TryGetValue (file, out var gitFileState)) { + AddStatus (repo, rev, file, versions, GitStatusCode.Unmodified, null); + } else + AddStatus (repo, rev, file, versions, gitFileState.StageState, null); } } - static void AddStatus (LibGit2Sharp.Repository repo, GitRevision rev, string file, List<VersionInfo> versions, FileStatus status, string directoryPath) + static void AddStatus (LibGit2Sharp.Repository repo, GitRevision rev, string file, List<VersionInfo> versions, GitStatusCode status, string directoryPath) { - VersionStatus fstatus = VersionStatus.Versioned; - - if (status != FileStatus.Unaltered) { - if ((status & FileStatus.NewInIndex) != 0) - fstatus |= VersionStatus.ScheduledAdd; - else if ((status & (FileStatus.DeletedFromIndex | FileStatus.DeletedFromWorkdir)) != 0) - fstatus |= VersionStatus.ScheduledDelete; - else if ((status & (FileStatus.TypeChangeInWorkdir | FileStatus.TypeChangeInIndex | FileStatus.ModifiedInWorkdir | FileStatus.ModifiedInIndex)) != 0) - fstatus |= VersionStatus.Modified; - else if ((status & (FileStatus.RenamedInIndex | FileStatus.RenamedInWorkdir)) != 0) - fstatus |= VersionStatus.ScheduledReplace; - else if ((status & (FileStatus.Nonexistent | FileStatus.NewInWorkdir)) != 0) - fstatus = VersionStatus.Unversioned; - else if ((status & FileStatus.Ignored) != 0) - fstatus = VersionStatus.Ignored; - } - - if (repo.Index.Conflicts [file] != null) - fstatus = VersionStatus.Versioned | VersionStatus.Conflicted; - + var fstatus = ConvertGitState (status); var versionPath = repo.FromGitPath (file); if (directoryPath != null && versionPath.ParentDirectory != directoryPath) { return; @@ -1065,20 +1049,50 @@ namespace MonoDevelop.VersionControl.Git throw new InvalidOperationException (); } - void GetDirectoryVersionInfoCore (LibGit2Sharp.Repository repo, GitRevision rev, FilePath directory, List<VersionInfo> versions, bool recursive) - { - if (IsDisposed) - return; - AssertIsGitThread (); - var relativePath = repo.ToGitPath (directory); - var status = repo.RetrieveStatus (new StatusOptions { - DisablePathSpecMatch = true, - PathSpec = relativePath != "." ? new [] { relativePath } : new string[0], - IncludeUnaltered = true, - }); + static VersionStatus ConvertGitState (GitStatusCode status) + {
+ switch (status) { + case GitStatusCode.Untracked: + return VersionStatus.Unversioned; + case GitStatusCode.Ignored: + return VersionStatus.Ignored; + default: + switch (status & GitStatusCode.INDEX_MASK) { + case GitStatusCode.IndexAdded: + return VersionStatus.Versioned | VersionStatus.ScheduledAdd; + case GitStatusCode.IndexDeleted: + return VersionStatus.Versioned | VersionStatus.ScheduledDelete; + case GitStatusCode.IndexTypeChanged: + case GitStatusCode.IndexModified: + return VersionStatus.Versioned | VersionStatus.Modified; + case GitStatusCode.IndexCopied: + return VersionStatus.Versioned | VersionStatus.ScheduledReplace; + case GitStatusCode.IndexUnmerged: + return VersionStatus.Versioned | VersionStatus.Conflicted; + } + + switch (status & GitStatusCode.WORKTREE_MASK) { + case GitStatusCode.WorktreeAdded: + return VersionStatus.Versioned | VersionStatus.ScheduledAdd; + case GitStatusCode.WorktreeDeleted: + return VersionStatus.Versioned | VersionStatus.ScheduledDelete; + case GitStatusCode.WorktreeTypeChanged: + case GitStatusCode.WorktreeModified: + return VersionStatus.Versioned | VersionStatus.Modified; + case GitStatusCode.WorktreeCopied: + return VersionStatus.Versioned | VersionStatus.ScheduledReplace; + case GitStatusCode.WorktreeUnmerged: + return VersionStatus.Versioned | VersionStatus.Conflicted; + } + return VersionStatus.Versioned; + } + } - foreach (var statusEntry in status) { - AddStatus (repo, rev, statusEntry.FilePath, versions, statusEntry.State, recursive ? null : directory); + static async Task GetDirectoryVersionInfoCoreAsync (LibGit2Sharp.Repository repo, GitRevision rev, FilePath directory, List<VersionInfo> versions, bool recursive) + { + var result = await GitStatus.GetStatusAsync (repo.Info.WorkingDirectory, new [] { directory.ToString() } , version: StatusVersion.v1); + foreach (var file in result) { + AddStatus (repo, rev, file.Key, versions, file.Value.StageState, recursive ? null : directory); } } @@ -1585,69 +1599,34 @@ namespace MonoDevelop.VersionControl.Git protected override async Task OnCheckoutAsync (FilePath targetLocalPath, Revision rev, bool recurse, ProgressMonitor monitor) { - int transferProgress = 0; - int checkoutProgress = 0; - try { - monitor.BeginTask (GettextCatalog.GetString ("Cloning…"), 2); - bool skipSubmodules = false; - var innerTask = await RunOperationAsync (() => RetryUntilSuccessAsync (monitor, credType => { - var options = new CloneOptions { - CredentialsProvider = (url, userFromUrl, types) => { - transferProgress = checkoutProgress = 0; - return GitCredentials.TryGet (url, userFromUrl, types, credType); - }, - RepositoryOperationStarting = ctx => { - Runtime.RunInMainThread (() => { - monitor.Log.WriteLine (GettextCatalog.GetString ("Checking out repository at '{0}'"), ctx.RepositoryPath); - }); - return true; - }, - OnTransferProgress = (tp) => OnTransferProgress (tp, monitor, ref transferProgress), - OnCheckoutProgress = (path, completedSteps, totalSteps) => { - OnCheckoutProgress (completedSteps, totalSteps, monitor, ref checkoutProgress); - Runtime.RunInMainThread (() => { - monitor.Log.WriteLine (GettextCatalog.GetString ("Checking out file '{0}'"), path); - }); - } - }; + monitor.BeginTask ("Cloning…", 100); + if (!Directory.Exists (targetLocalPath)) + Directory.CreateDirectory (targetLocalPath); + var options = new GitCloneOptions { RecurseSubmodules = recurse }; + var result = await GitClone.CloneAsync (Url, targetLocalPath, CreateCallbacks (monitor), options, monitor.CancellationToken); + if (result.Properties.TryGetValue (GitCloneOptions.CloneTargetProperty, out var rootPath)) { + RootPath = targetLocalPath.Combine (rootPath).CanonicalPath.ParentDirectory; + } else { + return; + } - try { - RootPath = LibGit2Sharp.Repository.Clone (Url, targetLocalPath, options); - } catch (UserCancelledException) { - return Task.CompletedTask; - } - var updateOptions = new SubmoduleUpdateOptions { - Init = true, - CredentialsProvider = options.CredentialsProvider, - OnTransferProgress = options.OnTransferProgress, - OnCheckoutProgress = options.OnCheckoutProgress, - }; - monitor.Step (1); - try { - if (!skipSubmodules) - RecursivelyCloneSubmodules (RootPath, updateOptions, monitor); - } catch (Exception e) { - LoggingService.LogError ("Cloning submodules failed", e); - FileService.DeleteDirectory (RootPath); - skipSubmodules = true; - return Task.FromException (e); - } - return Task.CompletedTask; - }), monitor.CancellationToken).ConfigureAwait (false); + if (monitor.CancellationToken.IsCancellationRequested) + return; await innerTask.ConfigureAwait (false); if (monitor.CancellationToken.IsCancellationRequested || RootPath.IsNull) return; - RootPath = RootPath.CanonicalPath.ParentDirectory; - - RootRepository = await CreateSafeRepositoryAsync (RootPath).ConfigureAwait (false); + RootRepository = new LibGit2Sharp.Repository (RootPath); InitFileWatcher (); - if (skipSubmodules) { - MessageService.ShowError (GettextCatalog.GetString("Cloning submodules failed"), GettextCatalog.GetString ("Please use the command line client to init the submodules manually.")); - } + } catch (OperationCanceledException) { + return; + } catch (GitExternalException e) { + LoggingService.LogInternalError ("Error while running git", e); + MessageService.ShowError (GettextCatalog.GetString ("Error while running git"), e.Message); + return; } catch (Exception e) { LoggingService.LogInternalError ("Error while cloning repository " + rev + " recuse: " + recurse, e); throw e; @@ -1656,39 +1635,57 @@ namespace MonoDevelop.VersionControl.Git } } - static void RecursivelyCloneSubmodules (string repoPath, SubmoduleUpdateOptions updateOptions, ProgressMonitor monitor) + GitCallbackHandler CreateCallbacks (ProgressMonitor monitor) { - var submodules = new List<string> (); - using (var repo = new LibGit2Sharp.Repository (repoPath)) { - // Iterate through the submodules (where the submodule is in the index), - // and clone them. - var submoduleArray = repo.Submodules.Where (sm => sm.RetrieveStatus ().HasFlag (SubmoduleStatus.InIndex)).ToArray (); - monitor.BeginTask (GettextCatalog.GetString ("Cloning submodules…"), submoduleArray.Length); - try { - foreach (var sm in submoduleArray) { - if (monitor.CancellationToken.IsCancellationRequested) { - throw new UserCancelledException ("Recursive clone of submodules was cancelled."); - } + var callbacks = new GitCallbackHandler { + GetCredentialsHandler = (url) => { + var cred = GitCredentials.TryGet (url, Environment.UserName, SupportedCredentialTypes.UsernamePassword, GitCredentialsType.Normal) as UsernamePasswordCredentials; + if (cred == null) + throw new InvalidOperationException ("Can't get username/password credentials."); + return new ClientLibrary.GitCredentials (cred.Username, cred.Password); + }, - Runtime.RunInMainThread (() => { - monitor.Log.WriteLine (GettextCatalog.GetString ("Checking out submodule at '{0}'…", sm.Path)); - monitor.Step (1); - }); - repo.Submodules.Update (sm.Name, updateOptions); + GetSSHPassword = (userName) => { + var cred = GitCredentials.TryGet (Url, userName, SupportedCredentialTypes.Ssh, GitCredentialsType.Normal) as SshUserKeyCredentials; + if (cred == null) + throw new InvalidOperationException ("Can't ssh password."); + return cred.Passphrase; + }, - submodules.Add (Path.Combine (repo.Info.WorkingDirectory, sm.Path)); - } - } finally { - monitor.EndTask (); + GetSSHPassphrase = (key) => { + var cred = GitCredentials.TryGet (Url, key, SupportedCredentialTypes.Ssh, GitCredentialsType.Normal) as SshUserKeyCredentials; + if (cred == null) + throw new InvalidOperationException ("Can't get ssh passphrase."); + return cred.Passphrase; + }, + + GetContinueConnecting = () => true + }; + + callbacks.Output += (sender, e) => { + Runtime.RunInMainThread (delegate { + monitor.Log.WriteLine (e); + }); + }; + string curTask = null; + int oldPercentage = 0; + callbacks.Progress += (sender, e) => { + if (curTask != e.Operation) { + if (curTask != null) + monitor.EndTask (); + monitor.BeginTask (e.Operation + "…", 100); + oldPercentage = 0; + curTask = e.Operation; } - } + monitor.Step (e.Percentage - oldPercentage); + oldPercentage = e.Percentage; - // If we are continuing the recursive operation, then - // recurse into nested submodules. - // Check submodules to see if they have their own submodules. - foreach (string path in submodules) { - RecursivelyCloneSubmodules (path, updateOptions, monitor); - } + if (e.Percentage == 100) { + monitor.EndTask (); + curTask = null; + } + }; + return callbacks; } protected override async Task OnRevertAsync (FilePath [] localPaths, bool recurse, ProgressMonitor monitor) diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs index afbafe0e84..3b0ab3acc7 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs @@ -990,6 +990,8 @@ namespace MonoDevelop.VersionControl.Views void OnFileStatusChanged (object s, FileUpdateEventArgs args) { + VersionControlService.FileStatusChanged -= OnFileStatusChanged; + try { if (args.Any (f => f.FilePath == filepath || (filepath != null && !f.FilePath.IsNullOrEmpty && f.FilePath.IsChildPathOf (filepath) && f.IsDirectory))) { StartUpdate (); @@ -1000,6 +1002,7 @@ namespace MonoDevelop.VersionControl.Views break; } UpdateControlStatus (); + VersionControlService.FileStatusChanged += OnFileStatusChanged; } catch (Exception e) { LoggingService.LogInternalError (e); } diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl/CheckoutCommand.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl/CheckoutCommand.cs index 065c117d4b..79f67f5242 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl/CheckoutCommand.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl/CheckoutCommand.cs @@ -51,7 +51,9 @@ namespace MonoDevelop.VersionControl {
return new MonoDevelop.Core.ProgressMonitoring.AggregatedProgressMonitor (
base.CreateProgressMonitor (),
- new MonoDevelop.Ide.ProgressMonitoring.MessageDialogProgressMonitor (true, true, true, true)
+ new MonoDevelop.Ide.ProgressMonitoring.MessageDialogProgressMonitor (true, true, true, true) { + Style = Ide.ProgressMonitoring.MessageDialogProgressStyle.ShowCurrentTaskPercentage + }
);
} |