Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Git command line changes for WSL #3893

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With so many nullable parameters can you split the constructor into multiple constructors so callers can more easily use this object.

I don't like calling constructors with multiple nullable parameters because the constructor does not tell me what combination of values ill throw an exception.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding more constructors instead of more nullable parameters.

{
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
Log.Error("Execute Git process exited unsuccessfully with exit code {ExitCode}", process.ExitCode);
Log.Error($"Execute Git process exited unsuccessfully with exit code {process.ExitCode}");

return new GitCommandRunnerResultInfo(ProviderOperationStatus.Failure, output, "Execute Git process exited unsuccessfully", string.Empty, null, arguments, process.ExitCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: exited with a failure error code "Exit unsuccessfully" tells me that the process was unsuccessful in exiting.

}

return new GitCommandRunnerResultInfo(ProviderOperationStatus.Success, output);
}
else
{
Log.Error("Failed to start the Git process: process is null");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: remove process is null The definition of a null process is one that did not start.

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}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe this needs to be changed? Do we still tell the user "Repo not found"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repository not found.

}
catch (Exception ex)
{
_log.Error("GitLocalRepositoryProviderFactory", "Failed to create GitLocalRepository", ex);
_log.Error(ex, "GitLocalRepositoryProviderFactory: Failed to create GitLocalRepository");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: nameof(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
Loading