diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml index d53fc75e..086f9b0a 100644 --- a/RMuseum/RMuseum.xml +++ b/RMuseum/RMuseum.xml @@ -24151,6 +24151,15 @@ user-secrets / environment variables, never committed to appsettings.json. + + + Full path to git.exe, e.g. "C:\Program Files\Git\cmd\git.exe". Optional — leave empty + to resolve "git" via PATH (with a fallback to common Git-for-Windows install + locations if that fails, see GitRepoPublisher.ResolveGitExecutable). Set this + explicitly if the process running this job has a PATH that doesn't include git for + any reason (e.g. a Windows service running under a service account with its own PATH). + + Thin wrapper around the native git CLI (not LibGit2Sharp) for the export job: sync a @@ -24185,6 +24194,24 @@ since last run — a normal, expected outcome on most nightly runs). + + + Resolves which git executable to launch, cached for the lifetime of this instance. + Prefers an explicitly configured path; otherwise tries "git" (PATH resolution via + Process/CreateProcess) and, only if that file genuinely doesn't exist anywhere on + PATH, falls back to checking the usual Git-for-Windows install locations directly — + covers the common case where the process that's running this job has a stale PATH + snapshot from before git was installed (e.g. Visual Studio/IIS Express started before + the PATH environment variable was updated). + + + + + checks every directory on this process's own PATH for the given executable, without + actually starting it — used only to decide whether to bother trying the Git-for-Windows + fallback locations + + keeps the auth token out of exception messages that might end up logged somewhere diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PublicDataExport.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PublicDataExport.cs index abc254b0..2a0520f0 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PublicDataExport.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PublicDataExport.cs @@ -155,6 +155,7 @@ namespace RMuseum.Services.Implementation PushEnabled = bool.TryParse(section["PushEnabled"], out var push) && push, GitUserName = section["GitUserName"], GitToken = section["GitToken"], + GitExecutablePath = section["GitExecutablePath"], }; } diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-TajikPublicDataExport.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-TajikPublicDataExport.cs index a99af26e..32939c93 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-TajikPublicDataExport.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-TajikPublicDataExport.cs @@ -139,6 +139,7 @@ namespace RMuseum.Services.Implementation PushEnabled = bool.TryParse(section["PushEnabled"], out var push) && push, GitUserName = section["GitUserName"], GitToken = section["GitToken"], + GitExecutablePath = section["GitExecutablePath"], }; } diff --git a/RMuseum/Utils/PublicDataExport/GitRepoPublisher.cs b/RMuseum/Utils/PublicDataExport/GitRepoPublisher.cs index f1e2325a..e898e9aa 100644 --- a/RMuseum/Utils/PublicDataExport/GitRepoPublisher.cs +++ b/RMuseum/Utils/PublicDataExport/GitRepoPublisher.cs @@ -45,6 +45,15 @@ namespace RMuseum.Utils.PublicDataExport /// user-secrets / environment variables, never committed to appsettings.json. /// public string GitToken { get; set; } + + /// + /// Full path to git.exe, e.g. "C:\Program Files\Git\cmd\git.exe". Optional — leave empty + /// to resolve "git" via PATH (with a fallback to common Git-for-Windows install + /// locations if that fails, see GitRepoPublisher.ResolveGitExecutable). Set this + /// explicitly if the process running this job has a PATH that doesn't include git for + /// any reason (e.g. a Windows service running under a service account with its own PATH). + /// + public string GitExecutablePath { get; set; } } /// @@ -67,6 +76,7 @@ namespace RMuseum.Utils.PublicDataExport public class GitRepoPublisher { private readonly GitRepoPublisherOptions _options; + private string _resolvedGitExecutable; public GitRepoPublisher(GitRepoPublisherOptions options) { @@ -171,7 +181,7 @@ namespace RMuseum.Utils.PublicDataExport { var psi = new ProcessStartInfo { - FileName = "git", + FileName = ResolveGitExecutable(), Arguments = arguments, WorkingDirectory = workingDirectory, RedirectStandardOutput = true, @@ -189,7 +199,12 @@ namespace RMuseum.Utils.PublicDataExport catch (Win32Exception exp) { throw new InvalidOperationException( - "Could not start 'git'. Make sure Git for Windows is installed and 'git' is on PATH.", exp); + "Could not start 'git'. This process's PATH may be stale (common right " + + "after installing Git while Visual Studio/IIS Express was already " + + "running — fully restart them so they pick up the new PATH), or git " + + "genuinely isn't installed. As a workaround, set " + + "PublicDataExport:GitExecutablePath (or TajikPublicDataExport:...) to " + + "the full path of git.exe.", exp); } string stdout = process.StandardOutput.ReadToEnd(); @@ -200,6 +215,83 @@ namespace RMuseum.Utils.PublicDataExport } } + /// + /// Resolves which git executable to launch, cached for the lifetime of this instance. + /// Prefers an explicitly configured path; otherwise tries "git" (PATH resolution via + /// Process/CreateProcess) and, only if that file genuinely doesn't exist anywhere on + /// PATH, falls back to checking the usual Git-for-Windows install locations directly — + /// covers the common case where the process that's running this job has a stale PATH + /// snapshot from before git was installed (e.g. Visual Studio/IIS Express started before + /// the PATH environment variable was updated). + /// + private string ResolveGitExecutable() + { + if (_resolvedGitExecutable != null) + return _resolvedGitExecutable; + + if (!string.IsNullOrEmpty(_options.GitExecutablePath)) + { + _resolvedGitExecutable = _options.GitExecutablePath; + return _resolvedGitExecutable; + } + + if (IsOnPath("git")) + { + _resolvedGitExecutable = "git"; + return _resolvedGitExecutable; + } + + string[] fallbackCandidates = + { + Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Git\cmd\git.exe"), + Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\Git\bin\git.exe"), + Environment.ExpandEnvironmentVariables(@"%ProgramFiles(x86)%\Git\cmd\git.exe"), + Environment.ExpandEnvironmentVariables(@"%ProgramFiles(x86)%\Git\bin\git.exe"), + Environment.ExpandEnvironmentVariables(@"%LocalAppData%\Programs\Git\cmd\git.exe"), + }; + + foreach (var candidate in fallbackCandidates) + { + if (File.Exists(candidate)) + { + _resolvedGitExecutable = candidate; + return _resolvedGitExecutable; + } + } + + // nothing found - fall through with plain "git" so the resulting Win32Exception + // message (with its own guidance) is what the caller sees + _resolvedGitExecutable = "git"; + return _resolvedGitExecutable; + } + + /// + /// checks every directory on this process's own PATH for the given executable, without + /// actually starting it — used only to decide whether to bother trying the Git-for-Windows + /// fallback locations + /// + private static bool IsOnPath(string executableName) + { + string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; + foreach (var dir in pathEnv.Split(Path.PathSeparator)) + { + if (string.IsNullOrWhiteSpace(dir)) + continue; + + try + { + string candidate = Path.Combine(dir, executableName + ".exe"); + if (File.Exists(candidate)) + return true; + } + catch (ArgumentException) + { + // malformed PATH entry - ignore and keep checking the rest + } + } + return false; + } + /// /// keeps the auth token out of exception messages that might end up logged somewhere /// diff --git a/RMuseum/appsettings.json b/RMuseum/appsettings.json index 926c580e..43f5bfb9 100644 --- a/RMuseum/appsettings.json +++ b/RMuseum/appsettings.json @@ -75,7 +75,8 @@ "CommitAuthorEmail": "bot@ganjoor.net", "PushEnabled": "False", "GitUserName": "", - "GitToken": "" + "GitToken": "", + "GitExecutablePath": "" }, "TajikPublicDataExport": { "LocalWorkingCopyPath": "C:\\ganjoor-tajik-public-data", @@ -85,7 +86,8 @@ "CommitAuthorEmail": "bot@ganjoor.net", "PushEnabled": "False", "GitUserName": "", - "GitToken": "" + "GitToken": "", + "GitExecutablePath": "" }, "ExternalFTPServer": { "Host": "localhost",