// File: ./README.md # repo2file-cli `repo2file-cli` is a command-line tool designed to consolidate a code repository into a single text file. This can be useful for archiving, analysis, or sharing purposes. ## Installation To install `repo2file-cli`, you need to have Rust installed. If you don't have Rust installed, you can get it from [rustup.rs](https://rustup.rs/). Once you have Rust, you can install the `repo2file-cli` using `cargo`: ```sh cargo install repo2file-cli ``` ## Usage To use `repo2file-cli`, you can run the following command: ```sh repo2file-cli [OPTIONS] ``` ### Arguments - ``: The directory or Git URL of the repository you want to process. ### Options - `--ignore-files `: Comma-separated list of files to ignore. - `--ignore-dirs `: Comma-separated list of directories to ignore. - `--include-files `: Comma-separated list of files to include exclusively (cannot be used with `--ignore-files` or `--ignore-dirs`). - `--output `: The output file. Defaults to a file named after the current directory. ### Examples #### Convert a local repository ```sh repo2file-cli /path/to/repository --output output.txt ``` #### Convert a GitHub repository ```sh repo2file-cli https://github.com/username/repo --output output.txt ``` #### Ignore specific files and directories ```sh repo2file-cli /path/to/repository --ignore-files *.md,*.json --ignore-dirs node_modules,.git ``` #### Include only specific files ```sh repo2file-cli /path/to/repository --include-files *.rs,*.toml ``` ## Contributing We welcome contributions! Please follow these steps to contribute: 1. Fork the repository. 2. Create a new branch (`git checkout -b feature-branch`). 3. Make your changes. 4. Commit your changes (`git commit -am 'Add new feature'`). 5. Push to the branch (`git push origin feature-branch`). 6. Create a new Pull Request. ### Development Setup To set up your development environment, follow these steps: 1. Clone the repository: ```sh git clone https://github.com/yourusername/repo2file-cli.git ``` 2. Change to the project directory: ```sh cd repo2file-cli ``` 3. Install the required extensions (if using VSCode): - [Rust (rls)](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) - [Crates](https://marketplace.visualstudio.com/items?itemName=serayuzgur.crates) - [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) ### Running Tests You can run the tests using the following command: ```sh cargo test ``` ### Build from source you can install the binary from source while youre devin' ```sh cargo install --path . ``` ## License This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details. // File: ./repo2file-cli // File: ./README.md # repo2file-cli `repo2file-cli` is a command-line tool designed to consolidate a code repository into a single text file. This can be useful for archiving, analysis, or sharing purposes. ## Installation To install `repo2file-cli`, you need to have Rust installed. If you don't have Rust installed, you can get it from [rustup.rs](https://rustup.rs/). Once you have Rust, you can install the `repo2file-cli` using `cargo`: ```sh cargo install repo2file-cli ``` ## Usage To use `repo2file-cli`, you can run the following command: ```sh repo2file-cli [OPTIONS] ``` ### Arguments - ``: The directory or Git URL of the repository you want to process. ### Options - `--ignore-files `: Comma-separated list of files to ignore. - `--ignore-dirs `: Comma-separated list of directories to ignore. - `--include-files `: Comma-separated list of files to include exclusively (cannot be used with `--ignore-files` or `--ignore-dirs`). - `--output `: The output file. Defaults to a file named after the current directory. ### Examples #### Convert a local repository ```sh repo2file-cli /path/to/repository --output output.txt ``` #### Convert a GitHub repository ```sh repo2file-cli https://github.com/username/repo --output output.txt ``` #### Ignore specific files and directories ```sh repo2file-cli /path/to/repository --ignore-files *.md,*.json --ignore-dirs node_modules,.git ``` #### Include only specific files ```sh repo2file-cli /path/to/repository --include-files *.rs,*.toml ``` ## Contributing We welcome contributions! Please follow these steps to contribute: 1. Fork the repository. 2. Create a new branch (`git checkout -b feature-branch`). 3. Make your changes. 4. Commit your changes (`git commit -am 'Add new feature'`). 5. Push to the branch (`git push origin feature-branch`). 6. Create a new Pull Request. ### Development Setup To set up your development environment, follow these steps: 1. Clone the repository: ```sh git clone https://github.com/yourusername/repo2file-cli.git ``` 2. Change to the project directory: ```sh cd repo2file-cli ``` 3. Install the required extensions (if using VSCode): - [Rust (rls)](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) - [Crates](https://marketplace.visualstudio.com/items?itemName=serayuzgur.crates) - [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) ### Running Tests You can run the tests using the following command: ```sh cargo test ``` ### Build from source you can install the binary from source while youre devin' ```sh cargo install --path . ``` ## License This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details. // File: ./repo2file-cli.code-workspace { "folders": [ { "path": "." } ], "settings": {}, "extensions": { "recommendations": [ "serayuzgur.crates", "1yib.rust-bundle", "tamasfe.even-better-toml" ] } } // File: ./src/default_ignore.rs use serde::Deserialize; #[derive(Deserialize)] pub struct DefaultIgnore { pub ignore_files: Vec, pub ignore_dirs: Vec, } impl Default for DefaultIgnore { fn default() -> Self { DefaultIgnore { ignore_dirs: ["node_modules", ".git", ".idea", ".vscode"] .iter() .map(|s| s.to_string()) .collect(), ignore_files: IntoIterator::into_iter([ "*LICENCE.md", "*CHANGELOG.md", "*.DS_Store", "*.all-contributorsrc", "*.yaml", "*.yml", "*.json", "*.csv", "*.svg", "*.conf", "*.ini", "*.env", "*.log", "*.tmp", "*.pyc", "*.class", "*.o", "*.obj", "*.exe", "*.dll", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", "*.lock", "*.toml", ".prettierrc.*", "*.txt", "Pipfile", "*.cfg", ".gitignore", ".gitattributes", ".dockerignore", ".env", ".flaskenv", ".editorconfig", "Makefile", "CMakeLists.txt", ]) .map(|s| s.to_string()) .collect(), } } } // File: ./src/main.rs mod default_ignore; use default_ignore::DefaultIgnore; use git2::Repository; use globset::{Glob, GlobSetBuilder}; use ignore::WalkBuilder; use std::fs::File; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use structopt::StructOpt; use tempdir::TempDir; #[derive(StructOpt)] #[structopt( name = "repo2file", about = "Turn a code repository into a single text file." )] struct Cli { /// The directory or Git URL of the repository #[structopt(parse(from_os_str))] input: PathBuf, /// Files to ignore, separated by commas #[structopt(long, use_delimiter = true)] ignore_files: Option>, /// Directories to ignore, separated by commas #[structopt(long, use_delimiter = true)] ignore_dirs: Option>, /// Files to include, separated by commas (exclusive with --ignore-files and --ignore-dirs) #[structopt(long, use_delimiter = true, conflicts_with_all = &["ignore_files", "ignore_dirs"])] include_files: Option>, /// Output file #[structopt(parse(from_os_str))] output: Option, } fn main() -> io::Result<()> { let args = Cli::from_args(); let input_path = if is_github_url(&args.input) { let temp_dir = clone_repo_to_temp(&args.input).map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("Failed to clone repository: {}", e), ) })?; temp_dir.path().to_owned() } else { args.input.clone() }; let output_path = args.output.clone().unwrap_or_else(|| { let current_dir = std::env::current_dir().unwrap(); current_dir.join(current_dir.file_name().unwrap()) }); let mut output_file = File::create(output_path)?; for entry in WalkBuilder::new(input_path) .add_custom_ignore_filename(".ignore") .build() .filter_map(Result::ok) .filter(|e| e.file_type().map_or(false, |ft| ft.is_file())) { let path = entry.path(); if should_include(path, &args, &DefaultIgnore::default()) { let content = std::fs::read_to_string(path)?; writeln!( output_file, "\n\n// File: {}\n\n{}", path.display(), content )?; } } Ok(()) } // Function to determine if a file should be included based on the arguments fn should_include(path: &Path, args: &Cli, config: &DefaultIgnore) -> bool { let mut ignore_files: Vec<&str> = config.ignore_files.iter().map(String::as_str).collect(); let mut ignore_dirs: Vec<&str> = config.ignore_dirs.iter().map(String::as_str).collect(); if let Some(user_ignore_files) = &args.ignore_files { ignore_files.extend(user_ignore_files.iter().map(String::as_str)); } if let Some(user_ignore_dirs) = &args.ignore_dirs { ignore_dirs.extend(user_ignore_dirs.iter().map(String::as_str)); } let mut glob_builder = GlobSetBuilder::new(); for pattern in &ignore_files { glob_builder.add(Glob::new(pattern).unwrap()); } let glob_set = glob_builder.build().unwrap(); if let Some(include_files) = &args.include_files { return include_files.iter().any(|f| path.ends_with(f)); } let path_str = path.to_str().unwrap_or_default(); if glob_set.is_match(path_str) || ignore_files.iter().any(|&f| path.ends_with(f)) { return false; } if ignore_dirs .iter() .any(|&d| path.components().any(|comp| comp.as_os_str() == d)) { return false; } true } fn is_github_url(path: &Path) -> bool { path.to_str() .map_or(false, |s| s.starts_with("https://github.com/")) } fn clone_repo_to_temp(url: &Path) -> Result { let temp_dir = TempDir::new("temp-repo2file")?; Repository::clone(url.to_str().unwrap(), temp_dir.path()).map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("Failed to clone repository: {}", e), ) })?; Ok(temp_dir) } #[cfg(test)] mod tests { use super::*; fn default_ignore_files() -> DefaultIgnore { return DefaultIgnore { ignore_files: vec![ "node_modules".to_string(), "target".to_string(), ".vscode".to_string(), "*.lock".to_string(), ], ignore_dirs: vec![ "node_modules".to_string(), "target".to_string(), ".vscode".to_string(), ], }; } #[test] fn test_should_include_no_ignore_no_include() { let args = Cli { input: PathBuf::from("input"), ignore_files: None, ignore_dirs: None, include_files: None, output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/test_file.txt"); assert!(should_include(&path, &args, &default_ignore_files())); let lock_path = PathBuf::from("input/Cargo.lock"); assert!(!should_include(&lock_path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_ignore_files() { let args = Cli { input: PathBuf::from("input"), ignore_files: Some(vec!["test_file.txt".to_string()]), ignore_dirs: None, include_files: None, output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/test_file.txt"); assert!(!should_include(&path, &args, &default_ignore_files())); let other_path = PathBuf::from("input/other_file.txt"); assert!(should_include(&other_path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_ignore_dirs() { let args = Cli { input: PathBuf::from("input"), ignore_files: None, ignore_dirs: Some(vec!["ignore_dir".to_string()]), include_files: None, output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/ignore_dir/test_file.txt"); assert!(!should_include(&path, &args, &default_ignore_files())); let other_path = PathBuf::from("input/other_dir/test_file.txt"); assert!(should_include(&other_path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_include_files() { let args = Cli { input: PathBuf::from("input"), ignore_files: None, ignore_dirs: None, include_files: Some(vec!["include_file.txt".to_string()]), output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/include_file.txt"); assert!(should_include(&path, &args, &default_ignore_files())); let other_path = PathBuf::from("input/other_file.txt"); assert!(!should_include(&other_path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_ignore_and_include() { let args = Cli { input: PathBuf::from("input"), ignore_files: Some(vec!["test_file.txt".to_string()]), ignore_dirs: Some(vec!["ignore_dir".to_string()]), include_files: None, output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/test_file.txt"); assert!(!should_include(&path, &args, &default_ignore_files())); let dir_path = PathBuf::from("input/ignore_dir/test_file.txt"); assert!(!should_include(&dir_path, &args, &default_ignore_files())); let other_path = PathBuf::from("input/other_file.txt"); assert!(should_include(&other_path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_multiple_ignore_files() { let args = Cli { input: PathBuf::from("input"), ignore_files: Some(vec![ "test_file.txt".to_string(), "ignore_file.txt".to_string(), ]), ignore_dirs: None, include_files: None, output: Some(PathBuf::from("output.txt")), }; let path = PathBuf::from("input/test_file.txt"); assert!(!should_include(&path, &args, &default_ignore_files())); let path = PathBuf::from("input/ignore_file.txt"); assert!(!should_include(&path, &args, &default_ignore_files())); let path = PathBuf::from("input/valid_file.txt"); assert!(should_include(&path, &args, &default_ignore_files())); } #[test] fn test_should_include_with_multiple_ignore_dirs() { let args = Cli { input: PathBuf::from("input"), ignore_files: None, ignore_dirs: Some(vec!["ignore_dir1".to_string(), "ignore_dir2".to_string()]), include_files: None, output: Some(PathBuf::from("output.txt")), }; let path1 = PathBuf::from("input/ignore_dir1/test_file.txt"); assert!(!should_include(&path1, &args, &default_ignore_files())); let path2 = PathBuf::from("input/ignore_dir2/test_file.txt"); assert!(!should_include(&path2, &args, &default_ignore_files())); let valid_path = PathBuf::from("input/valid_dir/test_file.txt"); assert!(should_include(&valid_path, &args, &default_ignore_files())); } #[test] fn test_is_github_url() { assert!(is_github_url(&PathBuf::from( "https://github.com/username/repo" ))); assert!(!is_github_url(&PathBuf::from( "http://github.com/username/repo" ))); assert!(!is_github_url(&PathBuf::from( "https://gitlab.com/username/repo" ))); assert!(!is_github_url(&PathBuf::from("/local/path/to/repo"))); } }