const HEADER_LINES: [&str; 5] = [
"# DO NOT EDIT THIS FILE",
"# Generated by gitignore.in",
"# See https://gitignore.in/",
"# Edit .gitignore.in instead of this file",
"# Run `gitignore.in` to build .gitignore",
];
const SEPARATOR: &str =
"# -----------------------------------------------------------------------------";
pub(crate) fn restore(text: &str) -> String {
let mut result = Vec::new();
let mut lines = text.lines().peekable();
while let Some(line) = lines.next() {
if HEADER_LINES.contains(&line) {
continue;
}
if line == SEPARATOR {
let Some(section_head) = lines.next() else {
break;
};
if let Some(target) = section_head.strip_prefix("# gibo dump ") {
result.push(format!("gibo dump {target}"));
skip_generated_section(&mut lines);
continue;
}
if let Some(target) = section_head.strip_prefix("# gi ") {
result.push(format!("gi {target}"));
skip_generated_section(&mut lines);
continue;
}
if !section_head.is_empty() {
result.push(format!("echo {}", shell_quote(section_head)));
}
continue;
}
if line.is_empty() {
result.push(line.to_string());
continue;
}
if let Some(comment) = restore_comment_line(line) {
result.push(comment);
continue;
}
result.push(format!("echo {}", shell_quote(line)));
}
result.join("\n") + "\n"
}
pub(crate) fn looks_generated(text: &str) -> bool {
HEADER_LINES.iter().all(|line| text.contains(line))
}
fn skip_generated_section<'a, I>(lines: &mut std::iter::Peekable<I>)
where
I: Iterator<Item = &'a str>,
{
while let Some(next) = lines.peek() {
if *next == SEPARATOR {
break;
}
lines.next();
}
}
fn restore_comment_line(line: &str) -> Option<String> {
line.strip_prefix("# #")
.map(|comment| format!("#{comment}"))
}
fn shell_quote(text: &str) -> String {
format!("'{}'", text.replace('\'', r#"'\''"#))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restores_generated_gitignore() {
let text = r#"# DO NOT EDIT THIS FILE
# Generated by gitignore.in
# See https://gitignore.in/
# Edit .gitignore.in instead of this file
# Run `gitignore.in` to build .gitignore
# # project comment
# -----------------------------------------------------------------------------
# gibo dump Linux
### Generated by gibo
foo
# -----------------------------------------------------------------------------
# gi Rust
### Created by gitignore.io
bar
# -----------------------------------------------------------------------------
!.env
"#;
let expected = "# project comment\ngibo dump Linux\ngi Rust\necho '!.env'\n";
assert_eq!(restore(text), expected);
}
#[test]
fn quotes_echo_content() {
let text = "# -----------------------------------------------------------------------------\na'b\n";
assert_eq!(restore(text), "echo 'a'\\''b'\n");
}
#[test]
fn restores_mixed_content_sections() {
let text = r#"# DO NOT EDIT THIS FILE
# Generated by gitignore.in
# See https://gitignore.in/
# Edit .gitignore.in instead of this file
# Run `gitignore.in` to build .gitignore
# # keep this comment
# -----------------------------------------------------------------------------
plain-entry
# -----------------------------------------------------------------------------
!important.txt
# trailing note
"#;
let expected =
"# keep this comment\necho 'plain-entry'\necho '!important.txt'\necho '# trailing note'\n";
assert_eq!(restore(text), expected);
}
#[test]
fn round_trips_generated_comment_shape() {
let restored = restore("# # original comment\n");
assert_eq!(restored, "# original comment\n");
}
}