using System.Globalization;
using Blog.Models;
using Blog.Services;
using Microsoft.AspNetCore.Components;
namespace Blog.Components.Pages.Git;
///
/// What every page under /git shares: the service, the repository the route named, the
/// branch the query string asked for, and the handful of renderings a repository browser repeats
/// on every screen.
///
///
/// A base class rather than a set of static helpers because the link builders need the repository,
/// and threading that through a dozen call sites per page is noise. Nothing here holds state
/// between requests.
///
public abstract class GitPage : ComponentBase
{
///
/// Named Service rather than Git: this namespace is itself called Git, and a property that
/// shadows the namespace it lives in is a puzzle nobody needs to solve twice.
///
[Inject]
public required GitService Service { get; set; }
///
/// The response, so a missing repository or revision can answer 404 rather than 200 with an
/// apology on it. Null outside a request.
///
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
/// The repository's directory name, straight off the route. Empty on the index.
[Parameter]
public string Repository { get; set; } = "";
///
/// Which branch, tag or commit the page is about. h is the name cgit used and the one
/// every link here carries, so old bookmarks keep working.
///
[SupplyParameterFromQuery(Name = "h")]
public string? Reference { get; set; }
/// Set by a page that found nothing, to answer 404 before the body goes out.
protected void NotFound()
{
if (HttpContext is { Response.HasStarted: false } context) context.Response.StatusCode = 404;
}
// -- Links ---------------------------------------------------------------
//
// Every path segment is escaped separately: a branch may be `feature/thing` and a file may be
// called anything at all, and neither should be able to change the shape of the URL.
protected string RepoLink() => $"/git/{Escape(Repository)}";
protected string LogLink(string? reference = null, string? path = null, int skip = 0)
{
var query = Query(
("h", reference ?? Reference),
("path", path),
("ofs", skip > 0 ? skip.ToString(CultureInfo.InvariantCulture) : null));
return $"{RepoLink()}/log{query}";
}
protected string TreeLink(string path = "", string? reference = null) =>
$"{RepoLink()}/tree{EscapePath(path)}{Query(("h", reference ?? Reference))}";
protected string RawLink(string path, string? reference = null) =>
$"{RepoLink()}/raw{EscapePath(path)}{Query(("h", reference ?? Reference))}";
protected string CommitLink(string sha) => $"{RepoLink()}/commit/{Escape(sha)}";
protected string RefsLink() => $"{RepoLink()}/refs";
private static string EscapePath(string path) =>
path.Length == 0 ? "" : "/" + string.Join('/', path.Split('/').Select(Escape));
private static string Escape(string value) => Uri.EscapeDataString(value);
private static string Query(params (string Key, string? Value)[] parts)
{
var set = parts
.Where(part => !string.IsNullOrEmpty(part.Value))
.Select(part => $"{part.Key}={Escape(part.Value!)}")
.ToArray();
return set.Length == 0 ? "" : "?" + string.Join('&', set);
}
// -- Renderings ----------------------------------------------------------
///
/// How long ago, in the one unit that carries the answer. A repository browser asks this of
/// every row, and "3 months" is the whole of what a reader wants from a commit that old.
///
protected static string Age(DateTimeOffset? when)
{
if (when is not { } at) return "";
var span = DateTimeOffset.UtcNow - at;
if (span < TimeSpan.Zero) span = TimeSpan.Zero;
return span switch
{
{ TotalSeconds: < 60 } => "just now",
{ TotalMinutes: < 60 } => $"{(int)span.TotalMinutes} min",
{ TotalHours: < 24 } => Count((int)span.TotalHours, "hour"),
{ TotalDays: < 14 } => Count((int)span.TotalDays, "day"),
{ TotalDays: < 60 } => Count((int)(span.TotalDays / 7), "week"),
{ TotalDays: < 730 } => Count((int)(span.TotalDays / 30), "month"),
_ => Count((int)(span.TotalDays / 365), "year")
};
}
private static string Count(int n, string unit) => $"{n} {unit}{(n == 1 ? "" : "s")}";
/// The exact time, for the title of whatever rendered.
protected static string Exact(DateTimeOffset? when) =>
when?.ToUniversalTime().ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture) ?? "";
protected static string Size(long bytes) => bytes switch
{
< 1024 => $"{bytes} B",
< 1024 * 1024 => (bytes / 1024.0).ToString("0.#", CultureInfo.InvariantCulture) + " K",
_ => (bytes / (1024.0 * 1024)).ToString("0.#", CultureInfo.InvariantCulture) + " M"
};
///
/// git's own octal file mode. There are only five of them, and to anyone reading a tree the
/// difference between 100644 and 100755 is the point of the column.
///
protected static string Mode(GitEntryKind kind) => kind switch
{
GitEntryKind.Directory => "040000",
GitEntryKind.Executable => "100755",
GitEntryKind.Symlink => "120000",
GitEntryKind.Submodule => "160000",
_ => "100644"
};
protected static string ChangeLabel(GitChange change) => change switch
{
GitChange.Added => "added",
GitChange.Deleted => "deleted",
GitChange.Renamed => "renamed",
GitChange.Copied => "copied",
GitChange.TypeChanged => "mode",
_ => ""
};
}