summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorA Farzat <a@farzat.xyz>2026-06-06 15:51:06 +0300
committerA Farzat <a@farzat.xyz>2026-06-06 15:51:06 +0300
commitf29eb66f252e86441551bb2eba52b032605fd9cf (patch)
tree5ea9fc0889b1d2074e674b30592e31620a2961db /src/main.rs
parent05a65916ae3df9be7b3c95e0291e2eadac2a68ee (diff)
downloadrepo2markdown-f29eb66f252e86441551bb2eba52b032605fd9cf.tar.gz
repo2markdown-f29eb66f252e86441551bb2eba52b032605fd9cf.zip
Move run to a separate module
Keeps main.rs for CLI logic only.
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs295
1 files changed, 2 insertions, 293 deletions
diff --git a/src/main.rs b/src/main.rs
index dbd1a60..d9ba728 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,12 @@
use std::{
- collections::HashSet,
env,
- ffi::OsStr,
- io::{self, Read, Write},
- os::unix::ffi::OsStrExt,
+ io::{self},
path::Path,
};
use repo2markdown::{
logger::{Logger, Verbosity},
- normalizer::Normalizer,
- renderer::Renderer,
+ run::run,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -55,290 +51,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
logger,
)
}
-
-const DEFAULT_PROJECT_NAME: &str = "Project Outline";
-
-pub fn run<R: Read, W: Write>(
- mut input: R,
- output: W,
- root: &Path,
- origin_base: &Path,
- project_name: Option<&str>,
- logger: Logger,
-) -> Result<(), Box<dyn std::error::Error>> {
- let mut buf = Vec::new();
- input.read_to_end(&mut buf)?;
-
- let normalizer = Normalizer::new(root, origin_base)?;
-
- let mut renderer = Renderer::new(output).with_logger(logger);
- let project_name = project_name.unwrap_or_else(|| derive_project_name(root));
- renderer.render_header(project_name)?;
-
- let mut seen_paths = HashSet::new();
- for segment in buf.split(|b| *b == 0) {
- if segment.is_empty() {
- continue;
- }
-
- let path = Path::new(OsStr::from_bytes(segment));
- let normalized_path = normalizer.normalize(path)?;
- if !seen_paths.insert(normalized_path.relative.clone()) {
- logger.warn(format!(
- "Duplicate file detected: {:?}",
- normalized_path.relative
- ));
- continue;
- }
- renderer.render_path(&normalized_path)?;
- }
- Ok(())
-}
-
-fn derive_project_name(root: &Path) -> &str {
- if let Some(os_str_name) = root.file_name()
- && let Some(name) = os_str_name.to_str()
- {
- name
- } else {
- DEFAULT_PROJECT_NAME
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::ffi::OsStr;
- use std::fs;
- use std::io::{Cursor, Read, Write};
- use std::os::unix::ffi::OsStrExt;
- use std::path::Path;
-
- use repo2markdown::logger::Logger;
- use tempfile::tempdir;
-
- use super::{DEFAULT_PROJECT_NAME, derive_project_name, run};
-
- fn paths_to_null_sep_bytes(file_paths: &[&Path]) -> Vec<u8> {
- let mut output = Vec::new();
- for path in file_paths {
- output.extend(path.as_os_str().as_encoded_bytes());
- output.push(0);
- }
- output
- }
-
- fn run_with_default_logger<R: Read, W: Write>(
- input: R,
- output: W,
- root: &Path,
- origin_base: &Path,
- project_name: Option<&str>,
- ) -> Result<(), Box<dyn std::error::Error>> {
- let logger = Logger::default();
- run(input, output, root, origin_base, project_name, logger)
- }
-
- #[test]
- fn cli_with_empty_input_produces_empty_project_with_specified_project_name() {
- let temp_dir = tempdir().unwrap();
- let input = Cursor::new(b"");
- let mut output = Vec::new();
- let root = temp_dir.path();
- let origin_base = temp_dir.path();
-
- run_with_default_logger(input, &mut output, root, origin_base, Some("Project name"))
- .unwrap();
-
- assert_eq!(String::from_utf8(output).unwrap(), "# Project name\n");
- }
-
- #[test]
- fn cli_reads_single_file_from_stdin() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let input = Cursor::new(b"test_main.rs\0");
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap();
-
- run_with_default_logger(input, &mut output, root, origin_base, None).unwrap();
-
- let output_str = String::from_utf8(output).unwrap();
-
- assert!(output_str.contains("## File: test_main.rs"));
- assert!(output_str.contains("fn main() {}"));
- }
-
- #[test]
- fn cli_reads_multiple_files_in_order() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let input = Cursor::new(b"a.rs\0b.rs\0");
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- fs::write(origin_base.join("a.rs"), "A").unwrap();
- fs::write(origin_base.join("b.rs"), "B").unwrap();
-
- run_with_default_logger(input, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- let a_pos = output.find("a.rs").unwrap();
- let b_pos = output.find("b.rs").unwrap();
-
- assert!(a_pos < b_pos);
- }
-
- #[test]
- fn cli_normalizes_paths_before_rendering() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let input = Cursor::new(b"test/./main.rs\0");
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- let write_dir = temp_dir.path().join("test");
- fs::create_dir_all(&write_dir).unwrap();
- fs::write(write_dir.join("main.rs"), "fn main() {}").unwrap();
-
- run_with_default_logger(input, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- assert!(output.contains("## File: test/main.rs"));
- }
-
- #[test]
- fn cli_reads_from_origin_but_outputs_relative_to_root() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path().join("sandbox/src");
- let input = Cursor::new(b"main.rs\0");
- let mut output = Vec::new();
- let root = temp_dir.path().join("project");
-
- fs::create_dir_all(&origin_base).unwrap();
- fs::write(origin_base.join("main.rs"), "fn main() {}").unwrap();
-
- run_with_default_logger(input, &mut output, &root, &origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- // Must contain file content → proves correct reading
- assert!(output.contains("fn main() {}"));
-
- // Must contain normalized path → proves normalization applied
- assert!(output.contains("sandbox/src/main.rs"));
- }
-
- #[test]
- fn cli_ignores_origin_when_input_path_is_absolute() {
- let temp_dir1 = tempdir().unwrap();
- let temp_dir2 = tempdir().unwrap();
- let origin_base = temp_dir2.path();
- let filepath = temp_dir1.path().join("test_main.rs");
- let input = Cursor::new(paths_to_null_sep_bytes(&[&filepath]));
- let mut output = Vec::new();
- let root = temp_dir2.path();
- fs::write(&filepath, "fn main() {}").unwrap();
-
- run_with_default_logger(input, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- // Must contain file content → proves correct reading
- assert!(output.contains("fn main() {}"));
- }
-
- #[test]
- fn duplicate_files_in_sequence_are_skipped() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::write(origin.join("a.rs"), "A").unwrap();
-
- let input = Cursor::new(b"a.rs\0a.rs\0");
- let mut output = Vec::new();
-
- run_with_default_logger(input, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- }
-
- #[test]
- fn duplicate_files_are_skipped_with_preserved_display_order_even_if_not_adjacent() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::write(origin.join("a.rs"), "A").unwrap();
- fs::write(origin.join("b.rs"), "B").unwrap();
-
- let input = Cursor::new(b"a.rs\0b.rs\0a.rs\0");
- let mut output = Vec::new();
-
- run_with_default_logger(input, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- assert_eq!(output.matches("## File: b.rs").count(), 1);
- let a_pos = output.find("a.rs").unwrap();
- let b_pos = output.find("b.rs").unwrap();
- assert!(a_pos < b_pos);
- }
-
- #[test]
- fn lexically_equivalent_paths_are_detected_as_duplicates() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::create_dir_all(origin.join("bla")).unwrap();
- fs::write(origin.join("a.rs"), "A").unwrap();
- fs::write(origin.join("b.rs"), "B").unwrap();
-
- let input = Cursor::new(b"a.rs\0b.rs\0bla/../a.rs\0");
- let mut output = Vec::new();
-
- run_with_default_logger(input, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- }
-
- #[test]
- fn project_name_is_derived_from_root_by_default_even_if_directory_does_not_exist() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let input = Cursor::new(b"");
- let mut output = Vec::new();
- let root = temp_dir.path().join("repo2markdown");
-
- run_with_default_logger(input, &mut output, &root, origin_base, None).unwrap();
-
- let output_str = String::from_utf8(output).unwrap();
-
- assert_eq!(output_str, "# repo2markdown\n");
- }
-
- #[test]
- fn project_name_fallsback_to_default_if_root_is_filesystem_root() {
- assert_eq!(derive_project_name(Path::new("/")), DEFAULT_PROJECT_NAME);
- }
-
- #[test]
- fn project_name_fallsback_if_root_ending_is_not_utf8() {
- let root = Path::new(OsStr::from_bytes(b"/root/fd\xC3"));
- assert_eq!(derive_project_name(root), DEFAULT_PROJECT_NAME);
- }
-
- #[test]
- fn deriving_project_name_from_root_ignores_trailing_slash() {
- let root = Path::new("/root/repo2markdown/");
- assert_eq!(derive_project_name(root), "repo2markdown");
- }
-}