LibGit2GitRepository.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. using LibGit2Sharp;
  2. using LibGit2Sharp.Handlers;
  3. namespace RackPeek.Domain.Git;
  4. public interface IGitCredentialsProvider {
  5. CredentialsHandler GetHandler();
  6. }
  7. public sealed class GitHubTokenCredentialsProvider(string username, string token) : IGitCredentialsProvider {
  8. private readonly string _username = username ?? throw new ArgumentNullException(nameof(username));
  9. private readonly string _token = token ?? throw new ArgumentNullException(nameof(token));
  10. public CredentialsHandler GetHandler() {
  11. return (_, _, _) => new UsernamePasswordCredentials {
  12. Username = _username,
  13. Password = _token
  14. };
  15. }
  16. }
  17. public sealed class LibGit2GitRepository(
  18. string configDirectory,
  19. IGitCredentialsProvider credentialsProvider) : IGitRepository {
  20. private readonly CredentialsHandler _credentials = credentialsProvider.GetHandler();
  21. private bool _isAvailable = Repository.IsValid(configDirectory);
  22. public bool IsAvailable => _isAvailable;
  23. public void Init() {
  24. Repository.Init(configDirectory);
  25. _isAvailable = true;
  26. }
  27. private Repository OpenRepo() => new(configDirectory);
  28. private static Signature GetSignature(Repository repo) {
  29. var name = repo.Config.Get<string>("user.name")?.Value ?? "RackPeek";
  30. var email = repo.Config.Get<string>("user.email")?.Value ?? "rackpeek@local";
  31. return new Signature(name, email, DateTimeOffset.Now);
  32. }
  33. private static Remote GetRemote(Repository repo) => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();
  34. public GitRepoStatus GetStatus() {
  35. if (!_isAvailable)
  36. return GitRepoStatus.NotAvailable;
  37. using Repository repo = OpenRepo();
  38. return repo.RetrieveStatus().IsDirty
  39. ? GitRepoStatus.Dirty
  40. : GitRepoStatus.Clean;
  41. }
  42. public void StageAll() {
  43. using Repository repo = OpenRepo();
  44. Commands.Stage(repo, "*");
  45. }
  46. public void Commit(string message) {
  47. using Repository repo = OpenRepo();
  48. Signature signature = GetSignature(repo);
  49. repo.Commit(message, signature, signature);
  50. }
  51. public string GetDiff() {
  52. using Repository repo = OpenRepo();
  53. Patch patch = repo.Diff.Compare<Patch>(
  54. repo.Head.Tip?.Tree,
  55. DiffTargets.Index | DiffTargets.WorkingDirectory);
  56. return patch?.Content ?? string.Empty;
  57. }
  58. public string[] GetChangedFiles() {
  59. using Repository repo = OpenRepo();
  60. return repo.RetrieveStatus()
  61. .Where(e => e.State != FileStatus.Ignored)
  62. .Select(e => $"{GetPrefix(e.State)} {e.FilePath}")
  63. .ToArray();
  64. }
  65. private static string GetPrefix(FileStatus state) => state switch {
  66. FileStatus.NewInWorkdir or FileStatus.NewInIndex => "A",
  67. FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => "D",
  68. FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => "R",
  69. _ when state.HasFlag(FileStatus.ModifiedInWorkdir)
  70. || state.HasFlag(FileStatus.ModifiedInIndex) => "M",
  71. _ => "?"
  72. };
  73. public void RestoreAll() {
  74. using Repository repo = OpenRepo();
  75. repo.CheckoutPaths(
  76. repo.Head.FriendlyName,
  77. ["*"],
  78. new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
  79. repo.RemoveUntrackedFiles();
  80. }
  81. public string GetCurrentBranch() {
  82. using Repository repo = OpenRepo();
  83. return repo.Head.FriendlyName;
  84. }
  85. public GitLogEntry[] GetLog(int count) {
  86. using Repository repo = OpenRepo();
  87. if (repo.Head.Tip is null)
  88. return [];
  89. return repo.Commits
  90. .Take(count)
  91. .Select(c => new GitLogEntry(
  92. c.Sha[..7],
  93. c.MessageShort,
  94. c.Author.Name,
  95. FormatRelativeDate(c.Author.When)))
  96. .ToArray();
  97. }
  98. public bool HasRemote() {
  99. using Repository repo = OpenRepo();
  100. return repo.Network.Remotes.Any();
  101. }
  102. public GitSyncStatus FetchAndGetSyncStatus() {
  103. using Repository repo = OpenRepo();
  104. if (!repo.Network.Remotes.Any())
  105. return new(0, 0, false);
  106. Remote remote = GetRemote(repo);
  107. Commands.Fetch(
  108. repo,
  109. remote.Name,
  110. remote.FetchRefSpecs.Select(r => r.Specification),
  111. new FetchOptions { CredentialsProvider = _credentials },
  112. null);
  113. Branch? tracking = repo.Head.TrackedBranch;
  114. if (tracking is null)
  115. return new(repo.Commits.Count(), 0, true);
  116. HistoryDivergence divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
  117. repo.Head.Tip,
  118. tracking.Tip);
  119. return new(
  120. divergence.AheadBy ?? 0,
  121. divergence.BehindBy ?? 0,
  122. true);
  123. }
  124. public void Push() {
  125. using Repository repo = OpenRepo();
  126. Remote remote = GetRemote(repo);
  127. repo.Network.Push(
  128. remote,
  129. $"refs/heads/{repo.Head.FriendlyName}",
  130. new PushOptions { CredentialsProvider = _credentials });
  131. if (repo.Head.TrackedBranch is null) {
  132. Branch remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
  133. if (remoteBranch != null) {
  134. repo.Branches.Update(repo.Head,
  135. b => b.TrackedBranch = remoteBranch.CanonicalName);
  136. }
  137. }
  138. }
  139. public void Pull()
  140. {
  141. using Repository repo = OpenRepo();
  142. Commands.Pull(
  143. repo,
  144. GetSignature(repo),
  145. new PullOptions
  146. {
  147. FetchOptions = new FetchOptions
  148. {
  149. CredentialsProvider = _credentials
  150. },
  151. MergeOptions = new MergeOptions
  152. {
  153. FailOnConflict = false
  154. }
  155. });
  156. }
  157. public void AddRemote(string name, string url) {
  158. using Repository repo = OpenRepo();
  159. repo.Network.Remotes.Add(name, url);
  160. }
  161. private static string FormatRelativeDate(DateTimeOffset date) {
  162. TimeSpan diff = DateTimeOffset.Now - date;
  163. if (diff.TotalMinutes < 1) return "just now";
  164. if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
  165. if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
  166. if (diff.TotalDays < 30) return $"{(int)diff.TotalDays} days ago";
  167. if (diff.TotalDays < 365) return $"{(int)(diff.TotalDays / 30)} months ago";
  168. return $"{(int)(diff.TotalDays / 365)} years ago";
  169. }
  170. }