From 77cb1588dc42ce90fd93d27201207dbf5a485342 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Tue, 17 Dec 2024 10:33:38 +0100
Subject: [PATCH 01/13] refactor: Joined git and serve method

---
 src/server/api/routes.rs |  58 +++++++++++++++++++-
 src/server/api/state.rs  |   8 +++
 src/server/app.rs        |   4 +-
 src/server/git.rs        | 112 ---------------------------------------
 src/server/mod.rs        |   1 -
 src/utils/cli.rs         |   8 ---
 6 files changed, 67 insertions(+), 124 deletions(-)
 delete mode 100644 src/server/git.rs

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index 41ea8c9..5555350 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -11,10 +11,18 @@ use actix_service::ServiceFactory;
 use actix_web::{
     body::MessageBody,
     dev::{ServiceRequest, ServiceResponse},
-    guard, web, App, Error, Scope,
+    guard, web, App, Error, Scope, Responder, route,
+    HttpResponse
 };
 
+use crate::utils::http::get_contenttype;
+use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
 use super::{serve::serve, state::Global, versions::versions};
+use crate::utils::paths::clean_path;
+use git2::{self, ErrorCode};
+use super::state::App as AppState;
+
+use super::super::errors::HTTPError;
 
 /// Name of the header to guard current documents
 static HEADER_NAME: OnceLock<String> = OnceLock::new();
@@ -315,3 +323,51 @@ fn register_dependent_routes(
     }
     Ok(())
 }
+
+
+/// Return the content in the stelae archive in the `{namespace}/{name}`
+/// repo at the `commitish` commit at the `remainder` path.
+/// Return 404 if any are not found or there are any errors.
+#[route(
+    "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
+    method = "GET",
+    method = "HEAD"
+)]
+#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))]
+async fn get_blob(
+    path: web::Path<(String, String, String, String)>,
+    data: web::Data<AppState>,
+) -> impl Responder {
+    let (namespace, name, commitish, remainder) = path.into_inner();
+    let archive_path = &data.archive_path;
+    let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
+    let blob_path = clean_path(&remainder);
+    let contenttype = get_contenttype(&blob_path);
+    match blob {
+        Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content),
+        Err(error) => blob_error_response(&error, &namespace, &name),
+    }
+}
+
+/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
+#[allow(clippy::wildcard_enum_match_arm)]
+#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
+fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
+    tracing::error!("{error}",);
+    if let Some(git_error) = error.downcast_ref::<git2::Error>() {
+        return match git_error.code() {
+            // TODO: check this is the right error
+            ErrorCode::NotFound => {
+                HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist"))
+            }
+            _ => HttpResponse::InternalServerError().body("Unexpected Git error"),
+        };
+    }
+    match error {
+        // TODO: Obviously it's better to use custom `Error` types
+        _ if error.to_string() == GIT_REQUEST_NOT_FOUND => {
+            HttpResponse::NotFound().body(HTTPError::NotFound.to_string())
+        }
+        _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
+    }
+}
\ No newline at end of file
diff --git a/src/server/api/state.rs b/src/server/api/state.rs
index 172be04..b78c7db 100644
--- a/src/server/api/state.rs
+++ b/src/server/api/state.rs
@@ -13,6 +13,8 @@ pub trait Global {
     fn archive(&self) -> &Archive;
     /// Database connection
     fn db(&self) -> &db::DatabaseConnection;
+    /// path to the Stelae archive
+    fn archive_path(&self) -> &PathBuf;
 }
 
 /// Application state
@@ -22,6 +24,8 @@ pub struct App {
     pub archive: Archive,
     /// Database connection
     pub db: db::DatabaseConnection,
+    /// path to the Stelae archive
+    pub archive_path: PathBuf,
 }
 
 impl Global for App {
@@ -32,6 +36,10 @@ impl Global for App {
     fn db(&self) -> &db::DatabaseConnection {
         &self.db
     }
+
+    fn archive_path(&self) -> &PathBuf {
+        &self.archive_path
+    }
 }
 
 /// Repository to serve
diff --git a/src/server/app.rs b/src/server/app.rs
index 0acf600..246e747 100644
--- a/src/server/app.rs
+++ b/src/server/app.rs
@@ -44,7 +44,7 @@ pub async fn serve_archive(
         }
     };
 
-    let archive = match Archive::parse(archive_path, &PathBuf::from(raw_archive_path), individual) {
+    let archive = match Archive::parse(archive_path.clone(), &PathBuf::from(raw_archive_path), individual) {
         Ok(archive) => archive,
         Err(err) => {
             tracing::error!("Unable to parse archive at '{raw_archive_path}'.");
@@ -53,7 +53,7 @@ pub async fn serve_archive(
         }
     };
 
-    let state = AppState { archive, db };
+    let state = AppState { archive, db, archive_path };
 
     HttpServer::new(move || {
         init(&state).unwrap_or_else(|err| {
diff --git a/src/server/git.rs b/src/server/git.rs
deleted file mode 100644
index 992f835..0000000
--- a/src/server/git.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-//! Legacy git microserver.
-
-use actix_web::{get, route, web, App, HttpResponse, HttpServer, Responder};
-use git2::{self, ErrorCode};
-use std::path::PathBuf;
-use tracing_actix_web::TracingLogger;
-
-use super::errors::{CliError, HTTPError, StelaeError};
-use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
-use crate::utils::http::get_contenttype;
-use crate::{server::tracing::StelaeRootSpanBuilder, utils::paths::clean_path};
-
-/// Global, read-only state passed into the actix app
-struct AppState {
-    /// path to the Stelae archive
-    archive_path: PathBuf,
-}
-
-/// Root index path
-#[get("/")]
-async fn index() -> &'static str {
-    "Welcome to Stelae"
-}
-
-/// Just for development purposes at the moment
-#[get("{path}")]
-async fn misc(path: web::Path<String>) -> actix_web::Result<&'static str, StelaeError> {
-    match path.as_str() {
-        "error" => Err(StelaeError::GitError),
-        _ => Ok("\u{2728}"),
-    }
-}
-
-/// Return the content in the stelae archive in the `{namespace}/{name}`
-/// repo at the `commitish` commit at the `remainder` path.
-/// Return 404 if any are not found or there are any errors.
-#[route(
-    "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
-    method = "GET",
-    method = "HEAD"
-)]
-#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))]
-async fn get_blob(
-    path: web::Path<(String, String, String, String)>,
-    data: web::Data<AppState>,
-) -> impl Responder {
-    let (namespace, name, commitish, remainder) = path.into_inner();
-    let archive_path = &data.archive_path;
-    let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
-    let blob_path = clean_path(&remainder);
-    let contenttype = get_contenttype(&blob_path);
-    match blob {
-        Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content),
-        Err(error) => blob_error_response(&error, &namespace, &name),
-    }
-}
-
-/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
-#[expect(
-    clippy::wildcard_enum_match_arm,
-    reason = "Default to wildcard match in case of unexpected errors"
-)]
-#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
-fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
-    tracing::error!("{error}",);
-    if let Some(git_error) = error.downcast_ref::<git2::Error>() {
-        return match git_error.code() {
-            // TODO: check this is the right error
-            ErrorCode::NotFound => {
-                HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist"))
-            }
-            _ => HttpResponse::InternalServerError().body("Unexpected Git error"),
-        };
-    }
-    match error {
-        // TODO: Obviously it's better to use custom `Error` types
-        _ if error.to_string() == GIT_REQUEST_NOT_FOUND => {
-            HttpResponse::NotFound().body(HTTPError::NotFound.to_string())
-        }
-        _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
-    }
-}
-
-/// Serve git repositories in the Stelae archive.
-#[actix_web::main] // or #[tokio::main]
-pub async fn serve_git(
-    raw_archive_path: &str,
-    archive_path: PathBuf,
-    port: u16,
-) -> Result<(), CliError> {
-    let bind = "127.0.0.1";
-    let message = "Serving content from the Stelae archive at";
-    tracing::info!("{message} '{raw_archive_path}' on http://{bind}:{port}.",);
-
-    HttpServer::new(move || {
-        App::new()
-            .wrap(TracingLogger::<StelaeRootSpanBuilder>::new())
-            .service(index)
-            .service(misc)
-            .service(get_blob)
-            .app_data(web::Data::new(AppState {
-                archive_path: archive_path.clone(),
-            }))
-    })
-    .bind((bind, port))?
-    .run()
-    .await
-    .map_err(|err| {
-        tracing::error!("Error running Git server: {err:?}");
-        CliError::GenericError
-    })
-}
diff --git a/src/server/mod.rs b/src/server/mod.rs
index c287916..286c7ec 100644
--- a/src/server/mod.rs
+++ b/src/server/mod.rs
@@ -5,5 +5,4 @@
 pub mod api;
 pub mod app;
 pub mod errors;
-pub mod git;
 pub mod tracing;
diff --git a/src/utils/cli.rs b/src/utils/cli.rs
index 1333d5f..ffd4c80 100644
--- a/src/utils/cli.rs
+++ b/src/utils/cli.rs
@@ -7,7 +7,6 @@
 use crate::history::changes;
 use crate::server::app::serve_archive;
 use crate::server::errors::CliError;
-use crate::server::git::serve_git;
 use crate::utils::archive::find_archive_path;
 use clap::Parser;
 use std::env;
@@ -41,12 +40,6 @@ struct Cli {
 /// Subcommands for the Stelae CLI
 #[derive(Clone, clap::Subcommand)]
 enum Subcommands {
-    /// Serve git repositories in the Stelae archive
-    Git {
-        /// Port on which to serve the archive.
-        #[arg(short, long, default_value_t = 8080)]
-        port: u16,
-    },
     /// Serve documents in a Stelae archive.
     Serve {
         /// Port on which to serve the archive.
@@ -115,7 +108,6 @@ fn init_tracing(archive_path: &Path) {
 /// This function returns the generic `CliError`, based on which we exit with a known exit code.
 fn execute_command(cli: &Cli, archive_path: PathBuf) -> Result<(), CliError> {
     match cli.subcommands {
-        Subcommands::Git { port } => serve_git(&cli.archive_path, archive_path, port),
         Subcommands::Serve { port, individual } => {
             serve_archive(&cli.archive_path, archive_path, port, individual)
         }

From 73925fdec600388a1e6f6cd3f147a168c1957e2b Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Mon, 9 Dec 2024 09:40:20 +0100
Subject: [PATCH 02/13] REFACTOR: Merged git and serve command

---
 src/server/api/routes.rs | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index 5555350..e8a4378 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -11,8 +11,13 @@ use actix_service::ServiceFactory;
 use actix_web::{
     body::MessageBody,
     dev::{ServiceRequest, ServiceResponse},
+<<<<<<< HEAD
     guard, web, App, Error, Scope, Responder, route,
     HttpResponse
+=======
+    guard, web, App, Error, Scope, get, Responder,
+    HttpResponse, HttpRequest
+>>>>>>> ac49acd (Merging git and serve)
 };
 
 use crate::utils::http::get_contenttype;
@@ -77,6 +82,13 @@ pub fn register_app<
                     .service(web::resource("/{path:.*}").to(versions))
                     .service(web::resource("").to(versions)),
             ),
+
+        )
+        .app_data(web::Data::new(state.clone()));
+
+        app = app
+        .service(
+            get_blob
         )
         .app_data(web::Data::new(state.clone()));
 
@@ -328,17 +340,23 @@ fn register_dependent_routes(
 /// Return the content in the stelae archive in the `{namespace}/{name}`
 /// repo at the `commitish` commit at the `remainder` path.
 /// Return 404 if any are not found or there are any errors.
+<<<<<<< HEAD
 #[route(
     "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
     method = "GET",
     method = "HEAD"
 )]
+=======
+#[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")]
+>>>>>>> ac49acd (Merging git and serve)
 #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))]
 async fn get_blob(
     path: web::Path<(String, String, String, String)>,
     data: web::Data<AppState>,
 ) -> impl Responder {
+    println!("TEST");
     let (namespace, name, commitish, remainder) = path.into_inner();
+    println!("{commitish}");
     let archive_path = &data.archive_path;
     let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
     let blob_path = clean_path(&remainder);

From 651bbb44b4e3ab120521fab667e3027fe2cdbcef Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 11 Dec 2024 09:38:51 +0100
Subject: [PATCH 03/13] REFACTOR: fixed merge conflicts

---
 src/server/api/routes.rs | 34 ++++++++++++++++++++++++++++------
 1 file changed, 28 insertions(+), 6 deletions(-)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index e8a4378..d4d13cb 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -16,9 +16,14 @@ use actix_web::{
     HttpResponse
 =======
     guard, web, App, Error, Scope, get, Responder,
+<<<<<<< HEAD
     HttpResponse, HttpRequest
 >>>>>>> ac49acd (Merging git and serve)
+=======
+    HttpResponse
+>>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code)
 };
+use serde::Deserialize;
 
 use crate::utils::http::get_contenttype;
 use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
@@ -88,7 +93,9 @@ pub fn register_app<
 
         app = app
         .service(
-            get_blob
+            web::scope("/_git").service(
+                get_blob
+            )
         )
         .app_data(web::Data::new(state.clone()));
 
@@ -336,11 +343,20 @@ fn register_dependent_routes(
     Ok(())
 }
 
+/// Structure for 
+#[derive(Debug, Deserialize)]
+struct Info {
+    /// commit of the repo
+    commitish: String,
+    /// path of the file
+    remainder: Option<String>,
+}
 
 /// Return the content in the stelae archive in the `{namespace}/{name}`
 /// repo at the `commitish` commit at the `remainder` path.
 /// Return 404 if any are not found or there are any errors.
 <<<<<<< HEAD
+<<<<<<< HEAD
 #[route(
     "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
     method = "GET",
@@ -350,13 +366,19 @@ fn register_dependent_routes(
 #[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")]
 >>>>>>> ac49acd (Merging git and serve)
 #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))]
+=======
+#[get("/{namespace}/{name}")]//:/+([^{}]*?)?/*}")]
+#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))]
+>>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code)
 async fn get_blob(
-    path: web::Path<(String, String, String, String)>,
-    data: web::Data<AppState>,
+    path: web::Path<(String, String)>,
+    info: web::Query<Info>,
+    data: web::Data<AppState>
 ) -> impl Responder {
-    println!("TEST");
-    let (namespace, name, commitish, remainder) = path.into_inner();
-    println!("{commitish}");
+    let (namespace, name/* , commitish, remainder*/) = path.into_inner();
+    let info_struct: Info = info.into_inner();
+    let commitish = info_struct.commitish;
+    let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string());
     let archive_path = &data.archive_path;
     let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
     let blob_path = clean_path(&remainder);

From 98b132f649342b2ce061b68377fa9fc82fb6610d Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Tue, 17 Dec 2024 11:30:22 +0100
Subject: [PATCH 04/13] REFACTOR: executed cargo fmt and removed stale merge
 confict tags

---
 src/server/api/routes.rs | 48 ++++++++++++----------------------------
 src/server/app.rs        | 12 ++++++++--
 2 files changed, 24 insertions(+), 36 deletions(-)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index d4d13cb..a48bf55 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -11,26 +11,16 @@ use actix_service::ServiceFactory;
 use actix_web::{
     body::MessageBody,
     dev::{ServiceRequest, ServiceResponse},
-<<<<<<< HEAD
-    guard, web, App, Error, Scope, Responder, route,
-    HttpResponse
-=======
-    guard, web, App, Error, Scope, get, Responder,
-<<<<<<< HEAD
-    HttpResponse, HttpRequest
->>>>>>> ac49acd (Merging git and serve)
-=======
-    HttpResponse
->>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code)
+    guard, route, web, App, Error, HttpResponse, Responder, Scope,
 };
 use serde::Deserialize;
 
-use crate::utils::http::get_contenttype;
-use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
+use super::state::App as AppState;
 use super::{serve::serve, state::Global, versions::versions};
+use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
+use crate::utils::http::get_contenttype;
 use crate::utils::paths::clean_path;
 use git2::{self, ErrorCode};
-use super::state::App as AppState;
 
 use super::super::errors::HTTPError;
 
@@ -87,16 +77,11 @@ pub fn register_app<
                     .service(web::resource("/{path:.*}").to(versions))
                     .service(web::resource("").to(versions)),
             ),
-
         )
         .app_data(web::Data::new(state.clone()));
 
-        app = app
-        .service(
-            web::scope("/_git").service(
-                get_blob
-            )
-        )
+    app = app
+        .service(web::scope("/_git").service(get_blob))
         .app_data(web::Data::new(state.clone()));
 
     app = register_dynamic_routes(app, state)?;
@@ -343,7 +328,7 @@ fn register_dependent_routes(
     Ok(())
 }
 
-/// Structure for 
+/// Structure for
 #[derive(Debug, Deserialize)]
 struct Info {
     /// commit of the repo
@@ -355,27 +340,22 @@ struct Info {
 /// Return the content in the stelae archive in the `{namespace}/{name}`
 /// repo at the `commitish` commit at the `remainder` path.
 /// Return 404 if any are not found or there are any errors.
-<<<<<<< HEAD
-<<<<<<< HEAD
 #[route(
     "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
     method = "GET",
     method = "HEAD"
 )]
-=======
-#[get("/{namespace}/{name}/ref_{commitish:.*}_/{remainder}")]//:/+([^{}]*?)?/*}")]
->>>>>>> ac49acd (Merging git and serve)
-#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data))]
-=======
-#[get("/{namespace}/{name}")]//:/+([^{}]*?)?/*}")]
 #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))]
->>>>>>> 6268aa5 (Updated path for stelae git and removed redundant code)
+#[expect(
+    clippy::future_not_send,
+    reason = "We don't worry about git2-rs not implementing `Send` trait"
+)]
 async fn get_blob(
     path: web::Path<(String, String)>,
     info: web::Query<Info>,
-    data: web::Data<AppState>
+    data: web::Data<AppState>,
 ) -> impl Responder {
-    let (namespace, name/* , commitish, remainder*/) = path.into_inner();
+    let (namespace, name /* , commitish, remainder*/) = path.into_inner();
     let info_struct: Info = info.into_inner();
     let commitish = info_struct.commitish;
     let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string());
@@ -410,4 +390,4 @@ fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> Ht
         }
         _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
     }
-}
\ No newline at end of file
+}
diff --git a/src/server/app.rs b/src/server/app.rs
index 246e747..7e645a6 100644
--- a/src/server/app.rs
+++ b/src/server/app.rs
@@ -44,7 +44,11 @@ pub async fn serve_archive(
         }
     };
 
-    let archive = match Archive::parse(archive_path.clone(), &PathBuf::from(raw_archive_path), individual) {
+    let archive = match Archive::parse(
+        archive_path.clone(),
+        &PathBuf::from(raw_archive_path),
+        individual,
+    ) {
         Ok(archive) => archive,
         Err(err) => {
             tracing::error!("Unable to parse archive at '{raw_archive_path}'.");
@@ -53,7 +57,11 @@ pub async fn serve_archive(
         }
     };
 
-    let state = AppState { archive, db, archive_path };
+    let state = AppState {
+        archive,
+        db,
+        archive_path,
+    };
 
     HttpServer::new(move || {
         init(&state).unwrap_or_else(|err| {

From 71ccad27c59d567a5d0aa40b1308153ef3ea5461 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Tue, 17 Dec 2024 11:48:31 +0100
Subject: [PATCH 05/13] FIX: Git command supports commit names that contains /

---
 src/server/api/routes.rs | 12 ++++--------
 tests/common/mod.rs      |  3 +++
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index a48bf55..5a37704 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -340,11 +340,7 @@ struct Info {
 /// Return the content in the stelae archive in the `{namespace}/{name}`
 /// repo at the `commitish` commit at the `remainder` path.
 /// Return 404 if any are not found or there are any errors.
-#[route(
-    "/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}",
-    method = "GET",
-    method = "HEAD"
-)]
+#[route("/{namespace}/{name}", method = "GET", method = "HEAD")]
 #[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))]
 #[expect(
     clippy::future_not_send,
@@ -355,10 +351,10 @@ async fn get_blob(
     info: web::Query<Info>,
     data: web::Data<AppState>,
 ) -> impl Responder {
-    let (namespace, name /* , commitish, remainder*/) = path.into_inner();
+    let (namespace, name) = path.into_inner();
     let info_struct: Info = info.into_inner();
     let commitish = info_struct.commitish;
-    let remainder = info_struct.remainder.unwrap_or_else(|| "".to_string());
+    let remainder = info_struct.remainder.unwrap_or_default();
     let archive_path = &data.archive_path;
     let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
     let blob_path = clean_path(&remainder);
@@ -370,7 +366,7 @@ async fn get_blob(
 }
 
 /// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
-#[allow(clippy::wildcard_enum_match_arm)]
+#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")]
 #[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
 fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
     tracing::error!("{error}",);
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 34b113b..a09bd71 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -44,6 +44,9 @@ impl Global for TestAppState {
     fn db(&self) -> &db::DatabaseConnection {
         unimplemented!()
     }
+    fn archive_path(&self) -> &PathBuf {
+        unimplemented!()
+    }
 }
 
 pub async fn initialize_app(

From 9f2660bc351564fc04ef7917ec2ba34419dc3c9c Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 1 Jan 2025 22:11:35 +0100
Subject: [PATCH 06/13] test: added test for get_blob api

---
 tests/api/archive_basic_test.rs   | 306 +++++++++++++++++++++++++++++-
 tests/archive_testtools/config.rs |   2 +
 tests/archive_testtools/mod.rs    |  23 +++
 tests/common/mod.rs               |   7 +-
 4 files changed, 331 insertions(+), 7 deletions(-)

diff --git a/tests/api/archive_basic_test.rs b/tests/api/archive_basic_test.rs
index 4c2fd4f..3955d92 100644
--- a/tests/api/archive_basic_test.rs
+++ b/tests/api/archive_basic_test.rs
@@ -1,6 +1,11 @@
 use crate::archive_testtools::config::{ArchiveType, Jurisdiction};
-use crate::common;
-use actix_web::test;
+use crate::archive_testtools::get_repository;
+use crate::common::{self};
+use actix_http::Request;
+use actix_service::Service;
+use actix_web::body::MessageBody;
+use actix_web::dev::ServiceResponse;
+use actix_web::{test, Error};
 
 #[actix_web::test]
 async fn test_resolve_law_html_request_with_full_path_expect_success() {
@@ -189,3 +194,300 @@ async fn get_law_pdf_request_with_incorrect_path_expect_not_found() {
     let expected = true;
     assert_eq!(actual, expected);
 }
+
+#[actix_web::test]
+async fn test_resolve_root_stele_all_repositories_request_with_full_path_expect_success() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+
+    // let git_repo = get_repository(&path, "law-html");
+    // let _ = git_repo.create_branch("test_branch");
+    println!("Testing law-html");
+    test_paths(
+        "law-html",
+        vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    println!("Testing law-other");
+    test_paths(
+        "law-other",
+        vec![
+            "",
+            "example.json",
+            "a/",
+            "a/e/_doc/f/",
+            "a/d/",
+            "a/b/",
+            "a/b/c.html",
+            "a/_doc/e/",
+            "_prefix/",
+            "_prefix/a/",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    println!("Testing law-pdf");
+    test_paths(
+        "law-pdf",
+        vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    println!("Testing law-rdf");
+    test_paths(
+        "law-rdf",
+        vec![
+            "index.rdf",
+            "a/index.rdf",
+            "a/b/index.rdf",
+            "a/d/index.rdf",
+            "a/b/c.rdf",
+            "a/b/c/index.rdf",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    println!("Testing law-xml");
+    test_paths(
+        "law-xml",
+        vec![
+            "index.xml",
+            "a/index.xml",
+            "a/b/index.xml",
+            "a/d/index.xml",
+            "a/b/c.xml",
+            "a/b/c/index.xml",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    println!("Testing law-xml-codified");
+    test_paths(
+        "law-xml-codified",
+        vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"],
+        "master",
+        &app,
+        true,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_resolve_root_stele_all_repositories_request_with_incorrect_path_expect_client_error()
+{
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+
+    println!("Testing law-html");
+    test_paths(
+        "law-html",
+        vec!["a/b/c/d", "a/index.css"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+
+    println!("Testing law-other");
+    test_paths(
+        "law-other",
+        vec!["a/b/c/d", "example1.json"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+
+    println!("Testing law-pdf");
+    test_paths(
+        "law-pdf",
+        vec!["/example1.pdf", "/c/example.pdf"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+
+    println!("Testing law-rdf");
+    test_paths(
+        "law-rdf",
+        vec!["index1.rdf", "z/index.rdf"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+
+    println!("Testing law-xml");
+    test_paths(
+        "law-xml",
+        vec!["index1.xml", "t/index.xml"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+
+    println!("Testing law-xml-codified");
+    test_paths(
+        "law-xml-codified",
+        vec!["index1.xml", "x/index.xml"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+}
+
+async fn test_paths(
+    repo_name: &str,
+    file_paths: Vec<&str>,
+    branch_name: &str,
+    app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
+    expected: bool,
+) {
+    for request_uri in file_paths {
+        let req = test::TestRequest::get()
+            .uri(&format!(
+                "/_stelae/test_org/{}?commitish={}&remainder={}",
+                repo_name, branch_name, request_uri
+            ))
+            .to_request();
+        let resp = test::call_service(&app, req).await;
+        let actual = if expected {
+            resp.status().is_success()
+        } else {
+            resp.status().is_client_error()
+        };
+        let expected = true;
+        assert_eq!(actual, expected);
+    }
+}
+
+#[actix_web::test]
+async fn test_resolve_root_stele_law_html_different_files_with_different_branches() {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+    let _ = git_repo.create_branch("test_branch");
+
+    let _ = git_repo.checkout("master");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for master branch");
+    let _ = git_repo.commit(None, "Adding data for master branch");
+
+    let _ = git_repo.checkout("test_branch");
+    let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch");
+    let _ = git_repo.commit(None, "Adding data for master branch");
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt"
+        ))
+        .to_request();
+    let resp = test::call_service(&app, req).await;
+    let actual = resp.status().is_success();
+    assert_eq!(actual, true);
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=master&remainder=/test1.txt"
+        ))
+        .to_request();
+    let resp = test::call_service(&app, req).await;
+    let actual = resp.status().is_success();
+    assert_eq!(actual, false);
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test.txt"
+        ))
+        .to_request();
+    let resp = test::call_service(&app, req).await;
+    let actual = resp.status().is_success();
+    assert_eq!(actual, false);
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test1.txt"
+        ))
+        .to_request();
+    let resp = test::call_service(&app, req).await;
+    let actual = resp.status().is_success();
+    assert_eq!(actual, true);
+}
+
+#[actix_web::test]
+async fn test_resolve_root_stele_law_html_file_content_with_different_branches_expect_success() {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+
+    let _ = git_repo.create_branch("test_branch");
+
+    let _ = git_repo.checkout("master");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for master branch");
+    let _ = git_repo.commit(None, "Adding data for master branch");
+
+    let _ = git_repo.checkout("test_branch");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for test branch");
+    let _ = git_repo.commit(None, "Adding data for test branch");
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt"
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for master branch";
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt"
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for test branch";
+    println!("{}", common::blob_to_string(actual.to_vec()));
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+}
diff --git a/tests/archive_testtools/config.rs b/tests/archive_testtools/config.rs
index 991aa30..dca7331 100644
--- a/tests/archive_testtools/config.rs
+++ b/tests/archive_testtools/config.rs
@@ -12,6 +12,7 @@ pub enum Jurisdiction {
     Multi,
 }
 
+#[derive(Debug)]
 pub enum TestDataRepositoryType {
     Html,
     Rdf,
@@ -23,6 +24,7 @@ pub enum TestDataRepositoryType {
 /// Information about a data repository.
 ///
 /// This struct is used to initialize a data repository in the test suite.
+#[derive(Debug)]
 pub struct TestDataRepositoryContext {
     /// The name of the data repository.
     pub name: String,
diff --git a/tests/archive_testtools/mod.rs b/tests/archive_testtools/mod.rs
index 0d08cd5..cbbee03 100644
--- a/tests/archive_testtools/mod.rs
+++ b/tests/archive_testtools/mod.rs
@@ -93,6 +93,29 @@ impl GitRepository {
             path: path.to_path_buf(),
         })
     }
+
+    pub fn create_branch(&self, branch_name: &str) -> Result<(), Error> {
+        let head = self.repo.head()?;
+        let head_commit = head.peel_to_commit()?;
+        let _ = self.repo.branch(branch_name, &head_commit, false);
+        Ok(())
+    }
+
+    pub fn checkout(&self, branch_name: &str) -> Result<(), Error> {
+        let branch = match self.repo.find_branch(branch_name, git2::BranchType::Local) {
+            Ok(branch) => branch,
+            Err(_) => {
+                let head_commit = self.repo.head()?.peel_to_commit()?;
+                self.repo.branch(branch_name, &head_commit, false)?
+            }
+        };
+
+        self.repo
+            .set_head(branch.into_reference().name().unwrap())?;
+        self.repo
+            .checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
+        Ok(())
+    }
 }
 
 impl Into<git2::Repository> for GitRepository {
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index a09bd71..df2abb7 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -34,7 +34,7 @@ pub fn blob_to_string(blob: Vec<u8>) -> String {
 
 #[derive(Debug, Clone)]
 pub struct TestAppState {
-    archive: Archive,
+    pub archive: Archive,
 }
 
 impl Global for TestAppState {
@@ -44,9 +44,6 @@ impl Global for TestAppState {
     fn db(&self) -> &db::DatabaseConnection {
         unimplemented!()
     }
-    fn archive_path(&self) -> &PathBuf {
-        unimplemented!()
-    }
 }
 
 pub async fn initialize_app(
@@ -61,7 +58,7 @@ pub async fn initialize_app(
 pub fn initialize_archive(archive_type: ArchiveType) -> Result<tempfile::TempDir> {
     match initialize_archive_without_bare(archive_type) {
         Ok(td) => {
-            if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) {
+             if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) {
                 return Err(err);
             }
             Ok(td)

From ba75f361781f9e53d223caca785decff2323c7aa Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 1 Jan 2025 22:19:03 +0100
Subject: [PATCH 07/13] refactor: Moved stelae git logic to
 src/server/api/stelae folder

---
 src/server/api/mod.rs                |  1 +
 src/server/api/routes.rs             | 83 ++++------------------------
 src/server/api/state.rs              |  8 ---
 src/server/api/stelae/mod.rs         | 68 +++++++++++++++++++++++
 src/server/api/stelae/request/mod.rs |  9 +++
 src/server/app.rs                    |  6 +-
 6 files changed, 90 insertions(+), 85 deletions(-)
 create mode 100644 src/server/api/stelae/mod.rs
 create mode 100644 src/server/api/stelae/request/mod.rs

diff --git a/src/server/api/mod.rs b/src/server/api/mod.rs
index 5349f87..7cc2307 100644
--- a/src/server/api/mod.rs
+++ b/src/server/api/mod.rs
@@ -2,4 +2,5 @@
 pub mod routes;
 pub mod serve;
 pub mod state;
+pub mod stelae;
 pub mod versions;
diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index 5a37704..15abc40 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -3,6 +3,7 @@
     clippy::exit,
     reason = "We exit with 1 error code on any application errors"
 )]
+use std::sync::Arc;
 use std::{process, sync::OnceLock};
 
 use crate::server::api::state;
@@ -11,18 +12,11 @@ use actix_service::ServiceFactory;
 use actix_web::{
     body::MessageBody,
     dev::{ServiceRequest, ServiceResponse},
-    guard, route, web, App, Error, HttpResponse, Responder, Scope,
+    guard, web, App, Error, Scope,
 };
-use serde::Deserialize;
 
-use super::state::App as AppState;
+use super::stelae::get_blob;
 use super::{serve::serve, state::Global, versions::versions};
-use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
-use crate::utils::http::get_contenttype;
-use crate::utils::paths::clean_path;
-use git2::{self, ErrorCode};
-
-use super::super::errors::HTTPError;
 
 /// Name of the header to guard current documents
 static HEADER_NAME: OnceLock<String> = OnceLock::new();
@@ -80,9 +74,14 @@ pub fn register_app<
         )
         .app_data(web::Data::new(state.clone()));
 
-    app = app
-        .service(web::scope("/_git").service(get_blob))
-        .app_data(web::Data::new(state.clone()));
+    let stelae_data: Arc<dyn Global> = Arc::new(state.clone());
+    app = app.app_data(web::Data::new(stelae_data.clone())).service(
+        web::scope("_stelae").service(
+            web::resource("/{namespace}/{name}")
+                .route(web::get().to(get_blob))
+                .route(web::head().to(get_blob)),
+        ),
+    );
 
     app = register_dynamic_routes(app, state)?;
     Ok(app)
@@ -327,63 +326,3 @@ fn register_dependent_routes(
     }
     Ok(())
 }
-
-/// Structure for
-#[derive(Debug, Deserialize)]
-struct Info {
-    /// commit of the repo
-    commitish: String,
-    /// path of the file
-    remainder: Option<String>,
-}
-
-/// Return the content in the stelae archive in the `{namespace}/{name}`
-/// repo at the `commitish` commit at the `remainder` path.
-/// Return 404 if any are not found or there are any errors.
-#[route("/{namespace}/{name}", method = "GET", method = "HEAD")]
-#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, info))]
-#[expect(
-    clippy::future_not_send,
-    reason = "We don't worry about git2-rs not implementing `Send` trait"
-)]
-async fn get_blob(
-    path: web::Path<(String, String)>,
-    info: web::Query<Info>,
-    data: web::Data<AppState>,
-) -> impl Responder {
-    let (namespace, name) = path.into_inner();
-    let info_struct: Info = info.into_inner();
-    let commitish = info_struct.commitish;
-    let remainder = info_struct.remainder.unwrap_or_default();
-    let archive_path = &data.archive_path;
-    let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
-    let blob_path = clean_path(&remainder);
-    let contenttype = get_contenttype(&blob_path);
-    match blob {
-        Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content),
-        Err(error) => blob_error_response(&error, &namespace, &name),
-    }
-}
-
-/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
-#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")]
-#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
-fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
-    tracing::error!("{error}",);
-    if let Some(git_error) = error.downcast_ref::<git2::Error>() {
-        return match git_error.code() {
-            // TODO: check this is the right error
-            ErrorCode::NotFound => {
-                HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist"))
-            }
-            _ => HttpResponse::InternalServerError().body("Unexpected Git error"),
-        };
-    }
-    match error {
-        // TODO: Obviously it's better to use custom `Error` types
-        _ if error.to_string() == GIT_REQUEST_NOT_FOUND => {
-            HttpResponse::NotFound().body(HTTPError::NotFound.to_string())
-        }
-        _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
-    }
-}
diff --git a/src/server/api/state.rs b/src/server/api/state.rs
index b78c7db..172be04 100644
--- a/src/server/api/state.rs
+++ b/src/server/api/state.rs
@@ -13,8 +13,6 @@ pub trait Global {
     fn archive(&self) -> &Archive;
     /// Database connection
     fn db(&self) -> &db::DatabaseConnection;
-    /// path to the Stelae archive
-    fn archive_path(&self) -> &PathBuf;
 }
 
 /// Application state
@@ -24,8 +22,6 @@ pub struct App {
     pub archive: Archive,
     /// Database connection
     pub db: db::DatabaseConnection,
-    /// path to the Stelae archive
-    pub archive_path: PathBuf,
 }
 
 impl Global for App {
@@ -36,10 +32,6 @@ impl Global for App {
     fn db(&self) -> &db::DatabaseConnection {
         &self.db
     }
-
-    fn archive_path(&self) -> &PathBuf {
-        &self.archive_path
-    }
 }
 
 /// Repository to serve
diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs
new file mode 100644
index 0000000..5b9b12b
--- /dev/null
+++ b/src/server/api/stelae/mod.rs
@@ -0,0 +1,68 @@
+//! API endpoint for serving git blobs.
+
+use std::sync::Arc;
+
+use actix_web::{web, HttpRequest, HttpResponse, Responder};
+use request::StelaeQueryData;
+
+use super::state::Global;
+use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
+use crate::utils::http::get_contenttype;
+use crate::utils::paths::clean_path;
+use git2::{self, ErrorCode};
+
+use super::super::errors::HTTPError;
+
+/// Module that maps the HTTP web request body to structs.
+pub mod request;
+
+/// Return the content in the stelae archive in the `{namespace}/{name}`
+/// repo at the `commitish` commit at the `remainder` path.
+/// Return 404 if any are not found or there are any errors.
+#[tracing::instrument(name = "Retrieving a Git blob", skip(path, data, query))]
+#[expect(
+    clippy::future_not_send,
+    reason = "We don't worry about git2-rs not implementing `Send` trait"
+)]
+pub async fn get_blob(
+    req: HttpRequest,
+    path: web::Path<(String, String)>,
+    query: web::Query<StelaeQueryData>,
+    data: web::Data<Arc<dyn Global>>,
+) -> impl Responder {
+    let (namespace, name) = path.into_inner();
+    let query_data: StelaeQueryData = query.into_inner();
+    let commitish = query_data.commitish.unwrap_or_default();
+    let remainder = query_data.remainder.unwrap_or_default();
+    let archive_path = &data.archive().path.clone();
+    let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
+    let blob_path = clean_path(&remainder);
+    let contenttype = get_contenttype(&blob_path);
+    match blob {
+        Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content),
+        Err(error) => blob_error_response(&error, &namespace, &name),
+    }
+}
+
+/// A centralised place to match potentially unsafe internal errors to safe user-facing error responses
+#[expect(clippy::wildcard_enum_match_arm, reason = "Allows _ for enum matching")]
+#[tracing::instrument(name = "Error with Git blob request", skip(error, namespace, name))]
+fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> HttpResponse {
+    tracing::error!("{error}",);
+    if let Some(git_error) = error.downcast_ref::<git2::Error>() {
+        return match git_error.code() {
+            // TODO: check this is the right error
+            ErrorCode::NotFound => {
+                HttpResponse::NotFound().body(format!("repo {namespace}/{name} does not exist"))
+            }
+            _ => HttpResponse::InternalServerError().body("Unexpected Git error"),
+        };
+    }
+    match error {
+        // TODO: Obviously it's better to use custom `Error` types
+        _ if error.to_string() == GIT_REQUEST_NOT_FOUND => {
+            HttpResponse::NotFound().body(HTTPError::NotFound.to_string())
+        }
+        _ => HttpResponse::InternalServerError().body(HTTPError::InternalServerError.to_string()),
+    }
+}
diff --git a/src/server/api/stelae/request/mod.rs b/src/server/api/stelae/request/mod.rs
new file mode 100644
index 0000000..d321a0f
--- /dev/null
+++ b/src/server/api/stelae/request/mod.rs
@@ -0,0 +1,9 @@
+use serde::Deserialize;
+/// Structure for
+#[derive(Debug, Deserialize)]
+pub struct StelaeQueryData {
+    /// commit of the repo
+    pub commitish: Option<String>,
+    /// path of the file
+    pub remainder: Option<String>,
+}
diff --git a/src/server/app.rs b/src/server/app.rs
index 7e645a6..ede799a 100644
--- a/src/server/app.rs
+++ b/src/server/app.rs
@@ -57,11 +57,7 @@ pub async fn serve_archive(
         }
     };
 
-    let state = AppState {
-        archive,
-        db,
-        archive_path,
-    };
+    let state = AppState { archive, db };
 
     HttpServer::new(move || {
         init(&state).unwrap_or_else(|err| {

From 83eb359e6fbd8244a4254128fc28cf075bded287 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 1 Jan 2025 22:19:45 +0100
Subject: [PATCH 08/13] docs: Added changes to CHANGELOG

---
 CHANGELOG.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c8bf94..78e2be3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,18 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_
 
 ### Added
 
+- Added tests fot `stelae git` ([64])
+
 ### Changed
 
+- Merged `stelae git` and `stelae serve` into single command ([64])
+
 ### Fixed
 
 ### Removed
 
+[64]: https://github.com/openlawlibrary/stelae/pull/64
+
 ## [0.3.2]
 
 ### Added

From e496668b5a8b882f04755c002776da529497e27a Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 1 Jan 2025 22:55:06 +0100
Subject: [PATCH 09/13] style: Not to use Arc in sending Data to endpoint

---
 src/server/api/routes.rs     | 4 +---
 src/server/api/stelae/mod.rs | 7 +++----
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index 15abc40..9a7c606 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -3,7 +3,6 @@
     clippy::exit,
     reason = "We exit with 1 error code on any application errors"
 )]
-use std::sync::Arc;
 use std::{process, sync::OnceLock};
 
 use crate::server::api::state;
@@ -74,8 +73,7 @@ pub fn register_app<
         )
         .app_data(web::Data::new(state.clone()));
 
-    let stelae_data: Arc<dyn Global> = Arc::new(state.clone());
-    app = app.app_data(web::Data::new(stelae_data.clone())).service(
+    app = app.app_data(web::Data::new(state.archive().path.clone())).service(
         web::scope("_stelae").service(
             web::resource("/{namespace}/{name}")
                 .route(web::get().to(get_blob))
diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs
index 5b9b12b..d405694 100644
--- a/src/server/api/stelae/mod.rs
+++ b/src/server/api/stelae/mod.rs
@@ -1,11 +1,10 @@
 //! API endpoint for serving git blobs.
 
-use std::sync::Arc;
+use std::path::PathBuf;
 
 use actix_web::{web, HttpRequest, HttpResponse, Responder};
 use request::StelaeQueryData;
 
-use super::state::Global;
 use crate::utils::git::{Repo, GIT_REQUEST_NOT_FOUND};
 use crate::utils::http::get_contenttype;
 use crate::utils::paths::clean_path;
@@ -28,13 +27,13 @@ pub async fn get_blob(
     req: HttpRequest,
     path: web::Path<(String, String)>,
     query: web::Query<StelaeQueryData>,
-    data: web::Data<Arc<dyn Global>>,
+    data: web::Data<PathBuf>,
 ) -> impl Responder {
     let (namespace, name) = path.into_inner();
     let query_data: StelaeQueryData = query.into_inner();
     let commitish = query_data.commitish.unwrap_or_default();
     let remainder = query_data.remainder.unwrap_or_default();
-    let archive_path = &data.archive().path.clone();
+    let archive_path = &data;
     let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
     let blob_path = clean_path(&remainder);
     let contenttype = get_contenttype(&blob_path);

From b043e2355448085f54562bf1e41a99bd7070f169 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Wed, 1 Jan 2025 22:55:18 +0100
Subject: [PATCH 10/13] docs: Added changes to CHANGELOG

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e2be3..c2590b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_
 
 ### Fixed
 
+- git serve now support commitish that contains / in name ([64])
+
 ### Removed
 
 [64]: https://github.com/openlawlibrary/stelae/pull/64

From fb256c50e1577888dc9c420016818b45a9bb0596 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Thu, 2 Jan 2025 11:07:45 +0100
Subject: [PATCH 11/13] refactor: run cargo fmt command

---
 src/server/api/routes.rs | 16 +++++++++-------
 tests/common/mod.rs      |  2 +-
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/server/api/routes.rs b/src/server/api/routes.rs
index 9a7c606..d6d30ea 100644
--- a/src/server/api/routes.rs
+++ b/src/server/api/routes.rs
@@ -73,13 +73,15 @@ pub fn register_app<
         )
         .app_data(web::Data::new(state.clone()));
 
-    app = app.app_data(web::Data::new(state.archive().path.clone())).service(
-        web::scope("_stelae").service(
-            web::resource("/{namespace}/{name}")
-                .route(web::get().to(get_blob))
-                .route(web::head().to(get_blob)),
-        ),
-    );
+    app = app
+        .app_data(web::Data::new(state.archive().path.clone()))
+        .service(
+            web::scope("_stelae").service(
+                web::resource("/{namespace}/{name}")
+                    .route(web::get().to(get_blob))
+                    .route(web::head().to(get_blob)),
+            ),
+        );
 
     app = register_dynamic_routes(app, state)?;
     Ok(app)
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index df2abb7..327d09f 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -58,7 +58,7 @@ pub async fn initialize_app(
 pub fn initialize_archive(archive_type: ArchiveType) -> Result<tempfile::TempDir> {
     match initialize_archive_without_bare(archive_type) {
         Ok(td) => {
-             if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) {
+            if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) {
                 return Err(err);
             }
             Ok(td)

From ea02fe054fa75e037a1b124793935457fca8f131 Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Thu, 9 Jan 2025 19:56:41 +0100
Subject: [PATCH 12/13] docs: Completed documentation for StelaeQueryData

---
 src/server/api/stelae/mod.rs         | 2 +-
 src/server/api/stelae/request/mod.rs | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs
index d405694..3ba1362 100644
--- a/src/server/api/stelae/mod.rs
+++ b/src/server/api/stelae/mod.rs
@@ -31,7 +31,7 @@ pub async fn get_blob(
 ) -> impl Responder {
     let (namespace, name) = path.into_inner();
     let query_data: StelaeQueryData = query.into_inner();
-    let commitish = query_data.commitish.unwrap_or_default();
+    let commitish = query_data.commitish;
     let remainder = query_data.remainder.unwrap_or_default();
     let archive_path = &data;
     let blob = Repo::find_blob(archive_path, &namespace, &name, &remainder, &commitish);
diff --git a/src/server/api/stelae/request/mod.rs b/src/server/api/stelae/request/mod.rs
index d321a0f..97d7e0a 100644
--- a/src/server/api/stelae/request/mod.rs
+++ b/src/server/api/stelae/request/mod.rs
@@ -1,9 +1,9 @@
 use serde::Deserialize;
-/// Structure for
+/// Structure for passing query parameters for stelae endpoint
 #[derive(Debug, Deserialize)]
 pub struct StelaeQueryData {
-    /// commit of the repo
-    pub commitish: Option<String>,
-    /// path of the file
+    /// commit (or reference) to the repo. Can pass in `HEAD`, a branch ref (e.g. main), or a commit SHA.
+    pub commitish: String,
+    /// path of the file (e.g. \us\ca\cities\san-mateo\index.html). If nothing is passed by default it will look for index.html
     pub remainder: Option<String>,
 }

From 7b51d53875fe08c8ea8388ce87cbef8745b583ba Mon Sep 17 00:00:00 2001
From: Bojan Galic <bojan.galic99@gmail.com>
Date: Thu, 9 Jan 2025 19:59:23 +0100
Subject: [PATCH 13/13] test: Added additional tests and moved steale test to
 its own directory

---
 tests/api/archive_basic_test.rs           | 304 +--------------
 tests/api/mod.rs                          |   1 +
 tests/api/stelae/mod.rs                   |  64 +++
 tests/api/stelae/stelae_basic_test.rs     | 452 ++++++++++++++++++++++
 tests/api/stelae/stelae_multihost_test.rs |  43 ++
 5 files changed, 561 insertions(+), 303 deletions(-)
 create mode 100644 tests/api/stelae/mod.rs
 create mode 100644 tests/api/stelae/stelae_basic_test.rs
 create mode 100644 tests/api/stelae/stelae_multihost_test.rs

diff --git a/tests/api/archive_basic_test.rs b/tests/api/archive_basic_test.rs
index 3955d92..4ad7ff7 100644
--- a/tests/api/archive_basic_test.rs
+++ b/tests/api/archive_basic_test.rs
@@ -1,11 +1,6 @@
 use crate::archive_testtools::config::{ArchiveType, Jurisdiction};
-use crate::archive_testtools::get_repository;
 use crate::common::{self};
-use actix_http::Request;
-use actix_service::Service;
-use actix_web::body::MessageBody;
-use actix_web::dev::ServiceResponse;
-use actix_web::{test, Error};
+use actix_web::test;
 
 #[actix_web::test]
 async fn test_resolve_law_html_request_with_full_path_expect_success() {
@@ -194,300 +189,3 @@ async fn get_law_pdf_request_with_incorrect_path_expect_not_found() {
     let expected = true;
     assert_eq!(actual, expected);
 }
-
-#[actix_web::test]
-async fn test_resolve_root_stele_all_repositories_request_with_full_path_expect_success() {
-    let archive_path =
-        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
-    let app = common::initialize_app(archive_path.path()).await;
-
-    let mut path = archive_path.path().to_path_buf();
-    path.push("test_org");
-
-    // let git_repo = get_repository(&path, "law-html");
-    // let _ = git_repo.create_branch("test_branch");
-    println!("Testing law-html");
-    test_paths(
-        "law-html",
-        vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"],
-        "HEAD",
-        &app,
-        true,
-    )
-    .await;
-
-    println!("Testing law-other");
-    test_paths(
-        "law-other",
-        vec![
-            "",
-            "example.json",
-            "a/",
-            "a/e/_doc/f/",
-            "a/d/",
-            "a/b/",
-            "a/b/c.html",
-            "a/_doc/e/",
-            "_prefix/",
-            "_prefix/a/",
-        ],
-        "HEAD",
-        &app,
-        true,
-    )
-    .await;
-
-    println!("Testing law-pdf");
-    test_paths(
-        "law-pdf",
-        vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"],
-        "HEAD",
-        &app,
-        true,
-    )
-    .await;
-
-    println!("Testing law-rdf");
-    test_paths(
-        "law-rdf",
-        vec![
-            "index.rdf",
-            "a/index.rdf",
-            "a/b/index.rdf",
-            "a/d/index.rdf",
-            "a/b/c.rdf",
-            "a/b/c/index.rdf",
-        ],
-        "HEAD",
-        &app,
-        true,
-    )
-    .await;
-
-    println!("Testing law-xml");
-    test_paths(
-        "law-xml",
-        vec![
-            "index.xml",
-            "a/index.xml",
-            "a/b/index.xml",
-            "a/d/index.xml",
-            "a/b/c.xml",
-            "a/b/c/index.xml",
-        ],
-        "HEAD",
-        &app,
-        true,
-    )
-    .await;
-
-    println!("Testing law-xml-codified");
-    test_paths(
-        "law-xml-codified",
-        vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"],
-        "master",
-        &app,
-        true,
-    )
-    .await;
-}
-
-#[actix_web::test]
-async fn test_resolve_root_stele_all_repositories_request_with_incorrect_path_expect_client_error()
-{
-    let archive_path =
-        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
-    let app = common::initialize_app(archive_path.path()).await;
-
-    let mut path = archive_path.path().to_path_buf();
-    path.push("test_org");
-
-    println!("Testing law-html");
-    test_paths(
-        "law-html",
-        vec!["a/b/c/d", "a/index.css"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-
-    println!("Testing law-other");
-    test_paths(
-        "law-other",
-        vec!["a/b/c/d", "example1.json"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-
-    println!("Testing law-pdf");
-    test_paths(
-        "law-pdf",
-        vec!["/example1.pdf", "/c/example.pdf"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-
-    println!("Testing law-rdf");
-    test_paths(
-        "law-rdf",
-        vec!["index1.rdf", "z/index.rdf"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-
-    println!("Testing law-xml");
-    test_paths(
-        "law-xml",
-        vec!["index1.xml", "t/index.xml"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-
-    println!("Testing law-xml-codified");
-    test_paths(
-        "law-xml-codified",
-        vec!["index1.xml", "x/index.xml"],
-        "HEAD",
-        &app,
-        false,
-    )
-    .await;
-}
-
-async fn test_paths(
-    repo_name: &str,
-    file_paths: Vec<&str>,
-    branch_name: &str,
-    app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
-    expected: bool,
-) {
-    for request_uri in file_paths {
-        let req = test::TestRequest::get()
-            .uri(&format!(
-                "/_stelae/test_org/{}?commitish={}&remainder={}",
-                repo_name, branch_name, request_uri
-            ))
-            .to_request();
-        let resp = test::call_service(&app, req).await;
-        let actual = if expected {
-            resp.status().is_success()
-        } else {
-            resp.status().is_client_error()
-        };
-        let expected = true;
-        assert_eq!(actual, expected);
-    }
-}
-
-#[actix_web::test]
-async fn test_resolve_root_stele_law_html_different_files_with_different_branches() {
-    let archive_path =
-        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
-    let app = common::initialize_app(archive_path.path()).await;
-
-    let mut path = archive_path.path().to_path_buf();
-    path.push("test_org");
-    let git_repo = get_repository(&path, "law-html");
-    path.push("law-html");
-    let _ = git_repo.create_branch("test_branch");
-
-    let _ = git_repo.checkout("master");
-    let _ = git_repo.add_file(&path, "test.txt", "Content for master branch");
-    let _ = git_repo.commit(None, "Adding data for master branch");
-
-    let _ = git_repo.checkout("test_branch");
-    let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch");
-    let _ = git_repo.commit(None, "Adding data for master branch");
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt"
-        ))
-        .to_request();
-    let resp = test::call_service(&app, req).await;
-    let actual = resp.status().is_success();
-    assert_eq!(actual, true);
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=master&remainder=/test1.txt"
-        ))
-        .to_request();
-    let resp = test::call_service(&app, req).await;
-    let actual = resp.status().is_success();
-    assert_eq!(actual, false);
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test.txt"
-        ))
-        .to_request();
-    let resp = test::call_service(&app, req).await;
-    let actual = resp.status().is_success();
-    assert_eq!(actual, false);
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=test_branch&remainder=/test1.txt"
-        ))
-        .to_request();
-    let resp = test::call_service(&app, req).await;
-    let actual = resp.status().is_success();
-    assert_eq!(actual, true);
-}
-
-#[actix_web::test]
-async fn test_resolve_root_stele_law_html_file_content_with_different_branches_expect_success() {
-    let archive_path =
-        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
-    let app = common::initialize_app(archive_path.path()).await;
-
-    let mut path = archive_path.path().to_path_buf();
-    path.push("test_org");
-    let git_repo = get_repository(&path, "law-html");
-    path.push("law-html");
-
-    let _ = git_repo.create_branch("test_branch");
-
-    let _ = git_repo.checkout("master");
-    let _ = git_repo.add_file(&path, "test.txt", "Content for master branch");
-    let _ = git_repo.commit(None, "Adding data for master branch");
-
-    let _ = git_repo.checkout("test_branch");
-    let _ = git_repo.add_file(&path, "test.txt", "Content for test branch");
-    let _ = git_repo.commit(None, "Adding data for test branch");
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=master&remainder=/test.txt"
-        ))
-        .to_request();
-    let actual = test::call_and_read_body(&app, req).await;
-    let expected = "Content for master branch";
-    assert!(
-        common::blob_to_string(actual.to_vec()).starts_with(expected),
-        "doesn't start with {expected}"
-    );
-
-    let req = test::TestRequest::get()
-        .uri(&format!(
-            "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt"
-        ))
-        .to_request();
-    let actual = test::call_and_read_body(&app, req).await;
-    let expected = "Content for test branch";
-    println!("{}", common::blob_to_string(actual.to_vec()));
-    assert!(
-        common::blob_to_string(actual.to_vec()).starts_with(expected),
-        "doesn't start with {expected}"
-    );
-}
diff --git a/tests/api/mod.rs b/tests/api/mod.rs
index 6983843..7dee2c4 100644
--- a/tests/api/mod.rs
+++ b/tests/api/mod.rs
@@ -1,3 +1,4 @@
 mod archive_basic_test;
 mod archive_multihost_test;
 mod archive_multijursidiction_test;
+mod stelae;
diff --git a/tests/api/stelae/mod.rs b/tests/api/stelae/mod.rs
new file mode 100644
index 0000000..00100e0
--- /dev/null
+++ b/tests/api/stelae/mod.rs
@@ -0,0 +1,64 @@
+use actix_http::{Method, Request};
+use actix_service::Service;
+use actix_web::body::MessageBody;
+use actix_web::dev::ServiceResponse;
+use actix_web::{test, Error};
+mod stelae_basic_test;
+mod stelae_multihost_test;
+
+/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result
+async fn test_stelae_paths(
+    org_name: &str,
+    repo_name: &str,
+    file_paths: Vec<&str>,
+    branch_name: &str,
+    app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
+    expected: bool,
+) {
+    for file_path in file_paths {
+        let req = test::TestRequest::get()
+            .uri(&format!(
+                "/_stelae/{}/{}?commitish={}&remainder={}",
+                org_name, repo_name, branch_name, file_path
+            ))
+            .to_request();
+
+        let resp = test::call_service(&app, req).await;
+        let actual = if expected {
+            resp.status().is_success()
+        } else {
+            resp.status().is_client_error()
+        };
+        let expected = true;
+        assert_eq!(actual, expected);
+    }
+}
+
+/// Helper method which test all `fille_paths`` in `org_name`/`repo_name` repository on `branch_name`` branch with `expected` result
+async fn test_stelae_paths_with_head_method(
+    org_name: &str,
+    repo_name: &str,
+    file_paths: Vec<&str>,
+    branch_name: &str,
+    app: &impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = Error>,
+    expected: bool,
+) {
+    for file_path in file_paths {
+        let req = test::TestRequest::default()
+            .method(Method::HEAD)
+            .uri(&format!(
+                "/_stelae/{}/{}?commitish={}&remainder={}",
+                org_name, repo_name, branch_name, file_path
+            ))
+            .to_request();
+
+        let resp = test::call_service(&app, req).await;
+        let actual = if expected {
+            resp.status().is_success()
+        } else {
+            resp.status().is_client_error()
+        };
+        let expected = true;
+        assert_eq!(actual, expected);
+    }
+}
diff --git a/tests/api/stelae/stelae_basic_test.rs b/tests/api/stelae/stelae_basic_test.rs
new file mode 100644
index 0000000..0db46c1
--- /dev/null
+++ b/tests/api/stelae/stelae_basic_test.rs
@@ -0,0 +1,452 @@
+use actix_web::test;
+
+use crate::api::stelae::test_stelae_paths;
+use crate::archive_testtools::config::{ArchiveType, Jurisdiction};
+use crate::archive_testtools::get_repository;
+use crate::common;
+
+use super::test_stelae_paths_with_head_method;
+
+#[actix_web::test]
+async fn test_stele_api_on_all_repositories_with_full_path_expect_success() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-other",
+        vec![
+            "",
+            "example.json",
+            "a/",
+            "a/e/_doc/f/",
+            "a/d/",
+            "a/b/",
+            "a/b/c.html",
+            "a/_doc/e/",
+            "_prefix/",
+            "_prefix/a/",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-pdf",
+        vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-rdf",
+        vec![
+            "index.rdf",
+            "a/index.rdf",
+            "a/b/index.rdf",
+            "a/d/index.rdf",
+            "a/b/c.rdf",
+            "a/b/c/index.rdf",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-xml",
+        vec![
+            "index.xml",
+            "a/index.xml",
+            "a/b/index.xml",
+            "a/d/index.xml",
+            "a/b/c.xml",
+            "a/b/c/index.xml",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-xml-codified",
+        vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_all_repositories_with_head_method_expect_success() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-html",
+        vec!["", "a/", "a/b/", "a/d/", "a/b/c.html", "a/b/c/"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-other",
+        vec![
+            "",
+            "example.json",
+            "a/",
+            "a/e/_doc/f/",
+            "a/d/",
+            "a/b/",
+            "a/b/c.html",
+            "a/_doc/e/",
+            "_prefix/",
+            "_prefix/a/",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-pdf",
+        vec!["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-rdf",
+        vec![
+            "index.rdf",
+            "a/index.rdf",
+            "a/b/index.rdf",
+            "a/d/index.rdf",
+            "a/b/c.rdf",
+            "a/b/c/index.rdf",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-xml",
+        vec![
+            "index.xml",
+            "a/index.xml",
+            "a/b/index.xml",
+            "a/d/index.xml",
+            "a/b/c.xml",
+            "a/b/c/index.xml",
+        ],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths_with_head_method(
+        "test_org",
+        "law-xml-codified",
+        vec!["index.xml", "e/index.xml", "e/f/index.xml", "e/g/index.xml"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_missing_branch_name_expect_client_error() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["", "a/index.html"],
+        "",
+        &app,
+        false,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_invalid_branch_name_expect_client_error() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["", "a/index.html"],
+        "notExistingBranch",
+        &app,
+        false,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_invalid_org_name_expect_client_error() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "not_test_org",
+        "law-html",
+        vec!["", "a/index.html"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_invalid_repo_name_expect_client_error() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "test_org",
+        "not_law-html",
+        vec!["", "a/index.html"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_incorrect_paths_expect_client_error() {
+    let archive_path =
+        common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["a/b/c/d", "a/index.css"],
+        "HEAD",
+        &app,
+        false,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_on_law_html_repository_with_different_files_on_different_branches_expect_success(
+) {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+    let _ = git_repo.create_branch("test_branch");
+    let _ = git_repo.create_branch("default_branch");
+
+    let _ = git_repo.checkout("default_branch");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for default branch");
+    let _ = git_repo.commit(None, "Adding data for default branch");
+
+    let _ = git_repo.checkout("test_branch");
+    let _ = git_repo.add_file(&path, "test1.txt", "Content for test branch");
+    let _ = git_repo.commit(None, "Adding data for test branch");
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["/test.txt"],
+        "default_branch",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["/test1.txt"],
+        "default_branch",
+        &app,
+        false,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["/test.txt"],
+        "test_branch",
+        &app,
+        false,
+    )
+    .await;
+
+    test_stelae_paths(
+        "test_org",
+        "law-html",
+        vec!["/test1.txt"],
+        "test_branch",
+        &app,
+        true,
+    )
+    .await;
+}
+
+#[actix_web::test]
+async fn test_stele_api_with_same_file_on_different_branches_expect_different_file_content_on_different_branches(
+) {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+
+    let _ = git_repo.create_branch("test_branch");
+    let _ = git_repo.create_branch("default_branch");
+
+    let _ = git_repo.checkout("default_branch");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for default branch");
+    let _ = git_repo.commit(None, "Adding data for default branch");
+
+    let _ = git_repo.checkout("test_branch");
+    let _ = git_repo.add_file(&path, "test.txt", "Content for test branch");
+    let _ = git_repo.commit(None, "Adding data for test branch");
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=default_branch&remainder=/test.txt"
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for default branch";
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish=test_branch&remainder=test.txt"
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for test branch";
+    println!("{}", common::blob_to_string(actual.to_vec()));
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+}
+
+#[actix_web::test]
+async fn test_stelae_api_where_branch_contains_slashs_expect_resolved_content() {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+    let branch_name = "test/branch/with/slash";
+    let _ = git_repo.create_branch(branch_name);
+
+    let _ = git_repo.checkout(branch_name);
+    let _ = git_repo.add_file(&path, "test.txt", "Content for test branch");
+    let _ = git_repo.commit(None, "Adding data for test branch");
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish={}&remainder=/test.txt",
+            branch_name
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for test branch";
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+}
+
+#[actix_web::test]
+async fn test_stelae_api_where_branch_is_commit_sha_expect_resolved_content() {
+    let archive_path =
+        common::initialize_archive_without_bare(ArchiveType::Basic(Jurisdiction::Single)).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    let mut path = archive_path.path().to_path_buf();
+    path.push("test_org");
+    let git_repo = get_repository(&path, "law-html");
+    path.push("law-html");
+    let branch_name = "test/branch/with/slash";
+    let _ = git_repo.create_branch(branch_name);
+
+    let _ = git_repo.checkout(branch_name);
+    let _ = git_repo.add_file(&path, "test.txt", "Content for test branch");
+    let commit_hash = git_repo.commit(None, "Adding data for test branch");
+    let sha_string = commit_hash.unwrap().to_string();
+
+    let req = test::TestRequest::get()
+        .uri(&format!(
+            "/_stelae/test_org/law-html?commitish={}&remainder=/test.txt",
+            sha_string
+        ))
+        .to_request();
+    let actual = test::call_and_read_body(&app, req).await;
+    let expected = "Content for test branch";
+    assert!(
+        common::blob_to_string(actual.to_vec()).starts_with(expected),
+        "doesn't start with {expected}"
+    );
+}
diff --git a/tests/api/stelae/stelae_multihost_test.rs b/tests/api/stelae/stelae_multihost_test.rs
new file mode 100644
index 0000000..3822fff
--- /dev/null
+++ b/tests/api/stelae/stelae_multihost_test.rs
@@ -0,0 +1,43 @@
+//# def test_<method/code under test>_where_<conditions/inputs/state>_expect_<result>
+
+use crate::{archive_testtools::config::ArchiveType, common};
+
+use super::test_stelae_paths;
+
+#[actix_web::test]
+async fn test_stelae_api_with_multiple_repositories_expect_success() {
+    let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap();
+    let app = common::initialize_app(archive_path.path()).await;
+
+    test_stelae_paths(
+        "stele_1",
+        "law-html",
+        vec!["/a/b/c.html"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "stele_1_1",
+        "law-pdf",
+        vec!["/a/b/example.pdf"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths(
+        "stele_1_2",
+        "law-xml",
+        vec!["/a/b/c/index.xml"],
+        "HEAD",
+        &app,
+        true,
+    )
+    .await;
+
+    test_stelae_paths("stele_2", "law-rdf", vec!["/a/b/c.rdf"], "HEAD", &app, true).await;
+}