Retain references across pages
Diff
Cargo.toml | 2 +-
src/git.rs | 48 ++++++++++++++++++++++++++++++++++++------------
src/main.rs | 3 +--
src/syntax_highlight.rs | 3 +--
src/database/indexer.rs | 11 ++++++++---
templates/repo/base.html | 11 ++++++-----
templates/repo/commit.html | 7 ++++---
templates/repo/file.html | 3 ++-
templates/repo/log.html | 7 ++-----
templates/repo/tag.html | 3 ++-
src/methods/repo/about.rs | 22 ++++++++++++++++++++--
src/methods/repo/commit.rs | 11 +++++++++--
src/methods/repo/diff.rs | 11 ++++++++---
src/methods/repo/log.rs | 21 +++++++++++++++++----
src/methods/repo/refs.rs | 4 +++-
src/methods/repo/smart_git.rs | 6 ++++--
src/methods/repo/summary.rs | 4 +++-
src/methods/repo/tag.rs | 13 +++++++++----
src/methods/repo/tree.rs | 35 ++++++++++++++++++++++++++---------
templates/repo/macros/link.html | 3 +++
20 files changed, 154 insertions(+), 74 deletions(-)
@@ -29,7 +29,7 @@
once_cell = "1.18"
path-clean = "1.0.1"
parking_lot = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive", "rc"] }
sha2 = "0.10"
syntect = "5"
sled = { version = "0.34", features = ["compression"] }
@@ -11,8 +11,8 @@
use bytes::{Bytes, BytesMut};
use comrak::{ComrakOptions, ComrakPlugins};
use git2::{
BranchType, DiffFormat, DiffLineType, DiffOptions, DiffStatsFormat, Email, EmailCreateOptions,
ObjectType, Oid, Signature,
DiffFormat, DiffLineType, DiffOptions, DiffStatsFormat, Email, EmailCreateOptions, ObjectType,
Oid, Signature,
};
use moka::future::Cache;
use parking_lot::Mutex;
@@ -51,7 +51,11 @@
impl Git {
#[instrument(skip(self))]
pub async fn repo(self: Arc<Self>, repo_path: PathBuf) -> Result<Arc<OpenRepository>> {
pub async fn repo(
self: Arc<Self>,
repo_path: PathBuf,
branch: Option<Arc<str>>,
) -> Result<Arc<OpenRepository>> {
let repo = tokio::task::spawn_blocking({
let repo_path = repo_path.clone();
move || git2::Repository::open(repo_path)
@@ -64,6 +68,7 @@
git: self,
cache_key: repo_path,
repo: Mutex::new(repo),
branch,
}))
}
}
@@ -72,6 +77,7 @@
git: Arc<Git>,
cache_key: PathBuf,
repo: Mutex<git2::Repository>,
branch: Option<Arc<str>>,
}
impl OpenRepository {
@@ -79,7 +85,6 @@
self: Arc<Self>,
path: Option<PathBuf>,
tree_id: Option<&str>,
branch: Option<String>,
formatted: bool,
) -> Result<PathDestination> {
let tree_id = tree_id
@@ -93,12 +98,11 @@
let mut tree = if let Some(tree_id) = tree_id {
repo.find_tree(tree_id)
.context("Couldn't find tree with given id")?
} else if let Some(branch) = branch {
let branch = repo.find_branch(&branch, BranchType::Local)?;
branch
.get()
} else if let Some(branch) = &self.branch {
let reference = repo.resolve_reference_from_short_name(branch)?;
reference
.peel_to_tree()
.context("Couldn't find tree for branch")?
.context("Couldn't find tree for reference")?
} else {
let head = repo.head()?;
head.peel_to_tree()
@@ -175,16 +179,14 @@
}
#[instrument(skip(self))]
pub async fn tag_info(self: Arc<Self>, tag_name: &str) -> Result<DetailedTag> {
let reference = format!("refs/tags/{tag_name}");
let tag_name = tag_name.to_string();
pub async fn tag_info(self: Arc<Self>) -> Result<DetailedTag> {
tokio::task::spawn_blocking(move || {
let tag_name = self.branch.clone().context("no tag given")?;
let repo = self.repo.lock();
let tag = repo
.find_reference(&reference)
.context("Given reference does not exist in repository")?
.find_reference(&format!("refs/tags/{tag_name}"))
.context("Given tag does not exist in repository")?
.peel_to_tag()
.context("Couldn't get to a tag from the given reference")?;
let tag_target = tag.target().context("Couldn't find tagged object")?;
@@ -221,8 +223,13 @@
.try_get_with(self.cache_key.clone(), async move {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
let head = if let Some(reference) = &self.branch {
repo.resolve_reference_from_short_name(reference)?
} else {
repo.head().context("Couldn't find HEAD of repository")?
};
let head = repo.head().context("Couldn't find HEAD of repository")?;
let commit = head.peel_to_commit().context(
"Couldn't find the commit that the HEAD of the repository refers to",
)?;
@@ -267,8 +274,13 @@
pub async fn latest_commit(self: Arc<Self>) -> Result<Commit> {
tokio::task::spawn_blocking(move || {
let repo = self.repo.lock();
let head = if let Some(reference) = &self.branch {
repo.resolve_reference_from_short_name(reference)?
} else {
repo.head().context("Couldn't find HEAD of repository")?
};
let head = repo.head().context("Couldn't find HEAD of repository")?;
let commit = head
.peel_to_commit()
.context("Couldn't find commit HEAD of repository refers to")?;
@@ -381,7 +393,7 @@
#[derive(Debug)]
pub struct DetailedTag {
pub name: String,
pub name: Arc<str>,
pub tagger: Option<CommitUser>,
pub message: String,
pub tagged_object: Option<TaggedObject>,
@@ -21,8 +21,7 @@
use bat::assets::HighlightingAssets;
use clap::Parser;
use once_cell::sync::{Lazy, OnceCell};
use sha2::digest::FixedOutput;
use sha2::Digest;
use sha2::{digest::FixedOutput, Digest};
use sled::Db;
use syntect::html::ClassStyle;
use tokio::{
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::io::Write;
use std::{collections::HashMap, io::Write};
use comrak::adapters::SyntaxHighlighterAdapter;
use syntect::{
@@ -7,7 +7,7 @@
use git2::Sort;
use ini::Ini;
use time::OffsetDateTime;
use tracing::{info, info_span};
use tracing::{error, info, info_span};
use crate::database::schema::{
commit::Commit,
@@ -89,7 +89,9 @@
let reference = reference.unwrap();
let reference_name = String::from_utf8_lossy(reference.name_bytes());
if !reference_name.starts_with("refs/heads/") {
if !reference_name.starts_with("refs/heads/")
&& !reference_name.starts_with("refs/tags/")
{
continue;
}
@@ -119,7 +121,10 @@
let mut revwalk = git_repository.revwalk().unwrap();
revwalk.set_sorting(Sort::REVERSE).unwrap();
revwalk.push_ref(&reference_name).unwrap();
if let Err(error) = revwalk.push_ref(&reference_name) {
error!(%error, "Failed to revwalk reference");
continue;
}
let mut i = 0;
for rev in revwalk {
@@ -1,3 +1,4 @@
{% import "macros/link.html" as link %}
{% extends "../base.html" %}
{% block title %}{{ repo.display() }}{% endblock %}
@@ -9,13 +10,13 @@
{% block nav %}
<nav>
<div>
<a href="/{{ repo.display() }}/about" class="{% block about_nav_class %}{% endblock %}">about</a>
<a href="/{{ repo.display() }}/about{% call link::maybe_branch(branch) %}" class="{% block about_nav_class %}{% endblock %}">about</a>
<a href="/{{ repo.display() }}" class="{% block summary_nav_class %}{% endblock %}">summary</a>
<a href="/{{ repo.display() }}/refs" class="{% block refs_nav_class %}{% endblock %}">refs</a>
<a href="/{{ repo.display() }}/log" class="{% block log_nav_class %}{% endblock %}">log</a>
<a href="/{{ repo.display() }}/tree" class="{% block tree_nav_class %}{% endblock %}">tree</a>
<a href="/{{ repo.display() }}/commit" class="{% block commit_nav_class %}{% endblock %}">commit</a>
<a href="/{{ repo.display() }}/diff" class="{% block diff_nav_class %}{% endblock %}">diff</a>
<a href="/{{ repo.display() }}/log{% call link::maybe_branch(branch) %}" class="{% block log_nav_class %}{% endblock %}">log</a>
<a href="/{{ repo.display() }}/tree{% call link::maybe_branch(branch) %}" class="{% block tree_nav_class %}{% endblock %}">tree</a>
<a href="/{{ repo.display() }}/commit{% call link::maybe_branch(branch) %}" class="{% block commit_nav_class %}{% endblock %}">commit</a>
<a href="/{{ repo.display() }}/diff{% call link::maybe_branch(branch) %}" class="{% block diff_nav_class %}{% endblock %}">diff</a>
</div>
<div class="grow"></div>
@@ -1,3 +1,4 @@
{% import "macros/link.html" as link %}
{% extends "repo/base.html" %}
{% block head %}
@@ -22,16 +23,16 @@
</tr>
<tr>
<th>commit</th>
<td colspan="2"><pre><a href="/{{ repo.display() }}/commit?id={{ commit.oid() }}" class="no-style">{{ commit.oid() }}</a> <a href="/{{ repo.display() }}/patch?id={{ commit.oid() }}">[patch]</a></pre></td>
<td colspan="2"><pre><a href="/{{ repo.display() }}/commit?id={{ commit.oid() }}{% call link::maybe_branch_suffix(branch) %}" class="no-style">{{ commit.oid() }}</a> <a href="/{{ repo.display() }}/patch?id={{ commit.oid() }}">[patch]</a></pre></td>
</tr>
<tr>
<th>tree</th>
<td colspan="2"><pre><a href="/{{ repo.display() }}/tree?id={{ commit.tree() }}" class="no-style">{{ commit.tree() }}</a></pre></td>
<td colspan="2"><pre><a href="/{{ repo.display() }}/tree?id={{ commit.tree() }}{% call link::maybe_branch_suffix(branch) %}" class="no-style">{{ commit.tree() }}</a></pre></td>
</tr>
{%- for parent in commit.parents() %}
<tr>
<th>parent</th>
<td colspan="2"><pre><a href="/{{ repo.display() }}/commit?id={{ parent }}" class="no-style">{{ parent }}</a></pre></td>
<td colspan="2"><pre><a href="/{{ repo.display() }}/commit?id={{ parent }}{% call link::maybe_branch_suffix(branch) %}" class="no-style">{{ parent }}</a></pre></td>
</tr>
{%- endfor %}
</tbody>
@@ -1,3 +1,4 @@
{% import "macros/link.html" as link %}
{% extends "repo/base.html" %}
{% block head %}
@@ -8,7 +9,7 @@
{% block tree_nav_class %}active{% endblock %}
{% block extra_nav_links %}
<a href="?raw=true">plain</a>
<a href="?raw=true{% call link::maybe_branch_suffix(branch) %}">plain</a>
{% endblock %}
{% block content %}
@@ -1,4 +1,5 @@
{% import "macros/refs.html" as refs %}
{% import "macros/link.html" as link %}
{% extends "repo/base.html" %}
{% block log_nav_class %}active{% endblock %}
@@ -10,11 +11,7 @@
{% if let Some(next_offset) = next_offset %}
<div class="mt-2 text-center">
{% if let Some(branch) = branch %}
<a href="?h={{ branch }}&ofs={{ next_offset }}">[next]</a>
{% else %}
<a href="?ofs={{ next_offset }}">[next]</a>
{% endif %}
<a href="?ofs={{ next_offset }}{% call link::maybe_branch_suffix(branch) %}">[next]</a>
</div>
{% endif %}
{% endblock %}
@@ -1,3 +1,4 @@
{% import "macros/link.html" as link %}
{% extends "repo/base.html" %}
{% block content %}
@@ -23,7 +24,7 @@
<td>
{% match tagged_object %}
{% when crate::git::TaggedObject::Commit with (commit) %}
<a href="/{{ repo.display() }}/commit?id={{ commit }}">commit {{ commit|truncate(10) }}...</a>
<a href="/{{ repo.display() }}/commit?id={{ commit }}{% call link::maybe_branch_suffix(branch) %}">commit {{ commit|truncate(10) }}...</a>
{% when crate::git::TaggedObject::Tree with (tree) %}
tree {{ tree }}
{% endmatch %}
@@ -1,7 +1,8 @@
use std::sync::Arc;
use askama::Template;
use axum::{response::Response, Extension};
use axum::{extract::Query, response::Response, Extension};
use serde::Deserialize;
use crate::{
git::ReadmeFormat,
@@ -10,20 +11,35 @@
Git,
};
#[derive(Deserialize)]
pub struct UriQuery {
#[serde(rename = "h")]
pub branch: Option<Arc<str>>,
}
#[derive(Template)]
#[template(path = "repo/about.html")]
pub struct View {
repo: Repository,
readme: Option<(ReadmeFormat, Arc<str>)>,
branch: Option<Arc<str>>,
}
pub async fn handle(
Extension(repo): Extension<Repository>,
Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.clone().repo(repository_path).await?;
let open_repo = git
.clone()
.repo(repository_path, query.branch.clone())
.await?;
let readme = open_repo.readme().await?;
Ok(into_response(&View { repo, readme }))
Ok(into_response(&View {
repo,
readme,
branch: query.branch,
}))
}
@@ -16,11 +16,14 @@
pub struct View {
pub repo: Repository,
pub commit: Arc<Commit>,
pub branch: Option<Arc<str>>,
}
#[derive(Deserialize)]
pub struct UriQuery {
pub id: Option<String>,
#[serde(rename = "h")]
pub branch: Option<Arc<str>>,
}
pub async fn handle(
@@ -29,12 +32,16 @@
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let open_repo = git.repo(repository_path, query.branch.clone()).await?;
let commit = if let Some(commit) = query.id {
open_repo.commit(&commit).await?
} else {
Arc::new(open_repo.latest_commit().await?)
};
Ok(into_response(&View { repo, commit }))
Ok(into_response(&View {
repo,
commit,
branch: query.branch,
}))
}
@@ -20,6 +20,7 @@
pub struct View {
pub repo: Repository,
pub commit: Arc<Commit>,
pub branch: Option<Arc<str>>,
}
pub async fn handle(
@@ -28,14 +29,18 @@
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let open_repo = git.repo(repository_path, query.branch.clone()).await?;
let commit = if let Some(commit) = query.id {
open_repo.commit(&commit).await?
} else {
Arc::new(open_repo.latest_commit().await?)
};
Ok(into_response(&View { repo, commit }))
Ok(into_response(&View {
repo,
commit,
branch: query.branch,
}))
}
pub async fn handle_plain(
@@ -43,7 +48,7 @@
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let open_repo = git.repo(repository_path, query.branch).await?;
let commit = if let Some(commit) = query.id {
open_repo.commit(&commit).await?
} else {
@@ -66,11 +66,22 @@
amount: usize,
offset: usize,
) -> Result<Vec<YokedCommit>> {
let reference = branch.map(|branch| format!("refs/heads/{branch}"));
if let Some(reference) = reference {
let commit_tree = repository.get().commit_tree(database, &reference)?;
return Ok(commit_tree.fetch_latest(amount, offset).await);
if let Some(reference) = branch {
let commit_tree = repository
.get()
.commit_tree(database, &format!("refs/heads/{reference}"))?;
let commit_tree = commit_tree.fetch_latest(amount, offset).await;
if !commit_tree.is_empty() {
return Ok(commit_tree);
}
let tag_tree = repository
.get()
.commit_tree(database, &format!("refs/tags/{reference}"))?;
let tag_tree = tag_tree.fetch_latest(amount, offset).await;
return Ok(tag_tree);
}
for branch in DEFAULT_BRANCHES {
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, sync::Arc};
use anyhow::Context;
use askama::Template;
@@ -17,6 +17,7 @@
pub struct View {
repo: Repository,
refs: Refs,
branch: Option<Arc<str>>,
}
#[allow(clippy::unused_async)]
@@ -46,5 +47,6 @@
Ok(into_response(&View {
repo,
refs: Refs { heads, tags },
branch: None,
}))
}
@@ -14,8 +14,10 @@
use tokio_util::io::StreamReader;
use tracing::warn;
use crate::methods::repo::{Repository, RepositoryPath, Result};
use crate::StatusCode;
use crate::{
methods::repo::{Repository, RepositoryPath, Result},
StatusCode,
};
#[allow(clippy::unused_async)]
pub async fn handle(
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, sync::Arc};
use anyhow::Context;
use askama::Template;
@@ -20,6 +20,7 @@
repo: Repository,
refs: Refs,
commit_list: Vec<&'a crate::database::schema::commit::Commit<'a>>,
branch: Option<Arc<str>>,
}
pub async fn handle(
@@ -51,6 +52,7 @@
repo,
refs: Refs { heads, tags },
commit_list,
branch: None,
}))
}
@@ -14,7 +14,7 @@
#[derive(Deserialize)]
pub struct UriQuery {
#[serde(rename = "h")]
name: String,
name: Arc<str>,
}
#[derive(Template)]
@@ -22,6 +22,7 @@
pub struct View {
repo: Repository,
tag: DetailedTag,
branch: Option<Arc<str>>,
}
pub async fn handle(
@@ -30,8 +31,12 @@
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let tag = open_repo.tag_info(&query.name).await?;
let open_repo = git.repo(repository_path, Some(query.name.clone())).await?;
let tag = open_repo.tag_info().await?;
Ok(into_response(&View { repo, tag }))
Ok(into_response(&View {
repo,
tag,
branch: Some(query.name),
}))
}
@@ -1,10 +1,15 @@
use std::{
fmt::{Display, Formatter},
sync::Arc,
};
use askama::Template;
use axum::{extract::Query, http, response::IntoResponse, response::Response, Extension};
use axum::{
extract::Query,
http,
response::{IntoResponse, Response},
Extension,
};
use serde::Deserialize;
use crate::{
@@ -20,10 +25,10 @@
#[derive(Deserialize)]
pub struct UriQuery {
id: Option<String>,
#[serde(rename = "h")]
branch: Option<String>,
#[serde(default)]
raw: bool,
#[serde(rename = "h")]
branch: Option<Arc<str>>,
}
impl Display for UriQuery {
@@ -50,6 +55,7 @@
pub repo: Repository,
pub items: Vec<TreeItem>,
pub query: UriQuery,
pub branch: Option<Arc<str>>,
}
#[derive(Template)]
@@ -57,6 +63,7 @@
pub struct FileView {
pub repo: Repository,
pub file: FileWithContent,
pub branch: Option<Arc<str>>,
}
pub async fn handle(
@@ -66,19 +73,19 @@
Extension(git): Extension<Arc<Git>>,
Query(query): Query<UriQuery>,
) -> Result<Response> {
let open_repo = git.repo(repository_path).await?;
let open_repo = git.repo(repository_path, query.branch.clone()).await?;
Ok(
match open_repo
.path(
child_path,
query.id.as_deref(),
query.branch.clone(),
!query.raw,
)
.path(child_path, query.id.as_deref(), !query.raw)
.await?
{
PathDestination::Tree(items) => into_response(&TreeView { repo, items, query }),
PathDestination::Tree(items) => into_response(&TreeView {
repo,
items,
branch: query.branch.clone(),
query,
}),
PathDestination::File(file) if query.raw => {
let headers = [(
http::header::CONTENT_TYPE,
@@ -87,7 +94,11 @@
(headers, file.content).into_response()
}
PathDestination::File(file) => into_response(&FileView { repo, file }),
PathDestination::File(file) => into_response(&FileView {
repo,
file,
branch: query.branch,
}),
},
)
}
@@ -1,0 +1,3 @@
{%- macro maybe_branch(branch) -%}{% if let Some(branch) = branch %}?h={{ branch }}{% endif %}{%- endmacro -%}
{%- macro maybe_branch_suffix(branch) -%}{% if let Some(branch) = branch %}&h={{ branch }}{% endif %}{%- endmacro -%}