Skip to content

Commit 89f473f

Browse files
committed
feat(ignore): merge new templates into existing .gitignore without duplicates
1 parent c2a43de commit 89f473f

1 file changed

Lines changed: 39 additions & 11 deletions

File tree

src/ignore/mod.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
22
use clap::Subcommand;
33
use std::fs;
44

5-
use crate::utils::{confirm, find_repo_root};
5+
use crate::utils::find_repo_root;
66

77
const API_BASE: &str = "https://www.toptal.com/developers/gitignore/api";
88

@@ -35,24 +35,20 @@ pub fn run(cmd: IgnoreCommand) -> Result<()> {
3535
}
3636
}
3737

38-
fn add(templates: &str, yes: bool, force: bool, dry_run: bool) -> Result<()> {
38+
fn add(templates: &str, _yes: bool, _force: bool, dry_run: bool) -> Result<()> {
3939
let root = find_repo_root()?;
4040
let path = root.join(".gitignore");
4141

42-
if path.exists() && !force && !confirm(".gitignore already exists. Overwrite?", yes) {
43-
println!("Aborted.");
44-
return Ok(());
45-
}
46-
47-
let content = resolve_templates(templates)?;
42+
let new_content = resolve_templates(templates)?;
43+
let merged = merge_gitignore(&path, &new_content);
4844

4945
if dry_run {
50-
println!("[dry-run] Would write .gitignore:\n{content}");
46+
println!("[dry-run] Would write .gitignore:\n{merged}");
5147
return Ok(());
5248
}
5349

54-
fs::write(&path, content).context("Failed to write .gitignore")?;
55-
println!("Generated .gitignore for: {templates}");
50+
fs::write(&path, merged).context("Failed to write .gitignore")?;
51+
println!("Updated .gitignore for: {templates}");
5652
Ok(())
5753
}
5854

@@ -113,6 +109,38 @@ fn list(filter: Option<&str>) -> Result<()> {
113109
Ok(())
114110
}
115111

112+
/// Merge new gitignore content into existing file, skipping lines already present.
113+
/// Preserves existing content and appends only new non-duplicate lines.
114+
fn merge_gitignore(path: &std::path::Path, new_content: &str) -> String {
115+
let existing = if path.exists() {
116+
fs::read_to_string(path).unwrap_or_default()
117+
} else {
118+
String::new()
119+
};
120+
121+
let existing_lines: std::collections::HashSet<&str> = existing.lines().collect();
122+
123+
let to_append: String = new_content
124+
.lines()
125+
.filter(|line| !existing_lines.contains(line))
126+
.fold(String::new(), |mut acc, line| {
127+
acc.push_str(line);
128+
acc.push('\n');
129+
acc
130+
});
131+
132+
if to_append.trim().is_empty() {
133+
return existing;
134+
}
135+
136+
let mut result = existing;
137+
if !result.ends_with('\n') && !result.is_empty() {
138+
result.push('\n');
139+
}
140+
result.push_str(&to_append);
141+
result
142+
}
143+
116144
mod builtins {
117145
pub(super) const NAMES: &[&str] = &["agentic"];
118146

0 commit comments

Comments
 (0)