feat: config struct, merge multiple language selections

This commit is contained in:
Chen Asraf
2023-03-08 22:55:06 +02:00
parent d8a9e5399a
commit 1997984647
9 changed files with 325 additions and 50 deletions

17
.vscode/launch.json vendored
View File

@@ -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
View File

@@ -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
View File

@@ -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"

View File

@@ -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"

View File

@@ -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,

View File

@@ -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
View 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
}

View File

@@ -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)
}
}

View File

@@ -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),
};