.claude/settings.local.json
+9
-0
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..0a12f45
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(dotnet restore:*)"
],
"deny": [],
"ask": []
}
}
CLAUDE.md
+80
-0
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b920156
@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
GitBrowser is an ASP.NET Core Blazor web application that provides single-user GitHub-like views for self-hosted git repositories. The project targets .NET 10.0 (preview) and uses Blazor for the frontend with a familiar GitHub-style interface for browsing files, commits, and repository contents.
## Build and Development Commands
### Build the project
```bash
dotnet build
```
### Run the application (development)
```bash
dotnet run --project GitBrowser
```
The application runs on:
- HTTP: http://localhost:5186
- HTTPS: https://localhost:7270
### Format code
Code formatting is handled automatically via CSharpier.MsBuild during build. To manually format:
```bash
dotnet csharpier format .
```
## Project Configuration
### SDK and Framework
- Targets .NET 10.0 (RC2, preview SDK)
- Uses Central Package Management (CPM) - package versions are defined in `Directory.Packages.props`
- Package lock files are enabled (`RestorePackagesWithLockFile`)
- Implicit usings are disabled - all namespaces must be explicitly imported
### Code Style
- EditorConfig enforces consistent formatting:
- C# files: 4 spaces, max 120 chars per line
- XML/JSON/config: 2 spaces
- HTML/Razor: 2 spaces
- CSS: 2 spaces
- Line endings: LF (Unix-style)
## Architecture
### Project Structure
- **GitBrowser/** - Main web application project
- **Components/** - Blazor components
- **Pages/** - Routable page components (Home, Error, NotFound)
- **Layout/** - Layout components (MainLayout, NavMenu)
- **Routes.razor** - Routing configuration
- **App.razor** - Root application component
- **_Imports.razor** - Global Razor imports
- **wwwroot/** - Static assets (CSS, favicon)
- **Program.cs** - Application entry point and configuration
### Application Setup (Program.cs)
- Uses ASP.NET Core minimal hosting model
- Razor Components are registered as services
- Static Assets pipeline enabled (replaces traditional static files)
- Antiforgery protection enabled
- Status code pages configured for 404 handling
- HSTS enabled in production
- Blazor navigation exceptions disabled via `BlazorDisableThrowNavigationException`
### Blazor Configuration
- Uses static rendering (no interactive render modes configured yet)
- Routes are defined in `Components/Routes.razor`
- Main layout in `Components/Layout/MainLayout.razor`
- Scoped CSS is supported (e.g., `MainLayout.razor.css`)
## Important Notes
- No implicit usings: Always add explicit `using` statements for all namespaces
- Package versions must be managed in `Directory.Packages.props` (CPM enabled)
- When adding new packages, update `packages.lock.json` via `dotnet restore`
- The project uses .NET 10.0 RC which requires the specific SDK version in `global.json`
Directory.Packages.props
+1
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d08b5ce..eff3123 100644
@@ -4,5 +4,6 @@
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="CSharpier.MsBuild" Version="1.0.2" />
<PackageVersion Include="LibGit2Sharp" Version="0.30.0" />
</ItemGroup>
</Project>
GitBrowser/Components/Layout/MainLayout.razor
+11
-10
diff --git a/GitBrowser/Components/Layout/MainLayout.razor b/GitBrowser/Components/Layout/MainLayout.razor
index 43a1ee6..e44a591 100644
@@ -1,18 +1,19 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
<header class="gh-header">
<div class="gh-header-content">
<a href="/" class="gh-logo">
<svg height="32" viewBox="0 0 16 16" width="32" fill="currentColor">
<path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"/>
</svg>
<span class="gh-title">GitBrowser</span>
</a>
</div>
</header>
<article class="content px-4">
@Body
</article>
<main class="gh-main">
@Body
</main>
</div>
GitBrowser/Components/Layout/MainLayout.razor.css
+69
-78
diff --git a/GitBrowser/Components/Layout/MainLayout.razor.css b/GitBrowser/Components/Layout/MainLayout.razor.css
index 38d1f25..c8453bc 100644
@@ -1,98 +1,89 @@
.page {
position: relative;
display: flex;
flex-direction: column;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #ffffff;
}
main {
flex: 1;
.gh-header {
background-color: #24292f;
border-bottom: 1px solid #d0d7de;
position: sticky;
top: 0;
z-index: 32;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
.gh-header-content {
max-width: 1280px;
margin: 0 auto;
padding: 16px 32px;
display: flex;
align-items: center;
gap: 16px;
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
.gh-logo {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
text-decoration: none;
font-weight: 600;
font-size: 16px;
transition: opacity 0.2s;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
.gh-logo:hover {
opacity: 0.8;
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.gh-logo svg {
flex-shrink: 0;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.gh-title {
color: #ffffff;
font-size: 16px;
font-weight: 600;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.gh-main {
flex: 1;
max-width: 1280px;
width: 100%;
margin: 0 auto;
padding: 24px 32px;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
@media (max-width: 768px) {
.gh-header-content {
padding: 16px 16px;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
.gh-main {
padding: 16px 16px;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
color-scheme: light only;
background: #fff3cd;
border-top: 1px solid #ffc107;
bottom: 0;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
color: #856404;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
GitBrowser/Components/Layout/NavMenu.razor
+0
-17
diff --git a/GitBrowser/Components/Layout/NavMenu.razor b/GitBrowser/Components/Layout/NavMenu.razor
deleted file mode 100644
index 437f199..0000000
@@ -1,17 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">GitBrowser</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
</nav>
</div>
GitBrowser/Components/Layout/NavMenu.razor.css
+0
-106
diff --git a/GitBrowser/Components/Layout/NavMenu.razor.css b/GitBrowser/Components/Layout/NavMenu.razor.css
deleted file mode 100644
index 99cd81e..0000000
@@ -1,106 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")
no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0, 0, 0, 0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255, 255, 255, 0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
GitBrowser/Components/Pages/Home.razor
+0
-7
diff --git a/GitBrowser/Components/Pages/Home.razor b/GitBrowser/Components/Pages/Home.razor
deleted file mode 100644
index 9001e0b..0000000
@@ -1,7 +0,0 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
GitBrowser/Components/Pages/Repo.razor
+76
-0
diff --git a/GitBrowser/Components/Pages/Repo.razor b/GitBrowser/Components/Pages/Repo.razor
new file mode 100644
index 0000000..2019f0e
@@ -0,0 +1,76 @@
@page "/repo"
@using LibGit2Sharp
@using Microsoft.Extensions.Options
@inject IOptions<RepositoryConfiguration> repoConfig
<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))
{
<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>
</tr>
}
</tbody>
</table>
</div>
}
</div>
@code {
private IEnumerable<TreeEntry>? entries;
protected override void OnInitialized()
{
var repoPath = repoConfig.Value.Path;
if (string.IsNullOrEmpty(repoPath) || !Repository.IsValid(repoPath))
{
entries = Enumerable.Empty<TreeEntry>();
return;
}
using var repo = new Repository(repoPath);
var commit = repo.Head.Tip;
entries = commit?.Tree.ToList() ?? Enumerable.Empty<TreeEntry>();
}
}
GitBrowser/Components/Pages/Repo.razor.css
+92
-0
diff --git a/GitBrowser/Components/Pages/Repo.razor.css b/GitBrowser/Components/Pages/Repo.razor.css
new file mode 100644
index 0000000..3d17b28
@@ -0,0 +1,92 @@
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
header {
border-bottom: 1px solid #d0d7de;
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #24292f;
}
.empty-state {
padding: 2rem;
text-align: center;
color: #57606a;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
}
.file-list {
border: 1px solid #d0d7de;
border-radius: 6px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
thead {
background: #f6f8fa;
border-bottom: 1px solid #d0d7de;
}
thead th {
padding: 0.5rem 1rem;
text-align: left;
font-size: 0.75rem;
font-weight: 600;
color: #57606a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
tbody tr {
border-top: 1px solid #d0d7de;
}
tbody tr:hover {
background: #f6f8fa;
cursor: pointer;
}
tbody td {
padding: 0.5rem 1rem;
color: #24292f;
}
.name-cell {
display: flex;
align-items: center;
gap: 0.5rem;
}
.icon {
font-size: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
}
.commit-message,
.commit-date {
color: #57606a;
font-size: 0.875rem;
}
.commit-date {
width: 180px;
}
GitBrowser/GitBrowser.csproj
+3
-0
diff --git a/GitBrowser/GitBrowser.csproj b/GitBrowser/GitBrowser.csproj
index 045902a..c6a721d 100644
@@ -2,4 +2,7 @@
<PropertyGroup>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibGit2Sharp" />
</ItemGroup>
</Project>
GitBrowser/Program.cs
+3
-0
diff --git a/GitBrowser/Program.cs b/GitBrowser/Program.cs
index 43af5d7..a62a5e0 100644
@@ -1,3 +1,4 @@
using GitBrowser;
using GitBrowser.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -8,6 +9,8 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
builder.Services.AddRepositoryConfiguration();
var app = builder.Build();
// Configure the HTTP request pipeline.
GitBrowser/RepositoryConfiguration.cs
+24
-0
diff --git a/GitBrowser/RepositoryConfiguration.cs b/GitBrowser/RepositoryConfiguration.cs
new file mode 100644
index 0000000..7395823
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace GitBrowser;
public sealed class RepositoryConfiguration
{
[Required]
public required string Path { get; set; }
}
[OptionsValidator]
public sealed partial class RepositoryConfigurationValidator : IValidateOptions<RepositoryConfiguration>;
public static class RepositoryConfigurationServiceCollectionExtensions
{
public static IServiceCollection AddRepositoryConfiguration(this IServiceCollection services)
{
services.AddOptions<RepositoryConfiguration>().BindConfiguration("Repository").ValidateOnStart();
services.AddTransient<IValidateOptions<RepositoryConfiguration>, RepositoryConfigurationValidator>();
return services;
}
}
GitBrowser/appsettings.Development.json
+3
-0
diff --git a/GitBrowser/appsettings.Development.json b/GitBrowser/appsettings.Development.json
index 0c208ae..69fb50c 100644
@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Repository": {
"Path": ".."
}
}
GitBrowser/appsettings.json
+2
-1
diff --git a/GitBrowser/appsettings.json b/GitBrowser/appsettings.json
index 10f68b8..3ddc4f0 100644
@@ -5,5 +5,6 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"RepositoryPath": ""
}
GitBrowser/packages.lock.json
+15
-7
diff --git a/GitBrowser/packages.lock.json b/GitBrowser/packages.lock.json
index 781986a..ae32120 100644
@@ -8,18 +8,26 @@
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.DotNet.ILCompiler": {
"LibGit2Sharp": {
"type": "Direct",
"requested": "[10.0.0-rc.2.25502.107, )",
"resolved": "10.0.0-rc.2.25502.107",
"contentHash": "jmMMOim4ZCn89tV0lzEZ5M5vq6jjd2i6eG8L3vulg72gLzlx8c5+NBbNv2GdIwxP7gZphDUr77fSx0Vhi0s7Cg=="
"requested": "[0.30.0, )",
"resolved": "0.30.0",
"contentHash": "1cBg/7nz6q+yzvComhqQaTS4nbXloLp/x29ZgY/Rr82ohpkuaWT8ZYzsPLFvs1zmCh/4xQbVt5sQxazGblgRig==",
"dependencies": {
"LibGit2Sharp.NativeBinaries": "[2.0.322]"
}
},
"Microsoft.NET.ILLink.Tasks": {
"Microsoft.AspNetCore.App.Internal.Assets": {
"type": "Direct",
"requested": "[10.0.0-rc.2.25502.107, )",
"resolved": "10.0.0-rc.2.25502.107",
"contentHash": "31DsUbLGwks+cdV++WjVY17hq0Dx21KA/F5MQRhBHSgpxFsdEXIJLIe3+2nzH9ZlDtAYR5qhQV5Abm+CIdvdzw=="
"contentHash": "/OmSSkaYxV5AWir+nmaZC0SHLheZmTZ5VXJIQGKGsb7dY/TrH+kQWD5KgJnmtGEeH3m5nXc1VdYMye5bv73U2g=="
},
"LibGit2Sharp.NativeBinaries": {
"type": "Transitive",
"resolved": "2.0.322",
"contentHash": "EWQaEevzc8uS1ZGS6T83jqUPBY//2WUSCHwbZHBoHOjlfSehqr30nm/VAhJPzjam/69sv7nlLKVcho9t2XuR/Q=="
}
}
}
}
}
\ No newline at end of file
GitBrowser/wwwroot/app.css
+138
-36
diff --git a/GitBrowser/wwwroot/app.css b/GitBrowser/wwwroot/app.css
index 00cace9..0ea3cb6 100644
@@ -1,70 +1,172 @@
/* GitHub-inspired color scheme and typography */
:root {
--gh-color-canvas-default: #ffffff;
--gh-color-canvas-subtle: #f6f8fa;
--gh-color-border-default: #d0d7de;
--gh-color-border-muted: #d8dee4;
--gh-color-fg-default: #1f2328;
--gh-color-fg-muted: #656d76;
--gh-color-accent-fg: #0969da;
--gh-color-accent-emphasis: #0969da;
--gh-color-danger-fg: #d1242f;
--gh-color-success-fg: #1a7f37;
--gh-color-btn-bg: #f6f8fa;
--gh-color-btn-border: rgba(31, 35, 40, 0.15);
--gh-color-btn-hover-bg: #f3f4f6;
--gh-color-btn-hover-border: rgba(31, 35, 40, 0.15);
}
html,
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 14px;
line-height: 1.5;
color: var(--gh-color-fg-default);
background-color: var(--gh-color-canvas-default);
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
a {
color: var(--gh-color-accent-fg);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 0;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
h1 {
font-size: 32px;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--gh-color-border-muted);
}
h2 {
font-size: 24px;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--gh-color-border-muted);
}
h3 {
font-size: 20px;
}
a,
.btn-link {
color: #006bb7;
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 12px;
color: var(--gh-color-fg-muted);
}
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: var(--gh-color-canvas-subtle);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
monospace;
}
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--gh-color-canvas-subtle);
border-radius: 6px;
}
pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
color: #ffffff;
background-color: #2da44e;
border: 1px solid rgba(31, 35, 40, 0.15);
padding: 5px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
border-radius: 6px;
cursor: pointer;
transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
}
.btn:focus,
.btn:active:focus,
.btn-link.nav-link:focus,
.form-control:focus,
.form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
.btn-primary:hover {
background-color: #2c974b;
border-color: rgba(31, 35, 40, 0.15);
}
.content {
padding-top: 1.1rem;
.btn {
color: var(--gh-color-fg-default);
background-color: var(--gh-color-btn-bg);
border: 1px solid var(--gh-color-btn-border);
padding: 5px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
border-radius: 6px;
cursor: pointer;
transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
}
h1:focus {
outline: none;
.btn:hover {
background-color: var(--gh-color-btn-hover-bg);
border-color: var(--gh-color-btn-hover-border);
}
.valid.modified:not([type="checkbox"]) {
outline: 1px solid #26b050;
border-color: var(--gh-color-success-fg);
}
.invalid {
outline: 1px solid #e50000;
border-color: var(--gh-color-danger-fg);
}
.validation-message {
color: #e50000;
color: var(--gh-color-danger-fg);
font-size: 12px;
margin-top: 4px;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=)
no-repeat 1rem/1.8rem,
#b32121;
#d1242f;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
border-radius: 6px;
margin: 16px;
}
.blazor-error-boundary::after {
content: "An error has occurred.";
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder,
.form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder,
.form-floating > .form-control:focus::placeholder {
text-align: start;
}
README.md
+3
-2
diff --git a/README.md b/README.md
index 603d8ed..4ae1915 100644
@@ -1,4 +1,5 @@
# GitBrowser
This project aims to provide a simple way of hosting web UIs for git
repositories.
GitBrowser provides single-user GitHub-like views for self-hosted git repositories.
This project aims to provide a simple way of hosting web UIs for git repositories, with a familiar GitHub-style interface for browsing files, commits, and repository contents.
docs/POC_PLAN.md
+80
-0
diff --git a/docs/POC_PLAN.md b/docs/POC_PLAN.md
new file mode 100644
index 0000000..0f8c73c
@@ -0,0 +1,80 @@
# GitBrowser POC Plan
## POC Scope
### Iteration 1: Basic Repository File View (Current)
**Goal:** Display root directory file listing at `/repo` path
**Features:**
- Single route: `/repo`
- Display repository file tree at root level (HEAD commit)
- Show file/folder list similar to GitHub's repository homepage
- Display basic file metadata:
- Name
- Type (file/directory)
- Last commit message
- Last commit date
- Navigate into subdirectories
- NO README viewing (next iteration)
- NO file content viewing (next iteration)
**Technical Approach:**
- Use LibGit2Sharp for all git operations directly in Razor pages
- LibGit2Sharp types serve directly as models (thin UI layer)
- Read-only operations only
- Repository path configured in appsettings.json
- NO service abstractions - inject IConfiguration and use LibGit2Sharp directly
- Keep it SIMPLE
**Out of Scope:**
- File viewing
- README rendering
- Multiple repositories
- Branch switching
- Search
- Diffs
- Commit history
### Future Iterations
**Iteration 2: File Viewing**
- Click on file to view contents
- Default to showing README.md of current directory
- Syntax highlighting for code files
**Iteration 3: Multiple Repositories**
- Change route from `/repo` to `/<repo-name>`
- Support multiple configured repositories
- Repository selection/listing page
**Iteration 4+: Enhanced Features**
- Commit history view
- Diff viewer
- Branch switching
- Search functionality
## Technical Stack
- **Git Integration:** LibGit2Sharp
- **UI Framework:** Blazor (Server-side rendering)
- **Styling:** Custom modern CSS (no frameworks, GitHub-inspired)
- **Operations:** Read-only git operations
## Configuration
Repository path will be stored in `appsettings.json`:
```json
{
"GitBrowser": {
"RepositoryPath": "D:\\path\\to\\repo"
}
}
```
## Architecture Notes
- Keep it simple: LibGit2Sharp types are the models
- NO service abstractions or interfaces
- Blazor pages use LibGit2Sharp directly
- Inject IConfiguration, use Repository directly in page
- No complex abstractions or mapping layers - KISS principle