Skip to content

Commit b3cadef

Browse files
committed
persistant cache
1 parent 89b3694 commit b3cadef

4 files changed

Lines changed: 406 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace RepoM.Core.Repositories.Persistence;
2+
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using RepoM.Core.Repositories.Model;
7+
8+
/// <summary>
9+
/// Persists the set of discovered repositories so the list can be shown immediately at startup,
10+
/// before the (slower) filesystem scan and git status reads complete.
11+
/// </summary>
12+
public interface IRepositorySnapshotStore
13+
{
14+
Task<IReadOnlyList<RepositoryInfo>> LoadAsync(CancellationToken ct = default);
15+
16+
Task SaveAsync(IEnumerable<RepositoryInfo> repositories, CancellationToken ct = default);
17+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
namespace RepoM.Core.Repositories.Persistence;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO.Abstractions;
6+
using System.Linq;
7+
using System.Text.Json;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.Logging;
11+
using RepoM.Core.Plugin.Repository;
12+
using RepoM.Core.Repositories.Model;
13+
14+
public sealed class RepositorySnapshotStore : IRepositorySnapshotStore
15+
{
16+
private const int CURRENT_VERSION = 1;
17+
18+
private static readonly JsonSerializerOptions _jsonOptions = new()
19+
{
20+
WriteIndented = false,
21+
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
22+
};
23+
24+
private readonly IFileSystem _fileSystem;
25+
private readonly ILogger _logger;
26+
private readonly string _filePath;
27+
28+
public RepositorySnapshotStore(IFileSystem fileSystem, RepositorySnapshotStoreSettings settings, ILogger logger)
29+
{
30+
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
31+
ArgumentNullException.ThrowIfNull(settings);
32+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
33+
_filePath = settings.FilePath;
34+
}
35+
36+
public async Task<IReadOnlyList<RepositoryInfo>> LoadAsync(CancellationToken ct = default)
37+
{
38+
if (!_fileSystem.File.Exists(_filePath))
39+
{
40+
return [];
41+
}
42+
43+
try
44+
{
45+
var json = await _fileSystem.File.ReadAllTextAsync(_filePath, ct).ConfigureAwait(false);
46+
if (string.IsNullOrWhiteSpace(json))
47+
{
48+
return [];
49+
}
50+
51+
SnapshotDocument? document = JsonSerializer.Deserialize<SnapshotDocument>(json, _jsonOptions);
52+
if (document?.Repositories is null || document.Version != CURRENT_VERSION)
53+
{
54+
return [];
55+
}
56+
57+
return [.. document.Repositories.Select(Map),];
58+
}
59+
catch (Exception ex)
60+
{
61+
_logger.LogDebug(ex, "Could not load repository snapshot from '{File}'", _filePath);
62+
return [];
63+
}
64+
}
65+
66+
public async Task SaveAsync(IEnumerable<RepositoryInfo> repositories, CancellationToken ct = default)
67+
{
68+
ArgumentNullException.ThrowIfNull(repositories);
69+
70+
try
71+
{
72+
var directory = _fileSystem.Path.GetDirectoryName(_filePath);
73+
if (!string.IsNullOrWhiteSpace(directory) && !_fileSystem.Directory.Exists(directory))
74+
{
75+
_fileSystem.Directory.CreateDirectory(directory);
76+
}
77+
78+
var document = new SnapshotDocument
79+
{
80+
Version = CURRENT_VERSION,
81+
Repositories = [.. repositories.Select(Map),],
82+
};
83+
84+
var json = JsonSerializer.Serialize(document, _jsonOptions);
85+
86+
// Write to a temporary file first, then replace, to avoid a corrupted snapshot on crash.
87+
var tempFile = _filePath + ".tmp";
88+
await _fileSystem.File.WriteAllTextAsync(tempFile, json, ct).ConfigureAwait(false);
89+
_fileSystem.File.Move(tempFile, _filePath, overwrite: true);
90+
}
91+
catch (Exception ex)
92+
{
93+
_logger.LogDebug(ex, "Could not save repository snapshot to '{File}'", _filePath);
94+
}
95+
}
96+
97+
private static RepositorySnapshotDto Map(RepositoryInfo info)
98+
{
99+
return new RepositorySnapshotDto
100+
{
101+
Path = info.Path,
102+
SafePath = info.SafePath,
103+
Name = info.Name,
104+
WindowsPath = info.WindowsPath,
105+
LinuxPath = info.LinuxPath,
106+
Location = info.Location,
107+
IsBare = info.IsBare,
108+
CurrentBranch = info.CurrentBranch,
109+
CurrentBranchHasUpstream = info.CurrentBranchHasUpstream,
110+
CurrentBranchIsDetached = info.CurrentBranchIsDetached,
111+
CurrentBranchIsOnTag = info.CurrentBranchIsOnTag,
112+
Branches = info.Branches,
113+
LocalBranches = info.LocalBranches,
114+
Tags = info.Tags,
115+
AheadBy = info.AheadBy,
116+
BehindBy = info.BehindBy,
117+
LocalUntracked = info.LocalUntracked,
118+
LocalModified = info.LocalModified,
119+
LocalMissing = info.LocalMissing,
120+
LocalAdded = info.LocalAdded,
121+
LocalStaged = info.LocalStaged,
122+
LocalRemoved = info.LocalRemoved,
123+
StashCount = info.StashCount,
124+
LastSeen = info.LastSeen,
125+
LastUpdated = info.LastUpdated,
126+
Remotes =
127+
[
128+
.. info.Remotes.Select(r => new RemoteDto
129+
{
130+
Key = r.Key,
131+
Url = r.Url,
132+
}),
133+
],
134+
};
135+
}
136+
137+
private static RepositoryInfo Map(RepositorySnapshotDto dto)
138+
{
139+
var info = new RepositoryInfo
140+
{
141+
Path = dto.Path,
142+
SafePath = dto.SafePath,
143+
Name = dto.Name,
144+
WindowsPath = dto.WindowsPath,
145+
LinuxPath = dto.LinuxPath,
146+
Location = dto.Location,
147+
IsBare = dto.IsBare,
148+
CurrentBranch = dto.CurrentBranch,
149+
CurrentBranchHasUpstream = dto.CurrentBranchHasUpstream,
150+
CurrentBranchIsDetached = dto.CurrentBranchIsDetached,
151+
CurrentBranchIsOnTag = dto.CurrentBranchIsOnTag,
152+
Branches = dto.Branches,
153+
LocalBranches = dto.LocalBranches,
154+
Tags = dto.Tags,
155+
AheadBy = dto.AheadBy,
156+
BehindBy = dto.BehindBy,
157+
LocalUntracked = dto.LocalUntracked,
158+
LocalModified = dto.LocalModified,
159+
LocalMissing = dto.LocalMissing,
160+
LocalAdded = dto.LocalAdded,
161+
LocalStaged = dto.LocalStaged,
162+
LocalRemoved = dto.LocalRemoved,
163+
StashCount = dto.StashCount,
164+
LastSeen = dto.LastSeen,
165+
LastUpdated = dto.LastUpdated,
166+
};
167+
168+
foreach (RemoteDto remote in dto.Remotes)
169+
{
170+
info.Remotes.Add(new Remote(remote.Key, remote.Url));
171+
}
172+
173+
return info;
174+
}
175+
176+
private sealed class SnapshotDocument
177+
{
178+
public int Version { get; set; }
179+
180+
public List<RepositorySnapshotDto> Repositories { get; set; } = [];
181+
}
182+
183+
private sealed class RepositorySnapshotDto
184+
{
185+
public string Path { get; set; } = string.Empty;
186+
187+
public string SafePath { get; set; } = string.Empty;
188+
189+
public string Name { get; set; } = string.Empty;
190+
191+
public string WindowsPath { get; set; } = string.Empty;
192+
193+
public string LinuxPath { get; set; } = string.Empty;
194+
195+
public string Location { get; set; } = string.Empty;
196+
197+
public bool IsBare { get; set; }
198+
199+
public string CurrentBranch { get; set; } = string.Empty;
200+
201+
public bool CurrentBranchHasUpstream { get; set; }
202+
203+
public bool CurrentBranchIsDetached { get; set; }
204+
205+
public bool CurrentBranchIsOnTag { get; set; }
206+
207+
public string[] Branches { get; set; } = [];
208+
209+
public string[] LocalBranches { get; set; } = [];
210+
211+
public string[] Tags { get; set; } = [];
212+
213+
public int? AheadBy { get; set; }
214+
215+
public int? BehindBy { get; set; }
216+
217+
public int? LocalUntracked { get; set; }
218+
219+
public int? LocalModified { get; set; }
220+
221+
public int? LocalMissing { get; set; }
222+
223+
public int? LocalAdded { get; set; }
224+
225+
public int? LocalStaged { get; set; }
226+
227+
public int? LocalRemoved { get; set; }
228+
229+
public int? StashCount { get; set; }
230+
231+
public DateTimeOffset LastSeen { get; set; }
232+
233+
public DateTimeOffset LastUpdated { get; set; }
234+
235+
public List<RemoteDto> Remotes { get; set; } = [];
236+
}
237+
238+
private sealed class RemoteDto
239+
{
240+
public string Key { get; set; } = string.Empty;
241+
242+
public string Url { get; set; } = string.Empty;
243+
}
244+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace RepoM.Core.Repositories.Persistence;
2+
3+
using System;
4+
5+
public sealed record RepositorySnapshotStoreSettings
6+
{
7+
public RepositorySnapshotStoreSettings(string filePath)
8+
{
9+
FilePath = string.IsNullOrWhiteSpace(filePath)
10+
? throw new ArgumentException("File path must not be empty.", nameof(filePath))
11+
: filePath;
12+
}
13+
14+
public string FilePath { get; }
15+
}

0 commit comments

Comments
 (0)