Files
git-tfs/GitTfs/Core/GitHelpers.cs
T
2014-03-16 15:34:59 +01:00

294 lines
10 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using StructureMap;
using System.Text;
namespace Sep.Git.Tfs.Core
{
public class GitHelpers : IGitHelpers
{
protected readonly TextWriter realStdout;
private readonly IContainer _container;
/// <summary>
/// Starting with version 1.7.10, Git uses UTF-8.
/// Use this encoding for Git input and output.
/// </summary>
private static Encoding _encoding = new UTF8Encoding(false, true);
public GitHelpers(TextWriter stdout, IContainer container)
{
realStdout = stdout;
_container = container;
}
/// <summary>
/// Runs the given git command, and returns the contents of its STDOUT.
/// </summary>
public string Command(params string[] command)
{
string retVal = null;
CommandOutputPipe(stdout => retVal = stdout.ReadToEnd(), command);
return retVal;
}
/// <summary>
/// Runs the given git command, and returns the first line of its STDOUT.
/// </summary>
public string CommandOneline(params string[] command)
{
string retVal = null;
CommandOutputPipe(stdout => retVal = stdout.ReadLine(), command);
return retVal;
}
/// <summary>
/// Runs the given git command, and passes STDOUT through to the current process's STDOUT.
/// </summary>
public void CommandNoisy(params string[] command)
{
CommandOutputPipe(stdout => realStdout.Write(stdout.ReadToEnd()), command);
}
/// <summary>
/// Runs the given git command, and redirects STDOUT to the provided action.
/// </summary>
public void CommandOutputPipe(Action<TextReader> handleOutput, params string[] command)
{
Time(command, () =>
{
AssertValidCommand(command);
var process = Start(command, RedirectStdout);
handleOutput(process.StandardOutput);
Close(process);
});
}
/// <summary>
/// Runs the given git command, and returns a reader for STDOUT. NOTE: The returned value MUST be disposed!
/// </summary>
public TextReader CommandOutputPipe(params string[] command)
{
AssertValidCommand(command);
var process = Start(command, RedirectStdout);
return new ProcessStdoutReader(this, process);
}
public class ProcessStdoutReader : TextReader
{
private readonly Process process;
private readonly GitHelpers helper;
public ProcessStdoutReader(GitHelpers helper, Process process)
{
this.helper = helper;
this.process = process;
}
public override void Close()
{
helper.Close(process);
}
public override System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType)
{
return process.StandardOutput.CreateObjRef(requestedType);
}
protected override void Dispose(bool disposing)
{
if(disposing && process != null)
{
Close();
}
base.Dispose(disposing);
}
public override bool Equals(object obj)
{
return process.StandardOutput.Equals(obj);
}
public override int GetHashCode()
{
return process.StandardOutput.GetHashCode();
}
public override object InitializeLifetimeService()
{
return process.StandardOutput.InitializeLifetimeService();
}
public override int Peek()
{
return process.StandardOutput.Peek();
}
public override int Read()
{
return process.StandardOutput.Read();
}
public override int Read(char[] buffer, int index, int count)
{
return process.StandardOutput.Read(buffer, index, count);
}
public override int ReadBlock(char[] buffer, int index, int count)
{
return process.StandardOutput.ReadBlock(buffer, index, count);
}
public override string ReadLine()
{
return process.StandardOutput.ReadLine();
}
public override string ReadToEnd()
{
return process.StandardOutput.ReadToEnd();
}
public override string ToString()
{
return process.StandardOutput.ToString();
}
}
public void CommandInputPipe(Action<TextWriter> action, params string[] command)
{
Time(command, () =>
{
AssertValidCommand(command);
var process = Start(command, RedirectStdin);
action(process.StandardInput.WithEncoding(_encoding));
Close(process);
});
}
public void CommandInputOutputPipe(Action<TextWriter, TextReader> interact, params string[] command)
{
Time(command, () =>
{
AssertValidCommand(command);
var process = Start(command, Ext.And<ProcessStartInfo>(RedirectStdin, RedirectStdout));
interact(process.StandardInput.WithEncoding(_encoding), process.StandardOutput);
Close(process);
});
}
private void Time(string[] command, Action action)
{
var start = DateTime.Now;
try
{
action();
}
finally
{
var end = DateTime.Now;
Trace.WriteLine(String.Format("[{0}] {1}", end - start, String.Join(" ", command)), "git command time");
}
}
private void Close(Process process)
{
// if caller doesn't read entire stdout to the EOF - it is possible that
// child process will hang waiting until there will be free space in stdout
// buffer to write the rest of the output.
// See https://github.com/git-tfs/git-tfs/issues/121 for details.
if (process.StartInfo.RedirectStandardOutput)
{
process.StandardOutput.BaseStream.CopyTo(Stream.Null);
process.StandardOutput.Close();
}
if (!process.WaitForExit((int)TimeSpan.FromSeconds(10).TotalMilliseconds))
throw new GitCommandException("Command did not terminate.", process);
if(process.ExitCode != 0)
throw new GitCommandException(string.Format("Command exited with error code: {0}", process.ExitCode), process);
}
private void RedirectStdout(ProcessStartInfo startInfo)
{
startInfo.RedirectStandardOutput = true;
startInfo.StandardOutputEncoding = _encoding;
}
private void RedirectStderr(ProcessStartInfo startInfo)
{
startInfo.RedirectStandardError= true;
startInfo.StandardErrorEncoding = _encoding;
}
private void RedirectStdin(ProcessStartInfo startInfo)
{
startInfo.RedirectStandardInput = true;
// there is no StandardInputEncoding property, use extension method StreamWriter.WithEncoding instead
}
private Process Start(string[] command)
{
return Start(command, x => {});
}
protected virtual Process Start(string [] command, Action<ProcessStartInfo> initialize)
{
var startInfo = new ProcessStartInfo();
startInfo.FileName = "git";
startInfo.SetArguments(command);
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = false;
startInfo.EnvironmentVariables.Add("GIT_PAGER", "cat");
RedirectStderr(startInfo);
initialize(startInfo);
Trace.WriteLine("Starting process: " + startInfo.FileName + " " + startInfo.Arguments, "git command");
var process = Process.Start(startInfo);
process.ErrorDataReceived += StdErrReceived;
process.BeginErrorReadLine();
return process;
}
private void StdErrReceived(object sender, DataReceivedEventArgs e)
{
if(e.Data != null && e.Data.Trim() != "")
{
Trace.WriteLine(e.Data.TrimEnd(), "git stderr");
}
}
/// <summary>
/// WrapGitCommandErrors the actions, and if there are any git exceptions, rethrow a new exception with the given message.
/// </summary>
/// <param name="exceptionMessage">A friendlier message to wrap the GitCommandException with. {0} is replaced with the command line and {1} is replaced with the exit code.</param>
/// <param name="action"></param>
public void WrapGitCommandErrors(string exceptionMessage, Action action)
{
try
{
action();
}
catch (GitCommandException e)
{
throw new Exception(String.Format(exceptionMessage, e.Process.StartInfo.FileName + " " + e.Process.StartInfo.Arguments, e.Process.ExitCode), e);
}
}
public IGitRepository MakeRepository(string dir)
{
return _container
.With("gitDir").EqualTo(dir)
.GetInstance<IGitRepository>();
}
private static readonly Regex ValidCommandName = new Regex("^[a-z0-9A-Z_-]+$");
private static void AssertValidCommand(string[] command)
{
if(command.Length < 1 || !ValidCommandName.IsMatch(command[0]))
throw new Exception("bad command: " + (command.Length == 0 ? "" : command[0]));
}
}
}