internal void ClearBlameResult() { m_blame = null; m_layout = null; SetHorizontalScrollInfo(1, null, 0); SetVerticalScrollInfo(1, null, 0); InvalidateMeasure(); RedrawSoon(); }
internal void SetBlameResult(BlameResult blame, int topLineNumber = 1) { if (m_blameSubscription != null) m_blameSubscription.Dispose(); double oldLineHeight = m_layout == null ? 1.0 : m_layout.LineHeight; m_blame = blame; m_layout = new BlameLayout(blame).WithTopLineNumber(1).WithLineHeight(oldLineHeight); m_lineCount = blame.Blocks.Sum(b => b.LineCount); m_blameSubscription = Observable.FromEventPattern<PropertyChangedEventArgs>(m_blame, "PropertyChanged").ObserveOnDispatcher().Subscribe(x => OnBlameResultPropertyChanged(x.EventArgs)); m_hoverCommitId = null; m_selectedCommitId = null; m_personBrush.Clear(); m_commitBrush.Clear(); m_commitAlpha.Clear(); CreateBrushesForAuthors(m_layout.AuthorCount); SetVerticalScrollInfo(m_lineCount + 1, null, topLineNumber - 1); InvalidateMeasure(); OnScrollChanged(); RedrawSoon(); }
private static BlameResult GetBlameOutput(string repositoryPath, string fileName, string blameCommitId, string[] currentLines) { BlameResult blameResult; using (var repo = new Repository(repositoryPath)) { // try to determine if the remote URL is plausibly a github.com or GitHub Enterprise URL Uri webRootUrl = repo.Network.Remotes .OrderBy(x => x.Name == "origin" ? 0 : 1) .ThenBy(x => x.Name) .Select(x => { Match m = Regex.Match(x.Url, @"^(git@(?'host'[^:]+):(?'user'[^/]+)/(?'repo'[^/]+)\.git|(git|https?)://(?'host'[^/]+)/(?'user'[^/]+)/(?'repo'[^/]+)\.git)$", RegexOptions.ExplicitCapture); if (m.Success) { string host = m.Groups["host"].Value; return(new Uri(string.Format("http{0}://{1}/{2}/{3}/", host == "github.com" ? "s" : "", host, m.Groups["user"].Value, m.Groups["repo"].Value))); } else { return(null); } }).FirstOrDefault(x => x != null); var loadingPerson = new Person("Loading…", "loading"); var commit = new Commit(UncommittedChangesCommitId, loadingPerson, DateTimeOffset.Now, loadingPerson, DateTimeOffset.Now, "", null, null); // create a fake blame result that assigns all the code to the HEAD revision blameResult = new BlameResult(webRootUrl, new[] { new Block(1, currentLines.Length, commit, fileName, 1) }.AsReadOnly(), currentLines.Select((l, n) => new Line(n + 1, l, true)).ToList(), new Dictionary <string, Commit> { { commit.Id, commit } }); } Task.Run(() => { // run "git blame" ExternalProcess git = new ExternalProcess(GetGitPath(), Path.GetDirectoryName(repositoryPath)); List <string> arguments = new List <string> { "blame", "--incremental", "--encoding=utf-8" }; if (blameCommitId != null) { arguments.Add(blameCommitId); } arguments.AddRange(new[] { "--", fileName }); var results = git.Run(new ProcessRunSettings(arguments.ToArray())); if (results.ExitCode != 0) { return; } // parse output List <Block> blocks = new List <Block>(); Dictionary <string, Commit> commits = new Dictionary <string, Commit>(); ParseBlameOutput(results.Output, blocks, commits); // allocate a (1-based) array for all lines in the file int lineCount = blocks.Sum(b => b.LineCount); Invariant.Assert(lineCount == currentLines.Length, "Unexpected number of lines in file."); // initialize all lines from current version Line[] lines = currentLines .Select((l, n) => new Line(n + 1, l, false)) .ToArray(); blameResult.SetData(blocks, lines, commits); Dictionary <string, Task <string> > getFileContentTasks = CreateGetFileContentTasks(repositoryPath, blocks, commits, currentLines); // process the blocks for each unique commit foreach (var groupLoopVariable in blocks.OrderBy(b => b.StartLine).GroupBy(b => b.Commit)) { // check if this commit modifies a previous one var group = groupLoopVariable; Commit commit = group.Key; string commitId = commit.Id; string previousCommitId = commit.PreviousCommitId; if (previousCommitId != null) { // diff the old and new file contents when they become available Task <string> getOldFileContentTask = getFileContentTasks[previousCommitId]; Task <string> getNewFileContentTask = getFileContentTasks[commitId]; Task.Factory.ContinueWhenAll(new[] { getOldFileContentTask, getNewFileContentTask }, tasks => { // diff the two versions var oldFileContents = tasks[0].Result; var newFileContents = tasks[1].Result; // diff_match_patch can generate incorrect output if there are more than 65536 lines being diffed var checkLines = GetLineCount(oldFileContents) < 65000 && GetLineCount(newFileContents) < 65000; var diff = new diff_match_patch { Diff_Timeout = 10 }; var diffs = diff.diff_main(oldFileContents, newFileContents, checkLines); diff.diff_cleanupSemantic(diffs); // process all the lines in the diff output, matching them to blocks using (IEnumerator <Line> lineEnumerator = ParseDiffOutput(diffs).GetEnumerator()) { // move to first line (which is expected to always be present) Invariant.Assert(lineEnumerator.MoveNext(), "Expected at least one line from diff output."); Line line = lineEnumerator.Current; // process all the blocks, finding the corresponding lines from the diff for each one foreach (Block block in group) { // skip all lines that occurred before the start of this block while (line.LineNumber < block.OriginalStartLine) { Invariant.Assert(lineEnumerator.MoveNext(), "diff does not contain the expected number of lines."); line = lineEnumerator.Current; } // process all lines in the current block while (line.LineNumber >= block.OriginalStartLine && line.LineNumber < block.OriginalStartLine + block.LineCount) { // assign this line to the correct index in the blamed version of the file blameResult.SetLine(line.LineNumber - block.OriginalStartLine + block.StartLine, line); // move to the next line (if available) if (lineEnumerator.MoveNext()) { line = lineEnumerator.Current; } else { break; } } } } }); } else { // this is the initial commit (but has not been modified since); grab its lines from the current version of the file foreach (Block block in group) { for (int lineNumber = block.StartLine; lineNumber < block.StartLine + block.LineCount; lineNumber++) { blameResult.SetLine(lineNumber, new Line(lineNumber, currentLines[lineNumber - 1], true)); } } } } }); return(blameResult); }
private static BlameResult GetBlameOutput(string repositoryPath, string fileName, string blameCommitId, string[] currentLines) { BlameResult blameResult; using (var repo = new Repository(repositoryPath)) { // try to determine if the remote URL is plausibly a github.com or GitHub Enterprise URL Uri webRootUrl = repo.Network.Remotes .OrderBy(x => x.Name == "origin" ? 0 : 1) .ThenBy(x => x.Name) .Select(x => { Match m = Regex.Match(x.Url, @"^(git@(?'host'[^:]+):(?'user'[^/]+)/(?'repo'[^/]+)\.git|(git|https?)://(?'host'[^/]+)/(?'user'[^/]+)/(?'repo'[^/]+)\.git)$", RegexOptions.ExplicitCapture); if (m.Success) { string host = m.Groups["host"].Value; return new Uri(string.Format("http{0}://{1}/{2}/{3}/", host == "github.com" ? "s" : "", host, m.Groups["user"].Value, m.Groups["repo"].Value)); } else { return null; } }).FirstOrDefault(x => x != null); var loadingPerson = new Person("Loading…", "loading"); var commit = new Commit(UncommittedChangesCommitId, loadingPerson, DateTimeOffset.Now, loadingPerson, DateTimeOffset.Now, "", null, null); // create a fake blame result that assigns all the code to the HEAD revision blameResult = new BlameResult(webRootUrl, new[] { new Block(1, currentLines.Length, commit, fileName, 1) }.AsReadOnly(), currentLines.Select((l, n) => new Line(n + 1, l, true)).ToList(), new Dictionary<string, Commit> { { commit.Id, commit } }); } Task.Run(() => { // run "git blame" ExternalProcess git = new ExternalProcess(GetGitPath(), Path.GetDirectoryName(repositoryPath)); List<string> arguments = new List<string> { "blame", "--incremental", "--encoding=utf-8" }; if (blameCommitId != null) arguments.Add(blameCommitId); arguments.AddRange(new[] { "--", fileName }); var results = git.Run(new ProcessRunSettings(arguments.ToArray())); if (results.ExitCode != 0) return; // parse output List<Block> blocks = new List<Block>(); Dictionary<string, Commit> commits = new Dictionary<string, Commit>(); ParseBlameOutput(results.Output, blocks, commits); // allocate a (1-based) array for all lines in the file int lineCount = blocks.Sum(b => b.LineCount); Invariant.Assert(lineCount == currentLines.Length, "Unexpected number of lines in file."); // initialize all lines from current version Line[] lines = currentLines .Select((l, n) => new Line(n + 1, l, false)) .ToArray(); blameResult.SetData(blocks, lines, commits); Dictionary<string, Task<string>> getFileContentTasks = CreateGetFileContentTasks(repositoryPath, blocks, commits, currentLines); // process the blocks for each unique commit foreach (var groupLoopVariable in blocks.OrderBy(b => b.StartLine).GroupBy(b => b.Commit)) { // check if this commit modifies a previous one var group = groupLoopVariable; Commit commit = group.Key; string commitId = commit.Id; string previousCommitId = commit.PreviousCommitId; if (previousCommitId != null) { // diff the old and new file contents when they become available Task<string> getOldFileContentTask = getFileContentTasks[previousCommitId]; Task<string> getNewFileContentTask = getFileContentTasks[commitId]; Task.Factory.ContinueWhenAll(new[] { getOldFileContentTask, getNewFileContentTask }, tasks => { // diff the two versions var oldFileContents = tasks[0].Result; var newFileContents = tasks[1].Result; // diff_match_patch can generate incorrect output if there are more than 65536 lines being diffed var checkLines = GetLineCount(oldFileContents) < 65000 && GetLineCount(newFileContents) < 65000; var diff = new diff_match_patch { Diff_Timeout = 10 }; var diffs = diff.diff_main(oldFileContents, newFileContents, checkLines); diff.diff_cleanupSemantic(diffs); // process all the lines in the diff output, matching them to blocks using (IEnumerator<Line> lineEnumerator = ParseDiffOutput(diffs).GetEnumerator()) { // move to first line (which is expected to always be present) Invariant.Assert(lineEnumerator.MoveNext(), "Expected at least one line from diff output."); Line line = lineEnumerator.Current; // process all the blocks, finding the corresponding lines from the diff for each one foreach (Block block in group) { // skip all lines that occurred before the start of this block while (line.LineNumber < block.OriginalStartLine) { Invariant.Assert(lineEnumerator.MoveNext(), "diff does not contain the expected number of lines."); line = lineEnumerator.Current; } // process all lines in the current block while (line.LineNumber >= block.OriginalStartLine && line.LineNumber < block.OriginalStartLine + block.LineCount) { // assign this line to the correct index in the blamed version of the file blameResult.SetLine(line.LineNumber - block.OriginalStartLine + block.StartLine, line); // move to the next line (if available) if (lineEnumerator.MoveNext()) line = lineEnumerator.Current; else break; } } } }); } else { // this is the initial commit (but has not been modified since); grab its lines from the current version of the file foreach (Block block in group) for (int lineNumber = block.StartLine; lineNumber < block.StartLine + block.LineCount; lineNumber++) blameResult.SetLine(lineNumber, new Line(lineNumber, currentLines[lineNumber - 1], true)); } } }); return blameResult; }
public BlameLayout(BlameResult blame) : this() { // get basic information from "blame" output m_blame = blame; m_authorIndex = GetBlameAuthors(m_blame); // track commit age (for fading out the blocks for each commit) m_oldestCommit = GetOldestCommit(m_blame); m_dateScale = GetDateScale(m_blame, m_oldestCommit); // set up default column widths m_columnWidths = new double[] { 210, 10, 0, 10, 5, 0 }; // set default values m_lineHeight = 1; m_topLineNumber = 1; }
private static DateTimeOffset GetOldestCommit(BlameResult blame) { return blame.Commits.Min(c => c.AuthorDate); }
private static double GetDateScale(BlameResult blame, DateTimeOffset oldestCommit) { DateTimeOffset newestCommit = blame.Commits.Max(c => c.AuthorDate); return 0.65 / (newestCommit - oldestCommit).TotalDays; }
private static Dictionary<Person, int> GetBlameAuthors(BlameResult blame) { return blame.Commits .GroupBy(c => c.Author) .OrderByDescending(g => g.Count()) .Select((g, n) => new KeyValuePair<Person, int>(g.Key, n)) .ToDictionary(); }
private BlameLayout(BlameLayout layout, int? topLineNumber = null, Size? renderSize = null, double? lineHeight = null, double? lineNumberColumnWidth = null, double? codeColumnWidth = null, bool fullRefresh = false) : this() { // copy values from other BlameLayout m_blame = layout.m_blame; m_columnWidths = layout.m_columnWidths; // copy or replace values from other BlameLayout m_topLineNumber = topLineNumber ?? layout.m_topLineNumber; m_renderSize = renderSize ?? layout.m_renderSize; m_lineHeight = lineHeight ?? layout.m_lineHeight; m_columnWidths[c_lineNumberColumnIndex] = lineNumberColumnWidth ?? m_columnWidths[c_lineNumberColumnIndex]; m_columnWidths[c_codeColumnIndex] = codeColumnWidth ?? m_columnWidths[c_codeColumnIndex]; m_authorIndex = fullRefresh ? GetBlameAuthors(m_blame) : layout.m_authorIndex; m_oldestCommit = fullRefresh ? GetOldestCommit(m_blame) : layout.m_oldestCommit; m_dateScale = fullRefresh ? GetDateScale(m_blame, m_oldestCommit) : layout.m_dateScale; // calculate new values m_lineCount = (int) Math.Ceiling(m_renderSize.Height / m_lineHeight); }