Skip to content

Commit

Permalink
Git command line changes for WSL (#3893)
Browse files Browse the repository at this point in the history
* git command line changes for WSL

* address PR feedback

* address PR feedback

* address PR feedback
  • Loading branch information
ssparach authored Sep 27, 2024
1 parent bf3d0aa commit c0936f9
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,22 @@ public class GitCommandRunnerResultInfo

public string? Arguments { get; set; }

public int? ProcessExitCode { get; set; }

public GitCommandRunnerResultInfo(ProviderOperationStatus status, string? output)
{
Status = status;
Output = output;
}

public GitCommandRunnerResultInfo(ProviderOperationStatus status, string? displayMessage, string? diagnosticText, Exception? ex, string? args)
public GitCommandRunnerResultInfo(ProviderOperationStatus status, string? output, string? displayMessage, string? diagnosticText, Exception? ex, string? args, int? processExitCode)
{
Status = status;
Output = output;
DisplayMessage = displayMessage;
DiagnosticText = diagnosticText;
Ex = ex;
Arguments = args;
ProcessExitCode = processExitCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,73 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using LibGit2Sharp;
using Serilog;

namespace FileExplorerGitIntegration.Models;

// LibGit2Sharp.CommitEnumerator are not reusable as they do not provide a way to reuse libgit2's revwalk object
// LibGit2's prepare_walk() does the expensive work of traversing and caching the commit graph
// Unfortunately LibGit2Sharp.CommitEnumerator.Reset() method resets the revwalk, but does not reinitialize the sort/push/hide options
// This leaves all that work wasted only to be done again.
// Furthermore, LibGit2 revwalk initialization takes locks on internal data, which causes contention in multithreaded scenarios as threads
// all scramble to initialize and re-initialize their own revwalk objects.
// Ideally, LibGit2Sharp improves the API to allow reusing the revwalk object, but that seems unlikely to happen soon.
internal sealed class CommitLogCache
{
private readonly List<Commit> _commits = new();
private readonly string _workingDirectory;

// For now, we'll use the command line to get the last commit for a file, on demand.
// In the future we may use some sort of heuristic to determine if we should use the command line or not.
private readonly bool _preferCommandLine = true;
private readonly bool _useCommandLine;
private readonly GitDetect _gitDetect = new();
private readonly bool _gitInstalled;

private readonly LruCacheDictionary<string, CommitWrapper> _cache = new();

private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(CommitLogCache));

public CommitLogCache(Repository repo)
public CommitLogCache(string workingDirectory)
{
_workingDirectory = repo.Info.WorkingDirectory;

// Use the command line to get the last commit for a file, on demand.
// PRO: If Git is installed, this will always succeed, and in a somewhat predictable amount of time.
// Doesn't consume memory for the entire commit log.
// CON: Spawning a process for each file may be slower than walking to recent commits.
// Does not work if Git isn't installed.
if (_preferCommandLine)
{
_gitInstalled = _gitDetect.DetectGit();
_useCommandLine = _gitInstalled;
}

if (!_useCommandLine)
{
// Greedily get the entire commit log for simplicity.
// PRO: No syncronization needed for the enumerator.
// CON: May take longer for the initial load and use more memory.
// For reference, I tested on my dev machine on a repo with an *large* number of commits
// https://github.com/python/cpython with > 120k commits. This was a one-time cost of 2-3 seconds, but also
// consumed several hundred MB of memory.
// Unfortunately, loading an *enormous* repo with 1M+ commits consumes a multiple GBs of memory.

// For smaller repos this method is faster, but the memory consumption is prohibitive on the huge ones.
// Additionally, virtualized repos (aka GVFS) may show the entire commit log, but each commit's tree isn't always hydrated.
// As a result, GVFS repos often fail to find the last commit for a file if it is older than some unknown threshold.

// Often, but not always, the root folder has some boilerplate/doc/config that rarely changes
// Therefore, populating the last commit for each file in the root folder often requires a large portion of the commit history anyway.
// This somewhat blunts the appeal of trying to load this incrementally.
_commits.AddRange(repo.Commits);
}
_workingDirectory = workingDirectory;
_gitInstalled = _gitDetect.DetectGit();
}

public CommitWrapper? FindLastCommit(string relativePath)
Expand All @@ -77,15 +30,7 @@ public CommitLogCache(Repository repo)
return cachedCommit;
}

CommitWrapper? result;
if (_useCommandLine)
{
result = FindLastCommitUsingCommandLine(relativePath);
}
else
{
result = FindLastCommitUsingLibGit2Sharp(relativePath);
}
var result = FindLastCommitUsingCommandLine(relativePath);

if (result != null)
{
Expand Down Expand Up @@ -130,54 +75,4 @@ public CommitLogCache(Repository repo)
string sha = parts[4];
return new CommitWrapper(message, authorName, authorEmail, authorWhen, sha);
}

private CommitWrapper? FindLastCommitUsingLibGit2Sharp(string relativePath)
{
var checkedFirstCommit = false;
foreach (var currentCommit in _commits)
{
// Now that CommitLogCache is caching the result of the revwalk, the next piece that is most expensive
// is obtaining relativePath's TreeEntry from the Tree (e.g. currentTree[relativePath].
// Digging into the git shows that number of directory entries and/or directory depth may play a factor.
// There may also be a lot of redundant lookups happening here, so it may make sense to do some LRU caching.
var currentTree = currentCommit.Tree;
var currentTreeEntry = currentTree[relativePath];
if (currentTreeEntry == null)
{
if (checkedFirstCommit)
{
continue;
}
else
{
// If this file isn't present in the most recent commit, then it's of no interest
return null;
}
}

checkedFirstCommit = true;
var parents = currentCommit.Parents;
var count = parents.Count();
if (count == 0)
{
return new CommitWrapper(currentCommit);
}
else if (count > 1)
{
// Multiple parents means a merge. Ignore.
continue;
}
else
{
var parentTree = parents.First();
var parentTreeEntry = parentTree[relativePath];
if (parentTreeEntry == null || parentTreeEntry.Target.Id != currentTreeEntry.Target.Id)
{
return new CommitWrapper(currentCommit);
}
}
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using LibGit2Sharp;

internal sealed class CommitWrapper
{
public string MessageShort { get; private set; }
Expand All @@ -15,15 +13,6 @@ internal sealed class CommitWrapper

public string Sha { get; private set; }

public CommitWrapper(Commit commit)
{
MessageShort = commit.MessageShort;
AuthorName = commit.Author.Name;
AuthorEmail = commit.Author.Email;
AuthorWhen = commit.Author.When;
Sha = commit.Sha;
}

public CommitWrapper(string messageShort, string authorName, string authorEmail, DateTimeOffset authorWhen, string sha)
{
MessageShort = messageShort;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FileExplorerGitIntegration.Models;

public partial class GitExecutableConfigOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,25 @@ public static GitCommandRunnerResultInfo ExecuteGitCommand(string gitApplication

// Add timeout for 1 minute
process.WaitForExit(TimeSpan.FromMinutes(1));

if (process.ExitCode != 0)
{
Log.Error("Execute Git process exited unsuccessfully with exit code {ExitCode}", process.ExitCode);
return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, output, "Execute Git process exited unsuccessfully", string.Empty, null, arguments, process.ExitCode);
}

return new GitCommandRunnerResultInfo(ProviderOperationStatus.Success, output);
}
else
{
Log.Error("Failed to start the Git process: process is null");
return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, "Git process is null", string.Empty, new InvalidOperationException("Failed to start the Git process: process is null"), null);
return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, null, "Git process is null", string.Empty, new InvalidOperationException("Failed to start the Git process: process is null"), null, null);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to invoke Git with arguments: {Argument}", arguments);
return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, "Failed to invoke Git with arguments", string.Empty, ex, arguments);
return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, null, "Failed to invoke Git with arguments", string.Empty, ex, arguments, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using System.Runtime.InteropServices;
using LibGit2Sharp;
using Microsoft.Windows.DevHome.SDK;
using Serilog;
using Windows.Foundation.Collections;
Expand All @@ -15,7 +14,7 @@ public sealed class GitLocalRepository : ILocalRepository
{
private readonly RepositoryCache? _repositoryCache;

private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(GitLocalRepository));
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(GitLocalRepository));

public string RootFolder
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Runtime.InteropServices;
using DevHome.Common.Services;
using LibGit2Sharp;
using Microsoft.Windows.DevHome.SDK;
using Serilog;

Expand Down Expand Up @@ -35,14 +34,14 @@ GetLocalRepositoryResult ILocalRepositoryProvider.GetRepository(string rootPath)
{
return new GetLocalRepositoryResult(new GitLocalRepository(rootPath, _repositoryCache));
}
catch (RepositoryNotFoundException libGitEx)
catch (ArgumentException ex)
{
_log.Error("GitLocalRepositoryProviderFactory", "Failed to create GitLocalRepository", libGitEx);
return new GetLocalRepositoryResult(libGitEx, _stringResource.GetLocalized("RepositoryNotFound"), $"Message: {libGitEx.Message} and HRESULT: {libGitEx.HResult}");
_log.Error(ex, "GitLocalRepositoryProviderFactory: Failed to create GitLocalRepository");
return new GetLocalRepositoryResult(ex, _stringResource.GetLocalized("RepositoryNotFound"), $"Message: {ex.Message} and HRESULT: {ex.HResult}");
}
catch (Exception ex)
{
_log.Error("GitLocalRepositoryProviderFactory", "Failed to create GitLocalRepository", ex);
_log.Error(ex, "GitLocalRepositoryProviderFactory: Failed to create GitLocalRepository");
if (ex.Message.Contains("not owned by current user") || ex.Message.Contains("detected dubious ownership in repository"))
{
return new GetLocalRepositoryResult(ex, _stringResource.GetLocalized("RepositoryNotOwnedByCurrentUser"), $"Message: {ex.Message} and HRESULT: {ex.HResult}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ internal sealed class GitRepositoryStatus
private readonly Dictionary<string, SubmoduleStatus> _submoduleEntries = new();
private readonly Dictionary<FileStatus, List<GitStatusEntry>> _statusEntries = new();

public string BranchName { get; set; } = string.Empty;

public bool IsHeadDetached { get; set; }

public string UpstreamBranch { get; set; } = string.Empty;

public int AheadBy { get; set; }

public int BehindBy { get; set; }

public string Sha { get; set; } = string.Empty;

public GitRepositoryStatus()
{
_statusEntries.Add(FileStatus.NewInIndex, new List<GitStatusEntry>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

using System.Collections.Concurrent;
using System.Diagnostics;
using LibGit2Sharp;
using Serilog;

namespace FileExplorerGitIntegration.Models;

internal sealed class RepositoryCache : IDisposable
{
private readonly ConcurrentDictionary<string, RepositoryWrapper> _repositoryCache = new();
private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryCache));
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryCache));
private bool _disposedValue;

public RepositoryWrapper GetRepository(string rootFolder)
Expand Down
Loading

0 comments on commit c0936f9

Please sign in to comment.