You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
933 lines
32 KiB
933 lines
32 KiB
using System.Diagnostics;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using GitCandy.Base;
|
|
using GitCandy.Git.Cache;
|
|
using GitCandy.Models;
|
|
using GitCandy.Web;
|
|
using GitCandy.Web.Extensions;
|
|
using LibGit2Sharp;
|
|
using NewLife;
|
|
using NewLife.Log;
|
|
|
|
namespace GitCandy.Git;
|
|
|
|
public class GitService : IDisposable
|
|
{
|
|
public const String UnknowString = "<Unknow>";
|
|
|
|
private readonly Repository _repository;
|
|
private readonly String _repositoryPath;
|
|
private readonly String _repoId = null;
|
|
private readonly Lazy<Encoding> _i18n;
|
|
private Boolean _disposed;
|
|
|
|
public Encoding I18n => _i18n.Value;
|
|
public String Owner { get; private set; }
|
|
public String Name { get; private set; }
|
|
public Repository Repository => _repository;
|
|
|
|
public GitService(String owner, String name)
|
|
{
|
|
var info = GetPath(owner, name).AsDirectory();
|
|
_repositoryPath = info.FullName;
|
|
Name = info.Name;
|
|
Owner = owner;
|
|
_repoId = $"{Owner}\\{Name}";
|
|
|
|
// 如果版本库无效,则创建
|
|
if (!Repository.IsValid(_repositoryPath)) CreateRepository(owner, name);
|
|
|
|
_repository = new Repository(_repositoryPath);
|
|
|
|
// 延迟加载编码
|
|
_i18n = new Lazy<Encoding>(() =>
|
|
{
|
|
var entry = _repository.Config.Get<String>("i18n.commitEncoding");
|
|
return entry == null
|
|
? null
|
|
: CpToEncoding(entry.Value);
|
|
});
|
|
}
|
|
|
|
#region Git Smart HTTP Transport
|
|
public async Task InfoRefs(String service, Stream inStream, Stream outStream)
|
|
{
|
|
Contract.Requires(service == "receive-pack" || service == "upload-pack");
|
|
await RunGitCmdAsync(service, true, inStream, outStream);
|
|
}
|
|
|
|
public async Task ExecutePack(String service, Stream inStream, Stream outStream)
|
|
{
|
|
Contract.Requires(service == "receive-pack" || service == "upload-pack");
|
|
await RunGitCmdAsync(service, false, inStream, outStream);
|
|
}
|
|
#endregion
|
|
|
|
#region Repository Browser
|
|
public TreeModel GetTree(String path)
|
|
{
|
|
var isEmptyPath = String.IsNullOrEmpty(path);
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null)
|
|
{
|
|
if (isEmptyPath)
|
|
{
|
|
var branch = _repository.Branches["master"] ?? _repository.Branches.FirstOrDefault();
|
|
return new TreeModel
|
|
{
|
|
ReferenceName = branch == null ? "HEAD" : branch.FriendlyName,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// 基本信息
|
|
var model = new TreeModel
|
|
{
|
|
Owner = Owner,
|
|
Name = Name,
|
|
ReferenceName = referenceName,
|
|
Path = String.IsNullOrEmpty(path) ? "" : path,
|
|
Commit = new CommitModel
|
|
{
|
|
Sha = commit.Sha,
|
|
Author = commit.Author,
|
|
Committer = commit.Committer,
|
|
CommitMessageShort = commit.MessageShort.RepetitionIfEmpty(UnknowString),
|
|
Parents = commit.Parents.Select(s => s.Sha).ToArray()
|
|
},
|
|
};
|
|
|
|
// 树结构
|
|
var tree = String.IsNullOrEmpty(path)
|
|
? commit.Tree
|
|
: commit[path] == null
|
|
? null
|
|
: commit[path].Target as Tree;
|
|
if (tree == null) return null;
|
|
|
|
var entries = (from entry in tree
|
|
//join item in items on entry.Name equals item.Name into g
|
|
//from item in g
|
|
select new TreeEntryModel
|
|
{
|
|
Name = entry.Name,
|
|
Path = entry.Path.Replace('\\', '/'),
|
|
Commit = new CommitModel
|
|
{
|
|
//Sha = item.CommitSha,
|
|
//CommitMessageShort = item.MessageShort,
|
|
//Author = CreateSafeSignature(item.AuthorName, item.AuthorEmail, item.AuthorWhen),
|
|
//Committer = CreateSafeSignature(item.CommitterName, item.CommitterEmail, item.CommitterWhen),
|
|
},
|
|
//Sha = item.CommitSha,
|
|
EntryType = entry.TargetType,
|
|
})
|
|
.OrderBy(s => s.EntryType == TreeEntryTargetType.Blob)
|
|
.ThenBy(s => s.Name, new StringLogicalComparer())
|
|
.ToList();
|
|
|
|
model.Entries = entries;
|
|
|
|
// 缓存加载摘要
|
|
var cfg = GitSetting.Current;
|
|
if (cfg.AllowSummary)
|
|
{
|
|
var summaryAccessor = GitCacheAccessor.Singleton(new SummaryAccessor(_repoId, _repository, commit, tree));
|
|
var items = summaryAccessor.Result.Value;
|
|
foreach (var entry in entries)
|
|
{
|
|
var item = items.FirstOrDefault(e => e.Name == entry.Name);
|
|
if (item != null)
|
|
{
|
|
entry.Commit = new CommitModel
|
|
{
|
|
Sha = item.CommitSha,
|
|
CommitMessageShort = item.MessageShort,
|
|
Author = CreateSafeSignature(item.AuthorName, item.AuthorEmail, item.AuthorWhen),
|
|
Committer = CreateSafeSignature(item.CommitterName, item.CommitterEmail, item.CommitterWhen),
|
|
};
|
|
entry.Sha = item.CommitSha;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 加载说明文件
|
|
model.Readme = entries.FirstOrDefault(s => s.EntryType == TreeEntryTargetType.Blob
|
|
&& (s.Name.EqualIgnoreCase($"readme.{CultureInfo.CurrentCulture.Name}.md", "readme", "readme.md")));
|
|
|
|
if (model.Readme != null)
|
|
{
|
|
var rm = model.Readme;
|
|
var entry = tree[rm.Name];
|
|
var blob = (Blob)entry.Target;
|
|
var data = blob.GetContentStream().ToBytes();
|
|
var encoding = FileHelper.DetectEncoding(data, CpToEncoding(commit.Encoding), _i18n.Value);
|
|
if (encoding == null)
|
|
{
|
|
rm.BlobType = BlobType.Binary;
|
|
}
|
|
else if (rm.Name.EndsWithIgnoreCase(".md"))
|
|
{
|
|
rm.BlobType = BlobType.MarkDown;
|
|
//rm.TextContent = FileHelper.ReadMarkdown(data, encoding, $"{model.ReferenceName ?? model.Commit.Sha}/{entry.Path}");
|
|
//rm.Path = $"{model.ReferenceName ?? model.Commit.Sha}/{entry.Path}";
|
|
rm.TextContent = data.ToStr(encoding);
|
|
rm.TextBrush = "no-highlight";
|
|
}
|
|
else
|
|
{
|
|
rm.BlobType = BlobType.Text;
|
|
rm.TextContent = data.ToStr(encoding);
|
|
rm.TextBrush = "no-highlight";
|
|
}
|
|
}
|
|
|
|
model.BranchSelector = GetBranchSelectorModel(referenceName, commit.Sha, path);
|
|
model.PathBar = new PathBarModel
|
|
{
|
|
Name = Name,
|
|
Action = "Tree",
|
|
Path = path,
|
|
ReferenceName = referenceName,
|
|
ReferenceSha = commit.Sha,
|
|
HideLastSlash = false,
|
|
};
|
|
|
|
if (model.IsRoot)
|
|
{
|
|
var scopeAccessor = GitCacheAccessor.Singleton(new ScopeAccessor(_repoId, _repository, commit));
|
|
model.Scope = scopeAccessor.Result.Value;
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
public TreeEntryModel GetBlob(String path)
|
|
{
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null) return null;
|
|
|
|
var entry = commit[path];
|
|
if (entry == null || entry.TargetType != TreeEntryTargetType.Blob) return null;
|
|
|
|
var blob = (Blob)entry.Target;
|
|
|
|
var cacheAccessor = GitCacheAccessor.Singleton(new LastCommitAccessor(_repoId, _repository, commit, path));
|
|
var lastCommitSha = cacheAccessor.Result.Value;
|
|
if (lastCommitSha != commit.Sha)
|
|
commit = _repository.Lookup<Commit>(lastCommitSha);
|
|
|
|
var data = blob.GetContentStream().ToBytes();
|
|
var encoding = FileHelper.DetectEncoding(data, CpToEncoding(commit.Encoding), _i18n.Value);
|
|
var extension = Path.GetExtension(entry.Name).ToLower();
|
|
var model = new TreeEntryModel
|
|
{
|
|
Name = entry.Name,
|
|
ReferenceName = referenceName,
|
|
Sha = commit.Sha,
|
|
Path = String.IsNullOrEmpty(path) ? "" : path,
|
|
Commit = new CommitModel
|
|
{
|
|
Sha = commit.Sha,
|
|
Author = commit.Author,
|
|
Committer = commit.Committer,
|
|
CommitMessage = commit.Message.RepetitionIfEmpty(UnknowString),
|
|
CommitMessageShort = commit.MessageShort.RepetitionIfEmpty(UnknowString),
|
|
Parents = commit.Parents.Select(s => s.Sha).ToArray()
|
|
},
|
|
EntryType = entry.TargetType,
|
|
RawData = data,
|
|
SizeString = FileHelper.GetSizeString(data.Length),
|
|
TextContent = data.ToStr(encoding),
|
|
TextBrush = FileHelper.BrushMapping.ContainsKey(extension)
|
|
? FileHelper.BrushMapping[extension]
|
|
: "no-highlight",
|
|
BlobType = encoding == null
|
|
? FileHelper.ImageSet.Contains(extension)
|
|
? BlobType.Image
|
|
: BlobType.Binary
|
|
: extension == ".md"
|
|
? BlobType.MarkDown
|
|
: BlobType.Text,
|
|
BlobEncoding = encoding,
|
|
BranchSelector = GetBranchSelectorModel(referenceName, commit.Sha, path),
|
|
PathBar = new PathBarModel
|
|
{
|
|
Name = Name,
|
|
Action = "Tree",
|
|
Path = path,
|
|
ReferenceName = referenceName,
|
|
ReferenceSha = commit.Sha,
|
|
HideLastSlash = true,
|
|
},
|
|
};
|
|
|
|
return model;
|
|
}
|
|
|
|
public CommitModel GetCommit(String path)
|
|
{
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null) return null;
|
|
|
|
var treeEntry = commit[path];
|
|
var isBlob = treeEntry != null && treeEntry.TargetType == TreeEntryTargetType.Blob;
|
|
var model = ToCommitModel(commit, referenceName, !isBlob, path);
|
|
model.PathBar = new PathBarModel
|
|
{
|
|
Name = Name,
|
|
Action = "Commit",
|
|
Path = path,
|
|
ReferenceName = referenceName,
|
|
ReferenceSha = commit.Sha,
|
|
HideLastSlash = isBlob,
|
|
};
|
|
return model;
|
|
}
|
|
|
|
public CompareModel GetCompare(String start, String end)
|
|
{
|
|
var commit1 = GetCommitByPath(ref start, out var name1);
|
|
var commit2 = GetCommitByPath(ref end, out var name2);
|
|
if (commit1 == null)
|
|
{
|
|
commit1 = _repository.Head.Tip;
|
|
name1 = _repository.Head.FriendlyName;
|
|
}
|
|
if (commit2 == null)
|
|
{
|
|
commit2 = _repository.Head.Tip;
|
|
name2 = _repository.Head.FriendlyName;
|
|
}
|
|
|
|
var walks = _repository.Commits
|
|
.QueryBy(new CommitFilter
|
|
{
|
|
IncludeReachableFrom = commit2,
|
|
ExcludeReachableFrom = commit1,
|
|
SortBy = CommitSortStrategies.Time
|
|
})
|
|
.Select(s => new CommitModel
|
|
{
|
|
Sha = s.Sha,
|
|
Committer = s.Committer,
|
|
CommitMessageShort = s.MessageShort.RepetitionIfEmpty(UnknowString),
|
|
})
|
|
.ToArray();
|
|
|
|
var fromBranchSelector = GetBranchSelectorModel(name1, commit1.Sha, null);
|
|
var toBranchSelector = GetBranchSelectorModel(name2, commit2.Sha, null);
|
|
var model = new CompareModel
|
|
{
|
|
BaseBranchSelector = fromBranchSelector,
|
|
CompareBranchSelector = toBranchSelector,
|
|
CompareResult = ToCommitModel(commit1, name1, true, "", commit2.Tree),
|
|
Walks = walks,
|
|
};
|
|
return model;
|
|
}
|
|
|
|
public CommitsModel GetCommits(String path, Int32 page = 1, Int32 pagesize = 20)
|
|
{
|
|
var cfg = GitSetting.Current;
|
|
if (!cfg.AllowCommits) return null;
|
|
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null) return null;
|
|
|
|
var commitsAccessor = GitCacheAccessor.Singleton(new CommitsAccessor(_repoId, _repository, commit, path, page, pagesize));
|
|
var scopeAccessor = GitCacheAccessor.Singleton(new ScopeAccessor(_repoId, _repository, commit, path));
|
|
|
|
var model = new CommitsModel
|
|
{
|
|
ReferenceName = referenceName,
|
|
Sha = commit.Sha,
|
|
Commits = commitsAccessor.Result.Value
|
|
.Select(s => new CommitModel
|
|
{
|
|
CommitMessageShort = s.MessageShort,
|
|
Sha = s.CommitSha,
|
|
Author = CreateSafeSignature(s.AuthorName, s.AuthorEmail, s.AuthorWhen),
|
|
Committer = CreateSafeSignature(s.CommitterName, s.CommitterEmail, s.CommitterWhen),
|
|
})
|
|
.ToList(),
|
|
CurrentPage = page,
|
|
ItemCount = scopeAccessor.Result.Value.Commits,
|
|
Path = String.IsNullOrEmpty(path) ? "" : path,
|
|
PathBar = new PathBarModel
|
|
{
|
|
Name = Name,
|
|
Action = "Commits",
|
|
Path = path,
|
|
ReferenceName = referenceName,
|
|
ReferenceSha = commit.Sha,
|
|
HideLastSlash = true, // I want a improvement here
|
|
},
|
|
};
|
|
|
|
return model;
|
|
}
|
|
|
|
public BlameModel GetBlame(String path)
|
|
{
|
|
var cfg = GitSetting.Current;
|
|
if (!cfg.AllowBlame) return null;
|
|
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null) return null;
|
|
|
|
var entry = commit[path];
|
|
if (entry == null || entry.TargetType != TreeEntryTargetType.Blob) return null;
|
|
|
|
var blob = (Blob)entry.Target;
|
|
|
|
var accessor = GitCacheAccessor.Singleton(new BlameAccessor(_repoId, _repository, commit, path, CpToEncoding(commit.Encoding), _i18n.Value));
|
|
var hunks = accessor.Result.Value;
|
|
|
|
var model = new BlameModel
|
|
{
|
|
ReferenceName = referenceName,
|
|
Sha = commit.Sha,
|
|
Path = String.IsNullOrEmpty(path) ? "" : path,
|
|
SizeString = FileHelper.GetSizeString(blob.Size),
|
|
Brush = FileHelper.GetBrush(path),
|
|
Hunks = hunks,
|
|
BranchSelector = GetBranchSelectorModel(referenceName, commit.Sha, path),
|
|
PathBar = new PathBarModel
|
|
{
|
|
Name = Name,
|
|
Action = "Tree",
|
|
Path = path,
|
|
ReferenceName = referenceName,
|
|
ReferenceSha = commit.Sha,
|
|
HideLastSlash = true,
|
|
},
|
|
};
|
|
|
|
return model;
|
|
}
|
|
|
|
public String GetArchiveFilename(String path, out String referenceName)
|
|
{
|
|
referenceName = null;
|
|
|
|
var cfg = GitSetting.Current;
|
|
if (!cfg.AllowArchive) return null;
|
|
|
|
var commit = GetCommitByPath(ref path, out referenceName);
|
|
if (commit == null) return null;
|
|
|
|
referenceName ??= commit.Sha;
|
|
|
|
var accessor = GitCacheAccessor.Singleton(new ArchiverAccessor(_repoId, _repository, commit, CpToEncoding(commit.Encoding), _i18n.Value));
|
|
|
|
return accessor.Result.Value;
|
|
}
|
|
|
|
public TagsModel GetTags()
|
|
{
|
|
var model = new TagsModel
|
|
{
|
|
Tags = (from tag in _repository.Tags
|
|
let commit = (tag.IsAnnotated ? tag.Annotation.Target : tag.Target) as Commit
|
|
where commit != null
|
|
select new TagModel
|
|
{
|
|
ReferenceName = tag.FriendlyName,
|
|
Sha = tag.Target.Sha,
|
|
When = ((Commit)tag.Target).Author.When,
|
|
MessageShort = ((Commit)tag.Target).MessageShort.RepetitionIfEmpty(UnknowString),
|
|
})
|
|
.OrderByDescending(s => s.When)
|
|
.ToArray()
|
|
};
|
|
return model;
|
|
}
|
|
|
|
public BranchesModel GetBranches()
|
|
{
|
|
var cfg = GitSetting.Current;
|
|
if (!cfg.AllowHistoryDivergence) return new BranchesModel();
|
|
|
|
var head = _repository.Head;
|
|
if (head.Tip == null) return new BranchesModel();
|
|
|
|
var key = CalcBranchesKey();
|
|
var accessor = GitCacheAccessor.Singleton(new HistoryDivergenceAccessor(_repoId, _repository, key));
|
|
var aheadBehinds = accessor.Result.Value;
|
|
var model = new BranchesModel
|
|
{
|
|
Commit = ToCommitModel(head.Tip, head.FriendlyName),
|
|
AheadBehinds = aheadBehinds.Select(s => new AheadBehindModel
|
|
{
|
|
Ahead = s.Ahead,
|
|
Behind = s.Behind,
|
|
Commit = new CommitModel
|
|
{
|
|
ReferenceName = s.Name,
|
|
Author = CreateSafeSignature(s.AuthorName, s.AuthorEmail, s.AuthorWhen),
|
|
Committer = CreateSafeSignature(s.CommitterName, s.CommitterEmail, s.CommitterWhen),
|
|
},
|
|
}).ToArray(),
|
|
};
|
|
return model;
|
|
}
|
|
|
|
public void DeleteBranch(String branch) => _repository.Branches.Remove(branch);
|
|
|
|
public void DeleteTag(String tag) => _repository.Tags.Remove(tag);
|
|
|
|
public ContributorsModel GetContributors(String path)
|
|
{
|
|
var cfg = GitSetting.Current;
|
|
if (!cfg.AllowContributors) return null;
|
|
|
|
var commit = GetCommitByPath(ref path, out var referenceName);
|
|
if (commit == null) return null;
|
|
|
|
var contributorsAccessor = GitCacheAccessor.Singleton(new ContributorsAccessor(_repoId, _repository, commit));
|
|
var contributors = contributorsAccessor.Result.Value;
|
|
contributors.OrderedCommits = contributors.OrderedCommits
|
|
.Take(GitSetting.Current.Contributors)
|
|
.ToArray();
|
|
var statistics = new RepositoryStatisticsModel
|
|
{
|
|
Current = contributors
|
|
};
|
|
statistics.Current.Branch = referenceName;
|
|
|
|
if (_repository.Head.Tip != commit)
|
|
{
|
|
contributorsAccessor = GitCacheAccessor.Singleton(new ContributorsAccessor(_repoId, _repository, _repository.Head.Tip));
|
|
statistics.Default = contributorsAccessor.Result.Value;
|
|
statistics.Default.Branch = _repository.Head.FriendlyName;
|
|
}
|
|
|
|
var key = CalcBranchesKey(true);
|
|
var repositorySizeAccessor = GitCacheAccessor.Singleton(new RepositorySizeAccessor(_repoId, _repository, key));
|
|
statistics.RepositorySize = repositorySizeAccessor.Result.Value;
|
|
|
|
var model = new ContributorsModel
|
|
{
|
|
Name = Name,
|
|
Statistics = statistics,
|
|
};
|
|
return model;
|
|
}
|
|
#endregion
|
|
|
|
#region Repository Settings
|
|
public String GetHeadBranch()
|
|
{
|
|
var head = _repository.Head;
|
|
if (head == null)
|
|
return null;
|
|
return head.FriendlyName;
|
|
}
|
|
|
|
public String[] GetLocalBranches() => _repository.Branches.Select(s => s.FriendlyName).OrderBy(s => s, new StringLogicalComparer()).ToArray();
|
|
|
|
public Boolean SetHeadBranch(String name)
|
|
{
|
|
var refs = _repository.Refs;
|
|
var refer = refs["refs/heads/" + (name ?? "master")];
|
|
if (refer == null)
|
|
return false;
|
|
refs.UpdateTarget(refs.Head, refer);
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
private BranchSelectorModel GetBranchSelectorModel(String referenceName, String refer, String path)
|
|
{
|
|
var model = new BranchSelectorModel
|
|
{
|
|
Branches = _repository.Branches.Select(s => s.FriendlyName).OrderBy(s => s, new StringLogicalComparer()).ToList(),
|
|
Tags = _repository.Tags.Select(s => s.FriendlyName).OrderByDescending(s => s, new StringLogicalComparer()).ToList(),
|
|
Current = referenceName ?? refer.ToShortSha(),
|
|
Path = path,
|
|
};
|
|
model.CurrentIsBranch = model.Branches.Contains(referenceName) || !model.Tags.Contains(referenceName);
|
|
|
|
return model;
|
|
}
|
|
|
|
private Commit GetCommitByPath(ref String path, out String referenceName)
|
|
{
|
|
referenceName = null;
|
|
|
|
if (String.IsNullOrEmpty(path))
|
|
{
|
|
referenceName = _repository.Head.FriendlyName;
|
|
path = "";
|
|
return _repository.Head.Tip;
|
|
}
|
|
|
|
path = path + "/";
|
|
var p = path;
|
|
var branch = _repository.Branches.FirstOrDefault(s => p.StartsWith(s.FriendlyName + "/"));
|
|
if (branch != null && branch.Tip != null)
|
|
{
|
|
referenceName = branch.FriendlyName;
|
|
path = path[referenceName.Length..].Trim('/');
|
|
return branch.Tip;
|
|
}
|
|
|
|
var tag = _repository.Tags.FirstOrDefault(s => p.StartsWith(s.FriendlyName + "/"));
|
|
if (tag != null && tag.Target is Commit)
|
|
{
|
|
referenceName = tag.FriendlyName;
|
|
path = path[referenceName.Length..].Trim('/');
|
|
return (Commit)tag.Target;
|
|
}
|
|
|
|
var index = path.IndexOf('/');
|
|
var commit = _repository.Lookup<Commit>(path[..index]);
|
|
path = path[(index + 1)..].Trim('/');
|
|
return commit;
|
|
}
|
|
|
|
private CommitModel ToCommitModel(Commit commit, String referenceName, Boolean isTree = true, String detailFilter = null, Tree compareWith = null)
|
|
{
|
|
if (commit == null)
|
|
return null;
|
|
|
|
var model = new CommitModel
|
|
{
|
|
ReferenceName = referenceName,
|
|
Sha = commit.Sha,
|
|
CommitMessageShort = commit.MessageShort.RepetitionIfEmpty(UnknowString),
|
|
CommitMessage = commit.Message.RepetitionIfEmpty(UnknowString),
|
|
Author = commit.Author,
|
|
Committer = commit.Committer,
|
|
Parents = commit.Parents.Select(e => e.Sha).ToArray(),
|
|
};
|
|
if (detailFilter != null)
|
|
{
|
|
if (detailFilter != "" && isTree)
|
|
detailFilter = detailFilter + "/";
|
|
var firstTree = compareWith != null
|
|
? commit.Tree
|
|
: commit.Parents.Any()
|
|
? commit.Parents.First().Tree
|
|
: null;
|
|
if (compareWith == null)
|
|
compareWith = commit.Tree;
|
|
var compareOptions = new LibGit2Sharp.CompareOptions
|
|
{
|
|
Similarity = SimilarityOptions.Renames,
|
|
};
|
|
var paths = detailFilter == ""
|
|
? null
|
|
: new[] { detailFilter };
|
|
var changes = _repository.Diff.Compare<TreeChanges>(firstTree, compareWith, paths, compareOptions: compareOptions);
|
|
var patches = _repository.Diff.Compare<Patch>(firstTree, compareWith, paths, compareOptions: compareOptions);
|
|
model.Changes = (from s in changes
|
|
where (s.Path.Replace('\\', '/') + '/').StartsWith(detailFilter)
|
|
orderby s.Path
|
|
let patch = patches[s.Path]
|
|
select new CommitChangeModel
|
|
{
|
|
//Name = s.Name,
|
|
OldPath = s.OldPath.Replace('\\', '/'),
|
|
Path = s.Path.Replace('\\', '/'),
|
|
ChangeKind = s.Status,
|
|
LinesAdded = patch.LinesAdded,
|
|
LinesDeleted = patch.LinesDeleted,
|
|
Patch = patch.Patch,
|
|
})
|
|
.ToArray();
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
private Encoding CpToEncoding(String encoding)
|
|
{
|
|
try
|
|
{
|
|
if (encoding.StartsWith("cp", StringComparison.OrdinalIgnoreCase))
|
|
return Encoding.GetEncoding(Int32.Parse(encoding[2..]));
|
|
|
|
return Encoding.GetEncoding(encoding);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private String CalcBranchesKey(Boolean includeTags = false)
|
|
{
|
|
var sb = new StringBuilder();
|
|
var head = _repository.Head;
|
|
sb.Append(":");
|
|
sb.Append(head.CanonicalName);
|
|
if (head.Tip != null)
|
|
sb.Append(head.Tip.Sha);
|
|
sb.Append(';');
|
|
foreach (var branch in _repository.Branches.OrderBy(s => s.CanonicalName))
|
|
{
|
|
sb.Append(':');
|
|
sb.Append(branch.CanonicalName);
|
|
if (branch.Tip != null)
|
|
sb.Append(branch.Tip.Sha);
|
|
}
|
|
if (includeTags)
|
|
{
|
|
sb.Append(';');
|
|
foreach (var tag in _repository.Tags.OrderBy(s => s.CanonicalName))
|
|
{
|
|
sb.Append(':');
|
|
sb.Append(tag.CanonicalName);
|
|
if (tag.Target != null)
|
|
sb.Append(tag.Target.Sha);
|
|
}
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private Signature CreateSafeSignature(String name, String email, DateTimeOffset when) => new(name.RepetitionIfEmpty(UnknowString), email.RepetitionIfEmpty(UnknowString), when);
|
|
#endregion
|
|
|
|
#region Static Methods
|
|
/// <summary>获取版本库的路径</summary>
|
|
/// <param name="owner"></param>
|
|
/// <param name="name"></param>
|
|
/// <returns></returns>
|
|
public static String GetPath(String owner, String name)
|
|
{
|
|
var p = GitSetting.Current.RepositoryPath;
|
|
p = p.CombinePath(owner, name).GetFullPath();
|
|
|
|
return p;
|
|
}
|
|
|
|
public static Boolean CreateRepository(String owner, String name, String remoteUrl = null)
|
|
{
|
|
var path = GetPath(owner, name);
|
|
try
|
|
{
|
|
using (var repo = new Repository(Repository.Init(path, true)))
|
|
{
|
|
repo.Config.Set("core.logallrefupdates", true);
|
|
if (remoteUrl != null)
|
|
{
|
|
repo.Network.Remotes.Add("origin", remoteUrl, "+refs/*:refs/*");
|
|
Task.Run(() =>
|
|
{
|
|
XTrace.WriteLine("[{0}/{1}]准备从远程拉取 {2}", owner, name, remoteUrl);
|
|
try
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
using (var fetch_repo = new Repository(repo.Info.Path))
|
|
{
|
|
//fetch_repo.Fetch("origin");
|
|
fetch_repo.Network.Fetch("origin", Array.Empty<String>());
|
|
}
|
|
sw.Stop();
|
|
XTrace.WriteLine("远程拉取成功,耗时 {0:n0}毫秒", sw.ElapsedMilliseconds);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
XTrace.WriteException(ex);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(path, true);
|
|
}
|
|
catch { }
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static Boolean DeleteRepository(String owner, String name)
|
|
{
|
|
var path = GetPath(owner, name);
|
|
var temp = path + "." + DateTime.Now.Ticks + ".del";
|
|
|
|
var retry = 3;
|
|
for (; retry > 0; retry--)
|
|
try
|
|
{
|
|
Directory.Move(path, temp);
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
Task.Delay(1000).Wait();
|
|
}
|
|
|
|
for (; retry > 0; retry--)
|
|
try
|
|
{
|
|
var di = new DirectoryInfo(temp);
|
|
|
|
foreach (var info in di.GetFileSystemInfos("*", SearchOption.AllDirectories))
|
|
info.Attributes = FileAttributes.Archive;
|
|
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
Task.Delay(1000).Wait();
|
|
}
|
|
|
|
for (; retry > 0; retry--)
|
|
try
|
|
{
|
|
Directory.Delete(temp, true);
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
Task.Delay(1000).Wait();
|
|
}
|
|
|
|
return retry > 0;
|
|
}
|
|
|
|
//private static DirectoryInfo GetDirectoryInfo(String project)
|
|
//{
|
|
// return new DirectoryInfo(Path.Combine(UserConfiguration.Current.RepositoryPath.GetFullPath(), project));
|
|
//}
|
|
#endregion
|
|
|
|
#region RunGitCmd
|
|
// un-safe implementation
|
|
private void RunGitCmd(String serviceName, Boolean advertiseRefs, Stream inStream, Stream outStream)
|
|
{
|
|
var args = serviceName + " --stateless-rpc";
|
|
if (advertiseRefs)
|
|
args += " --advertise-refs";
|
|
args += " \"" + _repositoryPath + "\"";
|
|
|
|
WriteLog("git.exe {0}", args);
|
|
var sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
var cfg = GitSetting.Current;
|
|
var info = new ProcessStartInfo(cfg.GetGitFile(), args)
|
|
{
|
|
CreateNoWindow = true,
|
|
RedirectStandardError = true,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
WorkingDirectory = Path.GetDirectoryName(cfg.RepositoryPath.GetFullPath()),
|
|
};
|
|
|
|
var rs = 0;
|
|
using (var process = Process.Start(info))
|
|
{
|
|
inStream.CopyTo(process.StandardInput.BaseStream);
|
|
process.StandardInput.Close();
|
|
process.StandardOutput.BaseStream.CopyTo(outStream);
|
|
|
|
if (!process.WaitForExit(15 * 1000))
|
|
{
|
|
process.Kill();
|
|
}
|
|
|
|
rs = process.ExitCode;
|
|
}
|
|
|
|
sw.Stop();
|
|
WriteLog("git.exe 完成 {0} 耗时 {1}", rs, sw.Elapsed);
|
|
}
|
|
|
|
private async Task RunGitCmdAsync(String serviceName, Boolean advertiseRefs, Stream inStream, Stream outStream)
|
|
{
|
|
var args = serviceName + " --stateless-rpc";
|
|
if (advertiseRefs)
|
|
args += " --advertise-refs";
|
|
args += " \"" + _repositoryPath + "\"";
|
|
|
|
WriteLog("git.exe {0}", args);
|
|
var sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
var cfg = GitSetting.Current;
|
|
var info = new ProcessStartInfo(cfg.GetGitFile(), args)
|
|
{
|
|
CreateNoWindow = true,
|
|
RedirectStandardError = true,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
WorkingDirectory = Path.GetDirectoryName(cfg.RepositoryPath.GetFullPath()),
|
|
};
|
|
|
|
var rs = 0;
|
|
using (var process = Process.Start(info))
|
|
{
|
|
await inStream.CopyToAsync(process.StandardInput.BaseStream);
|
|
process.StandardInput.Close();
|
|
await process.StandardOutput.BaseStream.CopyToAsync(outStream);
|
|
|
|
if (!process.WaitForExit(15 * 1000))
|
|
{
|
|
process.Kill();
|
|
}
|
|
|
|
rs = process.ExitCode;
|
|
}
|
|
|
|
sw.Stop();
|
|
WriteLog("git.exe 完成 {0} 耗时 {1}", rs, sw.Elapsed);
|
|
}
|
|
#endregion
|
|
|
|
#region IDisposable Members
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(Boolean disposing)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
if (_repository != null)
|
|
{
|
|
_repository.Dispose();
|
|
}
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
~GitService()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
#endregion
|
|
|
|
#region 日志
|
|
/// <summary>日志</summary>
|
|
public ILog Log { get; set; } = Logger.Null;
|
|
|
|
/// <summary>写日志</summary>
|
|
/// <param name="format"></param>
|
|
/// <param name="args"></param>
|
|
public void WriteLog(String format, params Object[] args)
|
|
{
|
|
if (Log != null && Log.Enable) Log.Info(format, args);
|
|
}
|
|
#endregion
|
|
}
|