From e343d318a7d849459d064f095ed61a31c07467a0 Mon Sep 17 00:00:00 2001
From: Todd Harrington <65243291+StellarTodd@users.noreply.github.com>
Date: Thu, 4 Sep 2025 04:13:43 -0600
Subject: [PATCH] Improved locating the git repository root
Added support projects where the solution file is not in the git repository.
Added support for working on files without a solution loaded.
Added support for worktrees and submodules (".git" is a file rather than directory)
Commit 5ef6f85365d31fe1ed3eb29b6d72a5d9c8e8cefa
Changed GetSolutionPath to return the repository root.
This was done by looking for ".git", however; it only checked for a directory by that name.
When using submodules and worktrees, ".git" is a file not a directory.
This also changed the behavior of the function without changing its name which is confusing.
Both of these issues were resolved.
PR #21 - Git worktree support
The above fix works for submodules and worktrees and thus these changes were no longer needed.
This removes a ton of code an unnecessary complexity.
PR #29 - Support for VS2022 on ARM
The code does not build with this change.
In order to get the code to build, NuGet packages would need to be updated.
I am not sure if that would break the VS2019 version, but that seems likely.
This change was removed in order to build the code.
PR #30 - Fix for auto-generated solutions such as CMake
Added an unnecessary preference setting.
Used the current file for "repository" commands, and thus did not work correctly.
PR #30 has been superseded by this commit.
Added GetRepositoryRootPath() to retrieve the git repository root path.
This searches using the current file, if there is one, then using the solution file if one is open.
Updated the code to GetRepositoryRootPath() in all locations that need the repository root.
Updated variables for correctness and clarity (i.e. gitRepoPath instead of solutionPath)
Updated error message when a git repository root cannot be found.
---
.../Config/Constants/PathConfiguration.cs | 135 +++++-------------
.../Resources/Resources.Designer.cs | 93 +++++++-----
.../Resources/Resources.resx | 134 ++++++++---------
.../Services/TortoiseGitLauncherService.cs | 24 ++--
.../TortoiseGitToolbar.Shared.projitems | 1 -
.../TortoiseGitToolbarPackage.cs | 1 -
.../TortoiseGitLauncherServiceTests.cs | 43 +++---
.../source.extension.vsixmanifest | 3 -
8 files changed, 193 insertions(+), 241 deletions(-)
diff --git a/TortoiseGitToolbar.Shared/Config/Constants/PathConfiguration.cs b/TortoiseGitToolbar.Shared/Config/Constants/PathConfiguration.cs
index 61b01f0..3878c34 100644
--- a/TortoiseGitToolbar.Shared/Config/Constants/PathConfiguration.cs
+++ b/TortoiseGitToolbar.Shared/Config/Constants/PathConfiguration.cs
@@ -1,9 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
-using System.Text;
using EnvDTE80;
-using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;
namespace MattDavies.TortoiseGitToolbar.Config.Constants
@@ -14,8 +12,6 @@ public static class PathConfiguration
private const string TortoiseGitx86 = @"C:\Program Files (x86)\TortoiseGit\bin\TortoiseGitProc.exe";
private const string GitBashx86 = @"C:\Program Files (x86)\Git\bin\sh.exe";
private const string GitBashx64 = @"C:\Program Files\Git\bin\sh.exe";
- private const string GitExex86 = @"C:\Program Files (x86)\Git\bin\git.exe";
- private const string GitExex64 = @"C:\Program Files\Git\bin\git.exe";
private static readonly RegistryKey TortoiseGitProcRegistryRoot = Registry.LocalMachine;
private const string TortoiseGitProcRegistryPath = @"SOFTWARE\TortoiseGit";
@@ -47,111 +43,62 @@ public static string GetGitBashPath()
: null;
}
- public static string GetGitExePath()
+ public static string GetSolutionFilePath(Solution2 solution)
{
- var path = GetGitExePathFromRegistry();
- if (path != null)
- return path;
- return File.Exists(GitExex64) ? GitExex64
- : File.Exists(GitExex86) ? GitExex86
- : null;
- }
-
- public static string GetSolutionPath(Solution2 solution)
- {
- if (solution != null && solution.IsOpen)
+ if (solution != null)
{
- var solutionPathFromSln = Path.GetDirectoryName(solution.FullName);
- Debug.WriteLine("Solution path is: " + solutionPathFromSln);
-
- var repositoryRootPath = GetRepositoryRootGit(solutionPathFromSln);
- if (repositoryRootPath == null)
- {
- Debug.WriteLine("Failed to get root path from git. Trying to filesystem-based approach");
- repositoryRootPath = GetRepositoryRootFs(solutionPathFromSln);
- if (repositoryRootPath == null)
- {
- Debug.WriteLine("No parent folder found. Using original path: " + solutionPathFromSln);
- return solutionPathFromSln;
- }
- }
-
- Debug.WriteLine("Using solution path: " + repositoryRootPath);
- return repositoryRootPath;
+ return solution.FullName;
}
-
return null;
}
- ///
- /// Find repository root by calling "git rev-parse --show-toplevel" command
- ///
- /// Path inside repository (working path for git command)
- /// Path to repository root, if found. Otherwise null.
- private static string GetRepositoryRootGit(string solutionPath)
+ private static string FindGitRepositoryRootPathFromFilePath(string filePath)
{
- var gitPath = GetGitExePath();
- if (gitPath == null)
- {
- return null;
- }
+ if (filePath == null) return null;
- var procInfo = new ProcessStartInfo
- {
- FileName = gitPath,
- Arguments = "rev-parse --show-toplevel",
- UseShellExecute = false,
- WorkingDirectory = solutionPath,
- RedirectStandardOutput = true,
- CreateNoWindow = true,
- };
+ FileInfo fileInfo = new FileInfo(filePath);
+ DirectoryInfo dirInfo = fileInfo.Directory;
- using (var process = Process.Start(procInfo))
+ while (dirInfo != null && dirInfo.Exists)
{
- var stdOut = process.StandardOutput.ReadToEnd();
- process.WaitForExit();
- var errCode = process.ExitCode;
-
- Debug.WriteLine($"git rev-parse --show-toplevel exited with code {errCode} and stdout: {stdOut}");
- if (errCode != 0 || string.IsNullOrWhiteSpace(stdOut))
+ string gitPath = dirInfo.FullName + "/.git";
+ // Check for a .git directory
+ if (Directory.Exists(gitPath))
{
- return null;
+ return dirInfo.FullName;
}
-
- try
+ // Check for a .git file (sub-module or work tree)
+ if (File.Exists(gitPath))
{
- return Path.GetFullPath(stdOut.Trim());
- }
- catch (Exception e)
- {
- Debug.WriteLine($"GetFullPath failed for {stdOut}, with {e} reason: {e.Message}");
- return null;
+ return dirInfo.FullName;
}
+ // git repo not found, look at the parent level.
+ dirInfo = dirInfo.Parent;
}
+
+ return null;
}
- ///
- /// Find repository root basing on filesystem by finding parent directory that contains .git folder
- ///
- /// Directory to start search from.
- /// Path to repository root, if found. Otherwise null.
- private static string GetRepositoryRootFs(string solutionPath)
+ public static string GetRepositoryRootPath(Solution2 solution)
{
- var solutionPathInfo = new DirectoryInfo(solutionPath);
-
- // find parent folder that holds the .git folder
- while (!Directory.Exists(Path.Combine(solutionPathInfo.FullName, ".git")))
+ string selectedFile = GetOpenedFilePath(solution);
+ if (!String.IsNullOrEmpty(selectedFile))
{
- Debug.WriteLine("No .git folder found in solution path.");
- if (solutionPathInfo.Parent == null)
+ var selectedInfo = new FileInfo(selectedFile);
+ string gitPath = FindGitRepositoryRootPathFromFilePath(selectedFile);
+ if (!String.IsNullOrEmpty(gitPath))
{
- return null;
+ return gitPath;
}
+ }
- solutionPathInfo = solutionPathInfo.Parent;
+ string solutionFile = GetSolutionFilePath(solution);
+ if (!String.IsNullOrEmpty(solutionFile))
+ {
+ return FindGitRepositoryRootPathFromFilePath(solutionFile);
}
- return solutionPathInfo.FullName;
+ return null;
}
public static string GetOpenedFilePath(Solution2 solution)
@@ -213,16 +160,6 @@ public static string GetTortoiseGitPathFromRegistry()
}
public static string GetGitBashPathFromRegistry()
- {
- return GetGitExecutablePathFromRegisty("sh.exe");
- }
-
- public static string GetGitExePathFromRegistry()
- {
- return GetGitExecutablePathFromRegisty("git.exe");
- }
-
- private static string GetGitExecutablePathFromRegisty(string executableName)
{
var path = GetValueFromRegistry(GitBashRegistryRoot, GitBashRegistryPath, GitBashRegistryKeyName);
Debug.WriteLine("Git bash path from registry: " + (path ?? "(null)"));
@@ -231,11 +168,11 @@ private static string GetGitExecutablePathFromRegisty(string executableName)
{
Debug.WriteLine("Git bash path from registry exists.");
- var exePath = Path.Combine(path, executableName);
- if (File.Exists(exePath))
+ var shPath = Path.Combine(path, "sh.exe");
+ if (File.Exists(shPath))
{
- Debug.WriteLine($"Git bash path {executableName} exists: " + exePath);
- return exePath;
+ Debug.WriteLine("Git bash path sh.exe exists: " + shPath);
+ return shPath;
}
}
diff --git a/TortoiseGitToolbar.Shared/Resources/Resources.Designer.cs b/TortoiseGitToolbar.Shared/Resources/Resources.Designer.cs
index c81967d..f7398c9 100644
--- a/TortoiseGitToolbar.Shared/Resources/Resources.Designer.cs
+++ b/TortoiseGitToolbar.Shared/Resources/Resources.Designer.cs
@@ -8,10 +8,11 @@
//
//------------------------------------------------------------------------------
-namespace MattDavies.TortoiseGitToolbar.Resources {
+namespace MattDavies.TortoiseGitToolbar.Resources
+{
using System;
-
-
+
+
///
/// A strongly-typed resource class, for looking up localized strings, etc.
///
@@ -22,97 +23,117 @@ namespace MattDavies.TortoiseGitToolbar.Resources {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resources {
-
+ internal class Resources
+ {
+
private static global::System.Resources.ResourceManager resourceMan;
-
+
private static global::System.Globalization.CultureInfo resourceCulture;
-
+
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resources() {
+ internal Resources()
+ {
}
-
+
///
/// Returns the cached ResourceManager instance used by this class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if (object.ReferenceEquals(resourceMan, null))
+ {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MattDavies.TortoiseGitToolbar.Resources.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
-
+
///
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
return resourceCulture;
}
- set {
+ set
+ {
resourceCulture = value;
}
}
-
+
///
/// Looks up a localized string similar to Could not find Git Bash in the registry or the standard install path..
///
- internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_Git_Bash_in_the_standard_install_path_ {
- get {
+ internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_Git_Bash_in_the_standard_install_path_
+ {
+ get
+ {
return ResourceManager.GetString("TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_Git_Bash_in_the_sta" +
"ndard_install_path_", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Could not find TortoiseGit in the registry or the standard install path..
///
- internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_TortoiseGit_in_the_standard_install_path_ {
- get {
+ internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_TortoiseGit_in_the_standard_install_path_
+ {
+ get
+ {
return ResourceManager.GetString("TortoiseGitLauncherService_ExecuteTortoiseProc_Could_not_find_TortoiseGit_in_the_" +
"standard_install_path_", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to Git Bash not found.
///
- internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Git_Bash_not_found {
- get {
+ internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_Git_Bash_not_found
+ {
+ get
+ {
return ResourceManager.GetString("TortoiseGitLauncherService_ExecuteTortoiseProc_Git_Bash_not_found", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to TortoiseGit not found.
///
- internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_TortoiseGit_not_found {
- get {
+ internal static string TortoiseGitLauncherService_ExecuteTortoiseProc_TortoiseGit_not_found
+ {
+ get
+ {
return ResourceManager.GetString("TortoiseGitLauncherService_ExecuteTortoiseProc_TortoiseGit_not_found", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to No solution found..
///
- internal static string TortoiseGitLauncherService_SolutionPath_No_solution_found {
- get {
- return ResourceManager.GetString("TortoiseGitLauncherService_SolutionPath_No_solution_found", resourceCulture);
+ internal static string TortoiseGitLauncherService_GitRepositoryNotFoundCaption
+ {
+ get
+ {
+ return ResourceManager.GetString("TortoiseGitLauncherService_GitRepositoryNotFoundCaption", resourceCulture);
}
}
-
+
///
/// Looks up a localized string similar to You need to open a solution first!.
///
- internal static string TortoiseGitLauncherService_SolutionPath_You_need_to_open_a_solution_first {
- get {
- return ResourceManager.GetString("TortoiseGitLauncherService_SolutionPath_You_need_to_open_a_solution_first", resourceCulture);
+ internal static string TortoiseGitLauncherService_GitRepositoryNotFound
+ {
+ get
+ {
+ return ResourceManager.GetString("TortoiseGitLauncherService_GitRepositoryNotFound", resourceCulture);
}
}
}
diff --git a/TortoiseGitToolbar.Shared/Resources/Resources.resx b/TortoiseGitToolbar.Shared/Resources/Resources.resx
index 9838cb5..8cef045 100644
--- a/TortoiseGitToolbar.Shared/Resources/Resources.resx
+++ b/TortoiseGitToolbar.Shared/Resources/Resources.resx
@@ -1,6 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- You need to open a solution first!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ You need to open a file or solution that resides inside a git repository!
-
- No solution found.
+
+ Git repository not found.
-
+
Could not find Git Bash in the registry or the standard install path.
-
+
Git Bash not found
-
+
Could not find TortoiseGit in the registry or the standard install path.
-
+
TortoiseGit not found
\ No newline at end of file
diff --git a/TortoiseGitToolbar.Shared/Services/TortoiseGitLauncherService.cs b/TortoiseGitToolbar.Shared/Services/TortoiseGitLauncherService.cs
index 6627619..c18161f 100644
--- a/TortoiseGitToolbar.Shared/Services/TortoiseGitLauncherService.cs
+++ b/TortoiseGitToolbar.Shared/Services/TortoiseGitLauncherService.cs
@@ -1,4 +1,6 @@
-using System.Diagnostics;
+using System;
+using System.Diagnostics;
+using System.IO;
using System.Windows.Forms;
using EnvDTE80;
using MattDavies.TortoiseGitToolbar.Config.Constants;
@@ -24,8 +26,8 @@ public TortoiseGitLauncherService(IProcessManagerService processManagerService,
public void ExecuteTortoiseProc(ToolbarCommand command)
{
- var solutionPath = PathConfiguration.GetSolutionPath(_solution);
var openedFilePath = PathConfiguration.GetOpenedFilePath(_solution);
+ var gitRepoPath = PathConfiguration.GetRepositoryRootPath(_solution);
// todo: make the bash/tortoise paths configurable
// todo: detect if the solution is a git solution first
if (command == ToolbarCommand.Bash && PathConfiguration.GetGitBashPath() == null)
@@ -38,11 +40,11 @@ public void ExecuteTortoiseProc(ToolbarCommand command)
);
return;
}
- if (command != ToolbarCommand.Bash && solutionPath == null)
+ if (command != ToolbarCommand.Bash && gitRepoPath == null)
{
MessageBox.Show(
- Resources.Resources.TortoiseGitLauncherService_SolutionPath_You_need_to_open_a_solution_first,
- Resources.Resources.TortoiseGitLauncherService_SolutionPath_No_solution_found,
+ Resources.Resources.TortoiseGitLauncherService_GitRepositoryNotFound,
+ Resources.Resources.TortoiseGitLauncherService_GitRepositoryNotFoundCaption,
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation
);
@@ -59,9 +61,6 @@ public void ExecuteTortoiseProc(ToolbarCommand command)
return;
}
- var preferredPath = Config.GlobalConfig.PreferFileOverSolution ? openedFilePath : solutionPath;
-
-
ProcessStartInfo process;
switch (command)
{
@@ -69,17 +68,16 @@ public void ExecuteTortoiseProc(ToolbarCommand command)
process = _processManagerService.GetProcess(
PathConfiguration.GetGitBashPath(),
"--login -i",
- preferredPath
+ gitRepoPath
);
break;
case ToolbarCommand.RebaseContinue:
process = _processManagerService.GetProcess(
PathConfiguration.GetGitBashPath(),
@"--login -i -c 'echo; echo ""Running git rebase --continue""; echo; git rebase --continue; echo; echo ""Please review the output above and press enter to continue.""; read'",
- preferredPath
+ gitRepoPath
);
break;
- case ToolbarCommand.Log:
case ToolbarCommand.FileLog:
case ToolbarCommand.FileDiff:
var commandParam = command.ToString().Replace("File", string.Empty).ToLower();
@@ -98,13 +96,13 @@ public void ExecuteTortoiseProc(ToolbarCommand command)
case ToolbarCommand.StashList:
process = _processManagerService.GetProcess(
PathConfiguration.GetTortoiseGitPath(),
- string.Format(@"/command:reflog /path:""{0}"" /ref:""refs/stash""", preferredPath)
+ string.Format(@"/command:reflog /path:""{0}"" /ref:""refs/stash""", gitRepoPath)
);
break;
default:
process = _processManagerService.GetProcess(
PathConfiguration.GetTortoiseGitPath(),
- string.Format(@"/command:{0} /path:""{1}""", command.ToString().ToLower(), preferredPath)
+ string.Format(@"/command:{0} /path:""{1}""", command.ToString().ToLower(), gitRepoPath)
);
break;
}
diff --git a/TortoiseGitToolbar.Shared/TortoiseGitToolbar.Shared.projitems b/TortoiseGitToolbar.Shared/TortoiseGitToolbar.Shared.projitems
index dad08d9..756ca44 100644
--- a/TortoiseGitToolbar.Shared/TortoiseGitToolbar.Shared.projitems
+++ b/TortoiseGitToolbar.Shared/TortoiseGitToolbar.Shared.projitems
@@ -11,7 +11,6 @@
-
True
True
diff --git a/TortoiseGitToolbar.Shared/TortoiseGitToolbarPackage.cs b/TortoiseGitToolbar.Shared/TortoiseGitToolbarPackage.cs
index ee7ffff..479fb51 100644
--- a/TortoiseGitToolbar.Shared/TortoiseGitToolbarPackage.cs
+++ b/TortoiseGitToolbar.Shared/TortoiseGitToolbarPackage.cs
@@ -14,7 +14,6 @@ namespace MattDavies.TortoiseGitToolbar
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageConstants.GuidTortoiseGitToolbarPkgString)]
[ProvideKeyBindingTable(PackageConstants.GuidTortoiseGitToolbarPkgString, 110)]
- [ProvideOptionPage(typeof(Config.SettingsDialog), Config.GlobalConfig.ExtensionName, "General", 0, 0, true)]
public sealed class TortoiseGitToolbarPackage : Package
{
private OleMenuCommandService _commandService;
diff --git a/TortoiseGitToolbar.UnitTests/Services/TortoiseGitLauncherServiceTests.cs b/TortoiseGitToolbar.UnitTests/Services/TortoiseGitLauncherServiceTests.cs
index 73d7bb1..7b86626 100644
--- a/TortoiseGitToolbar.UnitTests/Services/TortoiseGitLauncherServiceTests.cs
+++ b/TortoiseGitToolbar.UnitTests/Services/TortoiseGitLauncherServiceTests.cs
@@ -15,7 +15,7 @@ namespace TortoiseGitToolbar.UnitTests.Services
[Collection(MockedVS.Collection)]
public class TortoiseGitLauncherServiceShould
{
- public static IEnumerable