Blog/Components/Pages/Git/GitTree.razor 7.6 K · 213 lines · raw · history

1 @page "/git/{Repository}/tree"
2 @page "/git/{Repository}/tree/{*Path}"
3 @inherits GitPage
4
5 <PageTitle>@Title — git</PageTitle>
6
7 <GitBar Repository="@Repository" Reference="@At" Tab="tree" Path="@Crumbs"/>
8
9 <main class="git-main">
10 @if (View is not { } view)
11 {
12 <section class="block">
13 <p class="empty">
14 @(Empty
15 ? $"Nothing has been pushed to {Repository} yet."
16 : $"No revision called {Reference ?? "HEAD"} in {Repository}.")
17 </p>
18 </section>
19 }
20 else if (view.Entries is { } entries)
21 {
22 <section class="block tree">
23 <h2 class="block-head">
24 <span>@(Path is { Length: > 0 } ? Path : "/")</span>
25 <span class="block-note">
26 @Counted(entries) at
27 <a class="sha" href="@CommitLink(view.Commit.Sha)">@view.Commit.ShortSha</a>
28 </span>
29 </h2>
30
31 <div class="scroll">
32 <table>
33 <thead>
34 <tr>
35 <th>mode</th>
36 <th class="wide">name</th>
37 <th class="num">size</th>
38 </tr>
39 </thead>
40 <tbody>
41 @if (Parent is { } parent)
42 {
43 <tr>
44 <td class="mode"></td>
45 <td class="wide"><a href="@TreeLink(parent, At)">..</a></td>
46 <td class="num"></td>
47 </tr>
48 }
49 @foreach (var entry in entries)
50 {
51 <tr class="@(entry.Kind == GitEntryKind.Directory ? "dir" : null)">
52 <td class="mode dim">@Mode(entry.Kind)</td>
53 <td class="wide">
54 @if (entry.Kind == GitEntryKind.Submodule)
55 {
56 @entry.Name
57 <span class="ref">@entry.Sha[..10]</span>
58 }
59 else
60 {
61 <a href="@TreeLink(entry.Path, At)">@entry.Name</a>
62 }
63 </td>
64 <td class="num dim">@(entry.Kind == GitEntryKind.Directory ? "" : Size(entry.Size))</td>
65 </tr>
66 }
67 </tbody>
68 </table>
69 </div>
70 </section>
71 }
72 else if (view.Blob is { } blob)
73 {
74 <section class="block">
75 <h2 class="block-head">
76 <span>@blob.Path</span>
77 <span class="block-note">
78 @Size(blob.Size)@(Code.Count > 0 ? $" · {Counted(Code.Count, "line")}" : "")
79 · <a href="@RawLink(blob.Path, At)">raw</a>
80 · <a href="@LogLink(At, blob.Path)">history</a>
81 </span>
82 </h2>
83
84 @if (blob.IsBinary)
85 {
86 <p class="empty">
87 Binary file. <a href="@RawLink(blob.Path, At)">Download it</a> to look inside.
88 </p>
89 }
90 else if (blob.TooLarge)
91 {
92 <p class="empty">
93 Too large to print here. <a href="@RawLink(blob.Path, At)">Read it raw</a>.
94 </p>
95 }
96 else if (Code.Count == 0)
97 {
98 <p class="empty">Empty file.</p>
99 }
100 else
101 {
102 @* Every line is addressable: #L42 scrolls to it and marks it, so a line of code
103 can be linked to from anywhere. *@
104 <div class="scroll">
105 <table class="code-table">
106 <tbody>
107 @for (var number = 1; number <= Code.Count; number++)
108 {
109 <tr id="L@(number)">
110 <td class="ln"><a href="#L@(number)">@number</a></td>
111 <td class="code"><GitCode Tokens="@Code[number - 1]"/></td>
112 </tr>
113 }
114 </tbody>
115 </table>
116 </div>
117 }
118 </section>
119 }
120 else
121 {
122 <section class="block">
123 <p class="empty">Nothing browsable at @Path in @view.Reference.</p>
124 </section>
125 }
126 </main>
127
128 @code {
129 /// <summary>The path inside the repository, straight off the catch-all route segment.</summary>
130 [Parameter]
131 public string? Path { get; set; }
132
133 private GitPathView? View { get; set; }
134
135 /// <summary>
136 /// The revision every link on this page stays on: what was asked for, or the branch the
137 /// repository's HEAD is on. Following a tree into a directory should not silently move you.
138 /// </summary>
139 private string? At => Reference ?? View?.Reference;
140
141 private string Title =>
142 Path is { Length: > 0 } path ? $"{path} · {Repository}" : $"{Repository} tree";
143
144 private IReadOnlyList<GitBar.Crumb> Crumbs { get; set; } = [];
145
146 /// <summary>Set when the repository is real but has nothing in it yet.</summary>
147 private bool Empty { get; set; }
148
149 /// <summary>The file being shown, one list of tokens per line. Empty unless this is a blob.</summary>
150 private IReadOnlyList<IReadOnlyList<SyntaxToken>> Code { get; set; } = [];
151
152 /// <summary>The directory above, or null at the root.</summary>
153 private string? Parent
154 {
155 get
156 {
157 if (Path is not { Length: > 0 } path) return null;
158 var cut = path.LastIndexOf('/');
159 return cut < 0 ? "" : path[..cut];
160 }
161 }
162
163 protected override void OnParametersSet()
164 {
165 Path = Path?.Trim('/');
166 View = Service.GetPath(Repository, Reference, Path);
167
168 if (View is null)
169 {
170 // Nothing resolved, which is two different things. A repository that has refs was
171 // reached by a bad revision; one with none is a repository nobody has pushed to, and
172 // its tree tab should say so rather than answer 404.
173 Empty = Service.GetRefs(Repository) is { Branches.Count: 0 };
174 if (!Empty) NotFound();
175 }
176 else if (View.Entries is null && View.Blob is null)
177 {
178 NotFound();
179 }
180
181 Code = View?.Blob is { Text: { } text } blob
182 ? SyntaxHighlighter.Highlight(text, SyntaxHighlighter.GrammarFor(blob.Path))
183 : [];
184
185 Crumbs = BuildCrumbs();
186 }
187
188 /// <summary>
189 /// One step per path segment, the last of them the page you are on. The bar already holds the
190 /// repository, so this starts inside it.
191 /// </summary>
192 private IReadOnlyList<GitBar.Crumb> BuildCrumbs()
193 {
194 if (Path is not { Length: > 0 } path) return [];
195
196 var segments = path.Split('/');
197 var crumbs = new List<GitBar.Crumb>(segments.Length);
198 var walked = "";
199
200 foreach (var segment in segments)
201 {
202 walked = walked.Length == 0 ? segment : $"{walked}/{segment}";
203 crumbs.Add(new GitBar.Crumb(segment, walked == path ? null : TreeLink(walked, At)));
204 }
205
206 return crumbs;
207 }
208
209 private static string Counted(IReadOnlyList<GitTreeEntry> entries) => Counted(entries.Count, "entry", "entries");
210
211 private static string Counted(int n, string one, string? many = null) =>
212 n == 1 ? $"1 {one}" : $"{n} {many ?? one + "s"}";
213 }