LibGit2GitRepository.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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)
  34. => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();
  35. public GitRepoStatus GetStatus() {
  36. if (!_isAvailable)
  37. return GitRepoStatus.NotAvailable;
  38. using Repository repo = OpenRepo();
  39. return repo.RetrieveStatus().IsDirty
  40. ? GitRepoStatus.Dirty
  41. : GitRepoStatus.Clean;
  42. }
  43. public void StageAll() {
  44. using Repository repo = OpenRepo();
  45. var files = repo.RetrieveStatus()
  46. .Where(e => e.State != FileStatus.Ignored)
  47. .Select(e => e.FilePath)
  48. .ToList();
  49. if (files.Count == 0)
  50. return;
  51. Commands.Stage(repo, files);
  52. }
  53. public void Commit(string message) {
  54. using Repository repo = OpenRepo();
  55. Signature signature = GetSignature(repo);
  56. repo.Commit(message, signature, signature);
  57. }
  58. public string GetDiff() {
  59. using Repository repo = OpenRepo();
  60. Tree? tree = repo.Head.Tip?.Tree;
  61. Patch patch = repo.Diff.Compare<Patch>(
  62. tree,
  63. DiffTargets.Index | DiffTargets.WorkingDirectory);
  64. return patch?.Content ?? string.Empty;
  65. }
  66. public string[] GetChangedFiles() {
  67. using Repository repo = OpenRepo();
  68. return repo.RetrieveStatus()
  69. .Where(e => e.State != FileStatus.Ignored)
  70. .Select(e => $"{GetPrefix(e.State)} {e.FilePath}")
  71. .ToArray();
  72. }
  73. private static string GetPrefix(FileStatus state) => state switch {
  74. FileStatus.NewInWorkdir or FileStatus.NewInIndex => "A",
  75. FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => "D",
  76. FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => "R",
  77. _ when state.HasFlag(FileStatus.ModifiedInWorkdir)
  78. || state.HasFlag(FileStatus.ModifiedInIndex) => "M",
  79. _ => "?"
  80. };
  81. public void RestoreAll() {
  82. using Repository repo = OpenRepo();
  83. repo.CheckoutPaths(
  84. repo.Head.FriendlyName,
  85. ["*"],
  86. new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
  87. repo.RemoveUntrackedFiles();
  88. }
  89. public string GetCurrentBranch() {
  90. using Repository repo = OpenRepo();
  91. return repo.Head.FriendlyName;
  92. }
  93. public GitLogEntry[] GetLog(int count) {
  94. using Repository repo = OpenRepo();
  95. if (repo.Head.Tip is null)
  96. return [];
  97. return repo.Commits
  98. .Take(count)
  99. .Select(c => new GitLogEntry(
  100. c.Sha[..7],
  101. c.MessageShort,
  102. c.Author.Name,
  103. FormatRelativeDate(c.Author.When)))
  104. .ToArray();
  105. }
  106. public bool HasRemote() {
  107. using Repository repo = OpenRepo();
  108. return repo.Network.Remotes.Any();
  109. }
  110. public GitSyncStatus FetchAndGetSyncStatus() {
  111. using Repository repo = OpenRepo();
  112. if (!repo.Network.Remotes.Any())
  113. return new GitSyncStatus(0, 0, false);
  114. Remote remote = GetRemote(repo);
  115. Commands.Fetch(
  116. repo,
  117. remote.Name,
  118. remote.FetchRefSpecs.Select(r => r.Specification),
  119. new FetchOptions { CredentialsProvider = _credentials },
  120. null);
  121. // If the repo has no commits yet (unborn branch)
  122. if (repo.Head.Tip == null)
  123. return new GitSyncStatus(0, 0, true);
  124. Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
  125. if (remoteBranch?.Tip == null)
  126. return new GitSyncStatus(repo.Commits.Count(), 0, true);
  127. HistoryDivergence? divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
  128. repo.Head.Tip,
  129. remoteBranch.Tip);
  130. return new GitSyncStatus(
  131. divergence.AheadBy ?? 0,
  132. divergence.BehindBy ?? 0,
  133. true);
  134. }
  135. public void Push() {
  136. using Repository repo = OpenRepo();
  137. Remote remote = GetRemote(repo);
  138. var branch = repo.Head.FriendlyName;
  139. var refSpec = $"refs/heads/{branch}:refs/heads/{branch}";
  140. try {
  141. repo.Network.Push(
  142. remote,
  143. refSpec,
  144. new PushOptions { CredentialsProvider = _credentials });
  145. }
  146. catch (NonFastForwardException) {
  147. PullInternal(repo);
  148. repo.Network.Push(
  149. remote,
  150. refSpec,
  151. new PushOptions { CredentialsProvider = _credentials });
  152. }
  153. if (repo.Head.TrackedBranch is null) {
  154. repo.Branches.Update(repo.Head,
  155. b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{branch}");
  156. }
  157. }
  158. public void Pull() {
  159. using Repository repo = OpenRepo();
  160. PullInternal(repo);
  161. }
  162. private void PullInternal(Repository repo) {
  163. if (!repo.Network.Remotes.Any())
  164. return;
  165. Remote remote = GetRemote(repo);
  166. Commands.Fetch(
  167. repo,
  168. remote.Name,
  169. remote.FetchRefSpecs.Select(r => r.Specification),
  170. new FetchOptions { CredentialsProvider = _credentials },
  171. null);
  172. Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
  173. if (remoteBranch?.Tip == null)
  174. return;
  175. // hard reset to remote branch
  176. repo.Reset(ResetMode.Hard, remoteBranch.Tip);
  177. repo.Branches.Update(repo.Head,
  178. b => b.TrackedBranch = remoteBranch.CanonicalName);
  179. }
  180. public void AddRemote(string name, string url) {
  181. using Repository repo = OpenRepo();
  182. if (repo.Network.Remotes[name] != null)
  183. return;
  184. repo.Network.Remotes.Add(name, url);
  185. Remote remote = repo.Network.Remotes[name];
  186. // fetch remote state
  187. Commands.Fetch(
  188. repo,
  189. remote.Name,
  190. remote.FetchRefSpecs.Select(r => r.Specification),
  191. new FetchOptions { CredentialsProvider = _credentials },
  192. null);
  193. // detect if remote has a default branch
  194. Branch? remoteMain =
  195. repo.Branches[$"{remote.Name}/main"] ??
  196. repo.Branches[$"{remote.Name}/master"];
  197. var hasLocalFiles =
  198. repo.RetrieveStatus()
  199. .Any(e => e.State != FileStatus.Ignored);
  200. // CASE 1: remote repo already has commits
  201. if (remoteMain != null && remoteMain.Tip != null) {
  202. Branch local = repo.CreateBranch(remoteMain.FriendlyName, remoteMain.Tip);
  203. Commands.Checkout(repo, local);
  204. repo.Branches.Update(local,
  205. b => b.TrackedBranch = remoteMain.CanonicalName);
  206. if (hasLocalFiles) {
  207. // import existing config to a new branch
  208. var importBranchName = $"rackpeek-{DateTime.UtcNow:yyyyMMddHHmmss}";
  209. Branch importBranch = repo.CreateBranch(importBranchName);
  210. Commands.Checkout(repo, importBranch);
  211. Commands.Stage(repo, "*");
  212. Signature sig = GetSignature(repo);
  213. repo.Commit(
  214. "rackpeek: import existing config",
  215. sig,
  216. sig);
  217. repo.Network.Push(
  218. remote,
  219. $"refs/heads/{importBranchName}:refs/heads/{importBranchName}",
  220. new PushOptions { CredentialsProvider = _credentials });
  221. repo.Branches.Update(importBranch,
  222. b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{importBranchName}");
  223. }
  224. return;
  225. }
  226. // CASE 2: remote repo is empty
  227. if (hasLocalFiles) {
  228. var branchName = "main";
  229. Branch branch = repo.CreateBranch(branchName);
  230. Commands.Checkout(repo, branch);
  231. Commands.Stage(repo, "*");
  232. Signature sig = GetSignature(repo);
  233. repo.Commit(
  234. "rackpeek: initial config",
  235. sig,
  236. sig);
  237. repo.Network.Push(
  238. remote,
  239. $"refs/heads/{branchName}:refs/heads/{branchName}",
  240. new PushOptions { CredentialsProvider = _credentials });
  241. repo.Branches.Update(branch,
  242. b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{branchName}");
  243. }
  244. }
  245. private static string FormatRelativeDate(DateTimeOffset date) {
  246. TimeSpan diff = DateTimeOffset.Now - date;
  247. if (diff.TotalMinutes < 1) return "just now";
  248. if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
  249. if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
  250. if (diff.TotalDays < 30) return $"{(int)diff.TotalDays} days ago";
  251. if (diff.TotalDays < 365) return $"{(int)(diff.TotalDays / 30)} months ago";
  252. return $"{(int)(diff.TotalDays / 365)} years ago";
  253. }
  254. }