diff --git a/.cargo/config b/.cargo/config index a470044..ef0df66 100644 --- a/.cargo/config +++ b/.cargo/config @@ -4,8 +4,3 @@ #rustflags = ["-C", "link-arg=-fuse-ld=lld"] #[target.aarch64-unknown-linux-gnu] #rustflags = ["-C", "link-arg=-fuse-ld=lld"] - -# Statically link the MSVC C Runtime on Windows, so the EXEs can run without -# installing the C Runtime DLL. -[target.'cfg(all(target_env = "msvc"))'] -rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 122c264..ddbbc8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,6 +101,17 @@ jobs: use-cross: false steps: + + # Build all crates with a statically linked MSVCRT. Specifically crates + # that use `cc` in their build scripts (like libgit2-sys), will detect this + # and compile objects appropriatly. If there is a way to put this into a + # Cargo config file some where, let me know. .cargo/config did not work. + - name: Statically link MSVCRT + shell: bash + if: matrix.os == 'windows' + run: | + echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV + - name: Checkout repository uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae1f00..7630a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ WIP === +* Support reading files from a Git repo [#34](https://github.com/AustinWise/smeagol/issues/34) +* Support writing files to a Git repo [#35](https://github.com/AustinWise/smeagol/issues/35) * Rename the settings in `smeagol.toml` to use kebob-case, rather than snake_case. Specifically `h1_title` was renamed to `h1-title` and `index_page` was renamed to `index-page`. This matches `Cargo.toml`'s use of kebob-case. diff --git a/Cargo.lock b/Cargo.lock index da362a8..6270fee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,9 @@ name = "cc" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +dependencies = [ + "jobserver", +] [[package]] name = "census" @@ -485,6 +488,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fs2" version = "0.4.3" @@ -620,6 +633,19 @@ dependencies = [ "wasi 0.10.0+wasi-snapshot-preview1", ] +[[package]] +name = "git2" +version = "0.13.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glob" version = "0.3.0" @@ -751,6 +777,17 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -798,6 +835,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.55" @@ -825,6 +871,30 @@ version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +[[package]] +name = "libgit2-sys" +version = "0.12.26+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -885,6 +955,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "measure_time" version = "0.7.0" @@ -1120,6 +1196,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1617,7 +1699,9 @@ name = "smeagol-wiki" version = "0.3.1" dependencies = [ "askama", + "bitflags", "clap", + "git2", "log", "once_cell", "pretty_env_logger", @@ -1969,6 +2053,21 @@ dependencies = [ "syn", ] +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.15.0" @@ -2143,6 +2242,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -2161,6 +2275,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "utf8-ranges" version = "1.0.4" @@ -2177,6 +2303,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index b7b572f..7c7f7ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ ring = "0.16.20" [dependencies] askama = "0.11" +bitflags = "1.3.2" clap = { version = "3.0.0", features = ["derive"] } +git2 = { version = "0.13", default-features = false } log = "0.4" once_cell = "1.9.0" pretty_env_logger = "0.4" diff --git a/README.md b/README.md index be32f3d..1834473 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ There are a few command line options: access it. * `--port` - takes an argument that specifies which port to listen on. `8000` by default. +* `--fs` - instructs Smeagol to load and save using the file system. By default + Smeagol uses Git to load files committed to a Git repository and saves them by + committing them to the current branch. Additionally, the following settings can be put in a `smeagol.toml` file in the root directory of the wiki: @@ -63,6 +66,7 @@ root directory of the wiki: * `index-page` on Gollum defaults to `Home`. Smeagol defaults to `README` to be compatible with online code hosting systems such as GitHub and Azure Devops. +* The default port is `8000` rather than `4567`. ## Why Rust, please tell me more about why you love Rust diff --git a/build.rs b/build.rs index f1ebe0a..f43e918 100644 --- a/build.rs +++ b/build.rs @@ -97,7 +97,10 @@ impl EmbeddedFile { } fn rebuild_on_file_change(context: &mut ContextWriter, path: &Path) -> Result<(), io::Error> { - println!("cargo:rerun-if-changed={}", path.canonicalize()?.to_str().unwrap()); + println!( + "cargo:rerun-if-changed={}", + path.canonicalize()?.to_str().unwrap() + ); let mut file = File::open(path)?; io::copy(&mut file, context)?; Ok(()) @@ -146,6 +149,5 @@ fn main() -> Result<(), Box> { println!("cargo:rustc-link-arg-bins=/MANIFESTINPUT:windows_manifest.xml"); } - Ok(()) } diff --git a/src/error.rs b/src/error.rs index bfe0166..a3f59b7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,9 +2,16 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { + #[error("Bare Git repos are not yet supported.")] + BareGitRepo, #[error("This is not valid Wiki folder: {path}")] - GitRepoDoesNotExist { - path: std::path::PathBuf + GitRepoDoesNotExist { path: std::path::PathBuf }, + #[error("Failed to open Git repo: {err}")] + GitRepoDoesFailedToOpen { err: git2::Error }, + #[error("Error when performing git operation: {source}")] + GitError { + #[from] + source: git2::Error, }, #[error("Path is not valid.")] InvalidPath, @@ -19,7 +26,7 @@ pub enum MyError { source: askama::Error, }, #[error("Failed to read config file.")] - ConfigReadError { source: std::io::Error }, + ConfigReadError { source: Box }, #[error("Failed to parse config file.")] ConfigParseError { #[from] diff --git a/src/main.rs b/src/main.rs index eb160e5..c4773cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,9 @@ mod settings; mod templates; mod wiki; +use clap::StructOpt; use error::MyError; -use repository::create_file_system_repository; +use repository::create_repository; use settings::parse_settings_from_args; use wiki::Wiki; @@ -21,9 +22,13 @@ use rocket::request::{FromRequest, Outcome, Request}; static WIKI: OnceCell = OnceCell::new(); fn create_wiki() -> Result { - let settings = parse_settings_from_args()?; - let repo = create_file_system_repository(settings.git_repo().clone())?; - Wiki::new(settings, Box::new(repo)) + let args = settings::Args::parse(); + let git_repo = args + .git_repo() + .unwrap_or_else(|| std::env::current_dir().unwrap()); + let repo = create_repository(args.use_fs(), git_repo)?; + let settings = parse_settings_from_args(args, &repo)?; + Wiki::new(settings, repo) } #[rocket::async_trait] diff --git a/src/page.rs b/src/page.rs index a6f0b2a..3e0bbd3 100644 --- a/src/page.rs +++ b/src/page.rs @@ -99,12 +99,7 @@ impl MarkupLanguage { } } - fn raw>( - &self, - file_stem: &str, - file_contents: S, - settings: &Settings, - ) -> Page { + fn raw>(&self, file_stem: &str, file_contents: S, settings: &Settings) -> Page { let file_contents: String = file_contents.into(); match self { MarkupLanguage::Markdown => { diff --git a/src/repository.rs b/src/repository.rs index c52d31a..b4443a3 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,11 +1,21 @@ use std::{ io::{Read, Write}, ops::{Deref, DerefMut}, - path::PathBuf, + path::{Path, PathBuf}, + sync::Mutex, }; +use bitflags::bitflags; +use git2::ObjectType; + use crate::error::MyError; +bitflags! { + pub struct RepositoryCapability: u32 { + const SUPPORTS_EDIT_MESSAGE = 0b00000001; + } +} + //TODO: it is possible to use a borrowed string? Would that reduce copies? #[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] pub enum RepositoryItem { @@ -13,22 +23,16 @@ pub enum RepositoryItem { File(String), } -pub trait Repository: std::fmt::Debug { +pub trait Repository { + fn capabilities(&self) -> RepositoryCapability; fn read_file(&self, file_path: &[&str]) -> Result, MyError>; - fn write_file(&self, file_path: &[&str], content: &str) -> Result<(), MyError>; + fn write_file(&self, file_path: &[&str], message: &str, content: &str) -> Result<(), MyError>; fn directory_exists(&self, path: &[&str]) -> Result; fn enumerate_files(&self, directory: &[&str]) -> Result, MyError>; } -#[derive(Debug)] pub struct RepoBox(Box); -impl RepoBox { - pub fn new(repo: Box) -> Self { - Self(repo) - } -} - impl Deref for RepoBox { type Target = dyn Repository + Sync + Send; fn deref(&self) -> &Self::Target { @@ -46,7 +50,6 @@ fn path_element_ok(element: &str) -> bool { !element.starts_with('.') } -#[derive(Debug)] struct FileSystemRepository { root_dir: PathBuf, } @@ -70,6 +73,10 @@ impl FileSystemRepository { } impl Repository for FileSystemRepository { + fn capabilities(&self) -> RepositoryCapability { + RepositoryCapability::empty() + } + fn read_file(&self, file_path: &[&str]) -> Result, MyError> { let path = self.canonicalize_path(file_path)?; let mut f = std::fs::File::open(path)?; @@ -78,7 +85,7 @@ impl Repository for FileSystemRepository { Ok(buf) } - fn write_file(&self, file_path: &[&str], content: &str) -> Result<(), MyError> { + fn write_file(&self, file_path: &[&str], _message: &str, content: &str) -> Result<(), MyError> { let path = self.canonicalize_path(file_path)?; let mut f = std::fs::File::create(path)?; f.write_all(content.as_bytes())?; @@ -121,11 +128,149 @@ impl Repository for FileSystemRepository { } } -pub fn create_file_system_repository(dir_path: PathBuf) -> Result { - let root_dir = dir_path.canonicalize()?; - if root_dir.is_dir() { - Ok(FileSystemRepository { root_dir }) +struct GitRepository { + path: PathBuf, + repo: Mutex, +} + +fn get_git_dir<'repo>( + repo: &'repo std::sync::MutexGuard, + file_paths: &[&str], +) -> Result, MyError> { + let head = repo.head()?; + let mut root = head.peel_to_tree()?; + for path in file_paths { + let obj = match root.get_name(path) { + Some(te) => te.to_object(repo)?, + None => { + return Err(MyError::InvalidPath); + } + }; + root = match obj.as_tree() { + Some(tree) => tree.to_owned(), + None => { + return Err(MyError::InvalidPath); + } + }; + } + Ok(root) +} + +impl Repository for GitRepository { + fn capabilities(&self) -> RepositoryCapability { + RepositoryCapability::SUPPORTS_EDIT_MESSAGE + } + + fn read_file(&self, file_path: &[&str]) -> Result, MyError> { + let (filename, file_paths) = match file_path.split_last() { + Some(tup) => tup, + None => { + return Err(MyError::InvalidPath); + } + }; + + let repo = self.repo.lock().unwrap(); + let root = get_git_dir(&repo, file_paths)?; + + let file_obj = match root.get_name(filename) { + Some(te) => te.to_object(&repo)?, + None => { + return Err(MyError::InvalidPath); + } + }; + + match file_obj.as_blob() { + Some(b) => Ok(b.content().to_owned()), + None => Err(MyError::InvalidPath), + } + } + + fn write_file(&self, file_path: &[&str], message: &str, content: &str) -> Result<(), MyError> { + if file_path.is_empty() { + return Err(MyError::InvalidPath); + } + + let file_path = file_path.join("/"); + + // Get as many Git objects ready before writing the file. + let repo = self.repo.lock().unwrap(); + let mut index = repo.index()?; + let sig = repo.signature()?; + let head = repo.head()?; + let head_commit = head.peel_to_commit()?; + + let mut path = self.path.clone(); + path.push(&file_path); + let path = path.canonicalize()?; + if !path.starts_with(&self.path) { + return Err(MyError::InvalidPath); + } + + // TODO: maybe write the file directly into the index or as a tree. + // This would support bare Git repos. + std::fs::write(&path, content)?; + + index.add_path(Path::new(&file_path))?; + index.write()?; + let tree = index.write_tree()?; + let tree = repo.find_tree(tree)?; + + repo.commit(head.name(), &sig, &sig, message, &tree, &[&head_commit])?; + + Ok(()) + } + + fn directory_exists(&self, path: &[&str]) -> Result { + let repo = self.repo.lock().unwrap(); + let ret = get_git_dir(&repo, path).is_ok(); + Ok(ret) + } + + fn enumerate_files(&self, directory: &[&str]) -> Result, MyError> { + let repo = self.repo.lock().unwrap(); + let tree = get_git_dir(&repo, directory)?; + Ok(tree + .into_iter() + .filter_map(|te| match te.kind() { + Some(ObjectType::Blob) => Some(RepositoryItem::File(te.name().unwrap().to_owned())), + Some(ObjectType::Tree) => { + Some(RepositoryItem::Directory(te.name().unwrap().to_owned())) + } + _ => None, + }) + .collect()) + } +} + +pub fn create_git_repository(dir_path: PathBuf) -> Result { + let repo = match git2::Repository::open(&dir_path) { + Ok(repo) => repo, + Err(err) => { + return Err(MyError::GitRepoDoesFailedToOpen { err }); + } + }; + if repo.is_bare() { + return Err(MyError::BareGitRepo); + } + Ok(RepoBox(Box::new(GitRepository { + path: dir_path, + repo: Mutex::new(repo), + }))) +} + +pub fn create_repository(use_fs: bool, dir_path: PathBuf) -> Result { + let root_dir = match dir_path.canonicalize() { + Ok(dir) => dir, + Err(_) => { + return Err(MyError::GitRepoDoesNotExist { path: dir_path }); + } + }; + if !root_dir.is_dir() { + return Err(MyError::GitRepoDoesNotExist { path: root_dir }); + } + if use_fs { + Ok(RepoBox(Box::new(FileSystemRepository { root_dir }))) } else { - Err(MyError::GitRepoDoesNotExist { path: root_dir }) + create_git_repository(root_dir) } } diff --git a/src/requests.rs b/src/requests.rs index 7713d2d..94d3465 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use rocket::form::Form; use rocket::http::impl_from_uri_param_identity; use rocket::http::uri::fmt::Formatter; @@ -13,6 +15,7 @@ use rocket::{Build, Rocket}; use crate::error::MyError; use crate::repository; +use crate::repository::RepositoryCapability; use crate::templates; use crate::templates::render_search_results; use crate::templates::{ @@ -61,18 +64,24 @@ impl<'r> WikiPagePath<'r> { } } - fn file_name_and_extension(&self) -> Option<(&str, &str)> { + fn file_name(&self) -> Option<&str> { + let (file_name, _) = self.segments.split_last()?; + Some(file_name) + } + + fn file_stem_and_extension(&self) -> Option<(&str, &str)> { let (file_name, _) = self.segments.split_last()?; file_name.rsplit_once('.') } - fn file_name(&self) -> Option<&str> { - Some(self.file_name_and_extension()?.0) + #[cfg(test)] + fn file_stem(&self) -> Option<&str> { + Some(self.file_stem_and_extension()?.0) } #[cfg(test)] fn file_extension(&self) -> Option<&str> { - Some(self.file_name_and_extension()?.1) + Some(self.file_stem_and_extension()?.1) } fn breadcrumbs_helper Origin>( @@ -152,6 +161,22 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for MyError { #[derive(FromForm)] struct PageEditForm<'r> { content: &'r str, + message: &'r str, +} + +fn edit_save_inner( + path: WikiPagePath, + content: Form>, + w: Wiki, +) -> Result { + let message = if content.message.trim().is_empty() { + let message = default_edit_message(&path); + Cow::Owned(message) + } else { + Cow::Borrowed(content.message.trim()) + }; + w.write_file(&path.segments, &message, content.content)?; + Ok(response::Redirect::to(uri!(page(path)))) } #[post("/edit/", data = "")] @@ -160,27 +185,46 @@ fn edit_save( content: Form>, w: Wiki, ) -> Result { - w.write_file(&path.segments, content.content)?; - Ok(response::Redirect::to(uri!(page(path)))) + edit_save_inner(path, content, w) } -#[get("/edit/")] -fn edit_view(path: WikiPagePath, w: Wiki) -> Result, MyError> { +fn default_edit_message(path: &WikiPagePath) -> String { + format!("Update {}", path.file_name().expect("Ill-formed path")) +} + +fn edit_view_inner( + path: WikiPagePath, + w: Wiki, +) -> Result, MyError> { let content = w.read_file(&path.segments).unwrap_or_else(|_| vec![]); let content = std::str::from_utf8(&content)?; let post_url = uri!(edit_save(&path)); let view_url = uri!(page(&path)); - let file_stem = path.file_name().expect("Ill-formed path"); + let title = format!("Editing {}", path.file_name().expect("Ill-formed path")); + let message_placeholder = if w + .repo_capabilities() + .contains(RepositoryCapability::SUPPORTS_EDIT_MESSAGE) + { + Some(default_edit_message(&path)) + } else { + None + }; let html = render_edit_page( - file_stem, + &title, &post_url.to_string(), &view_url.to_string(), + message_placeholder, content, path.page_breadcrumbs(), )?; Ok(response::content::Html(html)) } +#[get("/edit/")] +fn edit_view(path: WikiPagePath, w: Wiki) -> Result, MyError> { + edit_view_inner(path, w) +} + fn page_response( page: crate::page::Page, path: &WikiPagePath, @@ -194,7 +238,7 @@ fn page_response( fn page_inner(path: WikiPagePath, w: Wiki) -> Result { match w.read_file(&path.segments) { Ok(bytes) => { - let file_info = path.file_name_and_extension(); + let file_info = path.file_stem_and_extension(); Ok(match file_info { Some((file_stem, file_ext)) => { match crate::page::get_page(file_stem, file_ext, &bytes, w.settings())? { @@ -220,7 +264,7 @@ fn page_inner(path: WikiPagePath, w: Wiki) -> Result page(path) )))) } else { - match path.file_name_and_extension() { + match path.file_stem_and_extension() { Some((file_stem, "md")) => { let create_url = uri!(edit_view(&path)); Ok(WikiPageResponder::PagePlaceholder( @@ -340,7 +384,7 @@ mod tests { let parsed = WikiPagePath::from_slice(input); assert_eq!( Some(expected_file_stem), - parsed.file_name(), + parsed.file_stem(), "Unexpected file_stem while parsing request: {:?}", input ); @@ -374,11 +418,11 @@ mod tests { fn test_request_path_parse_unsupported() { let empty = WikiPagePath::new(vec![]); assert!(empty.directories().is_empty()); - assert!(empty.file_name_and_extension().is_none()); + assert!(empty.file_stem_and_extension().is_none()); let extensionless_file = WikiPagePath::new(vec!["README"]); assert!(extensionless_file.directories().is_empty()); - assert!(extensionless_file.file_name_and_extension().is_none()); + assert!(extensionless_file.file_stem_and_extension().is_none()); } #[test] diff --git a/src/settings.rs b/src/settings.rs index 93a0ca6..81e29bc 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,16 +1,15 @@ use std::default::Default; -use std::fs::canonicalize; use std::net::{IpAddr, Ipv4Addr}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use clap::Parser; use serde::Deserialize; use crate::error::MyError; +use crate::repository::{RepoBox, RepositoryItem}; #[derive(clap::Parser, Debug, Clone)] #[clap(about, version, author)] -struct Args { +pub struct Args { /// Path to the directory containing the wiki Git repository. #[clap(parse(from_os_str))] git_repo: Option, @@ -20,6 +19,19 @@ struct Args { /// The TCP Port to bind to. Defaults to 8000 #[clap(long)] port: Option, + /// Use the file system to read the wiki, not Git. + #[clap(long)] + fs: bool, +} + +impl Args { + pub fn git_repo(&self) -> Option { + self.git_repo.clone() + } + + pub fn use_fs(&self) -> bool { + self.fs + } } #[derive(Default, Deserialize)] @@ -34,7 +46,6 @@ struct Config { #[derive(Debug, Clone)] pub struct Settings { - git_repo: PathBuf, index_page: String, h1_title: bool, host: IpAddr, @@ -45,7 +56,6 @@ impl Settings { #[cfg(test)] pub(crate) fn new(index_page: &str, h1_title: bool) -> Settings { Settings { - git_repo: PathBuf::new(), index_page: index_page.to_owned(), h1_title, host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -53,10 +63,6 @@ impl Settings { } } - pub fn git_repo(&self) -> &PathBuf { - &self.git_repo - } - pub fn index_page(&self) -> &str { &self.index_page } @@ -74,43 +80,33 @@ impl Settings { } } -fn load_config(git_repo: &Path) -> Result { - let mut config_path = git_repo.to_path_buf(); - config_path.push("smeagol.toml"); - if config_path.is_file() { - match std::fs::read_to_string(config_path) { - Ok(config_str) => Ok(toml::from_str(&config_str)?), - Err(err) => Err(MyError::ConfigReadError { source: err }), - } - } else { - Ok(Default::default()) - } -} +fn load_config(repo: &RepoBox) -> Result { + const CONFIG_FILE_NAME: &str = "smeagol.toml"; -pub fn parse_settings_from_args() -> Result { - let args = Args::parse(); - - let git_repo = if let Some(dir) = args.git_repo { - dir - } else { - std::env::current_dir()? + if !repo.enumerate_files(&[])?.iter().any(|f| match f { + RepositoryItem::File(name) => name == CONFIG_FILE_NAME, + _ => false, + }) { + return Ok(Default::default()); }; - let git_repo = match canonicalize(&git_repo) { - Ok(r) => r, - Err(_) => { - return Err(MyError::GitRepoDoesNotExist { path: git_repo }); + let file_contents = match repo.read_file(&[CONFIG_FILE_NAME]) { + Ok(bytes) => bytes, + Err(err) => { + return Err(MyError::ConfigReadError { + source: Box::new(err), + }) } }; + let config_str = std::str::from_utf8(&file_contents)?; - if !git_repo.is_dir() { - return Err(MyError::GitRepoDoesNotExist { path: git_repo }); - } + Ok(toml::from_str(config_str)?) +} - let config = load_config(&git_repo)?; +pub fn parse_settings_from_args(args: Args, repo: &RepoBox) -> Result { + let config = load_config(repo)?; let ret = Settings { - git_repo, index_page: config.index_page.unwrap_or_else(|| "README".into()), h1_title: config.h1_title.unwrap_or(false), host: args diff --git a/src/templates.rs b/src/templates.rs index ab0629b..396d65c 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -85,6 +85,7 @@ struct EditTemplate<'a> { title: &'a str, post_url: &'a str, view_url: &'a str, + message_placeholder: Option, content: &'a str, breadcrumbs: Vec>, } @@ -93,6 +94,7 @@ pub fn render_edit_page( title: &str, post_url: &str, view_url: &str, + message_placeholder: Option, content: &str, breadcrumbs: Vec>, ) -> askama::Result { @@ -104,6 +106,7 @@ pub fn render_edit_page( title, post_url, view_url, + message_placeholder, content, breadcrumbs, }; diff --git a/src/wiki.rs b/src/wiki.rs index 9fde4a0..0c9e749 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -13,12 +13,11 @@ use crate::error::MyError; use crate::page::get_raw_page; use crate::page::is_page; use crate::repository::RepoBox; -use crate::repository::Repository; +use crate::repository::RepositoryCapability; use crate::repository::RepositoryItem; use crate::settings::Settings; /// Wiki god object. -#[derive(Debug)] struct WikiInner { settings: Settings, repository: RepoBox, @@ -26,9 +25,15 @@ struct WikiInner { } // TODO: is there are away to share immutable global without the reference counting? A 'static lifetime somehow? -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Wiki(Arc); +impl std::fmt::Debug for Wiki { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Wiki").finish() + } +} + struct SearchFields { title: Field, path: Field, @@ -176,15 +181,11 @@ fn highlight(snippet: Snippet) -> String { } impl Wiki { - pub fn new( - settings: Settings, - repository: Box, - ) -> Result { - let repo_box = RepoBox::new(repository); - let index = create_index(&settings, &repo_box)?; + pub fn new(settings: Settings, repository: RepoBox) -> Result { + let index = create_index(&settings, &repository)?; let inner = WikiInner { settings, - repository: repo_box, + repository, index, }; Ok(Wiki(Arc::from(inner))) @@ -194,12 +195,21 @@ impl Wiki { &self.0.settings } + pub fn repo_capabilities(&self) -> RepositoryCapability { + self.0.repository.capabilities() + } + pub fn read_file(&self, file_path: &[&str]) -> Result, MyError> { self.0.repository.read_file(file_path) } - pub fn write_file(&self, file_path: &[&str], content: &str) -> Result<(), MyError> { - self.0.repository.write_file(file_path, content) + pub fn write_file( + &self, + file_path: &[&str], + message: &str, + content: &str, + ) -> Result<(), MyError> { + self.0.repository.write_file(file_path, message, content) } pub fn directory_exists(&self, path: &[&str]) -> Result { diff --git a/templates/edit_page.html b/templates/edit_page.html index 2dc0ba4..1c25c77 100644 --- a/templates/edit_page.html +++ b/templates/edit_page.html @@ -5,17 +5,29 @@ {% block content %}
-
- -
- +
+
+
-
- - Cancel +
+ +
+
+ {% match message_placeholder %} + {% when Some with (val) %} + + {% when None %} + + {% endmatch %} +
+
+ + Cancel +
+ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 4f35f4e..2c5e9cc 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -31,11 +31,14 @@ } .edit_box textarea { - margin: 1em; font-family: monospace; width: 100%; min-height: 500px; } + + fieldset div { + margin-bottom: 1em; + } {% block extra_scripts %} {% endblock %}