mirror of
https://github.com/chenasraf/gi_gen.git
synced 2026-05-17 17:48:01 +00:00
feat: config struct, merge multiple language selections
This commit is contained in:
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -3,5 +3,20 @@
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": []
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Cargo launch",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"args": [
|
||||
"--languages",
|
||||
"Node"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
12
.vscode/tasks.json
vendored
12
.vscode/tasks.json
vendored
@@ -9,6 +9,18 @@
|
||||
"command": "cargo run",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run program (-languages)",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"run",
|
||||
"--",
|
||||
"--languages",
|
||||
"Node,Rust"
|
||||
],
|
||||
"command": "cargo",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run tests",
|
||||
"type": "shell",
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -11,6 +11,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "args"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b7432c65177b8d5c032d56e020dd8d407e939468479fc8c300e2d93e6d970b"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -75,10 +85,21 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gi_gen"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"args",
|
||||
"getopts",
|
||||
"globset",
|
||||
"home",
|
||||
"tempfile",
|
||||
@@ -211,6 +232,12 @@ dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
||||
@@ -6,6 +6,8 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
args = "2.2.0"
|
||||
getopts = "0.2.21"
|
||||
globset = "0.4.10"
|
||||
home = "0.5.4"
|
||||
tempfile = "3.4.0"
|
||||
|
||||
44
README.md
44
README.md
@@ -61,18 +61,18 @@ $ gi_gen
|
||||
|
||||
You may pass additional flags to `gi_gen`. These are the currently available flags:
|
||||
|
||||
| Usage | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `-languages` \| `-l` | List the languages you want to use as templates.<br />To add multiple templates, use commas as separators, e.g.: `-languages Node,Python` |
|
||||
| `-auto-discover` \| `-d` | Use auto-discovery for project, detecting the project type and using the result as the pre-selected template list. |
|
||||
| `-clean-output` \| `-c` | Perform cleanup on the output .gitignore file, removing any unused patterns |
|
||||
| `-keep-output` \| `-k` | Do not perform cleanup on the output .gitignore file, keep all the original contents |
|
||||
| `-append` \| `-a` | Append to .gitignore file if it already exists |
|
||||
| `-overwrite` \| `-w` | Overwrite .gitignore file if it already exists |
|
||||
| `-detect-languages` | Outputs the automatically-detected languages, separated by newlines, and exits. Useful for outside tools detection. |
|
||||
| `-all-languages` | Outputs all the available languages, separated by newlines, and exits. Useful for outside tools detection. |
|
||||
| `-clear-cache` | Clear the .gitignore cache directory, for troubleshooting or for removing trace files of this program.<br />Exits after running, so other flags will be ignored. |
|
||||
| `-help` \| `-h` | Display help message |
|
||||
| Usage | Description |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--languages` \| `-l` | List the languages you want to use as templates.<br />To add multiple templates, use commas as separators, e.g.: `-languages Node,Python` |
|
||||
| `--auto-discover` \| `-d` | Use auto-discovery for project, detecting the project type and using the result as the pre-selected template list. |
|
||||
| `--clean-output` \| `-c` | Perform cleanup on the output .gitignore file, removing any unused patterns |
|
||||
| `--keep-output` \| `-k` | Do not perform cleanup on the output .gitignore file, keep all the original contents |
|
||||
| `--append` \| `-a` | Append to .gitignore file if it already exists |
|
||||
| `--overwrite` \| `-w` | Overwrite .gitignore file if it already exists |
|
||||
| `--detect-languages` | Outputs the automatically-detected languages, separated by newlines, and exits. Useful for outside tools detection. |
|
||||
| `--all-languages` | Outputs all the available languages, separated by newlines, and exits. Useful for outside tools detection. |
|
||||
| `--clear-cache` | Clear the .gitignore cache directory, for troubleshooting or for removing trace files of this program.<br />Exits after running, so other flags will be ignored. |
|
||||
| `--help` \| `-h` | Display help message |
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -85,28 +85,28 @@ You may pass additional flags to `gi_gen`. These are the currently available fla
|
||||
- Pre-select languages (skip prompt):
|
||||
|
||||
```shell
|
||||
gi_gen -languages Node # One language
|
||||
gi_gen -languages Node,Python # Multiple languages
|
||||
gi_gen --languages Node # One language
|
||||
gi_gen --languages Node,Python # Multiple languages
|
||||
```
|
||||
|
||||
- Perform clean up (skip prompt):
|
||||
|
||||
```shell
|
||||
gi_gen -clean-output # clean up
|
||||
gi_gen -keep-output # skip clean up
|
||||
gi_gen --clean-output # clean up
|
||||
gi_gen --keep-output # skip clean up
|
||||
```
|
||||
|
||||
- Use auto-discovery (skip prompt):
|
||||
|
||||
```shell
|
||||
gi_gen -auto-discover
|
||||
gi_gen --auto-discover
|
||||
```
|
||||
|
||||
- Existing file handlers (skip prompt):
|
||||
|
||||
```shell
|
||||
gi_gen -append # if file exists, add to end of it
|
||||
gi_gen -overwrite # if file exists, replace the existing content
|
||||
gi_gen --append # if file exists, add to end of it
|
||||
gi_gen --overwrite # if file exists, replace the existing content
|
||||
```
|
||||
|
||||
- Combined (skip all prompts):
|
||||
@@ -119,13 +119,13 @@ You may pass additional flags to `gi_gen`. These are the currently available fla
|
||||
- Clean cache directory and exit:
|
||||
|
||||
```shell
|
||||
gi_gen -clear-cache
|
||||
gi_gen --clear-cache
|
||||
```
|
||||
|
||||
- Detect languages and output the results, then exit:
|
||||
|
||||
```shell
|
||||
gi_gen -detect-languages
|
||||
gi_gen --detect-languages
|
||||
```
|
||||
|
||||
## Contribute
|
||||
@@ -134,7 +134,7 @@ Credits to [open-source-ideas][osi] for the idea for the tool.
|
||||
|
||||
Please feel free to open PRs or issues with bug fixes/reports, or feature requests.
|
||||
|
||||
This project was built using Go, and should run easily with the normal Go tools with no further
|
||||
This project was built using Rust, and should run easily with the normal Rust tools with no further
|
||||
configuration.
|
||||
|
||||
Tested on all major platforms, but feel free to report any issues on your platform if you have any,
|
||||
|
||||
33
src/cache.rs
33
src/cache.rs
@@ -60,13 +60,40 @@ pub fn prepare_cache() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_language_file(language: String) -> Result<String, Error> {
|
||||
pub struct LanguageFile {
|
||||
pub language: String,
|
||||
pub content: String,
|
||||
pub file_path: PathBuf,
|
||||
}
|
||||
|
||||
pub fn get_language_file(language: String) -> Result<LanguageFile, Error> {
|
||||
let cache_dir = get_cache_dir()?;
|
||||
let language_file = cache_dir.join(format!("{}.gitignore", language));
|
||||
// TODO make this a case-insensitive file search
|
||||
let language_file = &cache_dir.join(format!("{}.gitignore", language));
|
||||
let content = match std::fs::read_to_string(language_file) {
|
||||
Ok(content) => content,
|
||||
Err(_) => panic!("Could not read file"),
|
||||
};
|
||||
|
||||
Ok(content)
|
||||
Ok(LanguageFile {
|
||||
language,
|
||||
content,
|
||||
file_path: language_file.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_languages_contents(languages: Vec<String>) -> String {
|
||||
let mut output = String::new();
|
||||
for chosen in languages {
|
||||
let language_file = match get_language_file(chosen.to_string()) {
|
||||
Ok(info) => info,
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
};
|
||||
let content = language_file.content;
|
||||
let sep = "#========================================================================\n";
|
||||
output.push_str(format!("\n{sep}# {}\n{sep}\n", language_file.language).as_str());
|
||||
output.push_str(&content);
|
||||
}
|
||||
output = format!("{}\n", output.trim_end());
|
||||
output
|
||||
}
|
||||
|
||||
175
src/cli.rs
Normal file
175
src/cli.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Formatter},
|
||||
};
|
||||
|
||||
use args::Args;
|
||||
use getopts::Occur;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub languages: Vec<String>,
|
||||
pub auto_discover: bool,
|
||||
pub clean_output: bool,
|
||||
pub keep_output: bool,
|
||||
pub overwrite_file: bool,
|
||||
pub append_file: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Config {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Config{{languages: {:?}, auto_discover: {}, clean_output: {}, keep_output: {}, overwrite_file: {}, append_file: {}}}",
|
||||
self.languages, self.auto_discover, self.clean_output, self.keep_output, self.overwrite_file, self.append_file
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConfigErrorType {
|
||||
Parse,
|
||||
InvalidValue,
|
||||
Required,
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ConfigError{{option: {}, value: {}, message: {:?}, error_type: {:?}}}",
|
||||
self.option, self.value, self.message, self.error_type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfigError {
|
||||
option: String,
|
||||
value: String,
|
||||
message: Option<String>,
|
||||
error_type: ConfigErrorType,
|
||||
}
|
||||
|
||||
impl ConfigError {
|
||||
pub fn new(
|
||||
option: &str,
|
||||
value: Option<&str>,
|
||||
error_type: ConfigErrorType,
|
||||
message: Option<&str>,
|
||||
) -> ConfigError {
|
||||
ConfigError {
|
||||
option: option.to_string(),
|
||||
value: value.unwrap_or_default().to_string(),
|
||||
error_type,
|
||||
message: message.map(|s| s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(args: Vec<String>) -> Result<Config, ConfigError> {
|
||||
let mut parser = create_parser();
|
||||
match parser.parse(args) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
return Err(ConfigError::new(
|
||||
err.to_string().as_str(),
|
||||
None,
|
||||
ConfigErrorType::Parse,
|
||||
None,
|
||||
))
|
||||
}
|
||||
};
|
||||
let languages = match parser.values_of::<String>("languages") {
|
||||
Ok(languages) => languages,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let auto_discover = if parser.has_value("auto-discover") {
|
||||
parser.value_of("auto-discover").unwrap_or(true)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let clean_output = if parser.has_value("clean-output") {
|
||||
parser.value_of("clean-output").unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let keep_output = if parser.has_value("keep-output") {
|
||||
parser.value_of("keep-output").unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let overwrite_file = if parser.has_value("overwrite") {
|
||||
parser.value_of("overwrite").unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let append_file = if parser.has_value("append") {
|
||||
parser.value_of("append").unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let config = Config {
|
||||
languages,
|
||||
auto_discover,
|
||||
clean_output,
|
||||
keep_output,
|
||||
overwrite_file,
|
||||
append_file,
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn create_parser() -> Args {
|
||||
let mut parser = Args::new(
|
||||
"gi_gen",
|
||||
"Generate .gitignore files automatically for any project",
|
||||
);
|
||||
parser.option(
|
||||
"l",
|
||||
"languages",
|
||||
"Comma-separated list of languages to generate .gitignore for",
|
||||
"Node,Python,...",
|
||||
Occur::Optional,
|
||||
None,
|
||||
);
|
||||
parser.flag(
|
||||
"a",
|
||||
"auto-discover",
|
||||
"Automatically discover languages from project files",
|
||||
);
|
||||
parser.flag(
|
||||
"c",
|
||||
"clean-output",
|
||||
"Perform cleanup on the output .gitignore file, removing any unused patterns",
|
||||
);
|
||||
parser.flag(
|
||||
"k",
|
||||
"keep-output",
|
||||
"Do not perform cleanup on the output .gitignore file, keep all the original contents",
|
||||
);
|
||||
parser.flag(
|
||||
"o",
|
||||
"overwrite",
|
||||
"Overwrite the output .gitignore file if it already exists",
|
||||
);
|
||||
parser.flag(
|
||||
"a",
|
||||
"append",
|
||||
"Append to the output .gitignore file if it already exists",
|
||||
);
|
||||
parser.flag(
|
||||
"",
|
||||
"clear-cache",
|
||||
"Clear the local cache of .gitignore files",
|
||||
);
|
||||
parser.flag("", "all-languages", "List all supported languages");
|
||||
parser.flag(
|
||||
"",
|
||||
"detect-languages",
|
||||
"List the automatically-detected languages for the current project",
|
||||
);
|
||||
parser.flag("h", "help", "Print this help message");
|
||||
|
||||
parser
|
||||
}
|
||||
@@ -12,13 +12,6 @@ pub fn emit_file(path: &PathBuf, content: String, strategy: EmitStrategy) -> Res
|
||||
"Could not convert path to string",
|
||||
))?,
|
||||
};
|
||||
let mut file = match File::create(path_str) {
|
||||
Ok(file) => file,
|
||||
Err(_) => Err(Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Could not create file",
|
||||
))?,
|
||||
};
|
||||
match strategy {
|
||||
EmitStrategy::Append => {
|
||||
let original_content = match std::fs::read_to_string(path_str) {
|
||||
@@ -26,10 +19,12 @@ pub fn emit_file(path: &PathBuf, content: String, strategy: EmitStrategy) -> Res
|
||||
Err(_) => Err(Error::new(std::io::ErrorKind::Other, "Could not read file"))?,
|
||||
};
|
||||
let final_content = append_lines(original_content, content);
|
||||
let mut file = File::create(path_str)?;
|
||||
// TODO smart merge - remove duplicates
|
||||
file.write_all(final_content.as_bytes())?;
|
||||
}
|
||||
EmitStrategy::Overwrite => {
|
||||
let mut file = File::create(path_str)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
}
|
||||
EmitStrategy::Skip => (),
|
||||
@@ -46,7 +41,7 @@ fn append_lines(target: String, source: String) -> String {
|
||||
|
||||
// dedupe
|
||||
for line in content_lines.into_iter() {
|
||||
if !original_lines.contains(&line) {
|
||||
if line.trim() == "" || line.starts_with("#") || !original_lines.contains(&line) {
|
||||
filtered_lines.push_str(&line);
|
||||
filtered_lines.push_str("\n");
|
||||
}
|
||||
@@ -54,7 +49,8 @@ fn append_lines(target: String, source: String) -> String {
|
||||
|
||||
// append deduped lines to final output
|
||||
let mut final_content = target.clone();
|
||||
if !final_content.ends_with("\n") {
|
||||
final_content = final_content.trim().to_string();
|
||||
if final_content.len() > 0 {
|
||||
final_content.push_str("\n");
|
||||
}
|
||||
final_content.push_str(&filtered_lines);
|
||||
@@ -79,4 +75,24 @@ mod tests {
|
||||
let result = super::append_lines(original_content, content);
|
||||
assert_eq!(result, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_leading_newline() {
|
||||
let original_content = String::from("a\nb\nc");
|
||||
let content = String::from("a\nb\nc");
|
||||
|
||||
let expected = String::from("a\nb\nc\n");
|
||||
let result = super::append_lines(original_content, content);
|
||||
assert_eq!(result, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_comments() {
|
||||
let original_content = String::from("# ===\n# test\na\n# ===\nb\nc");
|
||||
let content = String::from("# ===\n# test 2\n# ===\na\nb\nc\nd");
|
||||
|
||||
let expected = String::from("# ===\n# test\na\n# ===\nb\nc\n# ===\n# test 2\n# ===\nd\n");
|
||||
let result = super::append_lines(original_content, content);
|
||||
assert_eq!(result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main.rs
31
src/main.rs
@@ -1,14 +1,19 @@
|
||||
use analyzer::get_language_candidates;
|
||||
use cache::get_language_file;
|
||||
use cache::{get_all_languages_contents, prepare_cache};
|
||||
use emitter::emit_file;
|
||||
|
||||
use crate::cache::prepare_cache;
|
||||
|
||||
mod analyzer;
|
||||
mod cache;
|
||||
mod cli;
|
||||
mod emitter;
|
||||
|
||||
fn main() {
|
||||
let args = std::env::args().collect::<Vec<String>>()[1..].to_vec();
|
||||
let config = match cli::parse(args) {
|
||||
Ok(config) => config,
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
};
|
||||
println!("{}", config);
|
||||
let wd = match std::env::current_dir() {
|
||||
Ok(path) => path,
|
||||
Err(e) => panic!("Could not get current directory: {}", e),
|
||||
@@ -17,20 +22,16 @@ fn main() {
|
||||
Ok(_) => println!("Cache prepared"),
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
}
|
||||
let languages = match get_language_candidates(&wd) {
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
};
|
||||
let chosen = match languages.get(0) {
|
||||
Some(language) => language,
|
||||
None => panic!("Could not find language"),
|
||||
};
|
||||
let content = match get_language_file(chosen.to_string()) {
|
||||
Ok(content) => content,
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
let languages = match config.languages.len() {
|
||||
0 => match get_language_candidates(&wd) {
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
},
|
||||
_ => config.languages,
|
||||
};
|
||||
let output = get_all_languages_contents(languages);
|
||||
let file = wd.join(".gitignore");
|
||||
match emit_file(&file, content, emitter::EmitStrategy::Append) {
|
||||
match emit_file(&file, output, emitter::EmitStrategy::Append) {
|
||||
Ok(_) => println!("File emitted: {}", file.display()),
|
||||
Err(e) => panic!("Error: {}", e),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user