Skip to content

Commit

Permalink
Move from libgit2 to gitoxide
Browse files Browse the repository at this point in the history
  • Loading branch information
w4 committed Sep 26, 2024
1 parent 9245909 commit 195062e
Show file tree
Hide file tree
Showing 14 changed files with 2,070 additions and 487 deletions.
1,270 changes: 1,123 additions & 147 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ console-subscriber = { version = "0.2", features = ["parking_lot"] }
comrak = "0.21.0"
clap = { version = "4.4.10", features = ["cargo", "derive"] }
futures = "0.3"
git2 = "0.18.0"
gix = "0.66"
hex = "0.4"
humantime = "2.1"
itertools = "0.12"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[See it in action!](https://git.inept.dev/)

A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, git2, Askama and RocksDB.
A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, gitoxide, Askama and RocksDB.

Includes a dark mode for late night committing.

Expand Down Expand Up @@ -38,13 +38,13 @@ Includes a dark mode for late night committing.
[RocksDB][] is used to store all metadata about a repository, including commits, branches, and tags. Metadata is reindexed, and the reindex interval is configurable (default: every 5 minutes), resulting in up to 97% faster load times for large repositories.

- **On-Demand Loading**
Files, trees, and diffs are loaded using [git2][] directly upon request. A small in-memory cache is included for rendered READMEs and diffs, enhancing performance.
Files, trees, and diffs are loaded using [gitoxide][] directly upon request. A small in-memory cache is included for rendered READMEs and diffs, enhancing performance.

- **Dark Mode Support**
Enjoy a dark mode for late-night committing, providing a visually comfortable experience during extended coding sessions.

[RocksDB]: https://github.com/facebook/rocksdb
[git2]: https://github.com/rust-lang/git2-rs
[gitoxide]: https://github.com/Byron/gitoxide

## Getting Started

Expand Down
2 changes: 1 addition & 1 deletion doc/man/rgit.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SYNOPSIS
DESCRIPTION
===========

A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, git2, Askama, and RocksDB.
A gitweb/cgit-like interface for the modern age. Written in Rust using Axum, gitoxide, Askama, and RocksDB.

_bind_address_

Expand Down
3 changes: 1 addition & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
defaultPackage = naersk-lib.buildPackage {
root = ./.;
nativeBuildInputs = with pkgs; [ pkg-config clang ];
buildInputs = with pkgs; [ openssl zlib libssh2 libgit2 ];
buildInputs = with pkgs; [ ];
LIBCLANG_PATH = "${pkgs.clang.cc.lib}/lib";
ROCKSDB_LIB_DIR = "${pkgs.rocksdb}/lib";
LIBSSH2_SYS_USE_PKG_CONFIG = "true";
};
devShell = with pkgs; mkShell {
buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy ];
Expand Down
114 changes: 72 additions & 42 deletions src/database/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use std::{
};

use anyhow::Context;
use git2::{ErrorCode, Reference, Sort};
use gix::bstr::ByteSlice;
use gix::refs::Category;
use gix::Reference;
use ini::Ini;
use itertools::Itertools;
use rocksdb::WriteBatch;
use time::OffsetDateTime;
use time::{OffsetDateTime, UtcOffset};
use tracing::{error, info, info_span, instrument, warn};

use crate::database::schema::{
Expand Down Expand Up @@ -69,14 +71,16 @@ fn update_repository_metadata(scan_path: &Path, db: &rocksdb::DB) {

let repository_path = scan_path.join(relative);

let git_repository = match git2::Repository::open(repository_path.clone()) {
let mut git_repository = match gix::open(repository_path.clone()) {
Ok(v) => v,
Err(error) => {
warn!(%error, "Failed to open repository {} to update metadata, skipping", relative.display());
continue;
}
};

git_repository.object_cache_size(10 * 1024 * 1024);

let res = Repository {
id,
name,
Expand All @@ -97,22 +101,26 @@ fn update_repository_metadata(scan_path: &Path, db: &rocksdb::DB) {
}
}

fn find_default_branch(repo: &git2::Repository) -> Result<Option<String>, git2::Error> {
Ok(repo.head()?.name().map(ToString::to_string))
fn find_default_branch(repo: &gix::Repository) -> Result<Option<String>, anyhow::Error> {
Ok(Some(repo.head()?.name().as_bstr().to_string()))
}

fn find_last_committed_time(repo: &git2::Repository) -> Result<OffsetDateTime, git2::Error> {
fn find_last_committed_time(repo: &gix::Repository) -> Result<OffsetDateTime, anyhow::Error> {
let mut timestamp = OffsetDateTime::UNIX_EPOCH;

for reference in repo.references()? {
let Ok(commit) = reference?.peel_to_commit() else {
for reference in repo.references()?.all()? {
let Ok(commit) = reference.unwrap().peel_to_commit() else {
continue;
};

let committed_time = commit.committer().when().seconds();
let committed_time = OffsetDateTime::from_unix_timestamp(committed_time)
let committer = commit.committer()?;
let mut committed_time = OffsetDateTime::from_unix_timestamp(committer.time.seconds)
.unwrap_or(OffsetDateTime::UNIX_EPOCH);

if let Ok(offset) = UtcOffset::from_whole_seconds(committer.time.offset) {
committed_time = committed_time.to_offset(offset);
}

if committed_time > timestamp {
timestamp = committed_time;
}
Expand Down Expand Up @@ -145,28 +153,44 @@ fn update_repository_reflog(scan_path: &Path, db: Arc<rocksdb::DB>) {
}
};

let references = match references.all() {
Ok(v) => v,
Err(error) => {
error!(%error, "Failed to read references for {relative_path}");
continue;
}
};

let mut valid_references = Vec::new();

for reference in references.filter_map(Result::ok) {
let reference_name = String::from_utf8_lossy(reference.name_bytes());
if !reference_name.starts_with("refs/heads/")
&& !reference_name.starts_with("refs/tags/")
{
for reference in references {
let mut reference = match reference {
Ok(v) => v,
Err(error) => {
error!(%error, "Failed to read reference for {relative_path}");
continue;
}
};

let reference_name = reference.name();
if !matches!(
reference_name.category(),
Some(Category::Tag | Category::LocalBranch)
) {
continue;
}

valid_references.push(reference_name.to_string());
valid_references.push(reference_name.as_bstr().to_string());

if let Err(error) = branch_index_update(
&reference,
&reference_name,
&mut reference,
&relative_path,
db_repository.get(),
db.clone(),
&git_repository,
false,
) {
error!(%error, "Failed to update reflog for {relative_path}@{reference_name}");
error!(%error, "Failed to update reflog for {relative_path}@{:?}", valid_references.last());
}
}

Expand All @@ -178,17 +202,16 @@ fn update_repository_reflog(scan_path: &Path, db: Arc<rocksdb::DB>) {

#[instrument(skip(reference, db_repository, db, git_repository))]
fn branch_index_update(
reference: &Reference<'_>,
reference_name: &str,
reference: &mut Reference<'_>,
relative_path: &str,
db_repository: &Repository<'_>,
db: Arc<rocksdb::DB>,
git_repository: &git2::Repository,
git_repository: &gix::Repository,
force_reindex: bool,
) -> Result<(), anyhow::Error> {
info!("Refreshing indexes");

let commit_tree = db_repository.commit_tree(db.clone(), reference_name);
let commit_tree = db_repository.commit_tree(db.clone(), reference.name().as_bstr().to_str()?);

if force_reindex {
commit_tree.drop_commits()?;
Expand All @@ -207,9 +230,13 @@ fn branch_index_update(
None
};

let mut revwalk = git_repository.revwalk()?;
revwalk.set_sorting(Sort::REVERSE)?;
revwalk.push_ref(reference_name)?;
// TODO: stop collecting into a vec
let revwalk = git_repository
.rev_walk([commit.id().detach()])
.all()?
.collect::<Vec<_>>()
.into_iter()
.rev();

let tree_len = commit_tree.len()?;
let mut seen = false;
Expand All @@ -221,7 +248,7 @@ fn branch_index_update(
let rev = rev?;

if let (false, Some(latest_indexed)) = (seen, &latest_indexed) {
if rev.as_bytes() == &*latest_indexed.get().hash {
if rev.id.as_bytes() == &*latest_indexed.get().hash {
seen = true;
}

Expand All @@ -234,11 +261,11 @@ fn branch_index_update(
info!("{} commits ingested", i + 1);
}

let commit = git_repository.find_commit(rev)?;
let author = commit.author();
let committer = commit.committer();
let commit = rev.object()?;
let author = commit.author()?;
let committer = commit.committer()?;

Commit::new(&commit, &author, &committer).insert(
Commit::new(&commit, author, committer)?.insert(
&commit_tree,
tree_len + i,
&mut batch,
Expand All @@ -255,7 +282,6 @@ fn branch_index_update(

return branch_index_update(
reference,
reference_name,
relative_path,
db_repository,
db,
Expand Down Expand Up @@ -299,16 +325,17 @@ fn tag_index_scan(
relative_path: &str,
db_repository: &Repository<'_>,
db: Arc<rocksdb::DB>,
git_repository: &git2::Repository,
git_repository: &gix::Repository,
) -> Result<(), anyhow::Error> {
let tag_tree = db_repository.tag_tree(db);

let git_tags: HashSet<_> = git_repository
.references()
.context("Failed to scan indexes on git repository")?
.all()?
.filter_map(Result::ok)
.filter(|v| v.name_bytes().starts_with(b"refs/tags/"))
.map(|v| String::from_utf8_lossy(v.name_bytes()).into_owned())
.filter(|v| v.name().category() == Some(Category::Tag))
.map(|v| v.name().as_bstr().to_string())
.collect();
let indexed_tags: HashSet<String> = tag_tree.list()?.into_iter().collect();

Expand All @@ -329,17 +356,17 @@ fn tag_index_scan(
#[instrument(skip(git_repository, tag_tree))]
fn tag_index_update(
tag_name: &str,
git_repository: &git2::Repository,
git_repository: &gix::Repository,
tag_tree: &TagTree,
) -> Result<(), anyhow::Error> {
let reference = git_repository
let mut reference = git_repository
.find_reference(tag_name)
.context("Failed to read newly discovered tag")?;

if let Ok(tag) = reference.peel_to_tag() {
info!("Inserting newly discovered tag to index");

Tag::new(tag.tagger().as_ref()).insert(tag_tree, tag_name)?;
Tag::new(tag.tagger()?)?.insert(tag_tree, tag_name)?;
}

Ok(())
Expand All @@ -359,10 +386,13 @@ fn open_repo<P: AsRef<Path> + Debug>(
relative_path: P,
db_repository: &Repository<'_>,
db: &rocksdb::DB,
) -> Option<git2::Repository> {
match git2::Repository::open(scan_path.join(relative_path.as_ref())) {
Ok(v) => Some(v),
Err(e) if e.code() == ErrorCode::NotFound => {
) -> Option<gix::Repository> {
match gix::open(scan_path.join(relative_path.as_ref())) {
Ok(mut v) => {
v.object_cache_size(10 * 1024 * 1024);
Some(v)
}
Err(gix::open::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
warn!("Repository gone from disk, removing from db");

if let Err(error) = db_repository.delete(db, relative_path) {
Expand Down
56 changes: 30 additions & 26 deletions src/database/schema/commit.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};

use anyhow::Context;
use git2::{Oid, Signature};
use gix::actor::SignatureRef;
use gix::bstr::ByteSlice;
use gix::ObjectId;
use rocksdb::{IteratorMode, ReadOptions, WriteBatch};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::OffsetDateTime;
use time::{OffsetDateTime, UtcOffset};
use tracing::debug;
use yoke::{Yoke, Yokeable};

Expand All @@ -27,21 +29,21 @@ pub struct Commit<'a> {

impl<'a> Commit<'a> {
pub fn new(
commit: &'a git2::Commit<'_>,
author: &'a git2::Signature<'_>,
committer: &'a git2::Signature<'_>,
) -> Self {
Self {
summary: commit
.summary_bytes()
.map_or(Cow::Borrowed(""), String::from_utf8_lossy),
message: commit
.body_bytes()
.map_or(Cow::Borrowed(""), String::from_utf8_lossy),
committer: committer.into(),
author: author.into(),
hash: CommitHash::Oid(commit.id()),
}
commit: &gix::Commit<'_>,
author: SignatureRef<'a>,
committer: SignatureRef<'a>,
) -> Result<Self, anyhow::Error> {
let message = commit.message()?;

Ok(Self {
summary: message.summary().to_string().into(),
message: message
.body
.map_or(Cow::Borrowed(""), |v| v.to_string().into()),
committer: committer.try_into()?,
author: author.try_into()?,
hash: CommitHash::Oid(commit.id().detach()),
})
}

pub fn insert(&self, tree: &CommitTree, id: u64, tx: &mut WriteBatch) -> anyhow::Result<()> {
Expand All @@ -51,7 +53,7 @@ impl<'a> Commit<'a> {

#[derive(Debug)]
pub enum CommitHash<'a> {
Oid(Oid),
Oid(ObjectId),
Bytes(&'a [u8]),
}

Expand Down Expand Up @@ -97,14 +99,16 @@ pub struct Author<'a> {
pub time: OffsetDateTime,
}

impl<'a> From<&'a git2::Signature<'_>> for Author<'a> {
fn from(author: &'a Signature<'_>) -> Self {
Self {
name: String::from_utf8_lossy(author.name_bytes()),
email: String::from_utf8_lossy(author.email_bytes()),
// TODO: this needs to deal with offset
time: OffsetDateTime::from_unix_timestamp(author.when().seconds()).unwrap(),
}
impl<'a> TryFrom<SignatureRef<'a>> for Author<'a> {
type Error = anyhow::Error;

fn try_from(author: SignatureRef<'a>) -> Result<Self, anyhow::Error> {
Ok(Self {
name: author.name.to_str_lossy(),
email: author.email.to_str_lossy(),
time: OffsetDateTime::from_unix_timestamp(author.time.seconds)?
.to_offset(UtcOffset::from_whole_seconds(author.time.offset)?),
})
}
}

Expand Down
Loading

0 comments on commit 195062e

Please sign in to comment.