// 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")));
}
}