diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Helpers/GitCommandRunnerResultInfo.cs b/extensions/GitExtension/FileExplorerGitIntegration/Helpers/GitCommandRunnerResultInfo.cs index 036277d76..7fc78b51b 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Helpers/GitCommandRunnerResultInfo.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Helpers/GitCommandRunnerResultInfo.cs @@ -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; } } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs index 053f01868..fc1c64c2d 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs @@ -1,32 +1,15 @@ // 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 _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; @@ -34,40 +17,10 @@ internal sealed class CommitLogCache 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) @@ -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) { @@ -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; - } } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs index a02557713..4ce5de58d 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs @@ -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; } @@ -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; diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExeceutableConfigOptions.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExeceutableConfigOptions.cs index 93780fc11..e4bced094 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExeceutableConfigOptions.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExeceutableConfigOptions.cs @@ -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 diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs index 71629e7bf..c233f950e 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs @@ -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); } } } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs index d8578adaa..f0e428a46 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs @@ -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; @@ -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 { diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepositoryProviderFactory.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepositoryProviderFactory.cs index 905eaadc0..ad62fc427 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepositoryProviderFactory.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepositoryProviderFactory.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using DevHome.Common.Services; -using LibGit2Sharp; using Microsoft.Windows.DevHome.SDK; using Serilog; @@ -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}"); diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitRepositoryStatus.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitRepositoryStatus.cs index 8f898bf2d..bb4c8a901 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitRepositoryStatus.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitRepositoryStatus.cs @@ -11,6 +11,18 @@ internal sealed class GitRepositoryStatus private readonly Dictionary _submoduleEntries = new(); private readonly Dictionary> _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()); diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryCache.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryCache.cs index 72e8e305c..32310a3dc 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryCache.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryCache.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; -using LibGit2Sharp; using Serilog; namespace FileExplorerGitIntegration.Models; @@ -11,7 +10,7 @@ namespace FileExplorerGitIntegration.Models; internal sealed class RepositoryCache : IDisposable { private readonly ConcurrentDictionary _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) diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs index d4314948a..39d3209b4 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Concurrent; -using System.Data; using System.Globalization; -using System.Management.Automation; using DevHome.Common.Services; using LibGit2Sharp; using Microsoft.Windows.DevHome.SDK; @@ -14,7 +11,7 @@ namespace FileExplorerGitIntegration.Models; internal sealed class RepositoryWrapper : IDisposable { - private readonly Repository _repo; + private readonly GitDetect _gitDetect = new(); private readonly ReaderWriterLockSlim _repoLock = new(); private readonly string _workingDirectory; @@ -40,15 +37,18 @@ internal sealed class RepositoryWrapper : IDisposable private readonly string _submoduleStatusStaged; private readonly string _submoduleStatusUntracked; - private Commit? _head; + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryWrapper)); + + private string? _head; private CommitLogCache? _commits; private bool _disposedValue; public RepositoryWrapper(string rootFolder) { - _repo = new Repository(rootFolder); - _workingDirectory = _repo.Info.WorkingDirectory; + _gitDetect.DetectGit(); + ValidateGitRepositoryRootPath(rootFolder); + _workingDirectory = string.Concat(rootFolder, Path.DirectorySeparatorChar.ToString()); _statusCache = new StatusCache(rootFolder); _folderStatusBranch = _stringResource.GetLocalized("FolderStatusBranch"); @@ -70,6 +70,36 @@ public RepositoryWrapper(string rootFolder) _submoduleStatusUntracked = _stringResource.GetLocalized("SubmoduleStatusUntracked"); } + public void ValidateGitRepositoryRootPath(string rootFolder) + { + var validateGitRootRepo = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), rootFolder, "rev-parse --show-toplevel"); + var output = validateGitRootRepo.Output; + if (validateGitRootRepo.Status != ProviderOperationStatus.Success || output is null || output.Contains("fatal: not a git repository")) + { + _log.Error(validateGitRootRepo.Ex, $"Failed to validate the git root repository using GitExecute. RootFolder: {rootFolder} Git output: {output} Process Error Code: {validateGitRootRepo.ProcessExitCode}"); + throw validateGitRootRepo.Ex ?? new ArgumentException($"Not a valid git repository root path: RootFolder: {rootFolder} Git output: {output}"); + } + + if (WslIntegrator.IsWSLRepo(rootFolder)) + { + var normalizedLinuxPath = WslIntegrator.GetNormalizedLinuxPath(rootFolder); + if (output.TrimEnd('\n') != normalizedLinuxPath) + { + _log.Error($"Not a valid WSL git repository root path: {rootFolder}"); + throw new ArgumentException($"Not a valid WSL git repository root path: {rootFolder}"); + } + + return; + } + + var normalizedRootFolderPath = rootFolder.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (output.TrimEnd('\n') != normalizedRootFolderPath) + { + _log.Error($"Not a valid git repository root path: {rootFolder}"); + throw new ArgumentException($"Not a valid git repository root path: {rootFolder}"); + } + } + public CommitWrapper? FindLastCommit(string relativePath) { // Fetching the most recent status to check if the file is renamed @@ -81,13 +111,24 @@ public RepositoryWrapper(string rootFolder) private CommitLogCache GetCommitLogCache() { - // Fast path: if we have an up-to-date commit log, return that + var result = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), _workingDirectory, "rev-parse HEAD"); + if (result.Status != ProviderOperationStatus.Success) + { + throw result.Ex ?? new InvalidOperationException(result.ProcessExitCode?.ToString(CultureInfo.InvariantCulture) ?? "Unknown error while obtaining HEAD commit"); + } + + string? head = result.Output?.Trim(); + if (string.IsNullOrEmpty(head)) + { + throw new InvalidOperationException("Git command output is null or the repository has no commits"); + } + if (_head != null && _commits != null) { _repoLock.EnterReadLock(); try { - if (_repo.Head.Tip == _head) + if (head == _head) { return _commits; } @@ -102,10 +143,10 @@ private CommitLogCache GetCommitLogCache() _repoLock.EnterWriteLock(); try { - if (_head == null || _commits == null || _repo.Head.Tip != _head) + if (_head == null || _commits == null || head != _head) { - _commits = new CommitLogCache(_repo); - _head = _repo.Head.Tip; + _commits = new CommitLogCache(_workingDirectory); + _head = head; } } finally @@ -125,13 +166,13 @@ public string GetRepoStatus(string relativePath) try { _repoLock.EnterWriteLock(); - branchName = _repo.Info.IsHeadDetached ? - string.Format(CultureInfo.CurrentCulture, _folderStatusDetached, _repo.Head.Tip.Sha[..7]) : - string.Format(CultureInfo.CurrentCulture, _folderStatusBranch, _repo.Head.FriendlyName); - if (_repo.Head.IsTracking) + branchName = repoStatus.IsHeadDetached ? + string.Format(CultureInfo.CurrentCulture, _folderStatusDetached, repoStatus.Sha[..7]) : + string.Format(CultureInfo.CurrentCulture, _folderStatusBranch, repoStatus.BranchName); + if (repoStatus.UpstreamBranch != string.Empty) { - var behind = _repo.Head.TrackingDetails.BehindBy; - var ahead = _repo.Head.TrackingDetails.AheadBy; + var behind = repoStatus.BehindBy; + var ahead = repoStatus.AheadBy; if (behind == 0 && ahead == 0) { branchStatus = " ≡"; @@ -296,7 +337,6 @@ internal void Dispose(bool disposing) { if (disposing) { - _repo.Dispose(); _repoLock.Dispose(); _statusCache.Dispose(); } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/StatusCache.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/StatusCache.cs index 6019829cf..a1a7ec39b 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/StatusCache.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/StatusCache.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Data; using System.Diagnostics; +using System.Globalization; using System.Runtime.InteropServices; -using System.Threading.Channels; using LibGit2Sharp; -using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Windows.DevHome.SDK; using Serilog; using Windows.Win32; @@ -25,7 +23,7 @@ internal sealed class StatusCache : IDisposable private readonly ReaderWriterLockSlim _statusLock = new(); private readonly GitDetect _gitDetect = new(); private readonly bool _gitInstalled; - private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(StatusCache)); + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(StatusCache)); private GitRepositoryStatus? _status; private bool _disposedValue; @@ -199,7 +197,23 @@ private void UpdateStatus(GitRepositoryStatus newStatus) // Disclaimer: I'm not sure how far back porcelain=v2 is supported, but I'm pretty sure it's at least 3-4 years. // There could be old Git installations that predate it. // -z : Terminate filenames and entries with NUL instead of space/LF. This helps us deal with filenames containing spaces. - var result = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), workingDirectory, "--no-optional-locks status --porcelain=v2 -z"); + var result = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), workingDirectory, "--no-optional-locks status --porcelain=v2 --branch -z"); + if (result.Status != ProviderOperationStatus.Success) + { + return null; + } + + return result.Output; + } + + private string? RetrieveBranchInformation(string workingDirectory) + { + // Options fully explained at https://git-scm.com/docs/git-branch + // --no-optional-locks : Since this we are essentially running in the background, don't take any optional git locks + // that could interfere with the user's work. This means calling "branch" won't auto-update the + // index to make future "branch" calls faster, but it's better to be unintrusive. + // --show--current : Show the current branch name. This is the only option we care about. + var result = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), workingDirectory, "--no-optional-locks branch --show-current"); if (result.Status != ProviderOperationStatus.Success) { return null; @@ -234,6 +248,7 @@ private GitRepositoryStatus RetrieveStatus() { var repoStatus = new GitRepositoryStatus(); ParseStatus(RetrieveStatusFromDirectory(_workingDirectory), repoStatus); + ParseBranchInformation(RetrieveBranchInformation(_workingDirectory), repoStatus); return repoStatus; } @@ -292,6 +307,31 @@ private void ParseStatus(string? statusString, GitRepositoryStatus repoStatus, s var filePath = Path.Combine(relativeDir, line.Substring(2)).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); repoStatus.Add(filePath, new GitStatusEntry(filePath, FileStatus.NewInWorkdir)); } + else if (line.StartsWith("# branch.oid ", StringComparison.Ordinal)) + { + // For porcelain=v2, the branch status line has the following format: + // # branch.oid + // For now, we only care about the . + repoStatus.Sha = line.Split(' ')[2]; + } + else if (line.StartsWith("# branch.ab ", StringComparison.Ordinal)) + { + // For porcelain=v2, the branch status line has the following format: + // # branch.ab + - + // For now, we only care about the and . + var pieces = line.Split(' ', 4); + var aheadBy = int.Parse(pieces[2][1..], CultureInfo.InvariantCulture); + var behindBy = int.Parse(pieces[3][1..], CultureInfo.InvariantCulture); + repoStatus.AheadBy = aheadBy; + repoStatus.BehindBy = behindBy; + } + else if (line.StartsWith("# branch.upstream ", StringComparison.Ordinal)) + { + // For porcelain=v2, the branch status line has the following format: + // # branch.upstream + // For now, we only care about the . + repoStatus.UpstreamBranch = line.Split(' ')[2]; + } else { _log.Warning($"Encountered unexpected line in ParseStatus: {line}"); @@ -299,6 +339,18 @@ private void ParseStatus(string? statusString, GitRepositoryStatus repoStatus, s } } + private void ParseBranchInformation(string? branchInfo, GitRepositoryStatus repoStatus) + { + if (string.IsNullOrEmpty(branchInfo)) + { + repoStatus.IsHeadDetached = true; + return; + } + + repoStatus.BranchName = branchInfo.TrimEnd('\n'); + repoStatus.IsHeadDetached = false; + } + private void ParseOrdinaryStatusEntry( GitRepositoryStatus repoStatus, string fileStatusString, diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/ThrottledTask.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/ThrottledTask.cs index dffae6e5c..b23733fd5 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/ThrottledTask.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/ThrottledTask.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Diagnostics; - namespace FileExplorerGitIntegration.Models; internal sealed class ThrottledTask