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 @@
+