Commit: 3634940
Parent: 9bb0c77

Multi-repo browsing

Mårten Åsberg committed on 2025-11-09 at 22:01
But only it you already know the names.
.claude/settings.local.json +2 -1
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c6b296d..b1016dc 100644
@@ -3,7 +3,8 @@
"allow": [
"Bash(dotnet restore:*)",
"Bash(dotnet build:*)",
"Bash(dotnet run:*)"
"Bash(dotnet run:*)",
"Bash(dir:*)"
],
"deny": [],
"ask": []
GitBrowser/Components/Pages/Commit.razor +7 -5
diff --git a/GitBrowser/Components/Pages/Commit.razor b/GitBrowser/Components/Pages/Commit.razor
index c918f83..e316531 100644
@@ -1,6 +1,6 @@
@page "/repo/commit/{Hash}"
@page "/{RepoName}/commit/{Hash}"
<PageTitle>Commit @Hash</PageTitle>
<PageTitle>@RepoName - Commit @Hash</PageTitle>
@if (notFound)
{
@@ -10,6 +10,8 @@
else if (commit != null)
{
<div class="container">
<h2 class="repo-title">@RepoName</h2>
<div class="commit-header">
<div class="commit-hashes">
<div class="commit-hash">
@@ -21,7 +23,7 @@ else if (commit != null)
{
<div class="parent-hash">
<span class="hash-label">Parent:</span>
<a href="/repo/commit/@commit.Parents.First().Sha">
<a href="/@Uri.EscapeDataString(RepoName)/commit/@commit.Parents.First().Sha">
<code>@commit.Parents.First().Sha.Substring(0, 7)</code>
</a>
</div>
@@ -30,7 +32,7 @@ else if (commit != null)
{
<div class="child-hash">
<span class="hash-label">Child:</span>
<a href="/repo/commit/@childCommit.Sha">
<a href="/@Uri.EscapeDataString(RepoName)/commit/@childCommit.Sha">
<code>@childCommit.Sha.Substring(0, 7)</code>
</a>
</div>
@@ -53,7 +55,7 @@ else if (commit != null)
}
<div class="commit-actions">
<a href="/repo/tree/@commit.Sha" class="browse-button">
<a href="/@Uri.EscapeDataString(RepoName)/tree/@commit.Sha" class="browse-button">
Browse files at this commit
</a>
</div>
GitBrowser/Components/Pages/Commit.razor.cs +50 -2
diff --git a/GitBrowser/Components/Pages/Commit.razor.cs b/GitBrowser/Components/Pages/Commit.razor.cs
index 56c8456..42cb650 100644
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Microsoft.AspNetCore.Components;
@@ -11,6 +12,9 @@ namespace GitBrowser.Components.Pages;
public partial class Commit : IDisposable
{
[Parameter]
public string? RepoName { get; set; }
[Parameter]
public string? Hash { get; set; }
[Inject]
@@ -19,6 +23,9 @@ public partial class Commit : IDisposable
[Inject]
public required IHttpContextAccessor HttpContextAccessor { get; set; }
[Inject]
public required Microsoft.AspNetCore.Components.NavigationManager NavigationManager { get; set; }
private Repository? repo = null;
private LibGit2Sharp.Commit? commit = null;
private LibGit2Sharp.Commit? childCommit = null;
@@ -44,19 +51,60 @@ public partial class Commit : IDisposable
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Hash))
if (string.IsNullOrEmpty(RepoName) || string.IsNullOrEmpty(Hash))
{
SetNotFound();
return;
}
var repoPath = RepoConfig.Value.Path;
var basePath = RepoConfig.Value.Path;
if (string.IsNullOrEmpty(basePath))
{
SetNotFound();
return;
}
// Find the actual repository directory (case-insensitive)
string? actualRepoName = null;
string? repoPath = null;
if (Directory.Exists(basePath))
{
var directories = Directory.GetDirectories(basePath);
foreach (var dir in directories)
{
var dirName = System.IO.Path.GetFileName(dir);
if (string.Equals(dirName, RepoName, StringComparison.OrdinalIgnoreCase))
{
actualRepoName = dirName;
repoPath = dir;
break;
}
}
}
if (string.IsNullOrEmpty(repoPath) || !Repository.IsValid(repoPath))
{
SetNotFound();
return;
}
// Check for git-daemon-export-ok file
var exportOkPath = System.IO.Path.Combine(repoPath, "git-daemon-export-ok");
if (!File.Exists(exportOkPath))
{
SetNotFound();
return;
}
// Check if casing matches - redirect if not
if (actualRepoName != RepoName)
{
var correctPath = $"/{Uri.EscapeDataString(actualRepoName)}/commit/{Hash}";
NavigationManager.NavigateTo(correctPath, forceLoad: true);
return;
}
repo = new Repository(repoPath);
try
GitBrowser/Components/Pages/Commit.razor.css +7 -0
diff --git a/GitBrowser/Components/Pages/Commit.razor.css b/GitBrowser/Components/Pages/Commit.razor.css
index 23f33f9..c9b1840 100644
@@ -4,6 +4,13 @@
padding: 2rem 1rem;
}
.repo-title {
font-size: 1.75rem;
font-weight: 600;
color: #24292f;
margin: 0 0 1.5rem 0;
}
.commit-header {
background: white;
border: 1px solid #d0d7de;
GitBrowser/Components/Pages/Repo.razor +10 -8
diff --git a/GitBrowser/Components/Pages/Repo.razor b/GitBrowser/Components/Pages/Repo.razor
index 72a89c3..c45b8e8 100644
@@ -1,8 +1,8 @@
@page "/repo"
@page "/repo/tree/{Branch}/{*Path}"
@page "/{RepoName}"
@page "/{RepoName}/tree/{Branch}/{*Path}"
@using LibGit2Sharp
<PageTitle>Repository</PageTitle>
<PageTitle>@RepoName</PageTitle>
@if (notFound)
{
@@ -12,8 +12,10 @@
else
{
<div class="container">
<h2 class="repo-title">@RepoName</h2>
<nav class="breadcrumb">
<a href="/repo/tree/@Uri.EscapeDataString(currentBranch)" class="branch-indicator">@currentBranch</a>
<a href="/@Uri.EscapeDataString(RepoName)/tree/@Uri.EscapeDataString(currentBranch)" class="branch-indicator">@currentBranch</a>
@if (!string.IsNullOrEmpty(currentPath))
{
var pathParts = currentPath.Split('/');
@@ -29,7 +31,7 @@ else
}
else
{
<a href="/repo/tree/@Uri.EscapeDataString(currentBranch)/@accumulatedPath">@part</a>
<a href="/@Uri.EscapeDataString(RepoName)/tree/@Uri.EscapeDataString(currentBranch)/@accumulatedPath">@part</a>
}
}
}
@@ -62,7 +64,7 @@ else
@foreach (var entry in entries.OrderByDescending(e => e.TargetType == TreeEntryTargetType.Tree).ThenBy(e => e.Name))
{
var entryPath = string.IsNullOrEmpty(displayPath) ? entry.Name : $"{displayPath}/{entry.Name}";
var entryUrl = $"/repo/tree/{Uri.EscapeDataString(currentBranch)}/{entryPath}";
var entryUrl = $"/{Uri.EscapeDataString(RepoName)}/tree/{Uri.EscapeDataString(currentBranch)}/{entryPath}";
var isCurrentFile = isViewingFile && entryPath == currentPath;
var commitInfo = GetLastCommitForPath(entryPath);
@@ -82,7 +84,7 @@ else
<td class="commit-message">
@if (commitInfo != null)
{
<a href="/repo/commit/@commitInfo.Sha">@commitInfo.MessageShort</a>
<a href="/@Uri.EscapeDataString(RepoName)/commit/@commitInfo.Sha">@commitInfo.MessageShort</a>
}
else
{
@@ -92,7 +94,7 @@ else
<td class="commit-date">
@if (commitInfo != null)
{
<a href="/repo/commit/@commitInfo.Sha" title="@commitInfo.When.ToString("yyyy-MM-dd HH:mm:ss")">@GetRelativeTime(commitInfo.When)</a>
<a href="/@Uri.EscapeDataString(RepoName)/commit/@commitInfo.Sha" title="@commitInfo.When.ToString("yyyy-MM-dd HH:mm:ss")">@GetRelativeTime(commitInfo.When)</a>
}
else
{
GitBrowser/Components/Pages/Repo.razor.cs +60 -2
diff --git a/GitBrowser/Components/Pages/Repo.razor.cs b/GitBrowser/Components/Pages/Repo.razor.cs
index 15797a2..a854b4a 100644
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Microsoft.AspNetCore.Components;
@@ -11,6 +12,9 @@ namespace GitBrowser.Components.Pages;
public partial class Repo : IDisposable
{
[Parameter]
public string? RepoName { get; set; }
[Parameter]
public string? Branch { get; set; }
[Parameter]
@@ -22,6 +26,9 @@ public partial class Repo : IDisposable
[Inject]
public required IHttpContextAccessor HttpContextAccessor { get; set; }
[Inject]
public required Microsoft.AspNetCore.Components.NavigationManager NavigationManager { get; set; }
private IEnumerable<TreeEntry>? entries;
private string currentBranch = "main";
private string currentPath = "";
@@ -45,10 +52,61 @@ public partial class Repo : IDisposable
protected override void OnInitialized()
{
var repoPath = RepoConfig.Value.Path;
if (string.IsNullOrEmpty(RepoName))
{
SetNotFound();
return;
}
var basePath = RepoConfig.Value.Path;
if (string.IsNullOrEmpty(basePath))
{
SetNotFound();
return;
}
// Find the actual repository directory (case-insensitive)
string? actualRepoName = null;
string? repoPath = null;
if (Directory.Exists(basePath))
{
var directories = Directory.GetDirectories(basePath);
foreach (var dir in directories)
{
var dirName = System.IO.Path.GetFileName(dir);
if (string.Equals(dirName, RepoName, StringComparison.OrdinalIgnoreCase))
{
actualRepoName = dirName;
repoPath = dir;
break;
}
}
}
if (string.IsNullOrEmpty(repoPath) || !Repository.IsValid(repoPath))
{
entries = Enumerable.Empty<TreeEntry>();
SetNotFound();
return;
}
// Check for git-daemon-export-ok file
var exportOkPath = System.IO.Path.Combine(repoPath, "git-daemon-export-ok");
if (!File.Exists(exportOkPath))
{
SetNotFound();
return;
}
// Check if casing matches - redirect if not
if (actualRepoName != RepoName)
{
var correctPath =
string.IsNullOrEmpty(Branch) ? $"/{Uri.EscapeDataString(actualRepoName)}"
: string.IsNullOrEmpty(Path) ? $"/{Uri.EscapeDataString(actualRepoName)}/tree/{Branch}"
: $"/{Uri.EscapeDataString(actualRepoName)}/tree/{Branch}/{Path}";
NavigationManager.NavigateTo(correctPath, forceLoad: true);
return;
}
GitBrowser/Components/Pages/Repo.razor.css +7 -0
diff --git a/GitBrowser/Components/Pages/Repo.razor.css b/GitBrowser/Components/Pages/Repo.razor.css
index d98c639..868d2a0 100644
@@ -4,6 +4,13 @@
padding: 0 1rem;
}
.repo-title {
font-size: 1.75rem;
font-weight: 600;
color: #24292f;
margin: 2rem 0 1.5rem 0;
}
.breadcrumb {
display: flex;
align-items: center;
GitBrowser/appsettings.Development.json +1 -1
diff --git a/GitBrowser/appsettings.Development.json b/GitBrowser/appsettings.Development.json
index 69fb50c..ec34a84 100644
@@ -6,6 +6,6 @@
}
},
"Repository": {
"Path": ".."
"Path": "../.."
}
}