using LibGit2Sharp; using System; using System.IO; using System.Linq; namespace RMuseum.Utils.PublicDataExport { /// /// Options for publishing the public export tree to a git remote. /// Bind this from the "PublicDataExport" configuration section. /// public class GitRepoPublisherOptions { /// /// local working copy path (e.g. C:\ganjoor-public-data or /var/ganjoor/public-data) /// public string LocalWorkingCopyPath { get; set; } /// /// remote URL, e.g. https://github.com/ganjoor/ganjoor-data.git /// public string RemoteUrl { get; set; } public string Branch { get; set; } = "main"; public string CommitAuthorName { get; set; } = "Ganjoor Export Bot"; public string CommitAuthorEmail { get; set; } = "bot@ganjoor.net"; /// /// if false, everything is written and committed locally but never pushed — /// useful for a first dry run before wiring up real credentials /// public bool PushEnabled { get; set; } /// /// git username for the push (for GitHub, a PAT is used as both username-independent /// and as the token below — GitHub only checks the token) /// public string GitUserName { get; set; } /// /// personal access token / app token with push rights to RemoteUrl. Keep this in /// user-secrets / environment variables, never committed to appsettings.json. /// public string GitToken { get; set; } } public class GitRepoPublisher { private readonly GitRepoPublisherOptions _options; public GitRepoPublisher(GitRepoPublisherOptions options) { _options = options; } /// /// Ensures the local working copy exists and is up to date with the remote before the /// export job starts writing files into it. Clones on first run, fetches + hard-resets /// to the remote branch on subsequent runs (this working copy is bot-owned; nobody /// should be hand-editing it, so a hard reset is safe and keeps the job idempotent). /// public void EnsureWorkingCopyUpToDate() { if (!Directory.Exists(_options.LocalWorkingCopyPath) || !Directory.Exists(Path.Combine(_options.LocalWorkingCopyPath, ".git"))) { Directory.CreateDirectory(_options.LocalWorkingCopyPath); var cloneOptions = new CloneOptions(); if (RemoteRequiresAuth()) { cloneOptions.FetchOptions.CredentialsProvider = CredentialsHandler; } Repository.Clone(_options.RemoteUrl, _options.LocalWorkingCopyPath, cloneOptions); return; } using var repo = new Repository(_options.LocalWorkingCopyPath); var remote = repo.Network.Remotes["origin"]; var fetchOptions = new FetchOptions(); if (RemoteRequiresAuth()) { fetchOptions.CredentialsProvider = CredentialsHandler; } Commands.Fetch(repo, remote.Name, remote.FetchRefSpecs.Select(r => r.Specification), fetchOptions, null); var remoteBranch = repo.Branches[$"origin/{_options.Branch}"]; if (remoteBranch != null) { repo.Reset(ResetMode.Hard, remoteBranch.Tip); } } /// /// Stages every change under the working copy and, if anything actually changed, /// commits and (if enabled) pushes. Returns the number of files touched (0 means /// nothing changed since last run — a normal, expected outcome on most nightly runs). /// public int CommitAndPush(string commitMessage) { using var repo = new Repository(_options.LocalWorkingCopyPath); Commands.Stage(repo, "*"); var status = repo.RetrieveStatus(); int changedCount = status.Count(s => s.State != FileStatus.Ignored && s.State != FileStatus.Unaltered); if (changedCount == 0) { return 0; } var signature = new Signature(_options.CommitAuthorName, _options.CommitAuthorEmail, DateTimeOffset.UtcNow); repo.Commit(commitMessage, signature, signature); if (_options.PushEnabled) { var pushOptions = new PushOptions(); if (RemoteRequiresAuth()) { pushOptions.CredentialsProvider = CredentialsHandler; } var branch = repo.Branches[_options.Branch] ?? repo.CreateBranch(_options.Branch); repo.Network.Push(branch, pushOptions); } return changedCount; } private bool RemoteRequiresAuth() => !string.IsNullOrEmpty(_options.GitToken); private Credentials CredentialsHandler(string url, string usernameFromUrl, SupportedCredentialTypes types) { return new UsernamePasswordCredentials { Username = string.IsNullOrEmpty(_options.GitUserName) ? _options.GitToken : _options.GitUserName, Password = _options.GitToken, }; } } }