diff --git a/src/GitTools.Core.Tests/Extensions/StringExtensionsFacts.cs b/src/GitTools.Core.Tests/Extensions/StringExtensionsFacts.cs new file mode 100644 index 0000000..1c0a435 --- /dev/null +++ b/src/GitTools.Core.Tests/Extensions/StringExtensionsFacts.cs @@ -0,0 +1,21 @@ +namespace GitTools.Tests +{ + using NUnit.Framework; + using Shouldly; + + [TestFixture] + public class StringExtensionsFacts + { + [TestCase("/develop", false)] + [TestCase("/master", false)] + [TestCase("/pr/25", true)] + [TestCase("/pull/25", true)] + [TestCase("/pull-requests/25", true)] + public void TheIsPullRequestMethod(string input, bool expectedValue) + { + var actualValue = input.IsPullRequest(); + + actualValue.ShouldBe(expectedValue); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core.Tests/GitTools.Core.Tests.csproj b/src/GitTools.Core.Tests/GitTools.Core.Tests.csproj index d611549..cec0fe1 100644 --- a/src/GitTools.Core.Tests/GitTools.Core.Tests.csproj +++ b/src/GitTools.Core.Tests/GitTools.Core.Tests.csproj @@ -39,6 +39,10 @@ ..\..\lib\NUnit.2.6.4\lib\nunit.framework.dll True + + ..\..\lib\Shouldly.2.5.0\lib\net40\Shouldly.dll + True + @@ -54,6 +58,7 @@ + @@ -70,6 +75,7 @@ + diff --git a/src/GitTools.Core.Tests/packages.config b/src/GitTools.Core.Tests/packages.config index 9c4a0fa..475a53d 100644 --- a/src/GitTools.Core.Tests/packages.config +++ b/src/GitTools.Core.Tests/packages.config @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/src/GitTools.Core/Constants.cs b/src/GitTools.Core/Constants.cs index 85791c3..39613c3 100644 --- a/src/GitTools.Core/Constants.cs +++ b/src/GitTools.Core/Constants.cs @@ -1,4 +1,4 @@ namespace GitTools { - // TODO: Constants go here + // TODO: constants classes go here } \ No newline at end of file diff --git a/src/GitTools.Core/Extensions/LibGitExtensions.cs b/src/GitTools.Core/Extensions/LibGitExtensions.cs index f94b4aa..ae2e1a3 100644 --- a/src/GitTools.Core/Extensions/LibGitExtensions.cs +++ b/src/GitTools.Core/Extensions/LibGitExtensions.cs @@ -2,16 +2,91 @@ { using System; using System.Collections.Generic; + using System.IO; using System.Linq; using LibGit2Sharp; + using Logging; public static class LibGitExtensions { + private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); + + public static DateTimeOffset When(this Commit commit) + { + return commit.Committer.When; + } + + public static Branch FindBranch(this IRepository repository, string branchName) + { + var exact = repository.Branches.FirstOrDefault(x => x.Name == branchName); + if (exact != null) + { + return exact; + } + + return repository.Branches.FirstOrDefault(x => x.Name == "origin/" + branchName); + } + public static bool IsDetachedHead(this Branch branch) { return branch.CanonicalName.Equals("(no branch)", StringComparison.OrdinalIgnoreCase); } + public static string GetRepositoryDirectory(this IRepository repository, bool omitGitPostFix = true) + { + var gitDirectory = repository.Info.Path; + + gitDirectory = gitDirectory.TrimEnd('\\'); + + if (omitGitPostFix && gitDirectory.EndsWith(".git")) + { + gitDirectory = gitDirectory.Substring(0, gitDirectory.Length - ".git".Length); + gitDirectory = gitDirectory.TrimEnd('\\'); + } + + return gitDirectory; + } + + public static void CheckoutFilesIfExist(this IRepository repository, params string[] fileNames) + { + if (fileNames == null || fileNames.Length == 0) + { + return; + } + + Log.Info("Checking out files that might be needed later in dynamic repository"); + + foreach (var fileName in fileNames) + { + try + { + Log.Info(" Trying to check out '{0}'", fileName); + + var headBranch = repository.Head; + var tip = headBranch.Tip; + + var treeEntry = tip[fileName]; + if (treeEntry == null) + { + continue; + } + + var fullPath = Path.Combine(repository.GetRepositoryDirectory(), fileName); + using (var stream = ((Blob) treeEntry.Target).GetContentStream()) + { + using (var streamReader = new BinaryReader(stream)) + { + File.WriteAllBytes(fullPath, streamReader.ReadBytes((int)stream.Length)); + } + } + } + catch (Exception ex) + { + Log.Warning(" An error occurred while checking out '{0}': '{1}'", fileName, ex.Message); + } + } + } + public static IEnumerable GetBranchesContainingCommit(this IRepository repository, string commitSha) { var directBranchHasBeenFound = false; diff --git a/src/GitTools.Core/Extensions/StringBuilderExtensions.cs b/src/GitTools.Core/Extensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..77b5d65 --- /dev/null +++ b/src/GitTools.Core/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,15 @@ +namespace GitTools +{ + using System.Text; + using JetBrains.Annotations; + + public static class StringBuilderExtensions + { + [StringFormatMethod("format")] + public static void AppendLineFormat(this StringBuilder stringBuilder, string format, params object[] args) + { + stringBuilder.AppendFormat(format, args); + stringBuilder.AppendLine(); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/FodyWeavers.xml b/src/GitTools.Core/FodyWeavers.xml index 39bf8d6..06a11b4 100644 --- a/src/GitTools.Core/FodyWeavers.xml +++ b/src/GitTools.Core/FodyWeavers.xml @@ -5,6 +5,7 @@ LibGit2Sharp - - + + + \ No newline at end of file diff --git a/src/GitTools.Core/GitTools.Core.csproj b/src/GitTools.Core/GitTools.Core.csproj index 95f4c28..47fa5d8 100644 --- a/src/GitTools.Core/GitTools.Core.csproj +++ b/src/GitTools.Core/GitTools.Core.csproj @@ -38,6 +38,10 @@ ..\..\output\release\GitTools.Core\GitTools.Core.XML + + ..\..\lib\JetBrainsAnnotations.Fody.1.0.2\Lib\JetBrains.Annotations.dll + False + ..\..\lib\LibGit2Sharp.0.21.0.176\lib\net40\LibGit2Sharp.dll True @@ -60,6 +64,7 @@ + @@ -68,6 +73,7 @@ + @@ -75,10 +81,15 @@ + + + + + diff --git a/src/GitTools.Core/Helpers/ProcessHelper.cs b/src/GitTools.Core/Helpers/ProcessHelper.cs new file mode 100644 index 0000000..ebe2e94 --- /dev/null +++ b/src/GitTools.Core/Helpers/ProcessHelper.cs @@ -0,0 +1,144 @@ +namespace GitTools +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Runtime.InteropServices; + using System.Threading; + + public static class ProcessHelper + { + private static volatile object _lockObject = new object(); + + // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/f6069441-4ab1-4299-ad6a-b8bb9ed36be3 + public static Process Start(ProcessStartInfo startInfo) + { + Process process; + + lock (_lockObject) + { + using (new ChangeErrorMode(ErrorModes.FailCriticalErrors | ErrorModes.NoGpFaultErrorBox)) + { + process = Process.Start(startInfo); + process.PriorityClass = ProcessPriorityClass.Idle; + } + } + + return process; + } + + // http://csharptest.net/532/using-processstart-to-capture-console-output/ + public static int Run(Action output, Action errorOutput, TextReader input, string exe, string args, string workingDirectory, params KeyValuePair[] environmentalVariables) + { + if (String.IsNullOrEmpty(exe)) + { + throw new FileNotFoundException(); + } + + if (output == null) + { + throw new ArgumentNullException("output"); + } + + var psi = new ProcessStartInfo + { + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + ErrorDialog = false, + WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, + FileName = exe, + Arguments = args + }; + + foreach (var environmentalVariable in environmentalVariables) + { + if (!psi.EnvironmentVariables.ContainsKey(environmentalVariable.Key) && environmentalVariable.Value != null) + psi.EnvironmentVariables.Add(environmentalVariable.Key, environmentalVariable.Value); + if (psi.EnvironmentVariables.ContainsKey(environmentalVariable.Key) && environmentalVariable.Value == null) + psi.EnvironmentVariables.Remove(environmentalVariable.Key); + } + + using (var process = Process.Start(psi)) + { + using (var mreOut = new ManualResetEvent(false)) + { + using (var mreErr = new ManualResetEvent(false)) + { + process.EnableRaisingEvents = true; + process.OutputDataReceived += (o, e) => + { + // ReSharper disable once AccessToDisposedClosure + if (e.Data == null) + { + mreOut.Set(); + } + else + { + output(e.Data); + } + }; + process.BeginOutputReadLine(); + process.ErrorDataReceived += (o, e) => + { + // ReSharper disable once AccessToDisposedClosure + if (e.Data == null) + { + mreErr.Set(); + } + else + { + errorOutput(e.Data); + } + }; + + process.BeginErrorReadLine(); + + string line; + while (input != null && null != (line = input.ReadLine())) + { + process.StandardInput.WriteLine(line); + } + + process.StandardInput.Close(); + process.WaitForExit(); + + mreOut.WaitOne(); + mreErr.WaitOne(); + + return process.ExitCode; + } + } + } + } + + [Flags] + public enum ErrorModes + { + Default = 0x0, + FailCriticalErrors = 0x1, + NoGpFaultErrorBox = 0x2, + NoAlignmentFaultExcept = 0x4, + NoOpenFileErrorBox = 0x8000 + } + + public struct ChangeErrorMode : IDisposable + { + private readonly int _oldMode; + + public ChangeErrorMode(ErrorModes mode) + { + _oldMode = SetErrorMode((int)mode); + } + + void IDisposable.Dispose() { SetErrorMode(_oldMode); } + + [DllImport("kernel32.dll")] + static extern int SetErrorMode(int newMode); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/Testing/Extensions/GitTestExtensions.cs b/src/GitTools.Core/Testing/Extensions/GitTestExtensions.cs new file mode 100644 index 0000000..905fa34 --- /dev/null +++ b/src/GitTools.Core/Testing/Extensions/GitTestExtensions.cs @@ -0,0 +1,103 @@ +namespace GitTools.Testing +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Text; + using LibGit2Sharp; + using Logging; + + public static class GitTestExtensions + { + private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); + + private static int pad = 1; + + public static void DumpGraph(this IRepository repository) + { + var output = new StringBuilder(); + + ProcessHelper.Run( + o => output.AppendLine(o), + e => output.AppendLineFormat("ERROR: {0}", e), + null, + "git", + @"log --graph --abbrev-commit --decorate --date=relative --all", + repository.Info.Path); + + Log.Info(output.ToString); + } + + public static Commit MakeACommit(this IRepository repository) + { + return CreateFileAndCommit(repository, Guid.NewGuid().ToString()); + } + + public static void MergeNoFF(this IRepository repository, string branch) + { + MergeNoFF(repository, branch, TestValues.SignatureNow()); + } + + public static void MergeNoFF(this IRepository repository, string branch, Signature sig) + { + // Fixes a race condition + repository.Merge(repository.FindBranch(branch), sig, new MergeOptions + { + FastForwardStrategy = FastForwardStrategy.NoFastFoward + }); + } + + public static Commit[] MakeCommits(this IRepository repository, int numCommitsToMake) + { + return Enumerable.Range(1, numCommitsToMake) + .Select(x => repository.MakeACommit()) + .ToArray(); + } + + public static Commit CreateFileAndCommit(this IRepository repository, string relativeFileName) + { + var randomFile = Path.Combine(repository.Info.WorkingDirectory, relativeFileName); + if (File.Exists(randomFile)) + { + File.Delete(randomFile); + } + + var totalWidth = 36 + (pad++ % 10); + var contents = Guid.NewGuid().ToString().PadRight(totalWidth, '.'); + File.WriteAllText(randomFile, contents); + + repository.Stage(randomFile); + + return repository.Commit(string.Format("Test Commit for file '{0}'", relativeFileName), + TestValues.SignatureNow(), TestValues.SignatureNow()); + } + + public static Tag MakeATaggedCommit(this IRepository repository, string tag) + { + var commit = repository.MakeACommit(); + var existingTag = repository.Tags.SingleOrDefault(t => t.Name == tag); + if (existingTag != null) + return existingTag; + return repository.Tags.Add(tag, commit); + } + + public static Branch CreatePullRequest(this IRepository repository, string from, string to, int prNumber = 2, bool isRemotePr = true) + { + repository.Checkout(to); + repository.MergeNoFF(from); + var branch = repository.CreateBranch("pull/" + prNumber + "/merge"); + repository.Checkout(branch.Name); + repository.Checkout(to); + repository.Reset(ResetMode.Hard, "HEAD~1"); + var pullBranch = repository.Checkout("pull/" + prNumber + "/merge"); + if (isRemotePr) + { + // If we delete the branch, it is effectively the same as remote PR + repository.Branches.Remove(from); + } + + return pullBranch; + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/Testing/Fixtures/EmptyRepositoryFixture.cs b/src/GitTools.Core/Testing/Fixtures/EmptyRepositoryFixture.cs new file mode 100644 index 0000000..2c9f6a2 --- /dev/null +++ b/src/GitTools.Core/Testing/Fixtures/EmptyRepositoryFixture.cs @@ -0,0 +1,26 @@ +namespace GitTools.Testing +{ + using System; + using LibGit2Sharp; + + public class EmptyRepositoryFixture : RepositoryFixtureBase + { + public EmptyRepositoryFixture() : + base(CreateNewRepository) + { + } + + public void DumpGraph() + { + Repository.DumpGraph(); + } + + private static IRepository CreateNewRepository(string path) + { + LibGit2Sharp.Repository.Init(path); + Console.WriteLine("Created git repository at '{0}'", path); + + return new Repository(path); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/Testing/Fixtures/RemoteRepositoryFixture.cs b/src/GitTools.Core/Testing/Fixtures/RemoteRepositoryFixture.cs new file mode 100644 index 0000000..2f6e607 --- /dev/null +++ b/src/GitTools.Core/Testing/Fixtures/RemoteRepositoryFixture.cs @@ -0,0 +1,41 @@ +namespace GitTools.Testing +{ + using System; + using LibGit2Sharp; + + public class RemoteRepositoryFixture : RepositoryFixtureBase + { + public IRepository LocalRepository; + public string LocalRepositoryPath; + + public RemoteRepositoryFixture() + : base(CreateNewRepository) + { + CloneRepository(); + } + + private static IRepository CreateNewRepository(string path) + { + LibGit2Sharp.Repository.Init(path); + Console.WriteLine("Created git repository at '{0}'", path); + + var repo = new Repository(path); + repo.MakeCommits(5); + return repo; + } + + private void CloneRepository() + { + LocalRepositoryPath = TemporaryFilesContext.GetDirectory("/"); + LibGit2Sharp.Repository.Clone(RepositoryPath, LocalRepositoryPath); + LocalRepository = new Repository(LocalRepositoryPath); + } + + public override void Dispose() + { + LocalRepository.Dispose(); + + base.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/Testing/Fixtures/RepositoryFixtureBase.cs b/src/GitTools.Core/Testing/Fixtures/RepositoryFixtureBase.cs new file mode 100644 index 0000000..40fc859 --- /dev/null +++ b/src/GitTools.Core/Testing/Fixtures/RepositoryFixtureBase.cs @@ -0,0 +1,41 @@ +namespace GitTools.Testing +{ + using System; + using LibGit2Sharp; + using Logging; + + public abstract class RepositoryFixtureBase : IDisposable + { + private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); + + public readonly IRepository Repository; + public readonly string RepositoryPath; + + protected RepositoryFixtureBase(Func repoBuilder) + { + TemporaryFilesContext = new TemporaryFilesContext(); + + RepositoryPath = TemporaryFilesContext.GetDirectory("/"); + Repository = repoBuilder(RepositoryPath); + Repository.Config.Set("user.name", "Test"); + Repository.Config.Set("user.email", "test@email.com"); + IsForTrackedBranchOnly = true; + } + + protected TemporaryFilesContext TemporaryFilesContext { get; private set; } + + public bool IsForTrackedBranchOnly { private get; set; } + + public virtual void Dispose() + { + var temporaryFilesContext = TemporaryFilesContext; + if (temporaryFilesContext != null) + { + temporaryFilesContext.Dispose(); + TemporaryFilesContext = null; + } + + Repository.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/Testing/TestValues.cs b/src/GitTools.Core/Testing/TestValues.cs new file mode 100644 index 0000000..18c31bd --- /dev/null +++ b/src/GitTools.Core/Testing/TestValues.cs @@ -0,0 +1,30 @@ +namespace GitTools.Testing +{ + using System; + using LibGit2Sharp; + + public static class TestValues + { + private static DateTimeOffset _simulatedTime = DateTimeOffset.Now.AddHours(-1); + + public static DateTimeOffset Now + { + get + { + _simulatedTime = _simulatedTime.AddMinutes(1); + return _simulatedTime; + } + } + + public static Signature SignatureNow() + { + var dateTimeOffset = Now; + return Signature(dateTimeOffset); + } + + public static Signature Signature(DateTimeOffset dateTimeOffset) + { + return new Signature("A. U. Thor", "thor@valhalla.asgard.com", dateTimeOffset); + } + } +} \ No newline at end of file diff --git a/src/GitTools.Core/packages.config b/src/GitTools.Core/packages.config index e0d15ca..90dcc74 100644 --- a/src/GitTools.Core/packages.config +++ b/src/GitTools.Core/packages.config @@ -2,6 +2,7 @@ +