diff --git a/.sqlx/query-e58a4211444bfe1c965c021085f0e204ca93fd93778b360730976a76e299ffef.json b/.sqlx/query-167a3c4e2ea3540b2608b75019a112916c8f4c413355366cca8b3e41715081c6.json similarity index 50% rename from .sqlx/query-e58a4211444bfe1c965c021085f0e204ca93fd93778b360730976a76e299ffef.json rename to .sqlx/query-167a3c4e2ea3540b2608b75019a112916c8f4c413355366cca8b3e41715081c6.json index aa3b2cb..e8a10bb 100644 --- a/.sqlx/query-e58a4211444bfe1c965c021085f0e204ca93fd93778b360730976a76e299ffef.json +++ b/.sqlx/query-167a3c4e2ea3540b2608b75019a112916c8f4c413355366cca8b3e41715081c6.json @@ -1,16 +1,21 @@ { "db_name": "SQLite", - "query": "\nSELECT child, reachable FROM commit_links\nJOIN commits ON hash = child\nWHERE parent = ?\n ", + "query": "SELECT hash, message, reachable FROM commits JOIN commit_links ON hash = parent WHERE child = ? ", "describe": { "columns": [ { - "name": "child", + "name": "hash", "ordinal": 0, "type_info": "Text" }, { - "name": "reachable", + "name": "message", "ordinal": 1, + "type_info": "Text" + }, + { + "name": "reachable", + "ordinal": 2, "type_info": "Int64" } ], @@ -18,9 +23,10 @@ "Right": 1 }, "nullable": [ + false, false, false ] }, - "hash": "e58a4211444bfe1c965c021085f0e204ca93fd93778b360730976a76e299ffef" + "hash": "167a3c4e2ea3540b2608b75019a112916c8f4c413355366cca8b3e41715081c6" } diff --git a/.sqlx/query-61bcc32d29fb7b162f3a51b5b463bc917ddce4a5fc292fb19036a88f697f9056.json b/.sqlx/query-61bcc32d29fb7b162f3a51b5b463bc917ddce4a5fc292fb19036a88f697f9056.json new file mode 100644 index 0000000..d491eb0 --- /dev/null +++ b/.sqlx/query-61bcc32d29fb7b162f3a51b5b463bc917ddce4a5fc292fb19036a88f697f9056.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM commits WHERE hash = ?", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "author", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "author_date", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "committer", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "committer_date", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "message", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "reachable", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "new", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "61bcc32d29fb7b162f3a51b5b463bc917ddce4a5fc292fb19036a88f697f9056" +} diff --git a/.sqlx/query-73bdd8a83f317d9f815d5d00215de3f740730b01048f73e62f8931e5dafa3d2f.json b/.sqlx/query-73bdd8a83f317d9f815d5d00215de3f740730b01048f73e62f8931e5dafa3d2f.json new file mode 100644 index 0000000..871e460 --- /dev/null +++ b/.sqlx/query-73bdd8a83f317d9f815d5d00215de3f740730b01048f73e62f8931e5dafa3d2f.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT hash, message, reachable FROM commits JOIN commit_links ON hash = child WHERE parent = ? ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "message", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "reachable", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "73bdd8a83f317d9f815d5d00215de3f740730b01048f73e62f8931e5dafa3d2f" +} diff --git a/.sqlx/query-35e5c16c2952f550783b234f27839cf5110d11798d749f523d0e0c98a77194f5.json b/.sqlx/query-a3771c256dde301f1e99aa87da9345a271287beb7e0fea8f90bff9475a8de568.json similarity index 72% rename from .sqlx/query-35e5c16c2952f550783b234f27839cf5110d11798d749f523d0e0c98a77194f5.json rename to .sqlx/query-a3771c256dde301f1e99aa87da9345a271287beb7e0fea8f90bff9475a8de568.json index cf4eeea..221b1c6 100644 --- a/.sqlx/query-35e5c16c2952f550783b234f27839cf5110d11798d749f523d0e0c98a77194f5.json +++ b/.sqlx/query-a3771c256dde301f1e99aa87da9345a271287beb7e0fea8f90bff9475a8de568.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\nINSERT OR IGNORE INTO commits (hash, author, author_date, committer, committer_date, message)\nVALUES (?, ?, ?, ?, ?, ?)\n ", + "query": "\nINSERT OR IGNORE INTO commits (hash, author, author_date, committer, committer_date, message)\nVALUES (?, ?, ?, ?, ?, ?)\n", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "35e5c16c2952f550783b234f27839cf5110d11798d749f523d0e0c98a77194f5" + "hash": "a3771c256dde301f1e99aa87da9345a271287beb7e0fea8f90bff9475a8de568" } diff --git a/Cargo.lock b/Cargo.lock index f13c386..0d21fa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2761,6 +2761,7 @@ dependencies = [ "directories", "futures", "gix", + "humantime", "humantime-serde", "mime_guess", "rust-embed", diff --git a/Cargo.toml b/Cargo.toml index 5741fea..68ee155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,13 @@ axum = { version = "0.6.19", features = ["macros"] } clap = { version = "4.3.19", features = ["derive", "deprecated"] } directories = "5.0.1" futures = "0.3.28" +humantime = "2.1.0" humantime-serde = "1.1.1" mime_guess = "2.0.4" rust-embed = "6.8.1" serde = { version = "1.0.181", features = ["derive"] } sqlx = { version = "0.7.1", features = ["runtime-tokio", "sqlite"] } -time = { version = "0.3.25", features = ["formatting", "macros"] } +time = { version = "0.3.25", features = ["formatting", "macros", "parsing"] } tokio = { version = "1.29.1", features = ["full"] } toml = "0.7.6" tracing = "0.1.37" diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..8e4fb8f --- /dev/null +++ b/src/db.rs @@ -0,0 +1,39 @@ +use std::time::Duration; + +use time::{format_description::well_known::Rfc3339, macros::format_description, OffsetDateTime}; + +use crate::somehow; + +pub fn format_time(time: &str) -> somehow::Result { + let now = OffsetDateTime::now_utc(); + let time = OffsetDateTime::parse(time, &Rfc3339)?; + let delta = time - now; + + let formatted_time = time.format(format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]" + ))?; + let formatted_delta = + humantime::format_duration(Duration::from_secs(delta.unsigned_abs().as_secs())); + Ok(if delta.is_positive() { + format!("{formatted_time} (in {formatted_delta})") + } else { + format!("{formatted_time} ({formatted_delta} ago)") + }) +} + +pub fn summary(message: &str) -> String { + // Take everything up to the first double newline + let title = message + .split_once("\n\n") + .map(|(t, _)| t) + .unwrap_or(message); + + // Turn consecutive whitespace into a single space + title.split_whitespace().collect::>().join(" ") +} + +pub fn format_commit_short(hash: &str, message: &str) -> String { + let short_hash = hash.chars().take(8).collect::(); + let summary = summary(message); + format!("{short_hash} ({summary})") +} diff --git a/src/main.rs b/src/main.rs index 09c3ce0..bfc3741 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod db; mod recurring; mod repo; mod somehow; diff --git a/src/repo.rs b/src/repo.rs index 6e8dd24..c8a3166 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,22 +1,17 @@ //! Utility functions for accessing a [`Repository`]. -use gix::{actor::IdentityRef, date::Time, Commit}; -use time::macros::format_description; +use gix::{actor::IdentityRef, Commit}; use crate::somehow; +// TODO Remove this function pub fn format_actor(author: IdentityRef<'_>) -> somehow::Result { let mut buffer = vec![]; author.trim().write_to(&mut buffer)?; Ok(String::from_utf8_lossy(&buffer).to_string()) } -pub fn format_time(time: Time) -> String { - time.format(format_description!( - "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]" - )) -} - +// TODO Remove this function pub fn format_commit_short(commit: &Commit<'_>) -> somehow::Result { let id = commit.id().shorten_or_id(); let summary = commit.message()?.summary(); diff --git a/src/web/commit_hash.rs b/src/web/commit_hash.rs index e59623b..c43eb7f 100644 --- a/src/web/commit_hash.rs +++ b/src/web/commit_hash.rs @@ -1,29 +1,27 @@ -use std::sync::Arc; - use askama::Template; use axum::{ extract::{Path, State}, - response::IntoResponse, + http::StatusCode, + response::{IntoResponse, Response}, }; -use gix::{prelude::ObjectIdExt, Id, ObjectId, ThreadSafeRepository}; +use futures::TryStreamExt; use sqlx::SqlitePool; -use crate::{config::Config, repo, somehow}; +use crate::{config::Config, db, somehow}; struct Commit { hash: String, description: String, - tracked: bool, + reachable: i64, } impl Commit { - fn new(id: Id<'_>, tracked: bool) -> somehow::Result { - let commit = id.object()?.try_into_commit()?; - Ok(Self { - hash: id.to_string(), - description: repo::format_commit_short(&commit)?, - tracked, - }) + fn new(hash: String, message: &str, reachable: i64) -> Self { + Self { + description: db::format_commit_short(&hash, message), + hash, + reachable, + } } } @@ -34,66 +32,69 @@ struct CommitIdTemplate { repo_name: String, current: String, hash: String, - summary: String, - message: String, author: String, author_date: String, commit: String, commit_date: String, parents: Vec, children: Vec, + summary: String, + message: String, + reachable: i64, } pub async fn get( Path(hash): Path, State(config): State<&'static Config>, State(db): State, - State(repo): State>, -) -> somehow::Result { - // Do this first because a &Repository can't be kept across awaits. - let child_rows = sqlx::query!( - " -SELECT child, reachable FROM commit_links -JOIN commits ON hash = child -WHERE parent = ? - ", +) -> somehow::Result { + let Some(commit) = sqlx::query!("SELECT * FROM commits WHERE hash = ?", hash) + .fetch_optional(&db) + .await? + else { + return Ok(StatusCode::NOT_FOUND.into_response()); + }; + + let parents = sqlx::query!( + "\ + SELECT hash, message, reachable FROM commits \ + JOIN commit_links ON hash = parent \ + WHERE child = ? \ + ", hash ) - .fetch_all(&db) + .fetch(&db) + .map_ok(|r| Commit::new(r.hash, &r.message, r.reachable)) + .try_collect::>() .await?; - // TODO Store commit info in db and avoid Repository - // TODO Include untracked info for current commit - let repo = repo.to_thread_local(); - let id = hash.parse::()?.attach(&repo); - let commit = id.object()?.try_into_commit()?; - let author_info = commit.author()?; - let committer_info = commit.committer()?; - - let mut parents = vec![]; - for id in commit.parent_ids() { - // TODO Include untracked info for parents - parents.push(Commit::new(id, true)?); - } - - let mut children = vec![]; - for row in child_rows { - let id = row.child.parse::()?.attach(&repo); - children.push(Commit::new(id, row.reachable != 0)?); - } + let children = sqlx::query!( + "\ + SELECT hash, message, reachable FROM commits \ + JOIN commit_links ON hash = child \ + WHERE parent = ? \ + ", + hash + ) + .fetch(&db) + .map_ok(|r| Commit::new(r.hash, &r.message, r.reachable)) + .try_collect::>() + .await?; Ok(CommitIdTemplate { base: config.web.base(), repo_name: config.repo.name(), current: "commit".to_string(), - hash: id.to_string(), - summary: commit.message()?.summary().to_string(), - message: commit.message_raw()?.to_string().trim_end().to_string(), - author: repo::format_actor(author_info.actor())?, - author_date: repo::format_time(author_info.time), - commit: repo::format_actor(committer_info.actor())?, - commit_date: repo::format_time(committer_info.time), + hash: commit.hash, + author: commit.author, + author_date: db::format_time(&commit.author_date)?, + commit: commit.committer, + commit_date: db::format_time(&commit.committer_date)?, parents, children, - }) + summary: db::summary(&commit.message), + message: commit.message.trim_end().to_string(), + reachable: commit.reachable, + } + .into_response()) } diff --git a/static/base.css b/static/base.css index b38ab31..01a7ba1 100644 --- a/static/base.css +++ b/static/base.css @@ -81,11 +81,16 @@ dd { column-gap: 1ch; } -.commit .untracked, -.commit .untracked a { +.commit .reachable, +.commit .reachable a { color: #777; } +.commit .orphaned, +.commit .orphaned a { + color: #a33; +} + .commit pre { white-space: pre-wrap; } diff --git a/templates/commit_hash.html b/templates/commit_hash.html index f8e462a..b7165a7 100644 --- a/templates/commit_hash.html +++ b/templates/commit_hash.html @@ -2,6 +2,26 @@ {% block title %}{{ summary }}{% endblock %} +{% macro r_class(reachable) %} +{%- if reachable == 0 -%} +orphaned +{%- else if reachable == 1 -%} +reachable +{%- else -%} +tracked +{%- endif -%} +{% endmacro %} + +{% macro r_title(reachable) %} +{%- if reachable == 0 -%} +This commit is orphaned. It can't be reached from any ref. +{%- else if reachable == 1 -%} +This commit can only be reached from untracked refs. +{%- else -%} +This commit can be reached from a tracked ref. +{%- endif -%} +{% endmacro %} + {% block body %}

Commit

@@ -20,20 +40,19 @@
{{ commit_date }}
{% for commit in parents %} -
Parent:
-
{{ commit.description }}
+
Parent:
+
+ {{ commit.description }} +
{% endfor %} {% for commit in children %} - {% if commit.tracked %} -
Child:
-
{{ commit.description }}
- {% else %} -
Child:
-
{{ commit.description }}
- {% endif %} +
Child:
+
+ {{ commit.description }} +
{% endfor %} -
{{ message }}
+
{{ message }}
{% endblock %}