Files
git-tfs/GitTfs/Core/GitRepository.cs
T
2012-11-22 19:08:40 +01:00

443 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Sep.Git.Tfs.Commands;
using StructureMap;
using FileMode = LibGit2Sharp.Mode;
using LibGit2Sharp;
namespace Sep.Git.Tfs.Core
{
public class GitRepository : GitHelpers, IGitRepository
{
private readonly IContainer _container;
private readonly Globals _globals;
private static readonly Regex configLineRegex = new Regex("^tfs-remote\\.(?<id>.+)\\.(?<key>[^.=]+)=(?<value>.*)$");
private IDictionary<string, IGitTfsRemote> _cachedRemotes;
private Repository _repository;
public GitRepository(TextWriter stdout, string gitDir, IContainer container, Globals globals)
: base(stdout, container)
{
_container = container;
_globals = globals;
GitDir = gitDir;
_repository = new LibGit2Sharp.Repository(GitDir);
}
~GitRepository()
{
if (_repository != null)
_repository.Dispose();
}
public string GitDir { get; set; }
public string WorkingCopyPath { get; set; }
public string WorkingCopySubdir { get; set; }
protected override Process Start(string[] command, Action<ProcessStartInfo> initialize)
{
return base.Start(command, initialize.And(SetUpPaths));
}
private void SetUpPaths(ProcessStartInfo gitCommand)
{
if (GitDir != null)
gitCommand.EnvironmentVariables["GIT_DIR"] = GitDir;
if (WorkingCopyPath != null)
gitCommand.WorkingDirectory = WorkingCopyPath;
if (WorkingCopySubdir != null)
gitCommand.WorkingDirectory = Path.Combine(gitCommand.WorkingDirectory, WorkingCopySubdir);
}
public IEnumerable<IGitTfsRemote> ReadAllTfsRemotes()
{
return GetTfsRemotes().Values;
}
public IGitTfsRemote ReadTfsRemote(string remoteId)
{
if (!HasRemote(remoteId))
throw new GitTfsException("Unable to locate git-tfs remote with id = " + remoteId)
.WithRecommendation("Try using `git tfs bootstrap` to auto-init TFS remotes.");
return GetTfsRemotes()[remoteId];
}
private IGitTfsRemote ReadTfsRemote(string tfsUrl, string tfsRepositoryPath, bool includeStubRemotes)
{
var allRemotes = GetTfsRemotes();
var matchingRemotes =
allRemotes.Values.Where(
remote => remote.Tfs.MatchesUrl(tfsUrl) && remote.TfsRepositoryPath == tfsRepositoryPath);
switch (matchingRemotes.Count())
{
case 0:
if (!includeStubRemotes)
throw new GitTfsException("Unable to locate a remote for <" + tfsUrl + ">" + tfsRepositoryPath)
.WithRecommendation("Try using `git tfs bootstrap` to auto-init TFS remotes.")
.WithRecommendation("Try setting a legacy-url for an existing remote.");
return new DerivedGitTfsRemote(tfsUrl, tfsRepositoryPath);
case 1:
return matchingRemotes.First();
default:
Trace.WriteLine("More than one remote matched!");
goto case 1;
}
}
private IDictionary<string, IGitTfsRemote> GetTfsRemotes()
{
return _cachedRemotes ?? (_cachedRemotes = ReadTfsRemotes());
}
private IDictionary<string, IGitTfsRemote> ReadTfsRemotes()
{
var remotes = new Dictionary<string, IGitTfsRemote>();
CommandOutputPipe(stdout => ParseRemoteConfig(stdout, remotes), "config", "--list");
return remotes;
}
public bool HasRemote(string remoteId)
{
return GetTfsRemotes().ContainsKey(remoteId);
}
public void MoveTfsRefForwardIfNeeded(IGitTfsRemote remote)
{
long currentMaxChangesetId = remote.MaxChangesetId;
var untrackedTfsChangesets = from cs in GetParentTfsCommits("refs/remotes/tfs/" + remote.Id + "..HEAD", false)
where cs.Remote.Id == remote.Id && cs.ChangesetId > currentMaxChangesetId
orderby cs.ChangesetId
select cs;
foreach (var cs in untrackedTfsChangesets)
{
// UpdateRef sets tag with TFS changeset id on each commit so we can't just update to latest
remote.UpdateRef(cs.GitCommit, cs.ChangesetId);
}
}
public void CreateTfsRemote(string remoteId, TfsChangesetInfo tfsHead, RemoteOptions remoteOptions)
{
CreateTfsRemote(remoteId, tfsHead.Remote.TfsUrl, tfsHead.Remote.TfsRepositoryPath, remoteOptions);
ReadTfsRemote(remoteId).UpdateRef(tfsHead.GitCommit, tfsHead.ChangesetId);
}
public void CreateTfsRemote(string remoteId, string tfsUrl, string tfsRepositoryPath, RemoteOptions remoteOptions)
{
if (HasRemote(remoteId))
throw new GitTfsException("A remote with id \"" + remoteId + "\" already exists.");
if (remoteOptions != null)
{
if (remoteOptions.NoMetaData) SetTfsConfig(remoteId, "no-meta-data", 1);
if (remoteOptions.IgnoreRegex != null) SetTfsConfig(remoteId, "ignore-paths", remoteOptions.IgnoreRegex);
if (!string.IsNullOrEmpty(remoteOptions.Username)) SetTfsConfig(remoteId, "username", remoteOptions.Username);
if (!string.IsNullOrEmpty(remoteOptions.Password)) SetTfsConfig(remoteId, "password", remoteOptions.Password);
}
SetTfsConfig(remoteId, "url", tfsUrl);
SetTfsConfig(remoteId, "repository", tfsRepositoryPath);
SetTfsConfig(remoteId, "fetch", "refs/remotes/" + remoteId + "/master");
Directory.CreateDirectory(Path.Combine(GitDir, "tfs"));
_cachedRemotes = null;
}
private void SetTfsConfig(string remoteId, string subkey, object value)
{
this.SetConfig(_globals.RemoteConfigKey(remoteId, subkey), value);
}
private void ParseRemoteConfig(TextReader stdout, IDictionary<string, IGitTfsRemote> remotes)
{
string line;
while ((line = stdout.ReadLine()) != null)
{
TryParseRemoteConfigLine(line, remotes);
}
foreach (var gitTfsRemotePair in remotes)
{
var remote = gitTfsRemotePair.Value;
remote.EnsureTfsAuthenticated();
}
}
private void TryParseRemoteConfigLine(string line, IDictionary<string, IGitTfsRemote> remotes)
{
var match = configLineRegex.Match(line);
if (match.Success)
{
var key = match.Groups["key"].Value;
var value = match.Groups["value"].Value;
var remoteId = match.Groups["id"].Value;
var remote = remotes.ContainsKey(remoteId)
? remotes[remoteId]
: (remotes[remoteId] = BuildRemote(remoteId));
try
{
SetRemoteConfigValue(remote, key, value);
}
catch(Exception e)
{
throw new GitTfsException("Malformed value for " + key + ": " + value, e);
}
}
}
private IGitTfsRemote BuildRemote(string id)
{
var remote = _container.GetInstance<IGitTfsRemote>();
remote.Repository = this;
remote.Id = id;
return remote;
}
private void SetRemoteConfigValue(IGitTfsRemote remote, string key, string value)
{
switch (key)
{
case "url":
remote.TfsUrl = value;
break;
case "legacy-urls":
remote.Tfs.LegacyUrls = value.Split(',');
break;
case "repository":
remote.TfsRepositoryPath = value;
break;
case "ignore-paths":
remote.IgnoreRegexExpression = value;
break;
case "username":
remote.TfsUsername = value;
break;
case "password":
remote.TfsPassword = value;
break;
case "autotag":
remote.Autotag = bool.Parse(value);
break;
}
}
public GitCommit GetCommit(string commitish)
{
return new GitCommit(_repository.Lookup<Commit>(commitish));
}
public IEnumerable<TfsChangesetInfo> GetLastParentTfsCommits(string head)
{
return GetLastParentTfsCommits(head, false);
}
public IEnumerable<TfsChangesetInfo> GetLastParentTfsCommits(string head, bool includeStubRemotes)
{
List<TfsChangesetInfo> tfsCommits = GetParentTfsCommits(head, includeStubRemotes);
return from commit in tfsCommits
group commit by commit.Remote
into remotes
select remotes.OrderBy(commit => -commit.ChangesetId).First();
}
private List<TfsChangesetInfo> GetParentTfsCommits(string head, bool includeStubRemotes)
{
var tfsCommits = new List<TfsChangesetInfo>();
try
{
CommandOutputPipe(stdout => FindTfsCommits(stdout, tfsCommits, includeStubRemotes),
"log", "--no-color", "--pretty=medium", head);
}
catch (GitCommandException e)
{
Trace.WriteLine("An error occurred while loading head " + head + " (maybe it doesn't exist?): " + e);
}
return tfsCommits;
}
private void FindTfsCommits(TextReader stdout, ICollection<TfsChangesetInfo> tfsCommits, bool includeStubRemotes)
{
string currentCommit = null;
string line;
while (null != (line = stdout.ReadLine()))
{
var match = GitTfsConstants.CommitRegex.Match(line);
if (match.Success)
{
currentCommit = match.Groups[1].Value;
continue;
}
var changesetInfo = TryParseChangesetInfo(line, currentCommit, includeStubRemotes);
if (changesetInfo != null)
{
tfsCommits.Add(changesetInfo);
currentCommit = null;
}
}
//stdout.Close();
}
private TfsChangesetInfo TryParseChangesetInfo(string gitTfsMetaInfo, string commit, bool includeStubRemotes)
{
var match = GitTfsConstants.TfsCommitInfoRegex.Match(gitTfsMetaInfo);
if (match.Success)
{
var commitInfo = _container.GetInstance<TfsChangesetInfo>();
commitInfo.Remote = ReadTfsRemote(match.Groups["url"].Value, match.Groups["repository"].Value, includeStubRemotes);
commitInfo.ChangesetId = Convert.ToInt32(match.Groups["changeset"].Value);
commitInfo.GitCommit = commit;
return commitInfo;
}
return null;
}
public IDictionary<string, GitObject> GetObjects(string commit)
{
var entries = GetObjects();
if (commit != null)
{
ParseEntries(entries, _repository.Lookup<Commit>(commit).Tree, commit);
}
return entries;
}
public Dictionary<string, GitObject> GetObjects()
{
return new Dictionary<string, GitObject>(StringComparer.InvariantCultureIgnoreCase);
}
public string GetCommitMessage(string head, string parentCommitish)
{
var message = new System.Text.StringBuilder();
foreach (LibGit2Sharp.Commit comm in
_repository.Commits.QueryBy(new LibGit2Sharp.Filter { Since = head, Until = parentCommitish }))
{
// Normalize commit message line endings to CR+LF style, so that message
// would be correctly shown in TFS commit dialog.
message.AppendLine(NormalizeLineEndings(comm.Message));
}
return GitTfsConstants.TfsCommitInfoRegex.Replace(message.ToString(), "").Trim(' ', '\r', '\n');
}
private static string NormalizeLineEndings(string input)
{
return string.IsNullOrEmpty(input)
? input
: input.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
}
private void ParseEntries(IDictionary<string, GitObject> entries, Tree treeInfo, string commit)
{
var treesToDescend = new Queue<Tree>(new[] {treeInfo});
while (treesToDescend.Any())
{
var currentTree = treesToDescend.Dequeue();
foreach (var item in currentTree)
{
if (item.Type == GitObjectType.Tree)
{
treesToDescend.Enqueue((Tree)item.Target);
}
var path = item.Path.Replace('\\', '/');
entries[path] = new GitObject
{
Mode = item.Mode.ToModeString(),
Sha = item.Target.Sha,
ObjectType = item.Type.ToString().ToLower(),
Path = path,
Commit = commit
};
}
}
}
public IEnumerable<IGitChangedFile> GetChangedFiles(string from, string to)
{
using (var diffOutput = CommandOutputPipe("diff-tree", "-r", "-M", from, to))
{
string line;
while (null != (line = diffOutput.ReadLine()))
{
var change = GitChangeInfo.Parse(line);
if (FileMode.GitLink == change.NewMode)
continue;
yield return BuildGitChangedFile(change);
}
}
}
private IGitChangedFile BuildGitChangedFile(GitChangeInfo change)
{
return change.ToGitChangedFile(_container.With((IGitRepository) this));
}
public bool WorkingCopyHasUnstagedOrUncommitedChanges
{
get
{
return (from
entry in _repository.Index.RetrieveStatus()
where
entry.State != FileStatus.Ignored &&
entry.State != FileStatus.Untracked
select entry).Count() > 0;
}
}
public void CopyBlob(string sha, string outputFile)
{
Blob blob;
var destination = new FileInfo(outputFile);
if (!destination.Directory.Exists)
destination.Directory.Create();
if ((blob = _repository.Lookup<Blob>(sha)) != null)
using (Stream stream = blob.ContentStream)
using (var outstream = File.Create(destination.FullName))
stream.CopyTo(outstream);
}
public string HashAndInsertObject(string filename)
{
return _repository.ObjectDatabase.CreateBlob(filename).Id.Sha;
}
public string AssertValidBranchName(string gitBranchName)
{
if (!_repository.Refs.IsValidName("refs/heads/" + gitBranchName))
throw new GitTfsException("The name specified for the new git branch is not allowed. Choose another one!");
return gitBranchName;
}
public bool CreateBranch(string gitBranchName, string target)
{
Reference reference;
try
{
reference = _repository.Refs.Create(gitBranchName, target);
}
catch (Exception)
{
return false;
}
return reference != null;
}
public string FindCommitHashByCommitMessage(string patternToFind)
{
var regex = new Regex(patternToFind);
foreach (var branch in _repository.Branches.Where(p => p.IsRemote).ToList())
{
var commit = branch.Commits.SingleOrDefault(c => regex.IsMatch(c.Message));
if (commit != null)
return commit.Sha;
}
return null;
}
}
}