@page "/{Owner}/{Name}/tree/{Branch}"
@page "/{Owner}/{Name}/tree/{Branch}/{*Path}"
@inject IRepositoryService RepositoryService
@inject IGitService GitService
<PageTitle>@(repository?.Owner)/@(repository?.Name) - Forge</PageTitle>
@if (repository == null)
{
<p>Loading...</p>
}
else
{
<div class="repo-header">
<h1>
<a href="/@repository.Owner">@repository.Owner</a> / <a href="/@repository.Owner/@repository.Name"><strong>@repository.Name</strong></a>
</h1>
</div>
<div class="breadcrumb">
<a href="/@repository.Owner/@repository.Name/tree/@Branch">@Branch</a>
@foreach (var segment in breadcrumbSegments)
{
<span> / </span>
<a href="/@repository.Owner/@repository.Name/tree/@Branch/@segment.Path">@segment.Name</a>
}
</div>
@if (loading)
{
<p>Loading...</p>
}
else if (error != null)
{
<p class="empty-state">@error</p>
}
else if (treeNodes == null || !treeNodes.Any())
{
<p class="empty-state">Empty directory</p>
}
else
{
<table class="file-list">
<tbody>
@foreach (var node in treeNodes)
{
<tr>
<td class="icon">
@if (node.Type == TreeEntryType.Directory)
{
<span>📁</span>
}
else
{
<span>📄</span>
}
</td>
<td class="name">
@if (node.Type == TreeEntryType.Directory)
{
<a href="/@repository.Owner/@repository.Name/tree/@Branch/@node.Path">@node.Name</a>
}
else
{
<a href="/@repository.Owner/@repository.Name/blob/@Branch/@node.Path">@node.Name</a>
}
</td>
<td class="size">
@if (node.Size.HasValue)
{
@FormatSize(node.Size.Value)
}
</td>
</tr>
}
</tbody>
</table>
}
}
@code {
[Parameter] public string Owner { get; set; } = "";
[Parameter] public string Name { get; set; } = "";
[Parameter] public string Branch { get; set; } = "";
[Parameter] public string Path { get; set; } = "";
private Repository? repository;
private IEnumerable<TreeNode>? treeNodes;
private bool loading = true;
private string? error;
private List<(string Path, string Name)> breadcrumbSegments = [];
private string? lastRouteKey;
protected override async Task OnParametersSetAsync()
{
// Always recompute breadcrumbs from current Path parameter
breadcrumbSegments = ComputeBreadcrumbSegments(Path);
var routeKey = $"{Owner}/{Name}:{Branch}:{Path}";
if (lastRouteKey != routeKey)
{
lastRouteKey = routeKey;
await LoadAsync();
}
}
private async Task LoadAsync()
{
loading = true;
error = null;
if (repository == null || repository.Owner != Owner || repository.Name != Name)
{
repository = await RepositoryService.GetByOwnerAndNameAsync(Owner, Name);
}
if (repository == null)
{
loading = false;
return;
}
if (!GitService.RepositoryExists(repository))
{
error = "Repository not found on disk";
loading = false;
return;
}
var branches = await GitService.GetBranchesAsync(repository);
var branchExists = branches.Any(b => b.Name == Branch);
if (!branchExists)
{
if (!branches.Any())
{
error = "This repository is empty. Push some code to get started!";
}
else
{
var branchList = string.Join(", ", branches.Select(b => b.Name));
error = $"Branch '{Branch}' not found. Available branches: {branchList}";
}
}
else
{
treeNodes = await GitService.GetTreeAsync(repository, Branch, Path);
if (treeNodes == null && !string.IsNullOrEmpty(Path))
{
error = $"Path '{Path}' not found in this branch";
}
}
loading = false;
}
private static List<(string Path, string Name)> ComputeBreadcrumbSegments(string? path)
{
if (string.IsNullOrEmpty(path))
return [];
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var result = new List<(string Path, string Name)>();
var accumulated = "";
foreach (var segment in segments)
{
accumulated = string.IsNullOrEmpty(accumulated) ? segment : $"{accumulated}/{segment}";
result.Add((accumulated, segment));
}
return result;
}
private static string FormatSize(long bytes)
{
string[] s = { "B", "KB", "MB", "GB" };
int i = 0;
double size = bytes;
while (size >= 1024 && i < s.Length - 1) { size /= 1024; i++; }
return $"{size:0.#} {s[i]}";
}
}