- Commit
- 92f78f9492e7b7fff5a1ccc9c879c964849766cd
- Parent
- c977f39cc20fe1f0fc37f8dfce8e8585f7f8069e
- Author
- Pablo <pablo-pie@riseup.net>
- Date
Modified the CLI
Made it so that REPOS_DIR is a compile-time config key instead of a command-line argument
Yet another static site generator for Git 🙀️
Modified the CLI
Made it so that REPOS_DIR is a compile-time config key instead of a command-line argument
4 files changed, 246 insertions, 237 deletions
Status | Name | Changes | Insertions | Deletions |
Modified | README.md | 2 files changed | 6 | 13 |
Added | src/command.rs | 1 file changed | 112 | 0 |
Added | src/config.rs | 1 file changed | 38 | 0 |
Modified | src/main.rs | 2 files changed | 90 | 224 |
diff --git a/README.md b/README.md @@ -25,26 +25,19 @@ application to adapt it for their own needs. To render the HTML pages for a single repository using yagit run: ```console -$ yagit render REPO_PATH OUTPUT_PATH +$ yagit render REPO_NAME ``` -The argument `REPO_PATH` should have the form `PARENT_PATH/REPO_NAME`, where -`PARENT_PATH` is the path to the parent directory of `REPO_PATH`. yagit will -generate the HTML pages for `REPO_PATH` at `OUTPUT_PATH/REPO_NAME`. yagit will -also generate an index of all git repositories in `PARENT_PATH` at -`OUTPUT_PATH/index.html`. +yagit will generate the HTML pages for `REPOS_DIR/REPO_NAME` at +`OUTPUT_PATH/REPO_NAME`. yagit will also generate an index of all git +repositories in `REPOS_DIR` at `OUTPUT_PATH/index.html`. -To render HTML pages for all repositories in a given directory in batch mode -run: +To render HTML pages for all repositories at `REPOS_DIR` run: ```console -$ yagit render-batch BATCH_PATH OUTPUT_PATH +$ yagit render-batch ``` -yagit will generate the HTML pages for `BATCH_PATH/REPO_NAME` at -`OUTPUT_PATH/REPO_NAME`, as well as an index of all git repositories in -`BATCH_PATH` at `OUTPUT_PATH/index.html`. - ## Installation yagit can be installed via Cargo by cloning this repository, as in:
diff --git /dev/null b/src/command.rs @@ -0,0 +1,112 @@ +use std::{env, ops::BitOrAssign}; +use crate::log; + +#[derive(Clone, Debug)] +pub struct Cmd { + pub sub_cmd: SubCmd, + pub flags: Flags, +} + +#[derive(Clone, Debug)] +pub enum SubCmd { + RenderBatch, + Render { + repo_name: String, + }, +} + +impl Cmd { + pub fn parse() -> Result<(Self, String), ()> { + let mut args = env::args(); + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum CmdTag { + RenderBatch, + Render, + } + + let program_name = args.next().unwrap(); + + let mut flags = Flags::EMPTY; + let cmd = loop { + match args.next() { + Some(arg) if arg == "render-batch" => break CmdTag::RenderBatch, + Some(arg) if arg == "render" => break CmdTag::Render, + + // TODO: documment these flags + Some(arg) if arg == "--full-build" => { + flags |= Flags::FULL_BUILD; + } + Some(arg) if arg == "--private" => { + flags |= Flags::PRIVATE; + } + + Some(arg) if arg.starts_with("--") => { + errorln!("Unknown flag {arg:?}"); + log::usage(&program_name); + return Err(()); + } + Some(arg) => { + errorln!("Unknown subcommand {arg:?}"); + log::usage(&program_name); + return Err(()); + } + None => { + errorln!("No subcommand provided"); + log::usage(&program_name); + return Err(()); + } + } + }; + + let sub_cmd = match cmd { + CmdTag::RenderBatch => { + SubCmd::RenderBatch + } + CmdTag::Render => { + let repo_name = if let Some(dir) = args.next() { + dir + } else { + errorln!("No repository name providade"); + log::usage(&program_name); + return Err(()); + }; + + SubCmd::Render { repo_name, } + } + }; + + if args.next().is_some() { + warnln!("Additional command line arguments provided. Ignoring trailing arguments..."); + log::usage(&program_name); + } + + Ok((Self { sub_cmd, flags, }, program_name)) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Flags(u8); + +impl Flags { + const FULL_BUILD_RAW: u8 = 0b00000001; + const PRIVATE_RAW: u8 = 0b00000010; + + pub const EMPTY: Self = Self(0); + pub const FULL_BUILD: Self = Self(Self ::FULL_BUILD_RAW); + pub const PRIVATE: Self = Self(Self ::PRIVATE_RAW); + + pub fn full_build(self) -> bool { + self.0 & Self::FULL_BUILD_RAW != 0 + } + + pub fn private(self) -> bool { + self.0 & Self::PRIVATE_RAW != 0 + } +} + +impl BitOrAssign for Flags { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +}
diff --git /dev/null b/src/config.rs @@ -0,0 +1,38 @@ +//! Compile-time configuration keys +// TODO: [feature]: read this from a TOML file at build-time? + +#[cfg(not(debug_assertions))] +pub const REPOS_DIR: &str = "/var/git/public"; + +#[cfg(debug_assertions)] +pub const REPOS_DIR: &str = "./test/public"; + + +#[cfg(not(debug_assertions))] +pub const PRIVATE_REPOS_DIR: &str = "/var/git/private"; + +#[cfg(debug_assertions)] +pub const PRIVATE_REPOS_DIR: &str = "./test/private"; + + +#[cfg(not(debug_assertions))] +pub const OUTPUT_PATH: &str = "/var/www/git"; + +#[cfg(debug_assertions)] +pub const OUTPUT_PATH: &str = "./site"; + + +#[cfg(not(debug_assertions))] +pub const PRIVATE_OUTPUT_PATH: &str = "/var/www/git/private"; + +#[cfg(debug_assertions)] +pub const PRIVATE_OUTPUT_PATH: &str = "./site/private"; + +pub const PRIVATE_OUTPUT_ROOT: &str = "/private"; + +#[cfg(not(debug_assertions))] +pub const GIT_USER: &str = "git"; + +pub const TREE_SUBDIR: &str = "tree"; +pub const BLOB_SUBDIR: &str = "blob"; +pub const COMMIT_SUBDIR: &str = "commit";
diff --git a/src/main.rs b/src/main.rs @@ -7,7 +7,6 @@ use std::{ ffi::OsStr, collections::HashMap, time::{Duration, SystemTime}, - env, process::ExitCode, cmp, }; @@ -25,19 +24,16 @@ use git2::{ Oid, }; use time::{DateTime, Date, FullDate}; +use command::{Cmd, SubCmd, Flags}; +use config::{TREE_SUBDIR, BLOB_SUBDIR, COMMIT_SUBDIR}; #[macro_use] mod log; mod markdown; mod time; - -#[cfg(not(debug_assertions))] -const GIT_USER: &str = "git"; - -const TREE_SUBDIR: &str = "tree"; -const BLOB_SUBDIR: &str = "blob"; -const COMMIT_SUBDIR: &str = "commit"; +mod command; +mod config; const README_NAMES: &[&str] = &["README", "README.txt", "README.md"]; const LICENSE_NAME: &str = "LICENSE"; @@ -191,14 +187,18 @@ impl RepoInfo { }) } - fn from_batch_path<P>(path: P) -> Result<Vec<Self>, ()> - where - P: AsRef<Path> + AsRef<OsStr> + fmt::Debug, - { - let mut result = Vec::new(); + /// Returns an (orderer) index of the repositories at `config::REPOS_DIR` or + /// `config::PRIVATE_REPOS_DIR`. + fn index(private: bool) -> Result<Vec<Self>, ()> { + let repos_dir = if private { + config::PRIVATE_REPOS_DIR + } else { + config::REPOS_DIR + }; - match fs::read_dir(&path) { + match fs::read_dir(repos_dir) { Ok(dir) => { + let mut result = Vec::new(); for entry in dir.flatten() { match entry.file_type() { Ok(ft) if ft.is_dir() => { @@ -218,7 +218,7 @@ impl RepoInfo { Ok(result) } Err(e) => { - errorln!("Could not read repositories at {path:?} dir: {e}"); + errorln!("Could not read repositories at {repos_dir:?}: {e}"); Err(()) } } @@ -238,25 +238,6 @@ struct Readme { format: ReadmeFormat, } -#[derive(Debug, Clone)] -// this is necessary for pages to link to the correct addresses when rendering -// private repos -/// A path describing the root in the output directory tree -enum RootPath { - Slash, - Path(String), -} - -impl Display for RootPath { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Self::Path(p) = self { - write!(f, "/{}", Escaped(p.trim_matches('/')))?; - } - - Ok(()) - } -} - struct RepoRenderer<'repo> { pub name: String, pub description: Option<String>, @@ -265,25 +246,18 @@ struct RepoRenderer<'repo> { pub head: Tree<'repo>, pub branch: String, - pub readme: Option<Readme>, - pub license: Option<String>, + pub readme: Option<Readme>, + pub license: Option<String>, + // cached constants which depend on command-line flags: + // these shouldn't be modified at runtime pub full_build: bool, - pub output_root: &'repo RootPath, pub output_path: PathBuf, - + pub output_root: &'static str, } impl<'repo> RepoRenderer<'repo> { - fn new<P>( - repo: RepoInfo, - output_path: P, - full_build: bool, - output_root: &'repo RootPath, - ) -> Result<Self, ()> - where - P: AsRef<Path> + AsRef<OsStr>, - { + fn new(repo: RepoInfo, flags: Flags) -> Result<Self, ()> { let (head, branch) = { match repo.repo.head() { Ok(head) => unsafe { @@ -361,16 +335,25 @@ impl<'repo> RepoRenderer<'repo> { } } + let (output_path, output_root) = if flags.private() { + (PathBuf::from(config::PRIVATE_OUTPUT_PATH), config::PRIVATE_OUTPUT_ROOT) + } else { + (PathBuf::from(config::OUTPUT_PATH), "") + }; + Ok(Self { name: repo.name, - head, - branch, description: repo.description, + repo: repo.repo, + head, + branch, + readme, license, - output_path: PathBuf::from(&output_path), - full_build, + + full_build: flags.full_build(), + output_path, output_root, }) } @@ -631,6 +614,9 @@ impl<'repo> RepoRenderer<'repo> { page_path.extend(&path); let page_path = format!("{}.html", page_path.to_string_lossy()); + // TODO: [optimize]: avoid late-stage decision-making by moving the 1st + // `if` to outside of the function body? + // // skip rendering the page if the commit the blob was last updated on is // older than the page if !self.full_build { @@ -872,6 +858,7 @@ impl<'repo> RepoRenderer<'repo> { last_commit_time.insert(id, commit_time); } + // TODO: [optmize]: refactor this? avoid late-stage decision making if should_skip { continue; } @@ -1403,11 +1390,19 @@ fn render_footer(f: &mut File) -> io::Result<()> { writeln!(f, "</footer>") } -fn render_index<P : AsRef<Path> + AsRef<OsStr>>( - output_path: P, - repos: &[RepoInfo], - output_root: &RootPath, -) -> io::Result<()> { +fn render_index(repos: &[RepoInfo], private: bool) -> io::Result<()> { + let output_path = if private { + config::PRIVATE_OUTPUT_PATH + } else { + config::OUTPUT_PATH + }; + + let output_root = if private { + config::PRIVATE_OUTPUT_ROOT + } else { + "" + }; + let mut path = PathBuf::from(&output_path); path.push("index.html"); @@ -1458,130 +1453,6 @@ fn render_index+ AsRef
Ok(()) } -#[derive(Clone, Debug)] -struct Cmd { - sub_cmd: SubCmd, - - full_build: bool, - output_root: RootPath, -} - -#[derive(Clone, Debug)] -enum SubCmd { - RenderBatch { - batch_path: String, - output_path: String, - }, - Render { - repo_name: String, - parent_path: String, - output_path: String, - }, -} - -impl Cmd { - pub fn parse() -> Result<(Self, String), ()> { - let mut args = env::args(); - - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum CmdTag { - RenderBatch, - Render, - } - - let program_name = args.next().unwrap(); - - let mut full_build = false; - let mut output_root = RootPath::Slash; - - let cmd = loop { - match args.next() { - Some(arg) if arg == "render-batch" => break CmdTag::RenderBatch, - Some(arg) if arg == "render" => break CmdTag::Render, - - // TODO: documment these flags - Some(arg) if arg == "--full-build" => { - full_build = true; - } - Some(arg) if arg == "--output-root" => { - if let Some(root) = args.next() { - output_root = RootPath::Path(root); - } else { - errorln!("No value provided for the `--output-root` flag"); - log::usage(&program_name); - return Err(()); - } - } - - Some(arg) => { - errorln!("Unknown flag {arg:?}"); - log::usage(&program_name); - return Err(()); - } - None => { - errorln!("No subcommand provided"); - log::usage(&program_name); - return Err(()); - } - } - }; - - let input_path = if let Some(dir) = args.next() { - dir - } else { - errorln!("No input path provided"); - log::usage(&program_name); - return Err(()); - }; - - let output_path = if let Some(dir) = args.next() { - dir - } else { - errorln!("No output path provided"); - log::usage(&program_name); - return Err(()); - }; - - if args.next().is_some() { - warnln!("Additional command line arguments provided. Ignoring trailing arguments..."); - log::usage(&program_name); - } - - let sub_cmd = match cmd { - CmdTag::RenderBatch => { - SubCmd::RenderBatch { batch_path: input_path, output_path, } - } - CmdTag::Render => { - let input_abs = match fs::canonicalize(&input_path) { - Ok(path) => path, - Err(e) => { - errorln!("Could not extract absolute path from {input_path:?}: {e}"); - return Err(()); - } - }; - - let parent_path = if let Some(parent) = input_abs.parent() { - parent.to_string_lossy().to_string() - } else { - errorln!("Could not extract parent path from {input_path:?}"); - return Err(()); - }; - - let repo_name = if let Some(name) = input_abs.file_name() { - name.to_string_lossy().to_string() - } else { - errorln!("Could not extract repository name from {input_path:?}"); - return Err(()); - }; - - SubCmd::Render { parent_path, repo_name, output_path, } - } - }; - - Ok((Self { sub_cmd, full_build, output_root, }, program_name)) - } -} - fn main() -> ExitCode { #[allow(unused_variables)] let (cmd, program_name) = if let Ok(cmd) = Cmd::parse() { @@ -1593,48 +1464,48 @@ fn main() -> ExitCode { #[cfg(not(debug_assertions))] unsafe { use std::ffi::CStr; + use config::GIT_USER; let uid = libc::getuid(); let pw = libc::getpwuid(uid); - if !pw.is_null() { - let user = CStr::from_ptr((*pw).pw_name).to_string_lossy(); + assert!(!pw.is_null()); - if user != GIT_USER { - errorln!("Running {program_name} as the {user:?} user. Re-run as {GIT_USER:?}"); - return ExitCode::FAILURE; - } + let user = CStr::from_ptr((*pw).pw_name).to_string_lossy(); + if user != GIT_USER { + errorln!("Running {program_name} as the {user:?} user. Re-run as {GIT_USER:?}"); + return ExitCode::FAILURE; } } - match cmd.sub_cmd { - SubCmd::RenderBatch { batch_path, output_path } => { - let repos = if let Ok(rs) = RepoInfo::from_batch_path(&batch_path) { - rs - } else { - return ExitCode::FAILURE; - }; + let repos_dir = if cmd.flags.private() { + config::PRIVATE_REPOS_DIR + } else { + config::REPOS_DIR + }; + let repos = if let Ok(repos) = RepoInfo::index(cmd.flags.private()) { + repos + } else { + return ExitCode::FAILURE; + }; + + match cmd.sub_cmd { + SubCmd::RenderBatch => { info!("Updating global repository index..."); - if let Err(e) = render_index(&output_path, &repos, &cmd.output_root) { + if let Err(e) = render_index(&repos, cmd.flags.private()) { errorln!("Failed rendering global repository index: {e}"); } info_done!(); let n_repos = repos.len(); job_counter_start!(n_repos); - infoln!("Updating pages for git repositories at {batch_path:?}..."); + infoln!("Updating pages for git repositories at {repos_dir:?}..."); for repo in repos { job_counter_increment!(repo.name); - let renderer = RepoRenderer::new( - repo, - &output_path, - cmd.full_build, - &cmd.output_root, - ); - - let renderer = if let Ok(r) = renderer { - r + let renderer = RepoRenderer::new(repo, cmd.flags); + let renderer = if let Ok(renderer) = renderer { + renderer } else { return ExitCode::FAILURE; }; @@ -1648,35 +1519,27 @@ fn main() -> ExitCode { } } - SubCmd::Render { parent_path, repo_name, output_path } => { - let repos = if let Ok(rs) = RepoInfo::from_batch_path(parent_path) { - rs - } else { - return ExitCode::FAILURE; - }; - + SubCmd::Render { repo_name } => { info!("Updating global repository index..."); - if let Err(e) = render_index(&output_path, &repos, &cmd.output_root) { + if let Err(e) = render_index(&repos, cmd.flags.private()) { errorln!("Failed rendering global repository index: {e}"); } info_done!(); - for repo in repos { - if *repo.name != *repo_name { - continue; + let mut repo = None; + for r in repos { + if *r.name == *repo_name { + repo = Some(r); + break; } + } + if let Some(repo) = repo { info!("Updating pages for {name:?}...", name = repo.name); - let renderer = RepoRenderer::new( - repo, - &output_path, - cmd.full_build, - &cmd.output_root, - ); - - let renderer = if let Ok(r) = renderer { - r + let renderer = RepoRenderer::new(repo, cmd.flags); + let renderer = if let Ok(renderer) = renderer { + renderer } else { return ExitCode::FAILURE; }; @@ -1687,6 +1550,9 @@ fn main() -> ExitCode { } info_done!(); + } else { + errorln!("Couldnt' find repository {repo_name:?} at {repos_dir:?}"); + return ExitCode::FAILURE; } } }>(