From 76bfcf961d25075c7c1fe4427420a6dbf3d27709 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sat, 12 Feb 2022 21:09:12 -0800 Subject: [PATCH 01/10] Refactor settings loading to use the Repository object to load config --- src/error.rs | 2 +- src/main.rs | 13 ++++++--- src/repository.rs | 24 ++++++++-------- src/settings.rs | 72 ++++++++++++++++++++++------------------------- src/wiki.rs | 8 ++---- 5 files changed, 60 insertions(+), 59 deletions(-) diff --git a/src/error.rs b/src/error.rs index bfe0166..635b947 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,7 +19,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/repository.rs b/src/repository.rs index c52d31a..c3e847e 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -23,12 +23,6 @@ pub trait Repository: std::fmt::Debug { #[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 { @@ -121,11 +115,19 @@ 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 }) +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 }) + unimplemented!("need to add git"); } } 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/wiki.rs b/src/wiki.rs index 9fde4a0..2905b87 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -13,7 +13,6 @@ 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::RepositoryItem; use crate::settings::Settings; @@ -178,13 +177,12 @@ fn highlight(snippet: Snippet) -> String { impl Wiki { pub fn new( settings: Settings, - repository: Box, + repository: RepoBox, ) -> Result { - let repo_box = RepoBox::new(repository); - let index = create_index(&settings, &repo_box)?; + let index = create_index(&settings, &repository)?; let inner = WikiInner { settings, - repository: repo_box, + repository, index, }; Ok(Wiki(Arc::from(inner))) From f63017deee4493debfd2c45a05879f2b5dffcd5f Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sat, 12 Feb 2022 22:38:01 -0800 Subject: [PATCH 02/10] Support reading from a git repo. --- .cargo/config | 4 +- Cargo.lock | 168 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/error.rs | 9 +++ src/repository.rs | 99 +++++++++++++++++++++++++-- src/wiki.rs | 9 ++- 6 files changed, 282 insertions(+), 8 deletions(-) diff --git a/.cargo/config b/.cargo/config index a470044..7b655dc 100644 --- a/.cargo/config +++ b/.cargo/config @@ -7,5 +7,5 @@ # 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"] +#[target.'cfg(all(target_env = "msvc"))'] +#rustflags = ["-C", "target-feature=+crt-static"] diff --git a/Cargo.lock b/Cargo.lock index da362a8..d58d3b7 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,21 @@ 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", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glob" version = "0.3.0" @@ -751,6 +779,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 +837,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 +873,46 @@ 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", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[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 +973,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" @@ -1036,6 +1130,25 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "os_str_bytes" version = "6.0.0" @@ -1120,6 +1233,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" @@ -1618,6 +1737,7 @@ version = "0.3.1" dependencies = [ "askama", "clap", + "git2", "log", "once_cell", "pretty_env_logger", @@ -1969,6 +2089,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 +2278,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 +2311,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 +2339,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..95bf7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ ring = "0.16.20" [dependencies] askama = "0.11" clap = { version = "3.0.0", features = ["derive"] } +git2 = "0.13" log = "0.4" once_cell = "1.9.0" pretty_env_logger = "0.4" diff --git a/src/error.rs b/src/error.rs index 635b947..212c03e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,15 @@ pub enum MyError { 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, #[error("io error")] diff --git a/src/repository.rs b/src/repository.rs index c3e847e..6c4910c 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -2,8 +2,11 @@ use std::{ io::{Read, Write}, ops::{Deref, DerefMut}, path::PathBuf, + sync::Mutex, }; +use git2::ObjectType; + use crate::error::MyError; //TODO: it is possible to use a borrowed string? Would that reduce copies? @@ -13,14 +16,13 @@ pub enum RepositoryItem { File(String), } -pub trait Repository: std::fmt::Debug { +pub trait Repository { fn read_file(&self, file_path: &[&str]) -> Result, MyError>; fn write_file(&self, file_path: &[&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 Deref for RepoBox { @@ -40,7 +42,6 @@ fn path_element_ok(element: &str) -> bool { !element.starts_with('.') } -#[derive(Debug)] struct FileSystemRepository { root_dir: PathBuf, } @@ -115,6 +116,96 @@ impl Repository for FileSystemRepository { } } +struct GitRepository { + 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 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], _content: &str) -> Result<(), MyError> { + todo!() + } + + 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 }); + } + }; + Ok(RepoBox(Box::new(GitRepository { + 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, @@ -128,6 +219,6 @@ pub fn create_repository(use_fs: bool, dir_path: PathBuf) -> Result); +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, From b90ef480b9001c749f5357fb503cc6bafbccde3b Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sat, 12 Feb 2022 23:15:00 -0800 Subject: [PATCH 03/10] Add a box to type a commit message. --- src/requests.rs | 33 ++++++++++++++++++++++----------- src/templates.rs | 3 +++ templates/edit_page.html | 30 +++++++++++++++++++----------- templates/layout.html | 5 ++++- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/requests.rs b/src/requests.rs index 7713d2d..3f1e441 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -61,18 +61,23 @@ 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) + 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>( @@ -164,17 +169,23 @@ fn edit_save( Ok(response::Redirect::to(uri!(page(path)))) } +fn default_edit_message(path: &WikiPagePath) -> String { + format!("Update {}", path.file_name().expect("Ill-formed path")) +} + #[get("/edit/")] fn edit_view(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 = default_edit_message(&path); let html = render_edit_page( - file_stem, + &title, &post_url.to_string(), &view_url.to_string(), + &message_placeholder, content, path.page_breadcrumbs(), )?; @@ -194,7 +205,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 +231,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 +351,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 +385,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/templates.rs b/src/templates.rs index ab0629b..427c48e 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: &'a str, content: &'a str, breadcrumbs: Vec>, } @@ -93,6 +94,7 @@ pub fn render_edit_page( title: &str, post_url: &str, view_url: &str, + message_placeholder: &str, 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/templates/edit_page.html b/templates/edit_page.html index 2dc0ba4..9847543 100644 --- a/templates/edit_page.html +++ b/templates/edit_page.html @@ -5,17 +5,25 @@ {% block content %}
-
- -
- +
+
+
-
- - Cancel +
+ +
+
+ +
+
+ + 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 %} From 7c758444170c2e20a989e853747823e630bcc006 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 10:06:31 -0800 Subject: [PATCH 04/10] Fix static linking with git2's C libraries. --- .cargo/config | 5 ----- .github/workflows/release.yml | 11 +++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.cargo/config b/.cargo/config index 7b655dc..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 From 7847ca462e75cb22ae1547c72f6dcbd9d343c3e7 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 10:33:00 -0800 Subject: [PATCH 05/10] Disable default features for Git. --- Cargo.lock | 37 ------------------------------------- Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d58d3b7..9c94f9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,8 +643,6 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", - "openssl-sys", "url", ] @@ -881,26 +879,10 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", - "libssh2-sys", "libz-sys", - "openssl-sys", "pkg-config", ] -[[package]] -name = "libssh2-sys" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - [[package]] name = "libz-sys" version = "1.1.3" @@ -1130,25 +1112,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "os_str_bytes" version = "6.0.0" diff --git a/Cargo.toml b/Cargo.toml index 95bf7fc..d480ae0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ ring = "0.16.20" [dependencies] askama = "0.11" clap = { version = "3.0.0", features = ["derive"] } -git2 = "0.13" +git2 = { version = "0.13", default-features = false } log = "0.4" once_cell = "1.9.0" pretty_env_logger = "0.4" From 8ddf5d2437e26b5172b20bb42f4736893c6a3c86 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 15:33:15 -0800 Subject: [PATCH 06/10] Write file and update Git index. --- Cargo.lock | 1 + Cargo.toml | 1 + src/error.rs | 12 ++++---- src/repository.rs | 59 +++++++++++++++++++++++++++++++++++----- src/requests.rs | 44 ++++++++++++++++++++++++++---- src/templates.rs | 4 +-- src/wiki.rs | 19 +++++++++---- templates/edit_page.html | 8 ++++-- 8 files changed, 118 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c94f9e..6270fee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1699,6 +1699,7 @@ name = "smeagol-wiki" version = "0.3.1" dependencies = [ "askama", + "bitflags", "clap", "git2", "log", diff --git a/Cargo.toml b/Cargo.toml index d480ae0..7c7f7ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ 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" diff --git a/src/error.rs b/src/error.rs index 212c03e..a3f59b7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,18 +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 - }, + GitRepoDoesFailedToOpen { err: git2::Error }, #[error("Error when performing git operation: {source}")] GitError { #[from] - source: git2::Error + source: git2::Error, }, #[error("Path is not valid.")] InvalidPath, diff --git a/src/repository.rs b/src/repository.rs index 6c4910c..d9cee8d 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,14 +1,22 @@ use std::{ io::{Read, Write}, ops::{Deref, DerefMut}, - path::PathBuf, + path::{Path, PathBuf}, sync::Mutex, }; -use git2::ObjectType; +use bitflags::bitflags; +use git2::Index; +use git2::{IndexEntry, IndexTime, 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 { @@ -17,8 +25,9 @@ pub enum RepositoryItem { } 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>; } @@ -65,6 +74,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)?; @@ -73,7 +86,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())?; @@ -117,6 +130,7 @@ impl Repository for FileSystemRepository { } struct GitRepository { + path: PathBuf, repo: Mutex, } @@ -144,6 +158,10 @@ fn get_git_dir<'repo>( } 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, @@ -168,8 +186,31 @@ impl Repository for GitRepository { } } - fn write_file(&self, _file_path: &[&str], _content: &str) -> Result<(), MyError> { - todo!() + 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("/"); + + let repo = self.repo.lock().unwrap(); + + 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)?; + + let mut index = repo.index()?; + index.add_path(&Path::new(&file_path))?; + index.write()?; + + Ok(()) } fn directory_exists(&self, path: &[&str]) -> Result { @@ -195,13 +236,17 @@ impl Repository for GitRepository { } pub fn create_git_repository(dir_path: PathBuf) -> Result { - let repo = match git2::Repository::open(dir_path) { + 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), }))) } diff --git a/src/requests.rs b/src/requests.rs index 3f1e441..3559c8d 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::{ @@ -157,6 +160,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 = "")] @@ -165,33 +184,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) } fn default_edit_message(path: &WikiPagePath) -> String { format!("Update {}", path.file_name().expect("Ill-formed path")) } -#[get("/edit/")] -fn edit_view(path: WikiPagePath, w: Wiki) -> Result, MyError> { +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 title = format!("Editing {}", path.file_name().expect("Ill-formed path")); - let message_placeholder = default_edit_message(&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( &title, &post_url.to_string(), &view_url.to_string(), - &message_placeholder, + 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, diff --git a/src/templates.rs b/src/templates.rs index 427c48e..396d65c 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -85,7 +85,7 @@ struct EditTemplate<'a> { title: &'a str, post_url: &'a str, view_url: &'a str, - message_placeholder: &'a str, + message_placeholder: Option, content: &'a str, breadcrumbs: Vec>, } @@ -94,7 +94,7 @@ pub fn render_edit_page( title: &str, post_url: &str, view_url: &str, - message_placeholder: &str, + message_placeholder: Option, content: &str, breadcrumbs: Vec>, ) -> askama::Result { diff --git a/src/wiki.rs b/src/wiki.rs index 71c976a..0c9e749 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -13,6 +13,7 @@ use crate::error::MyError; use crate::page::get_raw_page; use crate::page::is_page; use crate::repository::RepoBox; +use crate::repository::RepositoryCapability; use crate::repository::RepositoryItem; use crate::settings::Settings; @@ -180,10 +181,7 @@ fn highlight(snippet: Snippet) -> String { } impl Wiki { - pub fn new( - settings: Settings, - repository: RepoBox, - ) -> Result { + pub fn new(settings: Settings, repository: RepoBox) -> Result { let index = create_index(&settings, &repository)?; let inner = WikiInner { settings, @@ -197,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 9847543..1c25c77 100644 --- a/templates/edit_page.html +++ b/templates/edit_page.html @@ -16,8 +16,12 @@
- + {% match message_placeholder %} + {% when Some with (val) %} + + {% when None %} + + {% endmatch %}
From 60a6f86525564bc53d8570206ec770f17deb3bb1 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 15:37:06 -0800 Subject: [PATCH 07/10] Write to repository --- build.rs | 6 ++++-- src/page.rs | 7 +------ src/repository.rs | 17 ++++++++++++----- src/requests.rs | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) 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/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 d9cee8d..b4443a3 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -6,8 +6,7 @@ use std::{ }; use bitflags::bitflags; -use git2::Index; -use git2::{IndexEntry, IndexTime, ObjectType}; +use git2::ObjectType; use crate::error::MyError; @@ -173,7 +172,7 @@ impl Repository for GitRepository { let repo = self.repo.lock().unwrap(); let root = get_git_dir(&repo, file_paths)?; - let file_obj = match root.get_name(&filename) { + let file_obj = match root.get_name(filename) { Some(te) => te.to_object(&repo)?, None => { return Err(MyError::InvalidPath); @@ -193,7 +192,12 @@ impl Repository for GitRepository { 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); @@ -206,9 +210,12 @@ impl Repository for GitRepository { // This would support bare Git repos. std::fs::write(&path, content)?; - let mut index = repo.index()?; - index.add_path(&Path::new(&file_path))?; + 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(()) } diff --git a/src/requests.rs b/src/requests.rs index 3559c8d..6175726 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -74,7 +74,7 @@ impl<'r> WikiPagePath<'r> { file_name.rsplit_once('.') } - fn file_stem(&self) -> Option<&str> { + fn _file_stem(&self) -> Option<&str> { Some(self.file_stem_and_extension()?.0) } From 9e74ffbaaf03ac64622529db22c3f994a1241c8e Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 15:39:47 -0800 Subject: [PATCH 08/10] Update change log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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. From 4769a8342a6719ffa7bafec8be979a251d230e43 Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 15:47:48 -0800 Subject: [PATCH 09/10] Fix tests --- src/requests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/requests.rs b/src/requests.rs index 6175726..94d3465 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -74,7 +74,8 @@ impl<'r> WikiPagePath<'r> { file_name.rsplit_once('.') } - fn _file_stem(&self) -> Option<&str> { + #[cfg(test)] + fn file_stem(&self) -> Option<&str> { Some(self.file_stem_and_extension()?.0) } From 82635174157aff48938818b13755f17c1c13833d Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 13 Feb 2022 17:04:46 -0800 Subject: [PATCH 10/10] Document fs flag --- README.md | 4 ++++ 1 file changed, 4 insertions(+) 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