diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index efc1669..4326784 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,7 @@ jobs: create-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # production release - uses: taiki-e/create-gh-release-action@v1 with: @@ -37,7 +37,7 @@ jobs: - windows-latest runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: taiki-e/upload-rust-binary-action@v1 with: # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d90b5e..d747371 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,41 +10,37 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable - name: Install NuShell uses: hustcer/setup-nu@v3 env: GITHUB_TOKEN: ${{ secrets.STELAE_GITHUB_TOKEN }} - + - name: Install Just uses: extractions/setup-just@v1 env: GITHUB_TOKEN: ${{ secrets.STELAE_GITHUB_TOKEN }} - - name: Run tests - run: just test + - uses: taiki-e/install-action@v1 + with: + tool: nextest + - name: Run tests (nextest) + run: just nextest lints: name: Lints runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@1.70 # Pin based on current `rust-version` in Cargo.toml. IMPORTANT: Upgrade version when `rust-version` changes. with: - profile: minimal - toolchain: stable - override: true components: rustfmt, clippy - name: Install Just diff --git a/.gitignore b/.gitignore index ea8c4bf..c275506 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /target +# VS Code settings +.vscode/ + +!.vscode/settings.json diff --git a/Cargo.lock b/Cargo.lock index 19f07dd..0b19dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-sink", @@ -31,7 +31,7 @@ dependencies = [ "actix-utils", "ahash", "base64", - "bitflags", + "bitflags 1.3.2", "brotli", "bytes", "bytestring", @@ -257,6 +257,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.10.3" @@ -347,7 +353,7 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "textwrap", "unicode-width", ] @@ -358,7 +364,7 @@ version = "4.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0acbd8d28a0a60d7108d7ae850af6ba34cf2d1257fc646980e5f97ce14275966" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_derive", "clap_lex", "is-terminal", @@ -573,6 +579,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.2.8" @@ -584,6 +596,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -594,6 +616,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "flate2" version = "1.0.24" @@ -676,7 +704,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -697,7 +725,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.2", "slab", "tokio", "tokio-util", @@ -716,6 +744,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + [[package]] name = "heck" version = "0.4.0" @@ -780,7 +814,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", ] [[package]] @@ -790,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e394faa0efb47f9f227f1cd89978f854542b318a6f64fa695489c9c993056656" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -801,8 +845,8 @@ checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d" dependencies = [ "hermit-abi 0.2.6", "io-lifetimes", - "rustix", - "windows-sys", + "rustix 0.36.3", + "windows-sys 0.42.0", ] [[package]] @@ -858,9 +902,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgit2-sys" @@ -908,6 +952,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + [[package]] name = "local-channel" version = "0.1.3" @@ -962,9 +1012,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -994,7 +1044,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1087,9 +1137,9 @@ checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1277,7 +1327,16 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -1318,12 +1377,25 @@ version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e" dependencies = [ - "bitflags", - "errno", + "bitflags 1.3.2", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.1.3", + "windows-sys 0.42.0", +] + +[[package]] +name = "rustix" +version = "0.38.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +dependencies = [ + "bitflags 2.4.1", + "errno 0.3.7", + "libc", + "linux-raw-sys 0.4.11", + "windows-sys 0.48.0", ] [[package]] @@ -1371,9 +1443,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -1391,6 +1463,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1459,8 +1540,10 @@ dependencies = [ [[package]] name = "stelae" -version = "0.2.0" +version = "0.2.1" dependencies = [ + "actix-http", + "actix-service", "actix-web", "anyhow", "clap 4.0.27", @@ -1472,6 +1555,11 @@ dependencies = [ "mime_guess", "regex", "serde", + "serde_derive", + "serde_json", + "tempfile", + "toml", + "toml_edit", "tracing", "tracing-actix-web", "tracing-subscriber", @@ -1485,15 +1573,28 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.4.1", + "rustix 0.38.25", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1605,6 +1706,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.37" @@ -1874,13 +2009,37 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.0", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm 0.42.0", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1889,42 +2048,93 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 182d3b4..bf9586b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,26 +8,33 @@ license = "AGPL-3.0" keywords = ["authentication", "laws", "preservation"] categories = ["authentication", "web-programming::http-server"] repository = "https://github.com/openlawlibrary/stelae" -rust-version = "1.66" +rust-version = "1.70" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] actix-web = "4" -mime = "0.3.9" -mime_guess = "2.0.1" +actix-service = "2.0" +actix-http = "3.2" +mime = "0.3.17" +mime_guess = "2.0.4" anyhow = "1.0" clap = { version = "4.0.27", features = ["derive"] } git2 = "0.17" lazy_static = "1.4.0" regex = "1" serde = "1.0" +serde_json = "1.0" tracing = "0.1.37" tracing-subscriber = "0.3.16" tracing-actix-web = "0.6.2" derive_more = "0.99.17" +toml = "0.8.8" +toml_edit = "0.21.0" +serde_derive = "1.0.152" [dev-dependencies] criterion = "0.3" +tempfile = "3" [[bench]] name = "git_benchmark" diff --git a/README.md b/README.md index ef53ebf..f74d711 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ See [tracing-subscriber docs](https://docs.rs/tracing-subscriber/latest/tracing_ ## Q&A - Why do we suggest NuShell? - - NuShell is almost as fast on windows as cmd, but is compattible with bash. If you do not use NuShell on windows, you will need to make sure Git Bash is installed. If you have performance issues, consider switching to Nu. + - NuShell is almost as fast on Windows as cmd, but is compattible with bash. If you do not use NuShell on windows, you will need to make sure Git Bash is installed. If you have performance issues, consider switching to Nu. diff --git a/benches/git_benchmark.rs b/benches/git_benchmark.rs index 979022e..45d851e 100644 --- a/benches/git_benchmark.rs +++ b/benches/git_benchmark.rs @@ -10,22 +10,22 @@ use std::path::PathBuf; use std::sync::Once; use stelae::utils::git::Repo; -/// get the path to the test library at `$REPO_ROOT/tests/fixtures/library`. -fn get_test_library_path() -> PathBuf { +/// get the path to the test archive at `$REPO_ROOT/tests/fixtures/archive`. +fn get_test_archive_path() -> PathBuf { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("tests/fixtures/library"); + path.push("tests/fixtures/basic/archive"); path } /// ensure `initialize` function, below, is only called once static INIT: Once = Once::new(); -/// Bare git repo(s) in test library must have `refs/heads` and +/// Bare git repo(s) in test archive must have `refs/heads` and /// `refs/tags` folders. They are empty, so not stored in git, /// so must be created pub fn initialize() { INIT.call_once(|| { - let repo_path = get_test_library_path().join(PathBuf::from("test/law-html")); + let repo_path = get_test_archive_path().join(PathBuf::from("test/law-html")); let heads_path = repo_path.join(PathBuf::from("refs/heads")); create_dir_all(heads_path).expect("Something went wrong creating the refs/heads folder"); let tags_path = repo_path.join(PathBuf::from("refs/tags")); @@ -36,10 +36,10 @@ pub fn initialize() { /// Measure the speed of the git utils fn bench_repo() { initialize(); - let test_library_path = get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html") + let test_archive_path = get_test_archive_path(); + let repo = Repo::new(&test_archive_path, "test", "law-html") .expect("Something went wrong creating the repo"); - repo.get_bytes_at_path("ed782e08d119a580baa3067e2ea5df06f3d1cd05", "a/b/c.html") + repo.get_bytes_at_path("4ba432f61eec15194db527548be4cbc0105635b9", "a/b/c.html") .expect("Something went wrong calling `get_bytes_at_path`"); } diff --git a/justfile b/justfile index dcf5777..349f55e 100644 --- a/justfile +++ b/justfile @@ -14,6 +14,8 @@ format: # Run all tests test: cargo test +nextest: + cargo nextest run --all --no-fail-fast && cargo test --doc # Run clippy maximum strictness. Passes through any flags to clippy. clippy *FLAGS: @@ -23,7 +25,7 @@ clippy *FLAGS: -D warnings \ # Continuous integration - test, lint, benchmark -ci: lint test bench +ci: lint nextest bench # Run all benchmarks bench: diff --git a/src/lib.rs b/src/lib.rs index 32c5594..e8f912b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,4 +86,5 @@ )] pub mod server; +pub mod stelae; pub mod utils; diff --git a/src/server/git.rs b/src/server/git.rs index 715180f..4f13a78 100644 --- a/src/server/git.rs +++ b/src/server/git.rs @@ -21,8 +21,8 @@ use crate::utils::http::get_contenttype; /// Global, read-only state passed into the actix app struct AppState { - /// path to the Stelae library - library_path: PathBuf, + /// path to the Stelae archive + archive_path: PathBuf, } #[allow(clippy::expect_used)] @@ -49,7 +49,7 @@ async fn misc(path: web::Path) -> actix_web::Result<&'static str, Stelae } } -/// Return the content in the stelae library in the `{namespace}/{name}` +/// 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. #[get("/{namespace}/{name}/{commitish}{remainder:/+([^{}]*?)?/*}")] @@ -59,8 +59,8 @@ async fn get_blob( data: web::Data, ) -> impl Responder { let (namespace, name, commitish, remainder) = path.into_inner(); - let lib_path = &data.library_path; - let blob = find_blob(lib_path, &namespace, &name, &remainder, &commitish); + let archive_path = &data.archive_path; + let blob = find_blob(archive_path, &namespace, &name, &remainder, &commitish); let blob_path = clean_path(&remainder); let contenttype = get_contenttype(&blob_path); match blob { @@ -72,13 +72,13 @@ async fn get_blob( /// Do the work of looking for the requested Git object. // TODO: This, and `clean_path`, look like they could live in `utils::git::Repo` fn find_blob( - lib_path: &Path, + archive_path: &Path, namespace: &str, name: &str, remainder: &str, commitish: &str, ) -> anyhow::Result> { - let repo = Repo::new(lib_path, namespace, name)?; + let repo = Repo::new(archive_path, namespace, name)?; let blob_path = clean_path(remainder); let blob = repo.get_bytes_at_path(commitish, &blob_path)?; Ok(blob) @@ -107,16 +107,16 @@ fn blob_error_response(error: &anyhow::Error, namespace: &str, name: &str) -> Ht } } -/// Serve git repositories in the Stelae library. +/// Serve git repositories in the Stelae archive. #[actix_web::main] // or #[tokio::main] pub async fn serve_git( - raw_library_path: &str, - library_path: PathBuf, + raw_archive_path: &str, + archive_path: PathBuf, port: u16, ) -> std::io::Result<()> { let bind = "127.0.0.1"; - let message = "Serving content from the Stelae library at"; - tracing::info!("{message} '{raw_library_path}' on http://{bind}:{port}.",); + let message = "Serving content from the Stelae archive at"; + tracing::info!("{message} '{raw_archive_path}' on http://{bind}:{port}.",); HttpServer::new(move || { App::new() @@ -125,7 +125,7 @@ pub async fn serve_git( .service(misc) .service(get_blob) .app_data(web::Data::new(AppState { - library_path: library_path.clone(), + archive_path: archive_path.clone(), })) }) .bind((bind, port))? diff --git a/src/server/mod.rs b/src/server/mod.rs index bead404..2d9b564 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -4,4 +4,5 @@ pub mod errors; pub mod git; +pub mod publish; pub mod tracing; diff --git a/src/server/publish.rs b/src/server/publish.rs new file mode 100644 index 0000000..20f5382 --- /dev/null +++ b/src/server/publish.rs @@ -0,0 +1,415 @@ +//! Serve documents in a Stelae archive. +#![allow(clippy::exit)] +#![allow(clippy::unused_async)] +use crate::stelae::archive::Archive; +use crate::stelae::types::repositories::{Repositories, Repository}; +use crate::utils::archive::get_name_parts; +use crate::utils::git::Repo; +use crate::utils::http::get_contenttype; +use crate::{server::tracing::StelaeRootSpanBuilder, stelae::stele::Stele}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::{guard, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Scope}; +use git2::Repository as GitRepository; +use lazy_static::lazy_static; +use regex::Regex; +use std::{fmt, path::PathBuf}; +use tracing_actix_web::TracingLogger; + +use actix_http::body::MessageBody; +use actix_service::ServiceFactory; +use std::sync::OnceLock; + +/// Name of the header to guard current documents +static HEADER_NAME: OnceLock = OnceLock::new(); +/// Values of the header to guard current documents +static HEADER_VALUES: OnceLock> = OnceLock::new(); + +/// Most-recent git commit +const HEAD_COMMIT: &str = "HEAD"; + +#[allow(clippy::expect_used)] +/// Remove leading and trailing `/`s from the `path` string. +fn clean_path(path: &str) -> String { + lazy_static! { + static ref RE: Regex = Regex::new(r"(?:^/*|/*$)").expect("Failed to compile regex!?!"); + } + RE.replace_all(path, "").to_string() +} + +/// Global, read-only state +#[derive(Debug, Clone)] +pub struct AppState { + /// Fully initialized Stelae archive + pub archive: Archive, +} + +/// Git repository to serve +struct RepoState { + /// git2 repository pointing to the repo in the archive. + repo: Repo, + ///Latest or historical + serve: String, +} + +/// Shared, read-only app state +pub struct SharedState { + /// Repository to fall back to if the current one is not found + fallback: Option, +} + +impl fmt::Debug for RepoState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Repo for {} in the archive at {}", + self.repo.name, + self.repo.path.display() + ) + } +} + +impl fmt::Debug for SharedState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let fb = &self.fallback; + match *fb { + Some(ref fallback) => write!( + f, + "Repo for {} in the archive at {}", + fallback.repo.name, + fallback.repo.path.display() + ), + None => write!(f, "No fallback repo"), + } + } +} + +#[allow(clippy::missing_trait_methods)] +impl Clone for RepoState { + fn clone(&self) -> Self { + Self { + repo: self.repo.clone(), + serve: self.serve.clone(), + } + } +} + +#[allow(clippy::missing_trait_methods)] +impl Clone for SharedState { + fn clone(&self) -> Self { + Self { + fallback: self.fallback.clone(), + } + } +} + +/// Serve current document +#[allow(clippy::future_not_send)] +async fn serve( + req: HttpRequest, + shared: web::Data, + data: web::Data, +) -> impl Responder { + let prefix = req + .match_info() + .get("prefix") + .unwrap_or_default() + .to_owned(); + let tail = req.match_info().get("tail").unwrap_or_default().to_owned(); + let mut path = format!("{prefix}/{tail}"); + path = clean_path(&path); + let contenttype = get_contenttype(&path); + let blob = find_current_blob(&data.repo, &shared, &path); + match blob { + Ok(content) => HttpResponse::Ok().insert_header(contenttype).body(content), + Err(error) => { + tracing::debug!("{path}: {error}",); + HttpResponse::BadRequest().into() + } + } +} + +/// Find the latest blob for the given path from the given repo +/// Latest blob is found by looking at the HEAD commit +#[allow(clippy::panic_in_result_fn, clippy::unreachable)] +#[tracing::instrument(name = "Finding document", skip(repo, shared))] +fn find_current_blob(repo: &Repo, shared: &SharedState, path: &str) -> anyhow::Result> { + let blob = repo.get_bytes_at_path(HEAD_COMMIT, path); + match blob { + Ok(content) => Ok(content), + Err(error) => { + if let Some(ref fallback) = shared.fallback { + let fallback_blob = fallback.repo.get_bytes_at_path(HEAD_COMMIT, path); + return fallback_blob.map_or_else( + |err| anyhow::bail!("No fallback blob found - {}", err.to_string()), + Ok, + ); + } + anyhow::bail!("No fallback repo - {}", error.to_string()) + } + } +} + +/// Serve documents in a Stelae archive. +#[actix_web::main] +pub async fn serve_archive( + raw_archive_path: &str, + archive_path: PathBuf, + port: u16, + individual: bool, +) -> std::io::Result<()> { + let bind = "127.0.0.1"; + let message = "Running Publish Server on a Stelae archive at"; + tracing::info!("{message} '{raw_archive_path}' on http://{bind}:{port}.",); + + let archive = Archive::parse(archive_path, &PathBuf::from(raw_archive_path), individual) + .unwrap_or_else(|err| { + tracing::error!("Unable to parse archive at '{raw_archive_path}'."); + tracing::error!("Error: {:?}", err); + std::process::exit(1); + }); + let state = AppState { archive }; + + HttpServer::new(move || { + init_app(&state).unwrap_or_else(|err| { + tracing::error!("Unable to initialize app."); + tracing::error!("Error: {:?}", err); + std::process::exit(1); + }) + }) + .bind((bind, port))? + .run() + .await +} + +/// Initialize the application and all possible routing at start-up time. +/// +/// # Arguments +/// * `state` - The application state +/// # Errors +/// Will error if unable to initialize the application +pub fn init_app( + state: &AppState, +) -> anyhow::Result< + App< + impl ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Config = (), + InitError = (), + Error = Error, + >, + >, +> { + let config = state.archive.get_config()?; + let stelae_guard = config + .headers + .and_then(|headers| headers.current_documents_guard); + + stelae_guard.map_or_else( + || { + tracing::info!("Initializing app"); + let root = state.archive.get_root()?; + let shared_state = init_shared_app_state(root)?; + Ok(App::new().service( + web::scope("") + .app_data(web::Data::new(shared_state)) + .wrap(TracingLogger::::new()) + .configure(|cfg| { + register_routes(cfg, state).unwrap_or_else(|_| { + tracing::error!( + "Failed to initialize routes for root Stele: {}", + root.get_qualified_name() + ); + std::process::exit(1); + }); + }), + )) + }, + |guard| { + tracing::info!( + "Initializing guarded current documents with header: {}", + guard + ); + HEADER_NAME.get_or_init(|| guard); + HEADER_VALUES.get_or_init(|| { + state + .archive + .stelae + .keys() + .map(ToString::to_string) + .collect() + }); + + let mut app = App::new(); + if let (Some(guard_name), Some(guard_values)) = (HEADER_NAME.get(), HEADER_VALUES.get()) + { + for guard_value in guard_values { + let stele = state.archive.stelae.get(guard_value); + if let Some(guarded_stele) = stele { + let shared_state = init_shared_app_state(guarded_stele)?; + let mut stelae_scope = web::scope(""); + stelae_scope = stelae_scope.guard(guard::Header(guard_name, guard_value)); + app = app.service( + stelae_scope + .app_data(web::Data::new(shared_state)) + .wrap(TracingLogger::::new()) + .configure(|cfg| { + register_root_routes(cfg, guarded_stele).unwrap_or_else(|_| { + tracing::error!( + "Failed to initialize routes for Stele: {}", + guarded_stele.get_qualified_name() + ); + std::process::exit(1); + }); + }), + ); + } + } + } + Ok(app) + }, + ) +} + +/// Initialize the data repository used in the Actix route +/// Each Actix route has its own data repository +/// +/// # Errors +/// Will error if unable to initialize the data repository +fn init_repo_state(repo: &Repository, stele: &Stele) -> anyhow::Result { + let name = &repo.name; + let custom = &repo.custom; + let mut repo_path = stele.archive_path.to_string_lossy().into_owned(); + repo_path = format!("{repo_path}/{name}"); + Ok(RepoState { + repo: Repo { + archive_path: stele.archive_path.to_string_lossy().to_string(), + path: PathBuf::from(&repo_path), + org: stele.auth_repo.org.clone(), + name: name.clone(), + repo: GitRepository::open(&repo_path)?, + }, + serve: custom.serve.clone(), + }) +} + +/// Registers all routes for the given Archive +/// Each current document routes consists of two dynamic segments: `{prefix}/{tail}`. +/// prefix: the first part of the request uri, used to determine which dependent Stele to serve. +/// tail: the remaining glob pattern path of the request uri. +/// # Arguments +/// * `cfg` - The Actix `ServiceConfig` +/// * `state` - The application state +/// # Errors +/// Will error if unable to register routes (e.g. if git repository cannot be opened) +fn register_routes(cfg: &mut web::ServiceConfig, state: &AppState) -> anyhow::Result<()> { + for stele in state.archive.stelae.values() { + if let Some(repositories) = stele.repositories.as_ref() { + if stele.is_root() { + continue; + } + register_dependent_routes(cfg, stele, repositories)?; + } + } + let root = state.archive.get_root()?; + register_root_routes(cfg, root)?; + Ok(()) +} + +/// Initialize the shared application state +/// Currently shared application state consists of: +/// - fallback: used as a data repository to resolve data when no other url matches the request +/// # Returns +/// Returns a `SharedState` object +/// # Errors +/// Will error if unable to open the git repo for the fallback data repository +pub fn init_shared_app_state(stele: &Stele) -> anyhow::Result { + let fallback = stele + .get_fallback_repo() + .map(|repo| { + let (org, name) = get_name_parts(&repo.name)?; + Ok::(RepoState { + repo: Repo::new(&stele.archive_path, &org, &name)?, + serve: repo.custom.serve.clone(), + }) + }) + .transpose()?; + Ok(SharedState { fallback }) +} + +/// Register routes for the root Stele +/// Root Stele is the Stele specified in config.toml +/// # Arguments +/// * `cfg` - The Actix `ServiceConfig` +/// * `stele` - The root Stele +/// # Errors +/// Will error if unable to register routes (e.g. if git repository cannot be opened) +fn register_root_routes(cfg: &mut web::ServiceConfig, stele: &Stele) -> anyhow::Result<()> { + let mut root_scope: Scope = web::scope(""); + if let Some(repositories) = stele.repositories.as_ref() { + let sorted_repositories = repositories.get_sorted_repositories(); + for repository in sorted_repositories { + let custom = &repository.custom; + let repo_state = init_repo_state(repository, stele)?; + for route in custom.routes.iter().flat_map(|r| r.iter()) { + let actix_route = format!("/{{tail:{}}}", &route); + root_scope = root_scope.service( + web::resource(actix_route.as_str()) + .route(web::get().to(serve)) + .app_data(web::Data::new(repo_state.clone())), + ); + } + if let Some(underscore_scope) = custom.scope.as_ref() { + let actix_underscore_scope = web::scope(underscore_scope.as_str()).service( + web::scope("").service( + web::resource("/{tail:.*}") + .route(web::get().to(serve)) + .app_data(web::Data::new(repo_state.clone())), + ), + ); + cfg.service(actix_underscore_scope); + } + } + cfg.service(root_scope); + } + Ok(()) +} + +/// Register routes for dependent Stele +/// Dependent Stele are all Steles' specified in the root Stele's `dependencies.json` config file. +/// # Arguments +/// * `cfg` - The Actix `ServiceConfig` +/// * `stele` - The root Stele +/// * `repositories` - Data repositories of the dependent Stele +/// # Errors +/// Will error if unable to register routes (e.g. if git repository cannot be opened) +fn register_dependent_routes( + cfg: &mut web::ServiceConfig, + stele: &Stele, + repositories: &Repositories, +) -> anyhow::Result<()> { + let sorted_repositories = repositories.get_sorted_repositories(); + for scope in repositories.scopes.iter().flat_map(|s| s.iter()) { + let scope_str = format!("/{{prefix:{}}}", &scope.as_str()); + let mut actix_scope = web::scope(scope_str.as_str()); + for repository in &sorted_repositories { + let custom = &repository.custom; + let repo_state = init_repo_state(repository, stele)?; + for route in custom.routes.iter().flat_map(|r| r.iter()) { + if route.starts_with('_') { + // Ignore routes in dependent Stele that start with underscore + // These routes are handled by the root Stele. + continue; + } + let actix_route = format!("/{{tail:{}}}", &route); + actix_scope = actix_scope.service( + web::resource(actix_route.as_str()) + .route(web::get().to(serve)) + .app_data(web::Data::new(repo_state.clone())), + ); + } + } + cfg.service(actix_scope); + } + Ok(()) +} diff --git a/src/stelae/archive.rs b/src/stelae/archive.rs new file mode 100644 index 0000000..8756ff4 --- /dev/null +++ b/src/stelae/archive.rs @@ -0,0 +1,192 @@ +//! The archive module contains the Archive object for interacting with +//! Stelae Archives, as well as several factory methods. + +use crate::stelae::stele; +use crate::stelae::stele::Stele; +use crate::utils::archive::{find_archive_path, get_name_parts}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::{create_dir_all, read_to_string, write}; +use std::path::{Path, PathBuf}; + +/// The Archive struct is used for interacting with a Stelae Archive. +#[derive(Debug, Clone)] +pub struct Archive { + /// Path to the Archive + pub path: PathBuf, + /// map of auth repo name to Stele object + pub stelae: HashMap, +} + +impl Archive { + /// Get an archive's config object. + /// # Errors + /// Will error if unable to find or parse config file at `.stelae/config.toml` + pub fn get_config(&self) -> anyhow::Result { + let config_path = &self.path.join(PathBuf::from(".stelae/config.toml")); + let config_str = read_to_string(config_path)?; + let conf: Config = toml::from_str(&config_str)?; + Ok(conf) + } + + /// Get the Archive's root Stele. + /// # Errors + /// Will raise error if unable to find the current root Stele + pub fn get_root(&self) -> anyhow::Result<&Stele> { + let root = self + .stelae + .values() + .find(|s| s.is_root()) + .ok_or_else(|| anyhow::anyhow!("No root Stele found in archive"))?; + Ok(root) + } + + /// Set the Archive's root Stele. + /// # Errors + /// Will raise error if unable to determine the current + /// root Stele. + pub fn set_root(&mut self, path: Option) -> anyhow::Result<()> { + let root: Stele; + if let Some(individual_path) = path { + tracing::info!("Serving individual Stele at path: {:?}", individual_path); + root = Stele::new(&self.path, None, None, Some(individual_path), true)?; + } else { + let conf = self.get_config()?; + + let org = conf.root.org; + let name = conf.root.name; + + tracing::info!("Serving {}/{} at path: {:?}", &org, &name, self.path); + + root = Stele::new( + &self.path, + Some(name), + Some(org.clone()), + Some(self.path.clone().join(org)), + true, + )?; + } + self.stelae.insert(root.get_qualified_name(), root); + Ok(()) + } + + /// Parse an Archive. + /// # Errors + /// Will raise error if unable to determine the current root stele or if unable to traverse the child steles. + pub fn parse( + archive_path: PathBuf, + actual_path: &Path, + individual: bool, + ) -> anyhow::Result { + let mut archive = Self { + path: archive_path, + stelae: HashMap::new(), + }; + + let path = if individual { + actual_path.canonicalize().ok() + } else { + None + }; + archive.set_root(path)?; + + archive.traverse_children(&archive.get_root()?.clone())?; + Ok(archive) + } + + /// Traverse the child Steles of the current Stele. + /// # Errors + /// Will raise error if unable to traverse the child steles. + /// # Panics + /// If unable to unwrap the parent directory of the current path. + pub fn traverse_children(&mut self, current: &Stele) -> anyhow::Result<()> { + if let Some(dependencies) = current.get_dependencies()? { + for (qualified_name, _) in dependencies.dependencies { + let parent_dir = self.path.clone(); + let (org, name) = get_name_parts(&qualified_name)?; + if std::fs::metadata(parent_dir.join(&org).join(&name)).is_err() { + // Stele does not exist on the filesystem, continue to traverse other Steles + continue; + } + let child = Stele::new( + &self.path, + Some(name), + Some(org.clone()), + Some(parent_dir.join(org)), + false, + )?; + self.stelae + .entry(format!( + "{org}/{name}", + org = child.auth_repo.org, + name = child.auth_repo.name + )) + .or_insert_with(|| child.clone()); + self.traverse_children(&child)?; + } + } + Ok(()) + } +} + +/// Check if the `path` is inside an existing archive +/// # Errors +/// Return an error if the path is inside an existing archive. +fn raise_error_if_in_existing_archive(path: &Path) -> anyhow::Result { + let existing_archive_path = find_archive_path(path); + match existing_archive_path { + Ok(_) => anyhow::bail!("You cannot create a new archive inside of an existing archive."), + Err(_) => Ok(false), + } +} + +/// Config object for an Archive +#[derive(Deserialize, Serialize)] +pub struct Config { + /// The root Stele for this archive + pub root: stele::Config, + /// Whether this is a shallow archive (all repos depth=1) + pub shallow: bool, + /// Custom HTTP headers used to interact with the Stele + pub headers: Option, +} + +/// Optional Header configuration for an Archive +#[derive(Default, Deserialize, Serialize)] +pub struct Headers { + /// Specify a custom header guard to use when requesting a Stele's current documents. + pub current_documents_guard: Option, +} + +/// Create a new Stelae Archive at path, and return the new archive. +/// # Errors +/// Will error if archive is created inside of an existing archive. +pub fn init( + path: PathBuf, + root_name: String, + root_org: String, + root_hash: Option, + shallow: bool, + headers: Option, +) -> anyhow::Result> { + raise_error_if_in_existing_archive(&path)?; + let stelae_dir = path.join(PathBuf::from("./.stelae")); + create_dir_all(&stelae_dir)?; + let config_path = stelae_dir.join(PathBuf::from("./config.toml")); + let conf = Config { + root: stele::Config { + name: root_name, + org: root_org, + hash: root_hash, + }, + shallow, + headers, + }; + let conf_str = toml_edit::ser::to_string_pretty(&conf)?; + write(config_path, conf_str)?; + let archive = Archive { + path, + stelae: HashMap::new(), + }; + Ok(Box::new(archive)) +} diff --git a/src/stelae/mod.rs b/src/stelae/mod.rs new file mode 100644 index 0000000..c1f1b0b --- /dev/null +++ b/src/stelae/mod.rs @@ -0,0 +1,6 @@ +//! The Stelae module contains tools for interacting with Archives, +//! Stele, and Repositories. + +pub mod archive; +pub mod stele; +pub mod types; diff --git a/src/stelae/stele.rs b/src/stelae/stele.rs new file mode 100644 index 0000000..fb68ee2 --- /dev/null +++ b/src/stelae/stele.rs @@ -0,0 +1,143 @@ +//! The Stele module contains the Stele object for interacting with +//! Stelae. + +use std::path::{Path, PathBuf}; + +use super::types::repositories::Repository; +use crate::{ + stelae::types::{dependencies::Dependencies, repositories::Repositories}, + utils::git::Repo, +}; +use anyhow::Context; +use git2::Repository as GitRepository; +use serde_derive::{Deserialize, Serialize}; +use serde_json; + +/// Stele +#[derive(Debug, Clone)] +pub struct Stele { + /// Path to the containing Stelae archive. + pub archive_path: PathBuf, + /// Stele's repositories (as specified in repositories.json). + pub repositories: Option, + /// Indicates whether or not the Stele is the root Stele. + /// TODO: this does not seem correct + pub root: bool, + /// Stele's authentication repo. + pub auth_repo: Repo, +} + +impl Stele { + /// Create a new Stele object + /// # Errors + /// Will error if unable to find or parse repositories file at `targets/repositories.json` + /// # Panics + /// Will panic if unable to determine the current root Stele. + #[allow(clippy::shadow_reuse)] + pub fn new( + archive_path: &Path, + name: Option, + org: Option, + path: Option, + root: bool, + ) -> anyhow::Result { + let name = name.unwrap_or_else(|| "law".into()); + let org = if let Some(org) = org { + org + } else { + path.as_ref() + .context("path is None")? + .file_name() + .context("file_name is None")? + .to_str() + .context("to_str failed")? + .into() + }; + let path = path.unwrap_or_else(|| archive_path.join(&org)); + let mut stele = Self { + archive_path: archive_path.to_path_buf(), + repositories: None, + root, + auth_repo: Repo { + archive_path: archive_path.to_string_lossy().to_string(), + path: path.join(&name), + org, + name: name.clone(), + repo: GitRepository::open(path.join(&name))?, + }, + }; + stele.get_repositories()?; + Ok(stele) + } + + /// Get Stele's dependencies. + /// # Errors + /// Will error if unable to parse dependencies file from `targets/dependencies.json` + pub fn get_dependencies(&self) -> anyhow::Result> { + let blob = self + .auth_repo + .get_bytes_at_path("HEAD", "targets/dependencies.json"); + if let Ok(dependencies_blob) = blob { + let dependencies_str = String::from_utf8(dependencies_blob)?; + let dependencies = serde_json::from_str(&dependencies_str)?; + return Ok(Some(dependencies)); + } + Ok(None) + } + /// Get Stele's repositories. + /// # Errors + /// Will error if unable to find or parse repositories file at `targets/repositories.json` + pub fn get_repositories(&mut self) -> anyhow::Result> { + let blob = self + .auth_repo + .get_bytes_at_path("HEAD", "targets/repositories.json"); + if let Ok(repositories_blob) = blob { + let repositories_str = String::from_utf8(repositories_blob)?; + let repositories: Repositories = serde_json::from_str(&repositories_str)?; + self.repositories = Some(repositories.clone()); + return Ok(Some(repositories)); + } + Ok(None) + } + + /// Get Stele's qualified name. + #[must_use] + pub fn get_qualified_name(&self) -> String { + format!( + "{org}/{name}", + org = self.auth_repo.org, + name = self.auth_repo.name + ) + } + + /// Get Stele's fallback repo. + /// A fallback repository is a data repository which contains `is_fallback` = true in its custom field. + /// # Returns + /// Returns the first fallback repository found, or None if no fallback repository is found. + #[must_use] + pub fn get_fallback_repo(&self) -> Option<&Repository> { + self.repositories.as_ref().and_then(|repositories| { + repositories + .repositories + .values() + .find(|repository| repository.custom.is_fallback.unwrap_or(false)) + }) + } + + /// See if Stele is a root Stele. + #[must_use] + pub const fn is_root(&self) -> bool { + self.root + } +} + +///Config object for a Stele +#[derive(Deserialize, Serialize)] +pub struct Config { + /// Name of the authentication repo (e.g. law). + pub name: String, + /// Name of the Stele's directory, also known as Stele's organization (e.g. openlawlibrary). + pub org: String, + /// The out-of-band authenticated hash of the Stele. + pub hash: Option, +} diff --git a/src/stelae/types/dependencies.rs b/src/stelae/types/dependencies.rs new file mode 100644 index 0000000..fbdae9b --- /dev/null +++ b/src/stelae/types/dependencies.rs @@ -0,0 +1,20 @@ +//! A Stele's dependencies. +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// A map of Stele names to their dependencies. +#[derive(Serialize, Deserialize, Debug)] +pub struct Dependencies { + /// An inner map of Stele keys to their dependencies. + pub dependencies: HashMap, +} + +/// A single dependency as specified in a Stele's `dependencies.json` file. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Dependency { + /// The out-of-band authenticated hash of the Stele. + #[serde(rename = "out-of-band-authentication")] + pub out_of_band_authentication: String, + /// The default branch for a Stele. + pub branch: String, +} diff --git a/src/stelae/types/mod.rs b/src/stelae/types/mod.rs new file mode 100644 index 0000000..c52d4d8 --- /dev/null +++ b/src/stelae/types/mod.rs @@ -0,0 +1,4 @@ +//! The Types module contains data models for Stelae. + +pub mod dependencies; +pub mod repositories; diff --git a/src/stelae/types/repositories.rs b/src/stelae/types/repositories.rs new file mode 100644 index 0000000..c2aafd8 --- /dev/null +++ b/src/stelae/types/repositories.rs @@ -0,0 +1,201 @@ +//! A Stele's data repositories. +use std::{collections::HashMap, fmt}; + +use serde::{ + de::{self, MapAccess, Visitor}, + Deserialize, Deserializer, +}; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; + +/// Repositories object +/// +/// Represents data repositories in a Stele. +/// Repositories object is serialized from `repositories.json`. +/// +/// `repositories.json` is expected to exist in /targets/repositories.json in the authentication repository. +/// # Examples +/// +/// ```rust +/// use serde_json::json; +/// use stelae::stelae::types::repositories::Repositories; +/// +/// let data = r#" +/// { +/// "scopes": ["some/scope/path"], +/// "repositories": { +/// "test_org_1/data_repo_1": { +/// "custom": { +/// "serve": "latest", +/// "routes": ["example-route-glob-pattern-1"] +/// } +/// }, +/// "test_org_1/data_repo_2": { +/// "custom": { +/// "serve": "latest", +/// "serve-prefix": "_prefix", +/// "is_fallback": true +/// } +/// } +/// } +/// } +/// "#; +/// let repositories: Repositories = serde_json::from_str(data).unwrap(); +/// assert_eq!(repositories.scopes.unwrap(), vec!["some/scope/path"]); +/// assert!(repositories.repositories.contains_key("test_org_1/data_repo_1")); +/// assert!(repositories.repositories.contains_key("test_org_1/data_repo_2")); +/// ``` +#[derive(Debug, Clone, Serialize, Default)] +pub struct Repositories { + /// Scopes of the repositories + pub scopes: Option>, + /// Map of repositories. The key is the name of the repository. + pub repositories: HashMap, +} + +/// Repository object +/// +/// Represents one concrete data repository in a stele. +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct Repository { + /// Fully qualified name in `/` format. + /// This is the key of the `repositories` entries. + pub name: String, + /// Custom object + pub custom: Custom, +} + +/// Custom object +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct Custom { + #[serde(rename = "type")] + /// Type of data repository. e.g. `rdf`, `html`, `pdf`, `xml`, or any other. + pub repository_type: Option, + /// "latest" or "historical". Currently not used by the framework. + pub serve: String, + /// Vector of glob patterns used by the Actix framework to resolve url routing. + /// Routing to use when locating current blobs from the data repository. + /// Example: + /// + /// Given a `["_underscore/.*"] glob pattern, the following urls are expected to be routed to the current data repository: + /// + /// - `/_underscore/` + /// - `/_underscore/any/path` + /// - `/_underscore/any/path/with/any/number/of/segments` + pub routes: Option>, + #[serde(rename = "serve-prefix")] + /// Prefix to use when serving the data repository. + /// If `None`, the data repository will be served at the root. + /// If `Some("prefix")`, the data repository will be served from `/prefix/`. + pub scope: Option, + /// Whether the data repository is a fallback. + /// + /// When a data repository is a fallback, it is used to serve current blobs when no other data repository matches the request. + pub is_fallback: Option, +} + +impl Repositories { + /// Get the repositories sorted by the length of their routes, longest first. + /// + /// This is needed for serving current documents because Actix routes are matched in the order they are added. + #[must_use] + pub fn get_sorted_repositories(&self) -> Vec<&Repository> { + let mut result = Vec::new(); + for repository in self.repositories.values() { + result.push(repository); + } + result.sort_by(|repo1, repo2| { + let routes1 = repo1.custom.routes.as_ref().map_or(0, |r| { + r.iter().map(std::string::String::len).max().unwrap_or(0) + }); + let routes2 = repo2.custom.routes.as_ref().map_or(0, |r| { + r.iter().map(std::string::String::len).max().unwrap_or(0) + }); + routes2.cmp(&routes1) + }); + result + } +} + +#[allow(clippy::missing_trait_methods)] +impl<'de> Deserialize<'de> for Repositories { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + /// Visitor for the Repositories struct + struct RepositoriesVisitor; + + impl<'de> Visitor<'de> for RepositoriesVisitor { + type Value = Repositories; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Repositories") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + Self::deserialize_repositories(&mut map) + } + } + + impl RepositoriesVisitor { + /// Deserialize the repositories map from the `repositories.json` file. + fn deserialize_repositories<'de, V>(map: &mut V) -> Result + where + V: MapAccess<'de>, + { + let mut scopes = None; + let mut repositories = HashMap::new(); + while let Some(key) = map.next_key()? { + match key { + "scopes" => { + scopes = map.next_value()?; + } + "repositories" => { + repositories = Self::deserialize_repositories_values(map)?; + } + _ => { + return Err(de::Error::unknown_field(key, &["scopes", "repositories"])) + } + } + } + Ok(Repositories { + scopes, + repositories, + }) + } + + /// Deserialize individual repositories from the `repositories.json` file. + fn deserialize_repositories_values<'de, V>( + map: &mut V, + ) -> Result, V::Error> + where + V: MapAccess<'de>, + { + let repositories_json: HashMap = map.next_value()?; + let mut repositories = HashMap::new(); + for (map_key, value) in repositories_json { + let custom_value = value + .get("custom") + .ok_or_else(|| serde::de::Error::custom("Missing 'custom' field"))?; + let custom: Custom = + serde_json::from_value(custom_value.clone()).map_err(|e| { + serde::de::Error::custom(format!("Failed to deserialize 'custom': {e}")) + })?; + let repo = Repository { + name: map_key.clone(), + custom, + }; + repositories.insert(map_key, repo); + } + Ok(repositories) + } + } + /// Expected fields in the `repositories.json` file. + const FIELDS: &[&str] = &["scopes", "repositories"]; + deserializer.deserialize_struct("Repositories", FIELDS, RepositoriesVisitor) + } +} diff --git a/src/utils/archive.rs b/src/utils/archive.rs new file mode 100644 index 0000000..1e82f42 --- /dev/null +++ b/src/utils/archive.rs @@ -0,0 +1,56 @@ +//! The archive module contains structs for interacting with a Stele archive + +use super::paths::fix_unc_path; +use anyhow::Context; +use std::path::{Path, PathBuf}; + +/// given a &Path `path`, return the path to the containing archive. +/// +/// # Errors +/// Error if the path doesn't exist or isn't inside a Stele archive. +pub fn find_archive_path(path: &Path) -> anyhow::Result { + let abs_path = fix_unc_path(&path.canonicalize()?); + for working_path in abs_path.ancestors() { + if working_path.join(".stelae").exists() { + return Ok(working_path.to_owned()); + } + } + anyhow::bail!(format!( + "{} is not inside a Stelae Archive. Run `stelae init` to create a archive at this location.", + abs_path.to_string_lossy() + )) +} + +/// Get the qualified name as parts of a Stele from the {org}/{name} format. +/// # Errors +/// Will error if the qualified name is not in the {org}/{name} format. +pub fn get_name_parts(qualified_name: &str) -> anyhow::Result<(String, String)> { + let mut name_parts = qualified_name.split('/'); + let org = name_parts.next().context("No organization specified"); + let name = name_parts.next().context("No name specified"); + Ok((org?.into(), name?.into())) +} + +#[cfg(test)] +mod test { + use crate::utils::archive::get_name_parts; + + #[test] + fn get_name_parts_when_qualified_name_correct_expect_name_parts() { + let cut = get_name_parts; + let actual = cut("stele/test").unwrap(); + let expected = ("stele".to_owned(), "test".to_owned()); + assert_eq!(expected, actual); + } + + #[test] + fn get_name_parts_when_qualified_name_incorrect_expect_error() { + let cut = get_name_parts; + let actual = cut("test").unwrap_err(); + let expected = "No name specified"; + assert!( + actual.to_string().contains(expected), + "\"{actual}\" doesn't contain {expected}" + ); + } +} diff --git a/src/utils/cli.rs b/src/utils/cli.rs index c443187..fb3ae3d 100644 --- a/src/utils/cli.rs +++ b/src/utils/cli.rs @@ -4,20 +4,21 @@ #![allow(clippy::exit)] use crate::server::git::serve_git; -use crate::utils::library::find_library_path; +use crate::server::publish::serve_archive; +use crate::utils::archive::find_archive_path; use clap::Parser; use std::path::Path; use tracing; /// Stelae is currently just a simple git server. /// run from the library directory or pass -/// path to library. +/// path to archive. #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { - /// Path to the Stelae library. Defaults to cwd. + /// Path to the Stelae archive. Defaults to cwd. #[arg(short, long, default_value_t = String::from(".").to_owned())] - library_path: String, + archive_path: String, /// Stelae cli subcommands #[command(subcommand)] subcommands: Subcommands, @@ -26,12 +27,21 @@ struct Cli { /// #[derive(Clone, clap::Subcommand)] enum Subcommands { - /// Serve git repositories in the Stelae library + /// Serve git repositories in the Stelae archive Git { - /// Port on which to serve the library. + /// 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. + #[arg(short, long, default_value_t = 8080)] + port: u16, + #[arg(short, long, default_value_t = false)] + /// Serve an individual stele instead of the Stele specified in config.toml. + individual: bool, + }, } /// @@ -50,16 +60,19 @@ pub fn run() -> std::io::Result<()> { init_tracing(); tracing::debug!("Starting application"); let cli = Cli::parse(); - let library_path_wd = Path::new(&cli.library_path); - let Ok(library_path) = find_library_path(library_path_wd) else { + let archive_path_wd = Path::new(&cli.archive_path); + let Ok(archive_path) = find_archive_path(archive_path_wd) else { tracing::error!( "error: could not find `.stelae` folder in `{}` or any parent directory", - &cli.library_path + &cli.archive_path ); std::process::exit(1); }; match cli.subcommands { - Subcommands::Git { port } => serve_git(&cli.library_path, library_path, port), + Subcommands::Git { port } => serve_git(&cli.archive_path, archive_path, port), + Subcommands::Serve { port, individual } => { + serve_archive(&cli.archive_path, archive_path, port, individual) + } } } diff --git a/src/utils/git.rs b/src/utils/git.rs index 75b90c4..16d996c 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -1,52 +1,71 @@ //! The git module contains structs for interacting with git repositories -//! in the Stelae Library. +//! in the Stelae Archive. use anyhow::Context; use git2::Repository; -use std::{fmt, path::Path}; +use std::{ + fmt, + path::{Path, PathBuf}, +}; /// This is the first step towards having custom errors pub const GIT_REQUEST_NOT_FOUND: &str = "Git object doesn't exist"; -/// Represents a git repository within an oll library. includes helpers for +/// Represents a git repository within an oll archive. includes helpers for /// for interacting with the Git Repo. -/// Expects a path to the library, as well as the repo's namespace and name. +/// Expects a path to the archive, as well as the repo's organization and name. pub struct Repo { - /// Path to the library - lib_path: String, - /// Repo namespace - namespace: String, + /// Path to the archive + pub archive_path: String, + /// Path to the Stele + pub path: PathBuf, + /// Repo organization + pub org: String, /// Repo name - name: String, - /// git2 repository pointing to the repo in the library. - repo: Repository, + pub name: String, + /// git2 repository pointing to the repo in the archive. + pub repo: Repository, } impl fmt::Debug for Repo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "Repo for {}/{} in the library at {}", - self.namespace, self.name, self.lib_path + "Repo for {}/{} in the archive at {}", + self.org, self.name, self.archive_path ) } } +#[allow(clippy::missing_trait_methods, clippy::unwrap_used)] +impl Clone for Repo { + fn clone(&self) -> Self { + Self { + archive_path: self.archive_path.clone(), + org: self.org.clone(), + name: self.name.clone(), + path: self.path.clone(), + repo: Repository::open(self.path.clone()).unwrap(), + } + } +} + impl Repo { /// Create a new Repo object with helpers for interacting with a Git Repo. - /// Expects a path to the library, as well as the repo's namespace and name. + /// Expects a path to the archive, as well as the repo's org and name. /// /// # Errors /// - /// Will return `Err` if git repository does not exist at `{namespace}/{name}` - /// in library, or if there is something wrong with the git repository. - pub fn new(lib_path: &Path, namespace: &str, name: &str) -> anyhow::Result { - let lib_path_str = lib_path.to_string_lossy(); - tracing::debug!(namespace, name, "Creating new Repo at {lib_path_str}"); - let repo_path = format!("{lib_path_str}/{namespace}/{name}"); + /// Will return `Err` if git repository does not exist at `{org}/{name}` + /// in archive, or if there is something wrong with the git repository. + pub fn new(archive_path: &Path, org: &str, name: &str) -> anyhow::Result { + let archive_path_str = archive_path.to_string_lossy(); + tracing::debug!(org, name, "Creating new Repo at {archive_path_str}"); + let repo_path = format!("{archive_path_str}/{org}/{name}"); Ok(Self { - lib_path: lib_path_str.into(), - namespace: namespace.into(), + archive_path: archive_path_str.into(), + org: org.into(), name: name.into(), + path: PathBuf::from(repo_path.clone()), repo: Repository::open(repo_path)?, }) } diff --git a/src/utils/http.rs b/src/utils/http.rs index e87b404..7e5e7fa 100644 --- a/src/utils/http.rs +++ b/src/utils/http.rs @@ -8,12 +8,17 @@ use std::path::Path; /// for the content at `path`. If there is no extension, we assume it is /// html. If the extension cannot be converted to a str, then we return /// HTML. +/// Some browsers will not render `application/rdf+xml`, but instead will +/// download it. So we instead return `text/plain` for `.rdf` files. #[must_use] pub fn get_contenttype(path: &str) -> ContentType { let extension = Path::new(&path) .extension() .map_or("html", |ext| ext.to_str().map_or("", |ext_str| ext_str)); let mime = file_extension_to_mime(extension).first_or(mime::TEXT_HTML); + if (mime.type_(), mime.subtype().as_str()) == (mime::APPLICATION, "rdf") { + return ContentType(mime::TEXT_PLAIN); + } ContentType(mime) } @@ -53,6 +58,14 @@ mod test { assert_eq!(expected, actual); } + #[test] + fn test_get_contenttype_when_rdf_ext_expect_rdf() { + let cut = get_contenttype; + let actual = cut("a/b.rdf").to_string(); + let expected = String::from("text/plain"); + assert_eq!(expected, actual); + } + #[test] fn test_get_contenttype_when_incorrect_ext_expect_html() { let cut = get_contenttype; diff --git a/src/utils/library.rs b/src/utils/library.rs deleted file mode 100644 index 8579bbd..0000000 --- a/src/utils/library.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! The library module contains structs for interacting with a Stelae library - -use std::path::{Path, PathBuf}; - -/// given a &Path `path`, return the path to the containing library. -/// -/// # Errors -/// Error if the path doesn't exist or isn't inside a Stelae library. -pub fn find_library_path(path: &Path) -> anyhow::Result { - let abs_path = path.canonicalize()?; - for working_path in abs_path.ancestors() { - if working_path.join(".stelae").exists() { - return Ok(working_path.to_owned()); - } - } - anyhow::bail!(format!( - "{} is not inside a Stelae Library. Run `stelae init` to create a library at this location.", - abs_path.to_string_lossy() - )) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7c99583..c08d434 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ //! The utils module contains utility functions and structs. +pub mod archive; pub mod cli; pub mod git; pub mod http; -pub mod library; +pub mod paths; diff --git a/src/utils/paths.rs b/src/utils/paths.rs new file mode 100644 index 0000000..859acdf --- /dev/null +++ b/src/utils/paths.rs @@ -0,0 +1,14 @@ +//! Utility functions for working with paths +use std::path::{Path, PathBuf}; +/// On Windows removes the `\\?\\` prefix to UNC paths. +/// For other OS'es just turns the `Path` into a `PathBuf` +#[must_use] +pub fn fix_unc_path(absolute_path: &Path) -> PathBuf { + if cfg!(windows) { + let absolute_path_str = absolute_path.display().to_string(); + if absolute_path_str.starts_with(r#"\\?"#) { + return PathBuf::from(absolute_path_str.replace(r#"\\?\"#, "")); + } + } + absolute_path.to_path_buf() +} diff --git a/tests/api/basic_archive_test.rs b/tests/api/basic_archive_test.rs new file mode 100644 index 0000000..4c2fd4f --- /dev/null +++ b/tests/api/basic_archive_test.rs @@ -0,0 +1,191 @@ +use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; +use crate::common; +use actix_web::test; + +#[actix_web::test] +async fn test_resolve_law_html_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; + + for request_uri in &["/a/b/c.html", "/a/b/", "/a/b/c/", "/a/d/"] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_resolve_root_stele_law_html_request_with_full_path_no_trailing_slash_expect_success() +{ + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + for request_uri in &["/a/b/c.html", "/a/b", "/a/b/c", "/a/d"] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_resolve_law_html_request_with_empty_path_expect_success() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get().uri("/").to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_resolve_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 req = test::TestRequest::get().uri("/a/b/x").to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_law_html_request_content_expect_html_document_retrieved() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &["/a/b/c.html", "/a/b/", "/a/b/c/", "/a/d/"] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } +} + +#[actix_web::test] +async fn test_law_xml_request_content_expect_xml_document_retrieved() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/_xml/a/b/index.xml", + "/_xml/a/d/index.xml", + "/_xml/a/b/c.xml", + "/_xml/a/b/c/index.xml", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } +} + +#[actix_web::test] +async fn test_resolve_law_xml_request_without_serve_prefix_expect_client_error() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get().uri("/a/b/c.xml").to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_law_rdf_request_content_expect_rdf_document_retrieved() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/_rdf/index.rdf", + "/_rdf/a/b/c.rdf", + "/_rdf/a/b/index.rdf", + "/_rdf/a/d/index.rdf", + "/_rdf/a/b/c/index.rdf", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } +} + +#[actix_web::test] +async fn test_law_other_data_fallback_request_content_expect_document_retrieved() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get().uri("/example.json").to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "{ \"retrieved\": {\"json\": { \"key\": \"value\" } } }"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} + +#[actix_web::test] +async fn test_law_other_data_request_content_expect_other_document_retrieved() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/_prefix/a/index.html", + "/a/_doc/e/index.html", + "/a/e/_doc/f/index.html", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } +} + +#[actix_web::test] +async fn get_law_pdf_request_content_expect_success() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &["/example.pdf", "/a/example.pdf", "/a/b/example.pdf"] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn get_law_pdf_request_with_incorrect_path_expect_not_found() { + let archive_path = + common::initialize_archive(ArchiveType::Basic(Jurisdiction::Single)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get() + .uri("/does-not-exist.pdf") + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs new file mode 100644 index 0000000..5cd0054 --- /dev/null +++ b/tests/api/mod.rs @@ -0,0 +1,3 @@ +mod basic_archive_test; +mod multihost_archive_test; +mod multijurisdiction_archive_test; diff --git a/tests/api/multihost_archive_test.rs b/tests/api/multihost_archive_test.rs new file mode 100644 index 0000000..cedf335 --- /dev/null +++ b/tests/api/multihost_archive_test.rs @@ -0,0 +1,100 @@ +use crate::{archive_testtools::config::ArchiveType, common}; +use actix_web::test; + +#[actix_web::test] +async fn test_resolve_both_guarded_stele_law_html_request_with_full_path_expect_success() { + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + for guard_value in ["stele_1/law", "stele_2/law"] { + for request_uri in &["/a/b/c.html", "/a/b/", "/a/b/c/", "/a/d/"] { + let req = test::TestRequest::get() + .insert_header(("X-Current-Documents-Guard", guard_value)) + .uri(request_uri) + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } + } +} + +#[actix_web::test] +async fn test_resolve_guarded_stele_law_html_request_where_header_value_is_incorrect_expect_error() +{ + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get() + .insert_header(("X-Current-Documents-Guard", "xxx/xxx")) + .uri("/a/b/c.html") + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_resolve_guarded_stele_law_html_request_where_header_name_is_incorrect_expect_error() { + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get() + .insert_header(("X-Incorrect-Header-Name", "stele_1/law")) + .uri("/a/b/c.html") + .to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_resolve_guarded_stele_law_rdf_request_content_expect_rdf_document_retrieved() { + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for guard_value in ["stele_1/law", "stele_2/law"] { + for request_uri in &[ + "/_rdf/index.rdf", + "/_rdf/a/b/c.rdf", + "/_rdf/a/b/index.rdf", + "/_rdf/a/d/index.rdf", + "/_rdf/a/b/c/index.rdf", + ] { + let req = test::TestRequest::get() + .insert_header(("X-Current-Documents-Guard", guard_value)) + .uri(request_uri) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } + } +} + +#[actix_web::test] +async fn test_law_other_data_request_content_expect_other_document_retrieved() { + let archive_path = common::initialize_archive(ArchiveType::Multihost).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for guard_value in ["stele_1/law", "stele_2/law"] { + for request_uri in &[ + "/_prefix/a/index.html", + "/a/_doc/e/index.html", + "/a/e/_doc/f/index.html", + ] { + let req = test::TestRequest::get() + .insert_header(("X-Current-Documents-Guard", guard_value)) + .uri(request_uri) + .to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } + } +} diff --git a/tests/api/multijurisdiction_archive_test.rs b/tests/api/multijurisdiction_archive_test.rs new file mode 100644 index 0000000..1af0fbd --- /dev/null +++ b/tests/api/multijurisdiction_archive_test.rs @@ -0,0 +1,163 @@ +use crate::archive_testtools::config::{ArchiveType, Jurisdiction}; +use crate::common; +use actix_web::test; + +#[actix_web::test] +async fn test_resolve_root_stele_law_html_request_with_full_path_expect_success() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + + for request_uri in &["/a/b/c.html", "/a/b/", "/a/b/c/", "/a/d/"] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_root_stele_fallback_request_expect_success() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get().uri("/example.json").to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = "{ \"retrieved\": {\"json\": { \"key\": \"value\" } } }"; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); +} + +#[actix_web::test] +async fn test_dependent_stele_law_html_request_expect_success() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/sub/scope/1/a/b/c.html", + "/sub/scope/2/a/b/", + "/sub/scope/3/a/b/c/", + "/sub/scope/4/a/d/", + "/sub/scope/1/a/b/c.html", + "/sub/scope/2/a/b/", + "/sub/scope/3/a/b/c/", + "/sub/scope/4/a/d/", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_dependent_stele_fallback_request_when_only_root_fallback_is_supported_expect_not_found( +) { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/does-not-resolve.json", + "/a/does-not-resolve.json", + "/a/b/does-not-resolve.json", + "/does-not-resolve.html", + "/a/does-not-resolve.html", + "/a/b/does-not-resolve.html", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_dependent_stele_law_html_request_where_path_does_not_exist_not_found() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + let req = test::TestRequest::get().uri("/sub/scope/x/").to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); +} + +#[actix_web::test] +async fn test_root_stele_law_rdf_expect_rdf_document_retrieved() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/_rdf/index.rdf", + "/_rdf/a/b/c.rdf", + "/_rdf/a/b/index.rdf", + "/_rdf/a/d/index.rdf", + "/_rdf/a/b/c/index.rdf", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let actual = test::call_and_read_body(&app, req).await; + let expected = ""; + assert!( + common::blob_to_string(actual.to_vec()).starts_with(expected), + "doesn't start with {expected}" + ); + } +} + +#[actix_web::test] +async fn test_dependent_stele_law_rdf_expect_not_found() { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + // Even though the dependent RDF data repository exists, serving underscore routes for dependent stele is not supported + for request_uri in &[ + "/_rdf/sub/scope/1/index.rdf", + "/_rdf/sub/scope/2/a/b/c.rdf", + "/_rdf/sub/scope/3/a/b/index.rdf", + "/_rdf/sub/scope/4/a/d/index.rdf", + "/_rdf/sub/scope/1/a/b/c/index.rdf", + "/_rdf/sub/scope/2/index.rdf", + "/_rdf/sub/scope/3/a/b/c.rdf", + "/_rdf/sub/scope/4/a/b/index.rdf", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_dependent_stele_law_other_with_full_path_when_request_matches_glob_pattern_expect_success( +) { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + for request_uri in &[ + "/sub/scope/2/a/_doc/e/index.html", + "/sub/scope/4/a/e/_doc/f/index.html", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_success(); + let expected = true; + assert_eq!(actual, expected); + } +} + +#[actix_web::test] +async fn test_dependent_stele_law_other_with_full_path_when_underscore_routing_is_not_supported_expect_not_found( +) { + let archive_path = common::initialize_archive(ArchiveType::Basic(Jurisdiction::Multi)).unwrap(); + let app = common::initialize_app(archive_path.path()).await; + // Serving dependent stele routes that start with a `_` prefix glob pattern is only supported for root stele. + for request_uri in &[ + "/sub/scope/1/_prefix/index.html", + "/sub/scope/4/_prefix/a/index.html", + ] { + let req = test::TestRequest::get().uri(request_uri).to_request(); + let resp = test::call_service(&app, req).await; + let actual = resp.status().is_client_error(); + let expected = true; + assert_eq!(actual, expected); + } +} diff --git a/tests/archive_testtools/config.rs b/tests/archive_testtools/config.rs new file mode 100644 index 0000000..4466137 --- /dev/null +++ b/tests/archive_testtools/config.rs @@ -0,0 +1,302 @@ +use anyhow::Result; +use std::path::PathBuf; +pub use stelae::stelae::types::repositories::{Custom, Repositories, Repository}; + +pub enum ArchiveType { + Basic(Jurisdiction), + Multihost, +} + +pub enum Jurisdiction { + Single, + Multi, +} + +pub enum TestDataRepositoryType { + Html, + Rdf, + Xml, + Pdf, + Other(String), +} + +/// Information about a data repository. +/// +/// This struct is used to initialize a data repository in the test suite. +pub struct TestDataRepositoryContext { + /// The name of the data repository. + pub name: String, + /// The paths of the data repository. + pub paths: Vec, + /// The kind of data repository. + pub kind: TestDataRepositoryType, + /// The prefix to use when serving the data repository. + /// + /// If `None`, the data repository will be served at the root. + /// If `Some("prefix")`, the data repository will be served from `/prefix/`. + pub serve_prefix: Option, + /// The route glob patterns to use when serving the data repository. + pub route_glob_patterns: Option>, + /// Whether the data repository is a fallback. + pub is_fallback: bool, +} + +impl TestDataRepositoryContext { + pub fn new( + name: String, + paths: Vec, + kind: TestDataRepositoryType, + serve_prefix: Option, + route_glob_patterns: Option>, + is_fallback: bool, + ) -> Result { + if serve_prefix.is_none() { + if route_glob_patterns.is_none() { + return Err(anyhow::anyhow!( + "A test data repository must have either a serve prefix or route glob patterns." + )); + } + } + Ok(Self { + name, + paths, + kind, + serve_prefix, + route_glob_patterns, + is_fallback, + }) + } + + pub fn default_html_paths() -> Vec { + let paths = &[ + "./index.html", + "./a/index.html", + "./a/b/index.html", + "./a/d/index.html", + "./a/b/c.html", + "./a/b/c/index.html", + ]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } + + pub fn default_rdf_paths() -> Vec { + let paths = &[ + "./index.rdf", + "./a/index.rdf", + "./a/b/index.rdf", + "./a/d/index.rdf", + "./a/b/c.rdf", + "./a/b/c/index.rdf", + ]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } + + pub fn default_xml_paths() -> Vec { + let paths = &[ + "./index.xml", + "./a/index.xml", + "./a/b/index.xml", + "./a/d/index.xml", + "./a/b/c.xml", + "./a/b/c/index.xml", + ]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } + + pub fn default_pdf_paths() -> Vec { + let paths = &["./example.pdf", "./a/example.pdf", "./a/b/example.pdf"]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } + + pub fn default_json_paths() -> Vec { + let paths = &["./example.json", "./a/example.json", "./a/b/example.json"]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } + + pub fn default_other_paths() -> Vec { + let paths = &[ + "./index.html", + "./example.json", + "./a/index.html", + "./a/b/index.html", + "./a/b/c.html", + "./a/d/index.html", + "./_prefix/index.html", + "./_prefix/a/index.html", + "./a/_doc/e/index.html", + "./a/e/_doc/f/index.html", + ]; + paths.iter().map(|&x| PathBuf::from(x)).collect() + } +} + +pub fn get_basic_test_data_repositories() -> Result> { + Ok(vec![ + TestDataRepositoryContext::new( + "law-html".into(), + TestDataRepositoryContext::default_html_paths(), + TestDataRepositoryType::Html, + None, + Some(vec![".*".into()]), + false, + )?, + TestDataRepositoryContext::new( + "law-rdf".into(), + TestDataRepositoryContext::default_rdf_paths(), + TestDataRepositoryType::Rdf, + Some("_rdf".into()), + None, + false, + )?, + TestDataRepositoryContext::new( + "law-xml".into(), + TestDataRepositoryContext::default_xml_paths(), + TestDataRepositoryType::Xml, + Some("_xml".into()), + None, + false, + )?, + TestDataRepositoryContext::new( + "law-xml-codified".into(), + vec![ + "./index.xml".into(), + "./e/index.xml".into(), + "./e/f/index.xml".into(), + "./e/g/index.xml".into(), + ], + TestDataRepositoryType::Xml, + Some("_xml_codified".into()), + None, + false, + )?, + TestDataRepositoryContext::new( + "law-pdf".into(), + TestDataRepositoryContext::default_pdf_paths(), + TestDataRepositoryType::Pdf, + None, + Some(vec![".*\\.pdf".into()]), + false, + )?, + TestDataRepositoryContext::new( + "law-other".into(), + TestDataRepositoryContext::default_other_paths(), + TestDataRepositoryType::Other("example.json".to_string()), + None, + Some(vec![".*_doc/.*".into(), "_prefix/.*".into()]), + true, + )?, + ]) +} + +pub fn get_dependent_data_repositories_with_scopes( + scopes: &Vec, +) -> Result> { + let mut result = Vec::new(); + for kind in [ + TestDataRepositoryType::Html, + TestDataRepositoryType::Rdf, + TestDataRepositoryType::Xml, + TestDataRepositoryType::Pdf, + TestDataRepositoryType::Other("example.json".to_string()), + ] + .into_iter() + { + let mut paths = Vec::new(); + let name; + let mut serve_prefix = None; + let mut route_glob_patterns = None; + let mut is_fallback = false; + let mut default_paths; + + match kind { + TestDataRepositoryType::Html => { + name = "law-html".into(); + route_glob_patterns = Some(vec![".*".into()]); + default_paths = TestDataRepositoryContext::default_html_paths(); + + let paths = &[ + "./does-not-resolve.html", + "./a/does-not-resolve.html", + "./a/b/does-not-resolve.html", + ]; + + default_paths.extend(paths.iter().map(|&x| PathBuf::from(x)).collect::>()); + } + TestDataRepositoryType::Rdf => { + name = "law-rdf".into(); + serve_prefix = Some("_rdf".into()); + default_paths = TestDataRepositoryContext::default_rdf_paths(); + } + TestDataRepositoryType::Xml => { + name = "law-xml".into(); + serve_prefix = Some("_xml".into()); + default_paths = TestDataRepositoryContext::default_xml_paths() + } + TestDataRepositoryType::Pdf => { + name = "law-pdf".into(); + route_glob_patterns = Some(vec![".*\\.pdf".into()]); + default_paths = TestDataRepositoryContext::default_pdf_paths(); + } + TestDataRepositoryType::Other(_) => { + name = "law-other".into(); + route_glob_patterns = Some(vec![".*_doc/.*".into(), "_prefix/.*".into()]); + is_fallback = true; + default_paths = TestDataRepositoryContext::default_other_paths(); + + let paths = &[ + "./does-not-resolve.json", + "./a/does-not-resolve.json", + "./a/b/does-not-resolve.json", + ]; + default_paths.extend(paths.iter().map(|&x| PathBuf::from(x)).collect::>()); + } + } + for scope in scopes { + let additional_paths: Vec = default_paths + .iter() + .map(|path| { + PathBuf::from(format!( + "{scope}/{path}", + scope = scope, + path = path.display() + )) + }) + .collect::>(); + paths.extend(additional_paths); + } + + result.push(TestDataRepositoryContext::new( + name, + paths, + kind, + serve_prefix, + route_glob_patterns, + is_fallback, + )?); + } + Ok(result) +} + +impl From<&TestDataRepositoryContext> for Repository { + fn from(context: &TestDataRepositoryContext) -> Self { + let mut custom = Custom::default(); + custom.repository_type = Some(match context.kind { + TestDataRepositoryType::Html => "html".to_string(), + TestDataRepositoryType::Rdf => "rdf".to_string(), + TestDataRepositoryType::Xml => "xml".to_string(), + TestDataRepositoryType::Pdf => "pdf".to_string(), + TestDataRepositoryType::Other(_) => "other".to_string(), + }); + custom.serve = "latest".to_string(); + custom.scope = context.serve_prefix.clone().map(|s| s.to_string()); + custom.routes = context + .route_glob_patterns + .as_ref() + .map(|r| r.iter().map(|s| s.to_string()).collect()); + custom.is_fallback = Some(context.is_fallback); + Self { + name: context.name.to_string(), + custom, + } + } +} diff --git a/tests/archive_testtools/mod.rs b/tests/archive_testtools/mod.rs new file mode 100644 index 0000000..73df10f --- /dev/null +++ b/tests/archive_testtools/mod.rs @@ -0,0 +1,400 @@ +pub mod config; +pub mod utils; + +use anyhow::Result; +use git2::{Commit, Error, Oid}; +use std::collections::HashMap; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use stelae::stelae::archive::{self, Headers}; +use stelae::stelae::types::dependencies::{Dependencies, Dependency}; +use stelae::stelae::types::repositories::{Repositories, Repository}; +use tempfile::TempDir; + +use self::config::{ + get_basic_test_data_repositories, get_dependent_data_repositories_with_scopes, ArchiveType, + Jurisdiction, TestDataRepositoryContext, +}; + +pub fn get_default_static_filename(file_extension: &str) -> &str { + match file_extension { + "html" => "index.html", + "rdf" => "index.rdf", + "xml" => "index.xml", + "pdf" => "example.pdf", + "json" => "example.json", + "js" => "example.js", + _ => "index.html", + } +} + +pub fn copy_file(from: &Path, to: &Path) -> Result<()> { + std::fs::create_dir_all(&to.parent().unwrap()).unwrap(); + std::fs::copy(from, to).unwrap(); + Ok(()) +} + +pub struct GitRepository { + pub repo: git2::Repository, + pub path: PathBuf, +} + +impl GitRepository { + pub fn init(path: &Path) -> Result { + let repo = git2::Repository::init(path)?; + let mut config = repo.config().unwrap(); + config.set_str("user.name", "name").unwrap(); + config.set_str("user.email", "email").unwrap(); + Ok(Self { + repo, + path: path.to_path_buf(), + }) + } + + pub fn commit(&self, path_str: Option<&str>, commit_msg: &str) -> Result { + let mut index = self.repo.index().unwrap(); + if let Some(path_str) = path_str { + index.add_path(&PathBuf::from(path_str)).unwrap(); + } else { + index + .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None) + .unwrap(); + } + index.write().unwrap(); + let tree_id = index.write_tree().unwrap(); + let tree = self.repo.find_tree(tree_id).unwrap(); + let sig = self.repo.signature().unwrap(); + + let binding = self + .repo + .head() + .ok() + .and_then(|head| head.target()) + .and_then(|target_id| self.repo.find_commit(target_id).ok()) + .map(|parent_commit| vec![parent_commit]) + .unwrap_or_default(); + let parent_commits: Vec<&Commit> = binding.iter().collect(); + + self.repo + .commit(Some("HEAD"), &sig, &sig, commit_msg, &tree, &parent_commits) + } + + pub fn add_file(&self, path: &Path, file_name: &str, content: &str) -> Result<()> { + std::fs::create_dir_all(&path)?; + let path = path.join(file_name); + std::fs::write(path, content)?; + Ok(()) + } + + pub fn open(path: &Path) -> Result { + let repo = git2::Repository::open(path)?; + Ok(Self { + repo, + path: path.to_path_buf(), + }) + } +} + +impl Into for GitRepository { + fn into(self) -> git2::Repository { + self.repo + } +} + +impl Deref for GitRepository { + type Target = git2::Repository; + + fn deref(&self) -> &Self::Target { + &self.repo + } +} + +pub fn initialize_archive_inner(archive_type: ArchiveType, td: &TempDir) -> Result<()> { + match archive_type { + ArchiveType::Basic(Jurisdiction::Single) => initialize_archive_basic(td), + ArchiveType::Basic(Jurisdiction::Multi) => initialize_archive_multijurisdiction(td), + ArchiveType::Multihost => initialize_archive_multihost(td), + } +} + +fn initialize_archive_basic(td: &TempDir) -> Result<()> { + let org_name = "test_org"; + + archive::init( + td.path().to_owned(), + "law".into(), + org_name.into(), + None, + false, + None, + ) + .unwrap(); + initialize_stele( + td.path().to_path_buf(), + org_name, + get_basic_test_data_repositories().unwrap().as_slice(), + None, + ) + .unwrap(); + // anyhow::bail!("Something went wrong!"); + Ok(()) +} + +fn initialize_archive_multijurisdiction(td: &TempDir) -> Result<()> { + let root_org_name = "root_test_org"; + + archive::init( + td.path().to_owned(), + "law".into(), + root_org_name.into(), + None, + false, + None, + ) + .unwrap(); + + initialize_stele( + td.path().to_path_buf(), + root_org_name, + get_basic_test_data_repositories().unwrap().as_slice(), + None, + ) + .unwrap(); + + let dependent_stele_1_org_name = "dependent_stele_1"; + let dependent_stele_1_scopes: Vec = vec!["sub/scope/1".into(), "sub/scope/2".into()]; + + initialize_stele( + td.path().to_path_buf(), + dependent_stele_1_org_name, + get_dependent_data_repositories_with_scopes(&dependent_stele_1_scopes) + .unwrap() + .as_slice(), + Some(&dependent_stele_1_scopes), + ) + .unwrap(); + + let dependent_stele_2_org_name = "dependent_stele_2"; + let dependent_stele_2_scopes: Vec = vec!["sub/scope/3".into(), "sub/scope/4".into()]; + + initialize_stele( + td.path().to_path_buf(), + dependent_stele_2_org_name, + get_dependent_data_repositories_with_scopes(&dependent_stele_2_scopes) + .unwrap() + .as_slice(), + Some(&dependent_stele_2_scopes), + ) + .unwrap(); + + add_dependencies( + td.path(), + root_org_name, + vec![dependent_stele_1_org_name, dependent_stele_2_org_name], + )?; + + // anyhow::bail!("Something went wrong!"); + Ok(()) +} + +fn initialize_archive_multihost(td: &TempDir) -> Result<()> { + let root_org_name = "root_stele"; + + archive::init( + td.path().to_owned(), + "law".into(), + root_org_name.into(), + None, + false, + Some(Headers { + current_documents_guard: Some("X-Current-Documents-Guard".into()), + }), + ) + .unwrap(); + + initialize_stele( + td.path().to_path_buf(), + root_org_name, + get_basic_test_data_repositories().unwrap().as_slice(), + None, + ) + .unwrap(); + + let stele_1_org_name = "stele_1"; + + initialize_stele( + td.path().to_path_buf(), + stele_1_org_name, + get_basic_test_data_repositories().unwrap().as_slice(), + None, + ) + .unwrap(); + + let stele_2_org_name = "stele_2"; + + initialize_stele( + td.path().to_path_buf(), + stele_2_org_name, + get_basic_test_data_repositories().unwrap().as_slice(), + None, + ) + .unwrap(); + + add_dependencies( + td.path(), + root_org_name, + vec![stele_1_org_name, stele_2_org_name], + )?; + + Ok(()) +} + +pub fn initialize_stele( + path: PathBuf, + org_name: &str, + data_repositories: &[TestDataRepositoryContext], + scopes: Option<&Vec>, +) -> Result<()> { + let path = path.join(org_name); + init_data_repositories(&path, data_repositories)?; + init_auth_repository(&path, org_name, data_repositories, scopes)?; + Ok(()) +} + +pub fn init_auth_repository( + path: &Path, + org_name: &str, + data_repositories: &[TestDataRepositoryContext], + scopes: Option<&Vec>, +) -> Result { + let mut path = path.to_path_buf(); + path.push("law"); + std::fs::create_dir_all(&path).unwrap(); + + let repo = GitRepository::init(&path).unwrap(); + + path.push("targets"); + + let repositories: Repositories = + data_repositories + .iter() + .fold(Repositories::default(), |mut repositories, data_repo| { + let mut repository = Repository::from(data_repo); + repository.name = format!("{}/{}", org_name, repository.name); + repositories + .repositories + .entry(repository.name.clone()) + .or_insert(repository); + repositories.scopes = + scopes.map(|vec| vec.into_iter().map(|scope| scope.into()).collect()); + repositories + }); + let content = serde_json::to_string_pretty(&repositories).unwrap(); + + repo.add_file(&path, "repositories.json", &content).unwrap(); + repo.commit(Some("targets/repositories.json"), "Add repositories.json") + .unwrap(); + Ok(repo) +} + +pub fn init_data_repositories( + path: &Path, + data_repositories: &[TestDataRepositoryContext], +) -> Result> { + let mut data_git_repositories: Vec = Vec::new(); + for data_repo in data_repositories { + let mut path = path.to_path_buf(); + path.push(&data_repo.name); + std::fs::create_dir_all(&path).unwrap(); + let git_repo = GitRepository::init(&path).unwrap(); + init_data_repository(&git_repo, data_repo)?; + data_git_repositories.push(git_repo); + } + Ok(data_git_repositories) +} + +fn init_data_repository( + git_repo: &GitRepository, + data_repo: &TestDataRepositoryContext, +) -> Result<()> { + for path in data_repo.paths.iter() { + add_fixture_file_to_git_repo(git_repo, path)?; + } + git_repo.commit(None, "Add initial data").unwrap(); + Ok(()) +} + +fn add_fixture_file_to_git_repo(git_repo: &GitRepository, path: &Path) -> Result<()> { + let filename = path.file_name().unwrap().to_str().unwrap(); + let static_file_path = get_static_file_path(filename); + copy_file(&static_file_path, &git_repo.path.join(path)).unwrap(); + Ok(()) +} + +/// Returns a static file path for the given filename. +/// If the file does not exist, returns the default static file path. +fn get_static_file_path(filename: &str) -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/fixtures/static_files"); + let static_file_path = PathBuf::from(&path).join(filename); + + if static_file_path.exists() { + return static_file_path; + } + + let ext = Path::new(filename) + .extension() + .map_or("html", |ext| ext.to_str().map_or("", |ext_str| ext_str)); + let filename = get_default_static_filename(ext); + + PathBuf::from(path).join(filename) +} + +pub fn add_dependencies( + path: &Path, + root_org_name: &str, + dependent_stele_org_names: Vec<&str>, +) -> Result<()> { + let root_repo = get_repository(path, &format!("{root_org_name}/law")); + + let dependencies = Dependencies { + dependencies: { + let mut dependencies = HashMap::new(); + for dependent_stele_org_name in dependent_stele_org_names { + dependencies.insert( + format!( + "{dependent_stele_org_name}/law", + dependent_stele_org_name = dependent_stele_org_name + ), + Dependency { + out_of_band_authentication: "sha256".into(), + branch: "main".into(), + }, + ); + } + dependencies + } + .into_iter() + .collect(), + }; + let content = serde_json::to_string_pretty(&dependencies).unwrap(); + + root_repo + .add_file( + &path + .to_path_buf() + .join(format!("{root_org_name}/law/targets")), + "dependencies.json", + &content, + ) + .unwrap(); + root_repo + .commit(Some("targets/dependencies.json"), "Add dependencies.json") + .unwrap(); + Ok(()) +} +pub fn get_repository(path: &Path, name: &str) -> GitRepository { + let mut path = path.to_path_buf(); + path.push(name); + GitRepository::open(&path).unwrap() +} diff --git a/tests/archive_testtools/utils.rs b/tests/archive_testtools/utils.rs new file mode 100644 index 0000000..ac98147 --- /dev/null +++ b/tests/archive_testtools/utils.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use std::path::Path; +use tempfile::TempDir; + +/// Stele testing framework requires working with bare repositories. +/// One idea was to initialize the git2 repository as a bare repository, and add/commit files to the bare repo. +/// However, this approach does not work because index methods fail on bare repositories, see [1]. +/// Instead, we initialized the git2 repository as a normal repository, and then convert the repository to a bare repository. +/// +/// [1] - https://libgit2.org/libgit2/#HEAD/group/index/git_index_add_all +pub fn make_all_git_repos_bare_recursive(td: &TempDir) -> Result<()> { + visit_dirs(td.path()) +} + +fn visit_dirs(dir: &Path) -> Result<()> { + if dir.is_dir() { + process_directory(dir)?; + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path)?; + } + } + } + Ok(()) +} + +fn process_directory(dir_path: &Path) -> Result<()> { + let git_dir = dir_path.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + // Remove contents from the current directory, excluding the .git directory + for entry in std::fs::read_dir(dir_path)? { + let entry = entry?; + let entry_path = entry.path(); + + if entry_path != git_dir { + if entry_path.is_dir() { + std::fs::remove_dir_all(&entry_path)?; + } else { + std::fs::remove_file(&entry_path)?; + } + } + } + // Move contents of .git subdirectory to the current directory + for entry in std::fs::read_dir(&git_dir)? { + let entry = entry?; + let entry_path = entry.path(); + let new_path = dir_path.join(entry_path.file_name().unwrap()); + + std::fs::rename(&entry_path, &new_path)?; + } + // Remove the .git directory + std::fs::remove_dir_all(&git_dir)?; + } + Ok(()) +} diff --git a/tests/basic/archive_test.rs b/tests/basic/archive_test.rs new file mode 100644 index 0000000..b1d43d7 --- /dev/null +++ b/tests/basic/archive_test.rs @@ -0,0 +1,45 @@ +use stelae::utils::archive::find_archive_path; + +use crate::common::{self, BASIC_MODULE_NAME}; + +#[test] +fn test_find_archive_path_when_at_archive_expect_path() { + let archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let actual = find_archive_path(&archive_path).unwrap(); + let expected = archive_path; + assert_eq!(actual, expected); +} + +#[test] +fn test_find_archive_path_when_in_archive_expect_archive_path() { + let archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let cwd = archive_path.join("test"); + let actual = find_archive_path(&cwd).unwrap(); + let expected = archive_path; + assert_eq!(actual, expected); +} + +#[test] +fn test_find_archive_path_when_nonexistant_path_expect_error() { + let archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let cwd = archive_path.join("does_not_exist"); + let actual = find_archive_path(&cwd).unwrap_err(); + let expected = "(os error 2)"; + assert!( + actual.to_string().contains(expected), + "\"{actual}\" doesn't contain {expected}" + ); +} + +#[test] +fn test_find_archive_path_when_not_in_archive_expect_error() { + let archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let cwd = archive_path.parent().unwrap(); + let actual = find_archive_path(cwd).unwrap_err(); + let expected = + "is not inside a Stelae Archive. Run `stelae init` to create a archive at this location."; + assert!( + actual.to_string().contains(expected), + "\"{actual}\" doesn't contain {expected}" + ); +} diff --git a/tests/basic/gitrepo_test.rs b/tests/basic/gitrepo_test.rs index 44c2832..21b9a5c 100644 --- a/tests/basic/gitrepo_test.rs +++ b/tests/basic/gitrepo_test.rs @@ -1,70 +1,66 @@ use stelae::utils::git::{Repo, GIT_REQUEST_NOT_FOUND}; -use crate::common; +use crate::common::{self, BASIC_MODULE_NAME}; -const COMMIT: &str = "ed782e08d119a580baa3067e2ea5df06f3d1cd05"; - -fn blob_to_string(blob: Vec) -> String { - core::str::from_utf8(blob.as_slice()).unwrap().into() -} +const COMMIT: &str = "4ba432f61eec15194db527548be4cbc0105635b9"; #[test] fn test_get_bytes_at_path_when_empty_path_expect_index_html() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html").unwrap(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let repo = Repo::new(&test_archive_path, "test", "law-html").unwrap(); let actual = repo.get_bytes_at_path(COMMIT, "").unwrap(); let expected = ""; assert!( - blob_to_string(actual).starts_with(expected), + common::blob_to_string(actual).starts_with(expected), "doesn't start with {expected}" ); } #[test] fn test_get_bytes_at_path_when_full_path_expect_data() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html").unwrap(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let repo = Repo::new(&test_archive_path, "test", "law-html").unwrap(); let actual = repo.get_bytes_at_path(COMMIT, "a/b/c.html").unwrap(); let expected = ""; assert!( - blob_to_string(actual).starts_with(expected), + common::blob_to_string(actual).starts_with(expected), "doesn't start with {expected}" ); } #[test] fn test_get_bytes_at_path_when_omit_html_expect_data() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html").unwrap(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let repo = Repo::new(&test_archive_path, "test", "law-html").unwrap(); let actual = repo.get_bytes_at_path(COMMIT, "a/b/c").unwrap(); let expected = ""; assert!( - blob_to_string(actual).starts_with(expected), + common::blob_to_string(actual).starts_with(expected), "doesn't start with {expected}" ); } #[test] fn test_get_bytes_at_path_when_omit_index_expect_data() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html").unwrap(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let repo = Repo::new(&test_archive_path, "test", "law-html").unwrap(); let actual = repo.get_bytes_at_path(COMMIT, "a/b/d").unwrap(); let expected = ""; assert!( - blob_to_string(actual).starts_with(expected), + common::blob_to_string(actual).starts_with(expected), "doesn't start with {expected}" ); } #[test] fn test_get_bytes_at_path_when_invalid_repo_namespace_expect_error() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let actual = Repo::new(&test_library_path, "xxx", "law-html").unwrap_err(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let actual = Repo::new(&test_archive_path, "xxx", "law-html").unwrap_err(); let expected = "failed to resolve path"; assert!( actual.to_string().contains(expected), @@ -74,9 +70,9 @@ fn test_get_bytes_at_path_when_invalid_repo_namespace_expect_error() { #[test] fn test_get_bytes_at_path_when_invalid_repo_name_expect_error() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let actual = Repo::new(&test_library_path, "test", "xxx").unwrap_err(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let actual = Repo::new(&test_archive_path, "test", "xxx").unwrap_err(); let expected = "failed to resolve path"; assert!( actual.to_string().contains(expected), @@ -86,9 +82,9 @@ fn test_get_bytes_at_path_when_invalid_repo_name_expect_error() { #[test] fn test_get_bytes_at_path_when_invalid_path_expect_error() { - common::initialize(); - let test_library_path = common::get_test_library_path(); - let repo = Repo::new(&test_library_path, "test", "law-html").unwrap(); + common::initialize_git(); + let test_archive_path = common::get_test_archive_path(BASIC_MODULE_NAME); + let repo = Repo::new(&test_archive_path, "test", "law-html").unwrap(); let actual = repo.get_bytes_at_path(COMMIT, "a/b/x").unwrap_err(); let expected = GIT_REQUEST_NOT_FOUND; assert!( diff --git a/tests/basic/library_test.rs b/tests/basic/library_test.rs deleted file mode 100644 index 15c6355..0000000 --- a/tests/basic/library_test.rs +++ /dev/null @@ -1,45 +0,0 @@ -use stelae::utils::library::find_library_path; - -use crate::common; - -#[test] -fn test_find_library_path_when_at_library_expect_path() { - let library_path = common::get_test_library_path(); - let actual = find_library_path(&library_path).unwrap(); - let expected = library_path; - assert_eq!(actual, expected); -} - -#[test] -fn test_find_library_path_when_in_library_expect_library_path() { - let library_path = common::get_test_library_path(); - let cwd = library_path.join("test"); - let actual = find_library_path(&cwd).unwrap(); - let expected = library_path; - assert_eq!(actual, expected); -} - -#[test] -fn test_find_library_path_when_nonexistant_path_expect_error() { - let library_path = common::get_test_library_path(); - let cwd = library_path.join("does_not_exist"); - let actual = find_library_path(&cwd).unwrap_err(); - let expected = "(os error 2)"; - assert!( - actual.to_string().contains(expected), - "\"{actual}\" doesn't contain {expected}" - ); -} - -#[test] -fn test_find_library_path_when_not_in_library_expect_error() { - let library_path = common::get_test_library_path(); - let cwd = library_path.parent().unwrap(); - let actual = find_library_path(cwd).unwrap_err(); - let expected = - "is not inside a Stelae Library. Run `stelae init` to create a library at this location."; - assert!( - actual.to_string().contains(expected), - "\"{actual}\" doesn't contain {expected}" - ); -} diff --git a/tests/basic/mod.rs b/tests/basic/mod.rs index f0aaa5c..5cbd1b1 100644 --- a/tests/basic/mod.rs +++ b/tests/basic/mod.rs @@ -1,2 +1,2 @@ +mod archive_test; mod gitrepo_test; -mod library_test; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0b7abb6..e96e238 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,21 +1,89 @@ -use std::fs::create_dir_all; -use std::path::PathBuf; +use crate::archive_testtools::{self, config::ArchiveType, utils}; +use actix_http::Request; +use actix_service::Service; +use actix_web::{ + dev::ServiceResponse, + test::{self}, + Error, +}; +use anyhow::Result; +use std::path::{Path, PathBuf}; use std::sync::Once; - +use tempfile::Builder; static INIT: Once = Once::new(); -pub fn initialize() { +use actix_http::body::MessageBody; + +use stelae::server::publish::{init_app, AppState}; +use stelae::stelae::archive::Archive; + +pub const BASIC_MODULE_NAME: &str = "basic"; + +pub fn blob_to_string(blob: Vec) -> String { + core::str::from_utf8(blob.as_slice()).unwrap().into() +} + +// TODO: consider adding abort! test macro, +// which aborts the current test. +// then we can manually inspect the state of the test environment + +// to manually inspect state of test environment at present, +// we use anyhow::bail!() which aborts the entire test suite. + +pub async fn initialize_app( + archive_path: &Path, +) -> impl Service, Error = Error> { + let archive = Archive::parse(archive_path.to_path_buf(), archive_path, false).unwrap(); + let state = AppState { archive }; + let app = init_app(&state).unwrap(); + test::init_service(app).await +} + +pub fn initialize_archive(archive_type: ArchiveType) -> Result { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/fixtures/"); + + let td = Builder::new().tempdir_in(&path).unwrap(); + + match archive_testtools::initialize_archive_inner(archive_type, &td) { + Ok(_) => { + if let Err(err) = utils::make_all_git_repos_bare_recursive(&td) { + return Err(err); + } + Ok(td) + } + Err(err) => { + dbg!(&err); + use std::mem::ManuallyDrop; + let td = ManuallyDrop::new(td); + // TODO: better error handling on testing failure + let error_output_directory = path.clone().join(PathBuf::from("error_output_directory")); + std::fs::remove_dir_all(&error_output_directory).unwrap(); + std::fs::rename(td.path(), &error_output_directory) + .expect("Failed to move temp directory"); + eprintln!( + "{}", format!("Failed to remove '{error_output_directory:?}', please try to remove directory by hand. Original error: {err}") + ); + Err(err) + } + } +} + +/// Used to initialize the test environment for git micro-server. +pub fn initialize_git() { INIT.call_once(|| { - let repo_path = get_test_library_path().join(PathBuf::from("test/law-html")); + let repo_path = + get_test_archive_path(BASIC_MODULE_NAME).join(PathBuf::from("test/law-html")); let heads_path = repo_path.join(PathBuf::from("refs/heads")); - create_dir_all(heads_path).unwrap(); + std::fs::create_dir_all(heads_path).unwrap(); let tags_path = repo_path.join(PathBuf::from("refs/tags")); - create_dir_all(tags_path).unwrap(); + std::fs::create_dir_all(tags_path).unwrap(); }); } -pub fn get_test_library_path() -> PathBuf { +pub fn get_test_archive_path(mod_name: &str) -> PathBuf { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("tests/fixtures/library"); + path.push("tests/fixtures/"); + path.push(mod_name.to_owned() + "/archive"); path } diff --git a/tests/fixtures/library/.stelae/.keep b/tests/fixtures/basic/archive/.stelae/.keep similarity index 100% rename from tests/fixtures/library/.stelae/.keep rename to tests/fixtures/basic/archive/.stelae/.keep diff --git a/tests/fixtures/basic/archive/test/.gitignore b/tests/fixtures/basic/archive/test/.gitignore new file mode 100644 index 0000000..cd523b7 --- /dev/null +++ b/tests/fixtures/basic/archive/test/.gitignore @@ -0,0 +1,2 @@ +./law-html/logs/** +./law-html/hooks/** \ No newline at end of file diff --git a/tests/fixtures/library/test/law-html/HEAD b/tests/fixtures/basic/archive/test/law-html/HEAD similarity index 100% rename from tests/fixtures/library/test/law-html/HEAD rename to tests/fixtures/basic/archive/test/law-html/HEAD diff --git a/tests/fixtures/library/test/law-html/config b/tests/fixtures/basic/archive/test/law-html/config similarity index 60% rename from tests/fixtures/library/test/law-html/config rename to tests/fixtures/basic/archive/test/law-html/config index a08e237..5ed22e2 100644 --- a/tests/fixtures/library/test/law-html/config +++ b/tests/fixtures/basic/archive/test/law-html/config @@ -2,7 +2,6 @@ repositoryformatversion = 0 filemode = false bare = true + logallrefupdates = true symlinks = false ignorecase = true -[remote "origin"] - url = C:\\Users\\dreisen\\programming\\rust\\test diff --git a/tests/fixtures/library/test/law-html/description b/tests/fixtures/basic/archive/test/law-html/description similarity index 100% rename from tests/fixtures/library/test/law-html/description rename to tests/fixtures/basic/archive/test/law-html/description diff --git a/tests/fixtures/basic/archive/test/law-html/index b/tests/fixtures/basic/archive/test/law-html/index new file mode 100644 index 0000000..fcf100c Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/index differ diff --git a/tests/fixtures/library/test/law-html/info/exclude b/tests/fixtures/basic/archive/test/law-html/info/exclude similarity index 100% rename from tests/fixtures/library/test/law-html/info/exclude rename to tests/fixtures/basic/archive/test/law-html/info/exclude diff --git a/tests/fixtures/basic/archive/test/law-html/objects/12/62271f7a71597e346147577cee8ca6ca25276f b/tests/fixtures/basic/archive/test/law-html/objects/12/62271f7a71597e346147577cee8ca6ca25276f new file mode 100644 index 0000000..a2565d7 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/12/62271f7a71597e346147577cee8ca6ca25276f differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/3c/b85ad3ee2d7002577ce239ec0acf085900c72a b/tests/fixtures/basic/archive/test/law-html/objects/3c/b85ad3ee2d7002577ce239ec0acf085900c72a new file mode 100644 index 0000000..36b3abe Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/3c/b85ad3ee2d7002577ce239ec0acf085900c72a differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/40/fff4d3b59c2c3670a1ca8a1a0ad4031d1c5d42 b/tests/fixtures/basic/archive/test/law-html/objects/40/fff4d3b59c2c3670a1ca8a1a0ad4031d1c5d42 new file mode 100644 index 0000000..7feba49 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/40/fff4d3b59c2c3670a1ca8a1a0ad4031d1c5d42 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/42/94b342946e65f7f61c5266d097112408445288 b/tests/fixtures/basic/archive/test/law-html/objects/42/94b342946e65f7f61c5266d097112408445288 new file mode 100644 index 0000000..27cd241 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/42/94b342946e65f7f61c5266d097112408445288 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/48/6fb82fa5bf8815a12feea29536f1b45562a675 b/tests/fixtures/basic/archive/test/law-html/objects/48/6fb82fa5bf8815a12feea29536f1b45562a675 new file mode 100644 index 0000000..b7dca59 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/48/6fb82fa5bf8815a12feea29536f1b45562a675 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/4a/6f679af879bfd76e6c42ec770a78d9d20d060b b/tests/fixtures/basic/archive/test/law-html/objects/4a/6f679af879bfd76e6c42ec770a78d9d20d060b new file mode 100644 index 0000000..992903b Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/4a/6f679af879bfd76e6c42ec770a78d9d20d060b differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/4b/a432f61eec15194db527548be4cbc0105635b9 b/tests/fixtures/basic/archive/test/law-html/objects/4b/a432f61eec15194db527548be4cbc0105635b9 new file mode 100644 index 0000000..d03f4c3 --- /dev/null +++ b/tests/fixtures/basic/archive/test/law-html/objects/4b/a432f61eec15194db527548be4cbc0105635b9 @@ -0,0 +1 @@ +xA aǬ.@b܄ ܋ 5}mpxZKƪ]_% g }!AkKWn]h$ 7:L(_U=cܖ3Rcԣԣ *PZQJluG XN,9\{L: \ No newline at end of file diff --git a/tests/fixtures/basic/archive/test/law-html/objects/51/a0400e0596713cef58b5bf208b9a49812bf89d b/tests/fixtures/basic/archive/test/law-html/objects/51/a0400e0596713cef58b5bf208b9a49812bf89d new file mode 100644 index 0000000..3a1a25e Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/51/a0400e0596713cef58b5bf208b9a49812bf89d differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/5d/71f36374322ab2da151fbb76c1329c5856aa55 b/tests/fixtures/basic/archive/test/law-html/objects/5d/71f36374322ab2da151fbb76c1329c5856aa55 new file mode 100644 index 0000000..b79890b --- /dev/null +++ b/tests/fixtures/basic/archive/test/law-html/objects/5d/71f36374322ab2da151fbb76c1329c5856aa55 @@ -0,0 +1,3 @@ +x]?O0ř),^HJ Ht`J0:58&I#z|εjxWfԺjVN(SX:e @hS<% +p/W~)]a3yVL$!98R+W-HG8+5㞡GB动0I ZD $t`P%1]t\\6ZF,ˆj)FVB1ɘ_ \ No newline at end of file diff --git a/tests/fixtures/basic/archive/test/law-html/objects/64/215d0a7b9398be24035d95d574f329fd3140f5 b/tests/fixtures/basic/archive/test/law-html/objects/64/215d0a7b9398be24035d95d574f329fd3140f5 new file mode 100644 index 0000000..e8ee40b Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/64/215d0a7b9398be24035d95d574f329fd3140f5 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/65/2f17778fa3073d96baf1c9ef47a7536bd00487 b/tests/fixtures/basic/archive/test/law-html/objects/65/2f17778fa3073d96baf1c9ef47a7536bd00487 new file mode 100644 index 0000000..affcbdd Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/65/2f17778fa3073d96baf1c9ef47a7536bd00487 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/86/a2da90e28b46fd21aaa92e7040b191483d2786 b/tests/fixtures/basic/archive/test/law-html/objects/86/a2da90e28b46fd21aaa92e7040b191483d2786 new file mode 100644 index 0000000..2c10232 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/86/a2da90e28b46fd21aaa92e7040b191483d2786 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/a1/f6d6dc911d25b1a500a4227c94f8f03a581fc7 b/tests/fixtures/basic/archive/test/law-html/objects/a1/f6d6dc911d25b1a500a4227c94f8f03a581fc7 new file mode 100644 index 0000000..5409526 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/a1/f6d6dc911d25b1a500a4227c94f8f03a581fc7 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/a5/e774c4e462a53cc137293af98c546d1ad14b6c b/tests/fixtures/basic/archive/test/law-html/objects/a5/e774c4e462a53cc137293af98c546d1ad14b6c new file mode 100644 index 0000000..fc78dd2 Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/a5/e774c4e462a53cc137293af98c546d1ad14b6c differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/d5/1fc388ee6d58a5c834cf13a37bde3113f3797c b/tests/fixtures/basic/archive/test/law-html/objects/d5/1fc388ee6d58a5c834cf13a37bde3113f3797c new file mode 100644 index 0000000..8265a4f Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/d5/1fc388ee6d58a5c834cf13a37bde3113f3797c differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/eb/8451e57c3ea6b858db3df57f82b7d0d4b9d246 b/tests/fixtures/basic/archive/test/law-html/objects/eb/8451e57c3ea6b858db3df57f82b7d0d4b9d246 new file mode 100644 index 0000000..1b8ad9b Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/eb/8451e57c3ea6b858db3df57f82b7d0d4b9d246 differ diff --git a/tests/fixtures/basic/archive/test/law-html/objects/fe/5635eae3391b57501ea5e8bb40cf8109f78fd0 b/tests/fixtures/basic/archive/test/law-html/objects/fe/5635eae3391b57501ea5e8bb40cf8109f78fd0 new file mode 100644 index 0000000..ad0554f Binary files /dev/null and b/tests/fixtures/basic/archive/test/law-html/objects/fe/5635eae3391b57501ea5e8bb40cf8109f78fd0 differ diff --git a/tests/fixtures/basic/archive/test/law-html/refs/heads/main b/tests/fixtures/basic/archive/test/law-html/refs/heads/main new file mode 100644 index 0000000..884408b --- /dev/null +++ b/tests/fixtures/basic/archive/test/law-html/refs/heads/main @@ -0,0 +1 @@ +4ba432f61eec15194db527548be4cbc0105635b9 diff --git a/tests/fixtures/library/test/law-html/hooks/applypatch-msg.sample b/tests/fixtures/library/test/law-html/hooks/applypatch-msg.sample deleted file mode 100644 index a5d7b84..0000000 --- a/tests/fixtures/library/test/law-html/hooks/applypatch-msg.sample +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message taken by -# applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. The hook is -# allowed to edit the commit message file. -# -# To enable this hook, rename this file to "applypatch-msg". - -. git-sh-setup -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} -: diff --git a/tests/fixtures/library/test/law-html/hooks/commit-msg.sample b/tests/fixtures/library/test/law-html/hooks/commit-msg.sample deleted file mode 100644 index b58d118..0000000 --- a/tests/fixtures/library/test/law-html/hooks/commit-msg.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. -# -# To enable this hook, rename this file to "commit-msg". - -# Uncomment the below to add a Signed-off-by line to the message. -# Doing this in a hook is a bad idea in general, but the prepare-commit-msg -# hook is more suited to it. -# -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" - -# This example catches duplicate Signed-off-by lines. - -test "" = "$(grep '^Signed-off-by: ' "$1" | - sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { - echo >&2 Duplicate Signed-off-by lines. - exit 1 -} diff --git a/tests/fixtures/library/test/law-html/hooks/fsmonitor-watchman.sample b/tests/fixtures/library/test/law-html/hooks/fsmonitor-watchman.sample deleted file mode 100644 index ef94fa2..0000000 --- a/tests/fixtures/library/test/law-html/hooks/fsmonitor-watchman.sample +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use IPC::Open2; - -# An example hook script to integrate Watchman -# (https://facebook.github.io/watchman/) with git to speed up detecting -# new and modified files. -# -# The hook is passed a version (currently 1) and a time in nanoseconds -# formatted as a string and outputs to stdout all files that have been -# modified since the given time. Paths must be relative to the root of -# the working tree and separated by a single NUL. -# -# To enable this hook, rename this file to "query-watchman" and set -# 'git config core.fsmonitor .git/hooks/query-watchman' -# -my ($version, $time) = @ARGV; - -# Check the hook interface version - -if ($version == 1) { - # convert nanoseconds to seconds - # subtract one second to make sure watchman will return all changes - $time = int ($time / 1000000000) - 1; -} else { - die "Unsupported query-fsmonitor hook version '$version'.\n" . - "Falling back to scanning...\n"; -} - -my $git_work_tree; -if ($^O =~ 'msys' || $^O =~ 'cygwin') { - $git_work_tree = Win32::GetCwd(); - $git_work_tree =~ tr/\\/\//; -} else { - require Cwd; - $git_work_tree = Cwd::cwd(); -} - -my $retry = 1; - -launch_watchman(); - -sub launch_watchman { - - my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') - or die "open2() failed: $!\n" . - "Falling back to scanning...\n"; - - # In the query expression below we're asking for names of files that - # changed since $time but were not transient (ie created after - # $time but no longer exist). - # - # To accomplish this, we're using the "since" generator to use the - # recency index to select candidate nodes and "fields" to limit the - # output to file names only. - - my $query = <<" END"; - ["query", "$git_work_tree", { - "since": $time, - "fields": ["name"] - }] - END - - print CHLD_IN $query; - close CHLD_IN; - my $response = do {local $/; }; - - die "Watchman: command returned no output.\n" . - "Falling back to scanning...\n" if $response eq ""; - die "Watchman: command returned invalid output: $response\n" . - "Falling back to scanning...\n" unless $response =~ /^\{/; - - my $json_pkg; - eval { - require JSON::XS; - $json_pkg = "JSON::XS"; - 1; - } or do { - require JSON::PP; - $json_pkg = "JSON::PP"; - }; - - my $o = $json_pkg->new->utf8->decode($response); - - if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { - print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; - $retry--; - qx/watchman watch "$git_work_tree"/; - die "Failed to make watchman watch '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - - # Watchman will always return all files on the first query so - # return the fast "everything is dirty" flag to git and do the - # Watchman query just to get it over with now so we won't pay - # the cost in git to look up each individual file. - print "/\0"; - eval { launch_watchman() }; - exit 0; - } - - die "Watchman: $o->{error}.\n" . - "Falling back to scanning...\n" if $o->{error}; - - binmode STDOUT, ":utf8"; - local $, = "\0"; - print @{$o->{files}}; -} diff --git a/tests/fixtures/library/test/law-html/hooks/post-update.sample b/tests/fixtures/library/test/law-html/hooks/post-update.sample deleted file mode 100644 index ec17ec1..0000000 --- a/tests/fixtures/library/test/law-html/hooks/post-update.sample +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare a packed repository for use over -# dumb transports. -# -# To enable this hook, rename this file to "post-update". - -exec git update-server-info diff --git a/tests/fixtures/library/test/law-html/hooks/pre-applypatch.sample b/tests/fixtures/library/test/law-html/hooks/pre-applypatch.sample deleted file mode 100644 index 4142082..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-applypatch.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed -# by applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-applypatch". - -. git-sh-setup -precommit="$(git rev-parse --git-path hooks/pre-commit)" -test -x "$precommit" && exec "$precommit" ${1+"$@"} -: diff --git a/tests/fixtures/library/test/law-html/hooks/pre-commit.sample b/tests/fixtures/library/test/law-html/hooks/pre-commit.sample deleted file mode 100644 index 6a75641..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-commit.sample +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=$(git hash-object -t tree /dev/null) -fi - -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --bool hooks.allownonascii) - -# Redirect output to stderr. -exec 1>&2 - -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. - -This can cause problems if you want to work with people on other platforms. - -To be portable it is advisable to rename the file. - -If you know what you are doing you can disable this check using: - - git config hooks.allownonascii true -EOF - exit 1 -fi - -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- diff --git a/tests/fixtures/library/test/law-html/hooks/pre-merge-commit.sample b/tests/fixtures/library/test/law-html/hooks/pre-merge-commit.sample deleted file mode 100644 index 399eab1..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-merge-commit.sample +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git merge" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message to -# stderr if it wants to stop the merge commit. -# -# To enable this hook, rename this file to "pre-merge-commit". - -. git-sh-setup -test -x "$GIT_DIR/hooks/pre-commit" && - exec "$GIT_DIR/hooks/pre-commit" -: diff --git a/tests/fixtures/library/test/law-html/hooks/pre-push.sample b/tests/fixtures/library/test/law-html/hooks/pre-push.sample deleted file mode 100644 index 6187dbf..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-push.sample +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -# An example hook script to verify what is about to be pushed. Called by "git -# push" after it has checked the remote status, but before anything has been -# pushed. If this script exits with a non-zero status nothing will be pushed. -# -# This hook is called with the following parameters: -# -# $1 -- Name of the remote to which the push is being done -# $2 -- URL to which the push is being done -# -# If pushing without using a named remote those arguments will be equal. -# -# Information about the commits which are being pushed is supplied as lines to -# the standard input in the form: -# -# -# -# This sample shows how to prevent push of commits where the log message starts -# with "WIP" (work in progress). - -remote="$1" -url="$2" - -z40=0000000000000000000000000000000000000000 - -while read local_ref local_sha remote_ref remote_sha -do - if [ "$local_sha" = $z40 ] - then - # Handle delete - : - else - if [ "$remote_sha" = $z40 ] - then - # New branch, examine all commits - range="$local_sha" - else - # Update to existing branch, examine new commits - range="$remote_sha..$local_sha" - fi - - # Check for WIP commit - commit=`git rev-list -n 1 --grep '^WIP' "$range"` - if [ -n "$commit" ] - then - echo >&2 "Found WIP commit in $local_ref, not pushing" - exit 1 - fi - fi -done - -exit 0 diff --git a/tests/fixtures/library/test/law-html/hooks/pre-rebase.sample b/tests/fixtures/library/test/law-html/hooks/pre-rebase.sample deleted file mode 100644 index 6cbef5c..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-rebase.sample +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2006, 2008 Junio C Hamano -# -# The "pre-rebase" hook is run just before "git rebase" starts doing -# its job, and can prevent the command from running by exiting with -# non-zero status. -# -# The hook is called with the following parameters: -# -# $1 -- the upstream the series was forked from. -# $2 -- the branch being rebased (or empty when rebasing the current branch). -# -# This sample shows how to prevent topic branches that are already -# merged to 'next' branch from getting rebased, because allowing it -# would result in rebasing already published history. - -publish=next -basebranch="$1" -if test "$#" = 2 -then - topic="refs/heads/$2" -else - topic=`git symbolic-ref HEAD` || - exit 0 ;# we do not interrupt rebasing detached HEAD -fi - -case "$topic" in -refs/heads/??/*) - ;; -*) - exit 0 ;# we do not interrupt others. - ;; -esac - -# Now we are dealing with a topic branch being rebased -# on top of master. Is it OK to rebase it? - -# Does the topic really exist? -git show-ref -q "$topic" || { - echo >&2 "No such branch $topic" - exit 1 -} - -# Is topic fully merged to master? -not_in_master=`git rev-list --pretty=oneline ^master "$topic"` -if test -z "$not_in_master" -then - echo >&2 "$topic is fully merged to master; better remove it." - exit 1 ;# we could allow it, but there is no point. -fi - -# Is topic ever merged to next? If so you should not be rebasing it. -only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` -only_next_2=`git rev-list ^master ${publish} | sort` -if test "$only_next_1" = "$only_next_2" -then - not_in_topic=`git rev-list "^$topic" master` - if test -z "$not_in_topic" - then - echo >&2 "$topic is already up to date with master" - exit 1 ;# we could allow it, but there is no point. - else - exit 0 - fi -else - not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` - /usr/bin/perl -e ' - my $topic = $ARGV[0]; - my $msg = "* $topic has commits already merged to public branch:\n"; - my (%not_in_next) = map { - /^([0-9a-f]+) /; - ($1 => 1); - } split(/\n/, $ARGV[1]); - for my $elem (map { - /^([0-9a-f]+) (.*)$/; - [$1 => $2]; - } split(/\n/, $ARGV[2])) { - if (!exists $not_in_next{$elem->[0]}) { - if ($msg) { - print STDERR $msg; - undef $msg; - } - print STDERR " $elem->[1]\n"; - } - } - ' "$topic" "$not_in_next" "$not_in_master" - exit 1 -fi - -<<\DOC_END - -This sample hook safeguards topic branches that have been -published from being rewound. - -The workflow assumed here is: - - * Once a topic branch forks from "master", "master" is never - merged into it again (either directly or indirectly). - - * Once a topic branch is fully cooked and merged into "master", - it is deleted. If you need to build on top of it to correct - earlier mistakes, a new topic branch is created by forking at - the tip of the "master". This is not strictly necessary, but - it makes it easier to keep your history simple. - - * Whenever you need to test or publish your changes to topic - branches, merge them into "next" branch. - -The script, being an example, hardcodes the publish branch name -to be "next", but it is trivial to make it configurable via -$GIT_DIR/config mechanism. - -With this workflow, you would want to know: - -(1) ... if a topic branch has ever been merged to "next". Young - topic branches can have stupid mistakes you would rather - clean up before publishing, and things that have not been - merged into other branches can be easily rebased without - affecting other people. But once it is published, you would - not want to rewind it. - -(2) ... if a topic branch has been fully merged to "master". - Then you can delete it. More importantly, you should not - build on top of it -- other people may already want to - change things related to the topic as patches against your - "master", so if you need further changes, it is better to - fork the topic (perhaps with the same name) afresh from the - tip of "master". - -Let's look at this example: - - o---o---o---o---o---o---o---o---o---o "next" - / / / / - / a---a---b A / / - / / / / - / / c---c---c---c B / - / / / \ / - / / / b---b C \ / - / / / / \ / - ---o---o---o---o---o---o---o---o---o---o---o "master" - - -A, B and C are topic branches. - - * A has one fix since it was merged up to "next". - - * B has finished. It has been fully merged up to "master" and "next", - and is ready to be deleted. - - * C has not merged to "next" at all. - -We would want to allow C to be rebased, refuse A, and encourage -B to be deleted. - -To compute (1): - - git rev-list ^master ^topic next - git rev-list ^master next - - if these match, topic has not merged in next at all. - -To compute (2): - - git rev-list master..topic - - if this is empty, it is fully merged to "master". - -DOC_END diff --git a/tests/fixtures/library/test/law-html/hooks/pre-receive.sample b/tests/fixtures/library/test/law-html/hooks/pre-receive.sample deleted file mode 100644 index a1fd29e..0000000 --- a/tests/fixtures/library/test/law-html/hooks/pre-receive.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to make use of push options. -# The example simply echoes all push options that start with 'echoback=' -# and rejects all pushes when the "reject" push option is used. -# -# To enable this hook, rename this file to "pre-receive". - -if test -n "$GIT_PUSH_OPTION_COUNT" -then - i=0 - while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" - do - eval "value=\$GIT_PUSH_OPTION_$i" - case "$value" in - echoback=*) - echo "echo from the pre-receive-hook: ${value#*=}" >&2 - ;; - reject) - exit 1 - esac - i=$((i + 1)) - done -fi diff --git a/tests/fixtures/library/test/law-html/hooks/prepare-commit-msg.sample b/tests/fixtures/library/test/law-html/hooks/prepare-commit-msg.sample deleted file mode 100644 index 10fa14c..0000000 --- a/tests/fixtures/library/test/law-html/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare the commit log message. -# Called by "git commit" with the name of the file that has the -# commit message, followed by the description of the commit -# message's source. The hook's purpose is to edit the commit -# message file. If the hook fails with a non-zero status, -# the commit is aborted. -# -# To enable this hook, rename this file to "prepare-commit-msg". - -# This hook includes three examples. The first one removes the -# "# Please enter the commit message..." help message. -# -# The second includes the output of "git diff --name-status -r" -# into the message, just before the "git status" output. It is -# commented because it doesn't cope with --amend or with squashed -# commits. -# -# The third example adds a Signed-off-by line to the message, that can -# still be edited. This is rarely a good idea. - -COMMIT_MSG_FILE=$1 -COMMIT_SOURCE=$2 -SHA1=$3 - -/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" - -# case "$COMMIT_SOURCE,$SHA1" in -# ,|template,) -# /usr/bin/perl -i.bak -pe ' -# print "\n" . `git diff --cached --name-status -r` -# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; -# *) ;; -# esac - -# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" -# if test -z "$COMMIT_SOURCE" -# then -# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" -# fi diff --git a/tests/fixtures/library/test/law-html/hooks/update.sample b/tests/fixtures/library/test/law-html/hooks/update.sample deleted file mode 100644 index 80ba941..0000000 --- a/tests/fixtures/library/test/law-html/hooks/update.sample +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/sh -# -# An example hook script to block unannotated tags from entering. -# Called by "git receive-pack" with arguments: refname sha1-old sha1-new -# -# To enable this hook, rename this file to "update". -# -# Config -# ------ -# hooks.allowunannotated -# This boolean sets whether unannotated tags will be allowed into the -# repository. By default they won't be. -# hooks.allowdeletetag -# This boolean sets whether deleting tags will be allowed in the -# repository. By default they won't be. -# hooks.allowmodifytag -# This boolean sets whether a tag may be modified after creation. By default -# it won't be. -# hooks.allowdeletebranch -# This boolean sets whether deleting branches will be allowed in the -# repository. By default they won't be. -# hooks.denycreatebranch -# This boolean sets whether remotely creating branches will be denied -# in the repository. By default this is allowed. -# - -# --- Command line -refname="$1" -oldrev="$2" -newrev="$3" - -# --- Safety check -if [ -z "$GIT_DIR" ]; then - echo "Don't run this script from the command line." >&2 - echo " (if you want, you could supply GIT_DIR then run" >&2 - echo " $0 )" >&2 - exit 1 -fi - -if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -# --- Config -allowunannotated=$(git config --bool hooks.allowunannotated) -allowdeletebranch=$(git config --bool hooks.allowdeletebranch) -denycreatebranch=$(git config --bool hooks.denycreatebranch) -allowdeletetag=$(git config --bool hooks.allowdeletetag) -allowmodifytag=$(git config --bool hooks.allowmodifytag) - -# check for no description -projectdesc=$(sed -e '1q' "$GIT_DIR/description") -case "$projectdesc" in -"Unnamed repository"* | "") - echo "*** Project description file hasn't been set" >&2 - exit 1 - ;; -esac - -# --- Check types -# if $newrev is 0000...0000, it's a commit to delete a ref. -zero="0000000000000000000000000000000000000000" -if [ "$newrev" = "$zero" ]; then - newrev_type=delete -else - newrev_type=$(git cat-file -t $newrev) -fi - -case "$refname","$newrev_type" in - refs/tags/*,commit) - # un-annotated tag - short_refname=${refname##refs/tags/} - if [ "$allowunannotated" != "true" ]; then - echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 - echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 - exit 1 - fi - ;; - refs/tags/*,delete) - # delete tag - if [ "$allowdeletetag" != "true" ]; then - echo "*** Deleting a tag is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/tags/*,tag) - # annotated tag - if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 - then - echo "*** Tag '$refname' already exists." >&2 - echo "*** Modifying a tag is not allowed in this repository." >&2 - exit 1 - fi - ;; - refs/heads/*,commit) - # branch - if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then - echo "*** Creating a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/heads/*,delete) - # delete branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/remotes/*,commit) - # tracking branch - ;; - refs/remotes/*,delete) - # delete tracking branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a tracking branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - *) - # Anything else (is there anything else?) - echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 - exit 1 - ;; -esac - -# --- Finished -exit 0 diff --git a/tests/fixtures/library/test/law-html/objects/2c/cb5e590db7c6aba5166a600a9b18e9f9ec237c b/tests/fixtures/library/test/law-html/objects/2c/cb5e590db7c6aba5166a600a9b18e9f9ec237c deleted file mode 100644 index 20fe319..0000000 Binary files a/tests/fixtures/library/test/law-html/objects/2c/cb5e590db7c6aba5166a600a9b18e9f9ec237c and /dev/null differ diff --git a/tests/fixtures/library/test/law-html/objects/b6/4fe7f7ea66c117596363e1179e30892a40572c b/tests/fixtures/library/test/law-html/objects/b6/4fe7f7ea66c117596363e1179e30892a40572c deleted file mode 100644 index 44974b6..0000000 --- a/tests/fixtures/library/test/law-html/objects/b6/4fe7f7ea66c117596363e1179e30892a40572c +++ /dev/null @@ -1 +0,0 @@ -x}R=O0dpx!5]C L A%5v~{8*ǝ\&w2{zYnBkE9 d QpW<' >#Xw ԵJ!X [+QJIr\_ vG 'x%*.(-N0W0pb ( />vF8#kBĂ"JQzqF+ B GHB죤d9dYw d@ l4CJ#H&1Kj+m}ٓn]'Z|\5-eV E R?õ#! \ No newline at end of file diff --git a/tests/fixtures/library/test/law-html/objects/c5/3c5c71365e1d88752ed223b14f437d698c8806 b/tests/fixtures/library/test/law-html/objects/c5/3c5c71365e1d88752ed223b14f437d698c8806 deleted file mode 100644 index 82dea84..0000000 Binary files a/tests/fixtures/library/test/law-html/objects/c5/3c5c71365e1d88752ed223b14f437d698c8806 and /dev/null differ diff --git a/tests/fixtures/library/test/law-html/objects/info/commit-graph b/tests/fixtures/library/test/law-html/objects/info/commit-graph deleted file mode 100644 index 148a1c5..0000000 Binary files a/tests/fixtures/library/test/law-html/objects/info/commit-graph and /dev/null differ diff --git a/tests/fixtures/library/test/law-html/objects/info/packs b/tests/fixtures/library/test/law-html/objects/info/packs deleted file mode 100644 index 9ebd44d..0000000 --- a/tests/fixtures/library/test/law-html/objects/info/packs +++ /dev/null @@ -1,2 +0,0 @@ -P pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.pack - diff --git a/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.idx b/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.idx deleted file mode 100644 index 17a7ae2..0000000 Binary files a/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.idx and /dev/null differ diff --git a/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.pack b/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.pack deleted file mode 100644 index 8e06498..0000000 Binary files a/tests/fixtures/library/test/law-html/objects/pack/pack-0e5ff518cf56dc47a9481ab25de6b0fff831b5ce.pack and /dev/null differ diff --git a/tests/fixtures/library/test/law-html/packed-refs b/tests/fixtures/library/test/law-html/packed-refs deleted file mode 100644 index 0c62b69..0000000 --- a/tests/fixtures/library/test/law-html/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -ed782e08d119a580baa3067e2ea5df06f3d1cd05 refs/heads/main diff --git a/tests/fixtures/static_files/example.js b/tests/fixtures/static_files/example.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/static_files/example.json b/tests/fixtures/static_files/example.json new file mode 100644 index 0000000..6676f3b --- /dev/null +++ b/tests/fixtures/static_files/example.json @@ -0,0 +1 @@ +{ "retrieved": {"json": { "key": "value" } } } \ No newline at end of file diff --git a/tests/fixtures/static_files/example.pdf b/tests/fixtures/static_files/example.pdf new file mode 100644 index 0000000..6ec6ce0 Binary files /dev/null and b/tests/fixtures/static_files/example.pdf differ diff --git a/tests/fixtures/static_files/index.html b/tests/fixtures/static_files/index.html new file mode 100644 index 0000000..d8356a5 --- /dev/null +++ b/tests/fixtures/static_files/index.html @@ -0,0 +1,16 @@ + + + + + + index.html + + + + + +

index.html

+ +

+ + \ No newline at end of file diff --git a/tests/fixtures/static_files/index.rdf b/tests/fixtures/static_files/index.rdf new file mode 100644 index 0000000..05f598c --- /dev/null +++ b/tests/fixtures/static_files/index.rdf @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/fixtures/static_files/index.xml b/tests/fixtures/static_files/index.xml new file mode 100644 index 0000000..9c91740 --- /dev/null +++ b/tests/fixtures/static_files/index.xml @@ -0,0 +1,16 @@ + + + Law Library + + + + help@contact.org + + 2023-12-22 + + + Resolutions + + + + \ No newline at end of file diff --git a/tests/basic_test.rs b/tests/mod.rs similarity index 72% rename from tests/basic_test.rs rename to tests/mod.rs index 7093860..0c3f2d4 100644 --- a/tests/basic_test.rs +++ b/tests/mod.rs @@ -1,5 +1,7 @@ #![allow(clippy::pedantic)] #![allow(clippy::restriction)] +mod api; +mod archive_testtools; mod basic; mod common;