Say why the repository index is empty when it is empty

Directory.Exists is false for a directory that is not there and false for one the process cannot reach, so the page named the wrong problem: it said there were no repositories under the root when the root was sitting right there and unreadable. Worse, only the missing case was handled at all. The enumeration itself throws on an unreadable directory, and that came out as a 500 rather than as a page. Which is exactly what a real server does. A home directory is 0700 unless someone says otherwise, so a site pointed at /home/git for the first time hits the case that was not handled, and is told to look at the wrong thing. The failure is a value now, as it is for the drop tables and the rvrb node. There are two messages because there are two fixes behind them and they are in different places: a root that cannot be read is a group, and a root that is not there is either a wrong path or a systemd unit with ProtectHome hiding /home from the service outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-09-13 19:50 UTC
commit
dac630ab1020724d4cd7b93d925b37ffaceab77b
parent
9e99745e72
tree
browse at this commit

3 files changed +53 -21

Blog/Components/Pages/Git/GitIndex.razor +9 -3

@@ -12,7 +12,11 @@
12 12 <span class="block-note">@Repositories.Count</span>
13 13 </h2>
14 14
15 - @if (Repositories.Count == 0)
15 + @if (Index.Error is { } error)
16 + {
17 + <p class="empty">@error</p>
18 + }
19 + else if (Repositories.Count == 0)
16 20 {
17 21 <p class="empty">Nothing to browse: there are no repositories under @Service.Root.</p>
18 22 }
@@ -56,7 +60,9 @@
56 60 </main>
57 61
58 62 @code {
59 - private IReadOnlyList<GitRepoSummary> Repositories { get; set; } = [];
63 + private GitIndexView Index { get; set; } = new([], null);
64 +
65 + private IReadOnlyList<GitRepoSummary> Repositories => Index.Repositories;
60 66
61 67 /// <summary>
62 68 /// Owner comes from each repository's <c>gitweb.owner</c>, and most never set it. A column of
@@ -64,5 +70,5 @@
64 70 /// </summary>
65 71 private bool Owned => Repositories.Any(repository => repository.Owner is { Length: > 0 });
66 72
67 - protected override void OnParametersSet() => Repositories = Service.ListRepositories();
73 + protected override void OnParametersSet() => Index = Service.ListRepositories();
68 74 }

Blog/Models/GitRepositories.cs +11 -0

@@ -136,6 +136,17 @@ public sealed record GitDiff(IReadOnlyList<GitDiffFile> Files, int Added, int De
136 136 /// </summary>
137 137 public sealed record GitRawBlob(byte[] Bytes, bool IsBinary, string Name);
138 138
139 +/// <summary>
140 +/// What the index found under the repository root, and why it found nothing when it found nothing.
141 +/// </summary>
142 +/// <remarks>
143 +/// A failure is a value here, as it is for the drop tables and for the rvrb node. An unreadable
144 +/// root is a deployment problem, and the page that says which deployment problem is worth more
145 +/// than a stack trace: "there are no repositories" and "this process cannot read them" look
146 +/// identical from the outside and are fixed in completely different places.
147 +/// </remarks>
148 +public sealed record GitIndexView(IReadOnlyList<GitRepoSummary> Repositories, string? Error);
149 +
139 150 /// <summary>A commit and the diff that goes with it — everything the commit page renders.</summary>
140 151 public sealed record GitCommitView(GitCommitInfo Commit, GitDiff Diff);
141 152

Blog/Services/GitService.cs +33 -18

@@ -74,36 +74,51 @@ public sealed partial class GitService(IOptions<GitOptions> options, ILogger<Git
74 74 public string? CloneUrl(string repository) =>
75 75 _options.CloneUrl is { Length: > 0 } url ? url.Replace("{repo}", repository, StringComparison.Ordinal) : null;
76 76
77 - /// <summary>Every repository under the root, by name. Missing root reads as no repositories.</summary>
78 - public IReadOnlyList<GitRepoSummary> ListRepositories()
77 + /// <summary>Every repository under the root, by name, or why there are none.</summary>
78 + public GitIndexView ListRepositories()
79 79 {
80 80 if (!Directory.Exists(Root))
81 81 {
82 - logger.LogWarning("The git repository root {Root} does not exist", Root);
83 - return [];
82 + // Exists() is false for a directory that is not there and false for one this process
83 + // cannot reach, and on a hardened systemd unit ProtectHome hides /home outright, which
84 + // looks exactly the same. The message has to cover all of it.
85 + logger.LogWarning("The git repository root {Root} is missing or out of reach", Root);
86 + return new GitIndexView([], $"{Root} is not there, or this process cannot reach it.");
84 87 }
85 88
86 89 var found = new List<GitRepoSummary>();
87 90
88 - foreach (var directory in Directory.EnumerateDirectories(Root))
91 + try
89 92 {
90 - var name = Path.GetFileName(directory);
91 - if (!IsRepositoryName(name)) continue;
92 -
93 - try
94 - {
95 - using var repository = Open(directory);
96 - if (repository is null) continue;
97 - found.Add(Summarise(repository, name));
98 - }
99 - catch (Exception exception) when (exception is LibGit2SharpException or IOException or UnauthorizedAccessException)
93 + foreach (var directory in Directory.EnumerateDirectories(Root))
100 94 {
101 - // One unreadable repository is not a reason for the index to fail.
102 - logger.LogWarning(exception, "Could not read the repository at {Directory}", directory);
95 + var name = Path.GetFileName(directory);
96 + if (!IsRepositoryName(name)) continue;
97 +
98 + try
99 + {
100 + using var repository = Open(directory);
101 + if (repository is null) continue;
102 + found.Add(Summarise(repository, name));
103 + }
104 + catch (Exception exception) when (exception is LibGit2SharpException or IOException or UnauthorizedAccessException)
105 + {
106 + // One unreadable repository is not a reason for the index to fail.
107 + logger.LogWarning(exception, "Could not read the repository at {Directory}", directory);
108 + }
103 109 }
104 110 }
111 + catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
112 + {
113 + // The root itself. A home directory is 0700 unless someone says otherwise, so this is
114 + // the likely answer the first time this site is pointed at a real one.
115 + logger.LogError(exception, "Could not list the git repository root {Root}", Root);
116 + return new GitIndexView([], $"{Root} cannot be read by the user this site runs as.");
117 + }
105 118
106 - return found.OrderBy(repository => repository.DisplayName, StringComparer.OrdinalIgnoreCase).ToArray();
119 + return new GitIndexView(
120 + found.OrderBy(repository => repository.DisplayName, StringComparer.OrdinalIgnoreCase).ToArray(),
121 + null);
107 122 }
108 123
109 124 public GitRepoSummary? GetRepository(string name) =>