Blog/Components/Pages/Git/GitTree.razor 6.6 K · 188 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">No revision @(Reference ?? "HEAD") in @Repository.</p>
14 </section>
15 }
16 else if (view.Entries is { } entries)
17 {
18 <section class="block tree">
19 <h2 class="block-head">
20 <span>@(Path is { Length: > 0 } ? Path : "/")</span>
21 <span class="block-note">
22 @Counted(entries) at
23 <a class="sha" href="@CommitLink(view.Commit.Sha)">@view.Commit.ShortSha</a>
24 </span>
25 </h2>
26
27 <div class="scroll">
28 <table>
29 <thead>
30 <tr>
31 <th>mode</th>
32 <th class="wide">name</th>
33 <th class="num">size</th>
34 </tr>
35 </thead>
36 <tbody>
37 @if (Parent is { } parent)
38 {
39 <tr>
40 <td class="mode"></td>
41 <td class="wide"><a href="@TreeLink(parent, At)">..</a></td>
42 <td class="num"></td>
43 </tr>
44 }
45 @foreach (var entry in entries)
46 {
47 <tr class="@(entry.Kind == GitEntryKind.Directory ? "dir" : null)">
48 <td class="mode dim">@Mode(entry.Kind)</td>
49 <td class="wide">
50 @if (entry.Kind == GitEntryKind.Submodule)
51 {
52 @entry.Name
53 <span class="ref">@entry.Sha[..10]</span>
54 }
55 else
56 {
57 <a href="@TreeLink(entry.Path, At)">@entry.Name</a>
58 }
59 </td>
60 <td class="num dim">@(entry.Kind == GitEntryKind.Directory ? "" : Size(entry.Size))</td>
61 </tr>
62 }
63 </tbody>
64 </table>
65 </div>
66 </section>
67 }
68 else if (view.Blob is { } blob)
69 {
70 <section class="block">
71 <h2 class="block-head">
72 <span>@blob.Path</span>
73 <span class="block-note">
74 @Size(blob.Size)@(blob.Lines is { } counted ? $" · {Counted(counted.Count, "line")}" : "")
75 · <a href="@RawLink(blob.Path, At)">raw</a>
76 · <a href="@LogLink(At, blob.Path)">history</a>
77 </span>
78 </h2>
79
80 @if (blob.IsBinary)
81 {
82 <p class="empty">
83 Binary file. <a href="@RawLink(blob.Path, At)">Download it</a> to look inside.
84 </p>
85 }
86 else if (blob.TooLarge)
87 {
88 <p class="empty">
89 Too large to print here. <a href="@RawLink(blob.Path, At)">Read it raw</a>.
90 </p>
91 }
92 else if (blob.Lines is { Count: 0 })
93 {
94 <p class="empty">Empty file.</p>
95 }
96 else if (blob.Lines is { } lines)
97 {
98 @* Every line is addressable: #L42 scrolls to it and marks it, so a line of code
99 can be linked to from anywhere. *@
100 <div class="scroll">
101 <table class="code-table">
102 <tbody>
103 @for (var number = 1; number <= lines.Count; number++)
104 {
105 <tr id="L@(number)">
106 <td class="ln"><a href="#L@(number)">@number</a></td>
107 <td class="code">@lines[number - 1]</td>
108 </tr>
109 }
110 </tbody>
111 </table>
112 </div>
113 }
114 </section>
115 }
116 else
117 {
118 <section class="block">
119 <p class="empty">Nothing browsable at @Path in @view.Reference.</p>
120 </section>
121 }
122 </main>
123
124 @code {
125 /// <summary>The path inside the repository, straight off the catch-all route segment.</summary>
126 [Parameter]
127 public string? Path { get; set; }
128
129 private GitPathView? View { get; set; }
130
131 /// <summary>
132 /// The revision every link on this page stays on: what was asked for, or the branch the
133 /// repository's HEAD is on. Following a tree into a directory should not silently move you.
134 /// </summary>
135 private string? At => Reference ?? View?.Reference;
136
137 private string Title =>
138 Path is { Length: > 0 } path ? $"{path} · {Repository}" : $"{Repository} tree";
139
140 private IReadOnlyList<GitBar.Crumb> Crumbs { get; set; } = [];
141
142 /// <summary>The directory above, or null at the root.</summary>
143 private string? Parent
144 {
145 get
146 {
147 if (Path is not { Length: > 0 } path) return null;
148 var cut = path.LastIndexOf('/');
149 return cut < 0 ? "" : path[..cut];
150 }
151 }
152
153 protected override void OnParametersSet()
154 {
155 Path = Path?.Trim('/');
156 View = Service.GetPath(Repository, Reference, Path);
157
158 if (View is null || (View.Entries is null && View.Blob is null)) NotFound();
159
160 Crumbs = BuildCrumbs();
161 }
162
163 /// <summary>
164 /// One step per path segment, the last of them the page you are on. The bar already holds the
165 /// repository, so this starts inside it.
166 /// </summary>
167 private IReadOnlyList<GitBar.Crumb> BuildCrumbs()
168 {
169 if (Path is not { Length: > 0 } path) return [];
170
171 var segments = path.Split('/');
172 var crumbs = new List<GitBar.Crumb>(segments.Length);
173 var walked = "";
174
175 foreach (var segment in segments)
176 {
177 walked = walked.Length == 0 ? segment : $"{walked}/{segment}";
178 crumbs.Add(new GitBar.Crumb(segment, walked == path ? null : TreeLink(walked, At)));
179 }
180
181 return crumbs;
182 }
183
184 private static string Counted(IReadOnlyList<GitTreeEntry> entries) => Counted(entries.Count, "entry", "entries");
185
186 private static string Counted(int n, string one, string? many = null) =>
187 n == 1 ? $"1 {one}" : $"{n} {many ?? one + "s"}";
188 }