Example #1
0
 internal void ClearBlameResult()
 {
     m_blame = null;
     m_layout = null;
     SetHorizontalScrollInfo(1, null, 0);
     SetVerticalScrollInfo(1, null, 0);
     InvalidateMeasure();
     RedrawSoon();
 }
Example #2
0
        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();
        }
Example #3
0
        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);
        }
Example #4
0
        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;
        }
Example #5
0
        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;
        }
Example #6
0
 private static DateTimeOffset GetOldestCommit(BlameResult blame)
 {
     return blame.Commits.Min(c => c.AuthorDate);
 }
Example #7
0
 private static double GetDateScale(BlameResult blame, DateTimeOffset oldestCommit)
 {
     DateTimeOffset newestCommit = blame.Commits.Max(c => c.AuthorDate);
     return 0.65 / (newestCommit - oldestCommit).TotalDays;
 }
Example #8
0
 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();
 }
Example #9
0
        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);
        }