Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect/install command-line git and invoke #3419

Merged
Show file tree
Hide file tree
Changes from 3 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
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics;
using FileExplorerGitIntegration.Models;
using LibGit2Sharp;

namespace FileExplorerGitIntegration.UnitTest;

[TestClass]
public class GitCommandRunnerTests
{
private GitDetect GitDetector { get; set; } = new();

private static string RepoPath => Path.Combine(Path.GetTempPath(), "GitTestRepository");

[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
Debug.WriteLine("ClassInitialize");
const string url = "http://github.com/libgit2/TestGitRepository";
try
{
_ = Repository.Clone(url, RepoPath);
}
catch (NameConflictException)
{
// Clean stale test state and try again
if (Directory.Exists(RepoPath))
{
// Cloning the repo leads to files that are hidden and readonly (such as under the .git directory).
// Therefore, change the attribute so they can be deleted
var repoDirectory = new DirectoryInfo(RepoPath)
{
Attributes = System.IO.FileAttributes.Normal,
};

foreach (var dirInfo in repoDirectory.GetFileSystemInfos("*", SearchOption.AllDirectories))
{
dirInfo.Attributes = System.IO.FileAttributes.Normal;
}

Directory.Delete(RepoPath, true);
}

_ = Repository.Clone(url, RepoPath);
}
}

[ClassCleanup]
public static void ClassCleanup()
{
Debug.WriteLine("ClassCleanup");
if (Directory.Exists(RepoPath))
{
// Cloning the repo leads to files that are hidden and readonly (such as under the .git directory).
// Therefore, change the attribute so they can be deleted
var repoDirectory = new DirectoryInfo(RepoPath)
{
Attributes = FileAttributes.Normal,
};

foreach (var dirInfo in repoDirectory.GetFileSystemInfos("*", SearchOption.AllDirectories))
{
dirInfo.Attributes = FileAttributes.Normal;
}

Directory.Delete(RepoPath, true);
}
}

[TestMethod]
public void TestBasicInvokeGitFunctionality()
{
var isGitInstalled = GitDetector.DetectGit();
if (!isGitInstalled)
{
Assert.Inconclusive("Git is not installed. Test cannot run in this case.");
return;
}

var result = GitExecute.ExecuteGitCommand(GitDetector.GitConfiguration.ReadInstallPath(), RepoPath, "--version");
Assert.IsNotNull(result.Output);
Assert.IsTrue(result.Output.Contains("git version"));
}

[TestMethod]
public void TestInvokeGitFunctionalityForRawStatus()
{
var isGitInstalled = GitDetector.DetectGit();
if (!isGitInstalled)
{
Assert.Inconclusive("Git is not installed. Test cannot run in this case.");
return;
}

var result = GitExecute.ExecuteGitCommand(GitDetector.GitConfiguration.ReadInstallPath(), RepoPath, "status");
Assert.IsNotNull(result.Output);
Assert.IsTrue(result.Output.Contains("On branch"));
}

[TestCleanup]
public void TestCleanup()
{
if (File.Exists(Path.Combine(Path.GetTempPath(), "GitConfiguration.json")))
{
File.Delete(Path.Combine(Path.GetTempPath(), "GitConfiguration.json"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Windows.DevHome.SDK;

namespace FileExplorerGitIntegration.Helpers;

public class GitCommandRunnerResultInfo
{
public ProviderOperationStatus Status { get; set; }

public string? Output { get; set; }

public string? DisplayMessage { get; set; }

public string? DiagnosticText { get; set; }

public Exception? Ex { get; set; }

public string? Arguments { get; set; }

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

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

using DevHome.Common.Helpers;
using DevHome.Common.Services;
using Serilog;
using Windows.Storage;

namespace FileExplorerGitIntegration.Models;

public class GitConfiguration : IDisposable
{
public GitExecutableConfigOptions GitExecutableConfigOptions { get; set; }

private readonly FileService fileService;

private string GitExeInstallPath { get; set; } = string.Empty;

private readonly object fileLock = new();

private readonly ILogger log = Log.ForContext<GitDetect>();

private readonly string tempConfigurationFileName = "TemporaryGitConfiguration.json";

public GitConfiguration(string? path)
{
if (RuntimeHelper.IsMSIX)
{
GitExecutableConfigOptions = new GitExecutableConfigOptions
ssparach marked this conversation as resolved.
Show resolved Hide resolved
{
GitExecutableConfigFolderPath = ApplicationData.Current.LocalFolder.Path,
};
}
else
{
GitExecutableConfigOptions = new GitExecutableConfigOptions
{
GitExecutableConfigFolderPath = path ?? string.Empty,
};
}

fileService = new FileService();
EnsureConfigFileCreation();
}

public string ReadInstallPath()
{
lock (fileLock)
{
GitExeInstallPath = fileService.Read<string>(GitExecutableConfigOptions.GitExecutableConfigFolderPath, GitExecutableConfigOptions.GitExecutableConfigFileName);
return GitExeInstallPath;
}
}

public void EnsureConfigFileCreation()
{
lock (fileLock)
{
if (!Directory.Exists(GitExecutableConfigOptions.GitExecutableConfigFolderPath))
{
Directory.CreateDirectory(GitExecutableConfigOptions.GitExecutableConfigFolderPath);
}

var configFileFullPath = Path.Combine(GitExecutableConfigOptions.GitExecutableConfigFolderPath, GitExecutableConfigOptions.GitExecutableConfigFileName);
if (!File.Exists(configFileFullPath))
{
fileService.Save(GitExecutableConfigOptions.GitExecutableConfigFolderPath, GitExecutableConfigOptions.GitExecutableConfigFileName, string.Empty);
log.Information("The git configuration file did not exists and has just been created");
}
}
}

public bool IsGitExeInstallPathSet()
{
return !string.IsNullOrEmpty(GitExeInstallPath);
}

public bool StoreGitExeInstallPath(string path)
{
lock (fileLock)
dhoehna marked this conversation as resolved.
Show resolved Hide resolved
{
log.Information("Setting Git Exe Install Path");
GitExeInstallPath = path;

fileService.Save(GitExecutableConfigOptions.GitExecutableConfigFolderPath, tempConfigurationFileName, GitExeInstallPath);
File.Replace(Path.Combine(GitExecutableConfigOptions.GitExecutableConfigFolderPath, tempConfigurationFileName), Path.Combine(GitExecutableConfigOptions.GitExecutableConfigFolderPath, GitExecutableConfigOptions.GitExecutableConfigFileName), null);
log.Information("Git Exe Install Path stored successfully");
return true;
}
}

public void Dispose()
{
GC.SuppressFinalize(this);
}
}
119 changes: 119 additions & 0 deletions extensions/GitExtension/FileExplorerGitIntegration/Models/GitDetect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Win32;
using Microsoft.Windows.DevHome.SDK;
using Serilog;

namespace FileExplorerGitIntegration.Models;

public class GitDetect
{
public GitConfiguration GitConfiguration { get; set; }

private readonly ILogger log = Log.ForContext<GitDetect>();

public GitDetect()
{
GitConfiguration = new GitConfiguration(null);
}

public bool DetectGit()
{
var gitExeFound = false;

if (!gitExeFound)
{
// Check if git.exe is present in PATH environment variable
gitExeFound = ValidateGitConfigurationPath("git.exe");
if (gitExeFound)
{
GitConfiguration.StoreGitExeInstallPath("git.exe");
}
}

if (!gitExeFound)
{
// Check execution of git.exe by finding install location in registry keys
string[] registryPaths = { "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" };

foreach (var registryPath in registryPaths)
{
var gitPath = Registry.GetValue(registryPath, "InstallLocation", defaultValue: string.Empty) as string;
if (!string.IsNullOrEmpty(gitPath))
{
var paths = FindSubdirectories(gitPath);
gitExeFound = CheckForExeInPaths(paths);
if (gitExeFound)
{
break;
}
}
}
}

if (!gitExeFound)
{
// Search for git.exe in common file paths
var programFiles = Environment.GetEnvironmentVariable("ProgramFiles");
var programFilesX86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
string[] possiblePaths = { $"{programFiles}\\Git\\bin", $"{programFilesX86}\\Git\\bin", $"{programFiles}\\Git\\cmd", $"{programFilesX86}\\Git\\cmd" };
gitExeFound = CheckForExeInPaths(possiblePaths);
}

return gitExeFound;
}

private string[] FindSubdirectories(string installLocation)
{
try
{
if (Directory.Exists(installLocation))
{
return Directory.GetDirectories(installLocation);
}
else
{
log.Warning("Install location does not exist: {InstallLocation}", installLocation);
return Array.Empty<string>();
}
}
catch (Exception ex)
{
log.Warning(ex, "Failed to find subdirectories in install location: {InstallLocation}", installLocation);
return Array.Empty<string>();
}
}

private bool CheckForExeInPaths(string[] possiblePaths)
{
// Iterate through the possible paths to find the git.exe file
foreach (var path in possiblePaths.Where(x => !string.IsNullOrEmpty(x)))
{
var gitPath = Path.Combine(path, "git.exe");
var isValid = ValidateGitConfigurationPath(gitPath);

// If the git.exe file is found, store the install path and log the information
if (isValid)
{
GitConfiguration.StoreGitExeInstallPath(gitPath);
log.Information("Git Exe Install Path found");
return true;
}
}

log.Debug("Git.exe not found in paths examined");
return false;
}

public bool ValidateGitConfigurationPath(string path)
{
var result = GitExecute.ExecuteGitCommand(path, string.Empty, "--version");
if (result.Status == ProviderOperationStatus.Success && result.Output != null && result.Output.Contains("git version"))
{
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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
{
private const string GitExecutableConfigFileNameDefault = "GitConfiguration.json";

public string GitExecutableConfigFileName { get; set; } = GitExecutableConfigFileNameDefault;

private readonly string gitExecutableConfigFolderPathDefault = Path.Combine(Path.GetTempPath(), "FileExplorerGitIntegration");
DefaultRyan marked this conversation as resolved.
Show resolved Hide resolved

private string? gitExecutableConfigFolderPath;

public string GitExecutableConfigFolderPath
{
get => gitExecutableConfigFolderPath is null ? gitExecutableConfigFolderPathDefault : gitExecutableConfigFolderPath;
set => gitExecutableConfigFolderPath = string.IsNullOrEmpty(value) ? gitExecutableConfigFolderPathDefault : value;
}
}
Loading
Loading