.claude/settings.local.json
+2
-1
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 0a12f45..5fdef50 100644
@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(dotnet restore:*)"
"Bash(dotnet restore:*)",
"Bash(dotnet build:*)"
],
"deny": [],
"ask": []
GitBrowser/Components/Pages/Repo.razor
+139
-50
diff --git a/GitBrowser/Components/Pages/Repo.razor b/GitBrowser/Components/Pages/Repo.razor
index 2019f0e..72d25b7 100644
@@ -1,64 +1,107 @@
@page "/repo"
@page "/repo/tree/{Branch}/{*Path}"
@using LibGit2Sharp
@using Microsoft.Extensions.Options
@using Microsoft.AspNetCore.Http
@inject IOptions<RepositoryConfiguration> repoConfig
@inject IHttpContextAccessor HttpContextAccessor
<PageTitle>Repository</PageTitle>
<div class="container">
<header>
<h1>Repository Browser</h1>
</header>
@if (entries == null)
{
<p>Loading...</p>
}
else if (!entries.Any())
{
<div class="empty-state">
No files found in this repository.
</div>
}
else
{
<div class="file-list">
<table>
<thead>
<tr>
<th>Name</th>
<th>Message</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@foreach (var entry in entries.OrderByDescending(e => e.TargetType == TreeEntryTargetType.Tree).ThenBy(e =>
e.Name))
@if (notFound)
{
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
}
else
{
<div class="container">
<nav class="breadcrumb">
<a href="/repo/tree/@Uri.EscapeDataString(currentBranch)" class="branch-indicator">@currentBranch</a>
@if (!string.IsNullOrEmpty(currentPath))
{
var pathParts = currentPath.Split('/');
var accumulatedPath = "";
foreach (var part in pathParts)
{
accumulatedPath += (string.IsNullOrEmpty(accumulatedPath) ? "" : "/") + part;
var isLast = accumulatedPath == currentPath;
<span class="separator">/</span>
@if (isLast)
{
<span class="current">@part</span>
}
else
{
<a href="/repo/tree/@Uri.EscapeDataString(currentBranch)/@accumulatedPath">@part</a>
}
}
}
</nav>
@if (entries == null)
{
<p>Loading...</p>
}
else if (!entries.Any())
{
<div class="empty-state">
No files found in this repository.
</div>
}
else
{
<div class="file-list">
<table>
<thead>
<tr>
<td class="name-cell">
@if (entry.TargetType == TreeEntryTargetType.Tree)
{
<span class="icon">📁</span>
}
else
{
<span class="icon">📄</span>
}
<span>@entry.Name</span>
</td>
<td class="commit-message">-</td>
<td class="commit-date">-</td>
<th>Name</th>
<th>Message</th>
<th>Date</th>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</thead>
<tbody>
@foreach (var entry in entries.OrderByDescending(e => e.TargetType == TreeEntryTargetType.Tree).ThenBy(e =>
e.Name))
{
var entryPath = string.IsNullOrEmpty(currentPath) ? entry.Name : $"{currentPath}/{entry.Name}";
var entryUrl = $"/repo/tree/{Uri.EscapeDataString(currentBranch)}/{entryPath}";
<tr>
<td class="name-cell">
@if (entry.TargetType == TreeEntryTargetType.Tree)
{
<span class="icon">📁</span>
<a href="@entryUrl">@entry.Name</a>
}
else
{
<span class="icon">📄</span>
<span>@entry.Name</span>
}
</td>
<td class="commit-message">-</td>
<td class="commit-date">-</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
@code {
[Parameter]
public string? Branch { get; set; }
[Parameter]
public string? Path { get; set; }
private IEnumerable<TreeEntry>? entries;
private string currentBranch = "main";
private string currentPath = "";
private bool notFound = false;
protected override void OnInitialized()
{
@@ -70,7 +113,53 @@
}
using var repo = new Repository(repoPath);
var commit = repo.Head.Tip;
entries = commit?.Tree.ToList() ?? Enumerable.Empty<TreeEntry>();
// Determine which branch to use
currentBranch = Branch ?? repo.Head.FriendlyName;
currentPath = Path ?? "";
// Find the branch
var branch = repo.Branches[currentBranch];
if (branch == null)
{
SetNotFound();
return;
}
var commit = branch.Tip;
if (commit == null)
{
SetNotFound();
return;
}
// Navigate to the specified path
Tree tree = commit.Tree;
if (!string.IsNullOrEmpty(currentPath))
{
var treeEntry = commit[currentPath];
if (treeEntry?.TargetType == TreeEntryTargetType.Tree)
{
tree = (Tree)treeEntry.Target;
}
else
{
// Path doesn't exist or is not a directory
SetNotFound();
return;
}
}
entries = tree.ToList();
}
private void SetNotFound()
{
notFound = true;
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext != null)
{
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
}
}
}
GitBrowser/Components/Pages/Repo.razor.css
+45
-8
diff --git a/GitBrowser/Components/Pages/Repo.razor.css b/GitBrowser/Components/Pages/Repo.razor.css
index 3d17b28..f01c935 100644
@@ -1,20 +1,47 @@
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
padding: 0 1rem;
}
header {
border-bottom: 1px solid #d0d7de;
padding-bottom: 1rem;
margin-bottom: 1.5rem;
.breadcrumb {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.75rem 0;
margin-bottom: 1rem;
font-size: 0.875rem;
color: #24292f;
}
header h1 {
margin: 0;
font-size: 1.5rem;
.branch-indicator {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
font-weight: 600;
font-size: 0.75rem;
}
.breadcrumb .separator {
color: #57606a;
margin: 0 0.25rem;
}
.breadcrumb a {
color: #0969da;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb .current {
color: #24292f;
font-weight: 600;
}
.empty-state {
@@ -73,6 +100,16 @@ tbody td {
gap: 0.5rem;
}
.name-cell a {
color: #0969da;
text-decoration: none;
font-weight: 500;
}
.name-cell a:hover {
text-decoration: underline;
}
.icon {
font-size: 1rem;
display: inline-flex;
GitBrowser/Program.cs
+1
-0
diff --git a/GitBrowser/Program.cs b/GitBrowser/Program.cs
index a62a5e0..bbda766 100644
@@ -8,6 +8,7 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
builder.Services.AddHttpContextAccessor();
builder.Services.AddRepositoryConfiguration();