diff --git a/src/Cake.Common.Tests/Cake.Common.Tests.csproj b/src/Cake.Common.Tests/Cake.Common.Tests.csproj
index 6454989d79..083370e3ab 100644
--- a/src/Cake.Common.Tests/Cake.Common.Tests.csproj
+++ b/src/Cake.Common.Tests/Cake.Common.Tests.csproj
@@ -94,6 +94,7 @@
+
@@ -196,6 +197,7 @@
+
diff --git a/src/Cake.Common.Tests/Fixtures/Tools/GitLinkFixture.cs b/src/Cake.Common.Tests/Fixtures/Tools/GitLinkFixture.cs
new file mode 100644
index 0000000000..d2ff6c96db
--- /dev/null
+++ b/src/Cake.Common.Tests/Fixtures/Tools/GitLinkFixture.cs
@@ -0,0 +1,29 @@
+using Cake.Core.Diagnostics;
+using Cake.Core.IO;
+using Cake.Testing.Fixtures;
+using NSubstitute;
+
+namespace Cake.Common.Tests.Fixtures.Tools
+{
+ using Cake.Common.Tools.GitLink;
+ internal sealed class GitLinkFixture : ToolFixture
+ {
+ private readonly ICakeLog Log;
+
+ public DirectoryPath SolutionPath { get; set; }
+
+ public GitLinkFixture()
+ : base("gitlink.exe")
+ {
+ SolutionPath = new DirectoryPath("c:/temp");
+
+ Log = Substitute.For();
+ }
+
+ protected override void RunTool()
+ {
+ var tool = new GitLinkRunner(FileSystem, Environment, ProcessRunner, Globber, Log);
+ tool.Run(SolutionPath, Settings);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Common.Tests/Unit/Tools/GitLink/GitlinkRunnerTests.cs b/src/Cake.Common.Tests/Unit/Tools/GitLink/GitlinkRunnerTests.cs
new file mode 100644
index 0000000000..acda0adb3c
--- /dev/null
+++ b/src/Cake.Common.Tests/Unit/Tools/GitLink/GitlinkRunnerTests.cs
@@ -0,0 +1,241 @@
+using Cake.Common.Tests.Fixtures.Tools;
+using Cake.Core;
+using Cake.Core.IO;
+using Cake.Testing;
+using Xunit;
+
+namespace Cake.Common.Tests.Unit.Tools.GitLink
+{
+ using System;
+
+ public sealed class GitlinkRunnerTests
+ {
+ public sealed class TheRunMethod
+ {
+ [Fact]
+ public void Should_Throw_If_SolutionPath_Is_Null()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.SolutionPath = null;
+
+ // When
+ var result = Record.Exception(() => fixture.Run());
+
+ // Then
+ Assert.IsArgumentNullException(result, "solutionPath");
+ }
+
+ [Fact]
+ public void Should_Find_GitLink_Runner()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+
+ // When
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("/Working/tools/gitlink.exe", result.Path.FullPath);
+ }
+
+ [Fact]
+ public void Should_Throw_If_Process_Was_Not_Started()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.GivenProcessCannotStart();
+
+ // When
+ var result = Record.Exception(() => fixture.Run());
+
+ // Then
+ Assert.IsType(result);
+ Assert.Equal("GitLink: Process was not started.", result.Message);
+ }
+
+ [Fact]
+ public void Should_Throw_If_Has_A_Non_Zero_Exit_Code()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.GivenProcessExitsWithCode(1);
+
+ // When
+ var result = Record.Exception(() => fixture.Run());
+
+ // Then
+ Assert.IsType(result);
+ Assert.Equal("GitLink: Process returned an error.", result.Message);
+ }
+
+ [Fact]
+ public void Should_Use_Provided_SolutionPath_In_Process_Arguments()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.SolutionPath = "source";
+
+ // When
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"/Working/source\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_RepositoryUrl()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.RepositoryUrl = new Uri("http://mydomain.com");
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -u \"http://mydomain.com/\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_SolutionFileName()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.SolutionFileName = "solution.sln";
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -f \"solution.sln\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_Configuration()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.Configuration = "Release";
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -c \"Release\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_Platform()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.Platform = "AnyCPU";
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -p \"AnyCPU\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_Branch()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.Branch = "master";
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -b \"master\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_LogFilePath()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.LogFilePath = @"/temp/log.txt";
+
+ // When
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -l \"/temp/log.txt\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_ShaHash()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.ShaHash = "abcdef";
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -s \"abcdef\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_PdbDirectory()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.PdbDirectoryPath = DirectoryPath.FromString("pdb/");
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -d \"/Working/pdb\"", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_PowerShell_Switch()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.UsePowerShellCommand = true;
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -powershell", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_SkipVerify_Switch()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.SkipVerify = true;
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -skipverify", result.Args);
+ }
+
+ [Fact]
+ public void Should_Set_Debug_Switch()
+ {
+ // Given
+ var fixture = new GitLinkFixture();
+ fixture.Settings.Debug = true;
+
+ // Then
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("\"c:/temp\" -debug", result.Args);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Common/Cake.Common.csproj b/src/Cake.Common/Cake.Common.csproj
index 447aaaf4b6..eaf8f6f1eb 100644
--- a/src/Cake.Common/Cake.Common.csproj
+++ b/src/Cake.Common/Cake.Common.csproj
@@ -163,6 +163,9 @@
+
+
+
@@ -345,4 +348,4 @@
-->
-
+
\ No newline at end of file
diff --git a/src/Cake.Common/Tools/GitLink/GitLinkAliases.cs b/src/Cake.Common/Tools/GitLink/GitLinkAliases.cs
new file mode 100644
index 0000000000..c26d1f62c4
--- /dev/null
+++ b/src/Cake.Common/Tools/GitLink/GitLinkAliases.cs
@@ -0,0 +1,61 @@
+using System;
+using Cake.Core;
+using Cake.Core.Annotations;
+using Cake.Core.IO;
+
+namespace Cake.Common.Tools.GitLink
+{
+ ///
+ /// Contains functionality for working with GitLink.
+ ///
+ [CakeAliasCategory("GitTools")]
+ public static class GitLinkAliases
+ {
+ ///
+ /// Update pdb files to link all sources.
+ /// This will allow anyone to step through the source code while debugging without a symbol source server.
+ ///
+ /// The context.
+ /// The Solution File to analyze.
+ ///
+ ///
+ /// GitLink("C:/temp/solution");
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory("GitLink")]
+ public static void GitLink(this ICakeContext context, DirectoryPath solutionPath)
+ {
+ GitLink(context, solutionPath, new GitLinkSettings());
+ }
+
+ ///
+ /// Update pdb files to link all sources, using specified settings.
+ /// This will allow anyone to step through the source code while debugging without a symbol source server.
+ ///
+ /// The context.
+ /// The Solution File to analyze.
+ /// The settings.
+ ///
+ ///
+ /// GitLink("C:/temp/solution", new GitLinkSettings {
+ /// RepositoryUrl = new Uri("http://mydomain.com"),
+ /// Branch = "master",
+ /// ShaHash = "abcdef",
+ /// });
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory("GitLink")]
+ public static void GitLink(this ICakeContext context, DirectoryPath solutionPath, GitLinkSettings settings)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ var runner = new GitLinkRunner(context.FileSystem, context.Environment, context.ProcessRunner, context.Globber, context.Log);
+ runner.Run(solutionPath, settings);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Common/Tools/GitLink/GitLinkRunner.cs b/src/Cake.Common/Tools/GitLink/GitLinkRunner.cs
new file mode 100644
index 0000000000..9d2fd74a0d
--- /dev/null
+++ b/src/Cake.Common/Tools/GitLink/GitLinkRunner.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using Cake.Core;
+using Cake.Core.Diagnostics;
+using Cake.Core.IO;
+using Cake.Core.Tooling;
+
+namespace Cake.Common.Tools.GitLink
+{
+ ///
+ /// GitLink runner
+ ///
+ public sealed class GitLinkRunner : Tool
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ICakeEnvironment _environment;
+ private readonly ICakeLog _log;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The environment.
+ /// The process runner.
+ /// The globber.
+ /// The logger.
+ public GitLinkRunner(IFileSystem fileSystem, ICakeEnvironment environment, IProcessRunner processRunner, IGlobber globber, ICakeLog log)
+ : base(fileSystem, environment, processRunner, globber)
+ {
+ _fileSystem = fileSystem;
+ _environment = environment;
+ _log = log;
+ }
+
+ ///
+ /// Update pdb files to link all sources.
+ ///
+ /// The directory containing the solution with the pdb files.
+ /// The settings.
+ public void Run(DirectoryPath solutionPath, GitLinkSettings settings)
+ {
+ if (settings == null)
+ {
+ throw new ArgumentNullException("settings");
+ }
+
+ if (solutionPath == null)
+ {
+ throw new ArgumentNullException("solutionPath");
+ }
+
+ Run(settings, GetArguments(solutionPath, settings));
+ }
+
+ private ProcessArgumentBuilder GetArguments(DirectoryPath solutionPath, GitLinkSettings settings)
+ {
+ var builder = new ProcessArgumentBuilder();
+
+ builder.AppendQuoted(solutionPath.MakeAbsolute(_environment).FullPath);
+
+ if (settings.RepositoryUrl != null)
+ {
+ builder.Append("-u");
+ builder.AppendQuoted(settings.RepositoryUrl.ToString());
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.SolutionFileName))
+ {
+ builder.Append("-f");
+ builder.AppendQuoted(settings.SolutionFileName);
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.Configuration))
+ {
+ builder.Append("-c");
+ builder.AppendQuoted(settings.Configuration);
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.Platform))
+ {
+ builder.Append("-p");
+ builder.AppendQuoted(settings.Platform);
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.Branch))
+ {
+ builder.Append("-b");
+ builder.AppendQuoted(settings.Branch);
+ }
+
+ if (settings.LogFilePath != null)
+ {
+ builder.Append("-l");
+ builder.AppendQuoted(settings.LogFilePath.MakeAbsolute(_environment).FullPath);
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.ShaHash))
+ {
+ builder.Append("-s");
+ builder.AppendQuoted(settings.ShaHash);
+ }
+
+ if (settings.PdbDirectoryPath != null)
+ {
+ builder.Append("-d");
+ builder.AppendQuoted(settings.PdbDirectoryPath.MakeAbsolute(_environment).FullPath);
+ }
+
+ if (settings.UsePowerShellCommand)
+ {
+ builder.Append("-powershell");
+ }
+
+ if (settings.ErrorsAsWarnings)
+ {
+ builder.Append("-errorsaswarnings");
+ }
+
+ if (settings.SkipVerify)
+ {
+ builder.Append("-skipverify");
+ }
+
+ if (settings.Debug)
+ {
+ builder.Append("-debug");
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Gets the name of the tool.
+ ///
+ /// The name of the tool.
+ protected override string GetToolName()
+ {
+ return "GitLink";
+ }
+
+ ///
+ /// Gets the possible names of the tool executable.
+ ///
+ /// The tool executable name.
+ protected override IEnumerable GetToolExecutableNames()
+ {
+ return new[] { "gitlink.exe" };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Common/Tools/GitLink/GitLinkSettings.cs b/src/Cake.Common/Tools/GitLink/GitLinkSettings.cs
new file mode 100644
index 0000000000..e564389a24
--- /dev/null
+++ b/src/Cake.Common/Tools/GitLink/GitLinkSettings.cs
@@ -0,0 +1,74 @@
+using System;
+using Cake.Core.IO;
+using Cake.Core.Tooling;
+
+namespace Cake.Common.Tools.GitLink
+{
+ ///
+ /// Contains settings used by .
+ ///
+ public sealed class GitLinkSettings : ToolSettings
+ {
+ ///
+ /// Gets or sets the Url to remote git repository.
+ ///
+ public Uri RepositoryUrl { get; set; }
+
+ ///
+ /// Gets or sets the Solution file name.
+ ///
+ public string SolutionFileName { get; set; }
+
+ ///
+ /// Gets or sets the name of the configuration.
+ ///
+ /// Default is Release
+ public string Configuration { get; set; }
+
+ ///
+ /// Gets or sets the name of the platform.
+ ///
+ /// Default is AnyCPU
+ public string Platform { get; set; }
+
+ ///
+ /// Gets or sets the name of the branch to use on the remote repository.
+ ///
+ public string Branch { get; set; }
+
+ ///
+ /// Gets or sets the path to the GitLink log file.
+ ///
+ public FilePath LogFilePath { get; set; }
+
+ ///
+ /// Gets or sets the SHA-1 hash of the git commit to be used.
+ ///
+ public string ShaHash { get; set; }
+
+ ///
+ /// Gets or sets the directory where the PDB files are located.
+ ///
+ public DirectoryPath PdbDirectoryPath { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the Use PowerShell Command option should be enabled.
+ ///
+ public bool UsePowerShellCommand { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the ErrorsAsWarnings option should be enabled.
+ ///
+ public bool ErrorsAsWarnings { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the Skip Verify option should be enabled.
+ ///
+ public bool SkipVerify { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the debug output should be enabled.
+ ///
+ public bool Debug { get; set; }
+ }
+}
\ No newline at end of file