diff --git a/CHANGELOG.md b/CHANGELOG.md index b1424c5..dca29a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,24 @@ and this project adheres to a _modified_ form of _[Semantic Versioning][semver]_ ### Removed +## [0.5.0-alpha.1] + +### Added + +- Added tests fot `stelae git` ([64]) + +### Changed + +- Merged `stelae git` and `stelae serve` into single command ([64]) + +### Fixed + +- git serve now support commitish that contains / in name ([64]) + +### Removed + +[64]: https://github.com/openlawlibrary/stelae/pull/64 + ## [0.4.1] ### Added diff --git a/Cargo.lock b/Cargo.lock index 33229ff..633f639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3110,7 +3110,7 @@ dependencies = [ [[package]] name = "stelae" -version = "0.4.1" +version = "0.5.0-alpha.1" dependencies = [ "actix-http", "actix-service", diff --git a/Cargo.toml b/Cargo.toml index 626ac0d..e31cb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stelae" description = "A collection of tools in Rust and Python for preserving, authenticating, and accessing laws in perpetuity." -version = "0.4.1" +version = "0.5.0-alpha.1" edition = "2021" readme = "README.md" license = "AGPL-3.0" diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..0817158 --- /dev/null +++ b/dockerfile @@ -0,0 +1,7 @@ +FROM rust:1.83 + +# Install necessary tools (if needed) +RUN apt-get update && apt-get install -y bash + +# Set the container's working directory +WORKDIR /workspace \ No newline at end of file diff --git a/docs/build_stelae_binaries_for_linux.md b/docs/build_stelae_binaries_for_linux.md new file mode 100644 index 0000000..fdcf001 --- /dev/null +++ b/docs/build_stelae_binaries_for_linux.md @@ -0,0 +1,3 @@ +1. Install Docker +2. From powershell run `cd ` and then run `docker build -t rust-compiler .` to download image +3. After image is downloaded run `docker run --rm -v "C:\\:/workspace" rust-compiler bash -c "cargo build --release"` \ No newline at end of file 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 1cf4e6d..3982709 100644 --- a/src/server/api/routes.rs +++ b/src/server/api/routes.rs @@ -14,6 +14,7 @@ use actix_web::{ guard, web, App, Error, Scope, }; +use super::stelae::get_blob; use super::{serve::serve, state::Global, versions::versions}; /// Name of the header to guard current documents @@ -76,6 +77,16 @@ 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 = register_dynamic_routes(app, state)?; Ok(app) } diff --git a/src/server/api/stelae/mod.rs b/src/server/api/stelae/mod.rs new file mode 100644 index 0000000..07c55cc --- /dev/null +++ b/src/server/api/stelae/mod.rs @@ -0,0 +1,67 @@ +//! API endpoint for serving git blobs. + +use std::path::PathBuf; + +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use request::StelaeQueryData; + +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, + data: web::Data, +) -> impl Responder { + let (namespace, name) = path.into_inner(); + let query_data: StelaeQueryData = query.into_inner(); + let commitish = query_data.commitish.unwrap_or_else(|| String::from("HEAD")); + let remainder = query_data.remainder.unwrap_or_default(); + 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); + 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::() { + 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..39850df --- /dev/null +++ b/src/server/api/stelae/request/mod.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; +/// Structure for passing query parameters for stelae endpoint +#[derive(Debug, Deserialize)] +pub struct StelaeQueryData { + /// commit (or reference) to the repo. Can pass in `HEAD`, a branch ref (e.g. main), or a commit SHA. If nothing is passed by default it will look for HEAD + pub commitish: Option, + /// 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, +} diff --git a/src/server/app.rs b/src/server/app.rs index 0acf600..ede799a 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, &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}'."); diff --git a/src/server/git.rs b/src/server/git.rs index 992f835..48c32a8 100644 --- a/src/server/git.rs +++ b/src/server/git.rs @@ -1,5 +1,4 @@ //! Legacy git microserver. - use actix_web::{get, route, web, App, HttpResponse, HttpServer, Responder}; use git2::{self, ErrorCode}; use std::path::PathBuf; diff --git a/tests/api/archive_basic_test.rs b/tests/api/archive_basic_test.rs index 4c2fd4f..4ad7ff7 100644 --- a/tests/api/archive_basic_test.rs +++ b/tests/api/archive_basic_test.rs @@ -1,5 +1,5 @@ use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; -use crate::common; +use crate::common::{self}; use actix_web::test; #[actix_web::test] 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, 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, 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__where__expect_ + +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; +} 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 for GitRepository { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 34b113b..327d09f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -34,7 +34,7 @@ pub fn blob_to_string(blob: Vec) -> String { #[derive(Debug, Clone)] pub struct TestAppState { - archive: Archive, + pub archive: Archive, } impl Global for TestAppState {