LibGit2GitRepository.cs 7.2 KB

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