Skip to content

Commit 99a23c0

Browse files
committed
avoid duplicates in ignore files
existing ignore files are now checked for duplicates. If the file already contains an entry it is commented out in the section added by cargo. fixes #6377
1 parent dd97f8b commit 99a23c0

File tree

3 files changed

+176
-45
lines changed

3 files changed

+176
-45
lines changed

src/cargo/ops/cargo_new.rs

+125-45
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
22
use std::env;
33
use std::fmt;
44
use std::fs;
5+
use std::io::{BufReader, BufRead, ErrorKind};
56
use std::path::{Path, PathBuf};
67

78
use git2::Config as GitConfig;
@@ -409,69 +410,117 @@ pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
409410
Ok(())
410411
}
411412

412-
fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
413-
let path = opts.path;
414-
let name = opts.name;
415-
let cfg = global_config(config)?;
416-
// Please ensure that ignore and hgignore are in sync.
417-
let ignore = [
418-
"/target\n",
419-
"**/*.rs.bk\n",
420-
if !opts.bin { "Cargo.lock\n" } else { "" },
421-
]
422-
.concat();
423-
// Mercurial glob ignores can't be rooted, so just sticking a 'syntax: glob' at the top of the
424-
// file will exclude too much. Instead, use regexp-based ignores. See 'hg help ignore' for
425-
// more.
426-
let hgignore = [
427-
"^target/\n",
428-
"glob:*.rs.bk\n",
429-
if !opts.bin { "glob:Cargo.lock\n" } else { "" },
430-
]
431-
.concat();
432413

433-
let vcs = opts.version_control.unwrap_or_else(|| {
434-
let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), config.cwd());
435-
match (cfg.version_control, in_existing_vcs) {
436-
(None, false) => VersionControl::Git,
437-
(Some(opt), false) => opt,
438-
(_, true) => VersionControl::NoVcs,
414+
/// IgnoreList
415+
struct IgnoreList {
416+
/// git like formatted entries
417+
ignore: Vec<String>,
418+
/// mercurial formatted entries
419+
hg_ignore: Vec<String>,
420+
}
421+
422+
impl IgnoreList {
423+
/// constructor to build a new ignore file
424+
fn new() -> IgnoreList {
425+
return IgnoreList{
426+
ignore: Vec::new(),
427+
hg_ignore: Vec::new(),
439428
}
440-
});
429+
}
430+
431+
/// add a new entry to the ignore list. Requires two arguments with the
432+
/// entry in two different formats. One for "git style" entries and one for
433+
/// "mercurial like" entries.
434+
fn push(&mut self, ignore: &str, hg_ignore: &str) {
435+
self.ignore.push(ignore.to_string());
436+
self.hg_ignore.push(hg_ignore.to_string());
437+
}
438+
439+
/// Return the correctly formatted content of the ignore file for the given
440+
/// version control system as `String`.
441+
fn format_new(&self, vcs: VersionControl) -> String {
442+
match vcs {
443+
VersionControl::Hg => return self.hg_ignore.join("\n"),
444+
_ => return self.ignore.join("\n"),
445+
};
446+
}
447+
448+
/// format_existing is used to format the IgnoreList when the ignore file
449+
/// already exists. It reads the contents of the given `BufRead` and
450+
/// checks if the contents of the ignore list are already existing in the
451+
/// file.
452+
fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> String {
453+
// TODO: is unwrap safe?
454+
let existing_items = existing.lines().collect::<Result<Vec<_>, _>>().unwrap();
455+
456+
let ignore_items = match vcs {
457+
VersionControl::Hg => &self.hg_ignore,
458+
_ => &self.ignore,
459+
};
460+
461+
let mut out = "\n\n#Added by cargo\n\
462+
#\n\
463+
#already existing elements are commented out\n".
464+
to_string();
465+
466+
for item in ignore_items {
467+
out.push('\n');
468+
if existing_items.contains(item) {
469+
out.push('#');
470+
}
471+
out.push_str(item)
472+
}
473+
474+
out
475+
}
476+
}
477+
478+
/// write the ignore file to the given directory. If the ignore file for the
479+
/// given vcs system already exists, its content is read and duplicate ignore
480+
/// file entries are filtered out.
481+
fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<String>{
482+
let fp_ignore = match vcs {
483+
VersionControl::Git => base_path.join(".gitignore"),
484+
VersionControl::Hg => base_path.join(".hgignore"),
485+
VersionControl::Pijul => base_path.join(".ignore"),
486+
VersionControl::Fossil => return Ok("".to_string()),
487+
VersionControl::NoVcs => return Ok("".to_string()),
488+
};
489+
490+
let ignore: String = match fs::File::open(&fp_ignore) {
491+
Err(why) => {
492+
match why.kind() {
493+
ErrorKind::NotFound => list.format_new(vcs),
494+
_ => return Err(failure::format_err!("{}", why)),
495+
}
496+
},
497+
Ok(file) => {
498+
list.format_existing(BufReader::new(file), vcs)
499+
},
500+
};
441501

502+
paths::append(&fp_ignore, ignore.as_bytes())?;
503+
504+
return Ok(ignore)
505+
}
506+
507+
/// initialize the correct vcs system based on the provided config
508+
fn init_vcs(path: &Path, vcs: VersionControl, config: &Config) -> CargoResult<()> {
442509
match vcs {
443510
VersionControl::Git => {
444511
if !path.join(".git").exists() {
445512
GitRepo::init(path, config.cwd())?;
446513
}
447-
let ignore = if path.join(".gitignore").exists() {
448-
format!("\n{}", ignore)
449-
} else {
450-
ignore
451-
};
452-
paths::append(&path.join(".gitignore"), ignore.as_bytes())?;
453514
}
454515
VersionControl::Hg => {
455516
if !path.join(".hg").exists() {
456517
HgRepo::init(path, config.cwd())?;
457518
}
458-
let hgignore = if path.join(".hgignore").exists() {
459-
format!("\n{}", hgignore)
460-
} else {
461-
hgignore
462-
};
463-
paths::append(&path.join(".hgignore"), hgignore.as_bytes())?;
464519
}
465520
VersionControl::Pijul => {
466521
if !path.join(".pijul").exists() {
467522
PijulRepo::init(path, config.cwd())?;
468523
}
469-
let ignore = if path.join(".ignore").exists() {
470-
format!("\n{}", ignore)
471-
} else {
472-
ignore
473-
};
474-
paths::append(&path.join(".ignore"), ignore.as_bytes())?;
475524
}
476525
VersionControl::Fossil => {
477526
if path.join(".fossil").exists() {
@@ -483,6 +532,37 @@ fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
483532
}
484533
};
485534

535+
Ok(())
536+
}
537+
538+
fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
539+
let path = opts.path;
540+
let name = opts.name;
541+
let cfg = global_config(config)?;
542+
543+
544+
// using the push method with two arguments ensures that the entries for
545+
// both ignore and hgignore are in sync.
546+
let mut ignore = IgnoreList::new();
547+
ignore.push("/target", "^target/");
548+
ignore.push("**/*.rs.bk", "glob:*.rs.bk\n");
549+
if !opts.bin {
550+
ignore.push("Cargo.lock", "glob:Cargo.lock");
551+
}
552+
553+
let vcs = opts.version_control.unwrap_or_else(|| {
554+
let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), config.cwd());
555+
match (cfg.version_control, in_existing_vcs) {
556+
(None, false) => VersionControl::Git,
557+
(Some(opt), false) => opt,
558+
(_, true) => VersionControl::NoVcs,
559+
}
560+
});
561+
562+
init_vcs(path, vcs, config)?;
563+
write_ignore_file(path, &ignore, vcs)?;
564+
565+
486566
let (author_name, email) = discover_author()?;
487567
// Hoo boy, sure glad we've got exhaustiveness checking behind us.
488568
let author = match (cfg.name, cfg.email, author_name, email) {

tests/testsuite/init.rs

+39
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ fn simple_bin() {
4444
.is_file());
4545
}
4646

47+
#[test]
48+
fn simple_git_ignore_exists() {
49+
// write a .gitignore file with one entry
50+
fs::create_dir_all(paths::root().join("foo")).unwrap();
51+
let mut ignore_file = File::create(paths::root().join("foo/.gitignore")).unwrap();
52+
ignore_file.write("/target\n**/some.file".as_bytes()).unwrap();
53+
54+
cargo_process("init --lib foo --edition 2015")
55+
.env("USER", "foo")
56+
.run();
57+
58+
assert!(paths::root().is_dir());
59+
assert!(paths::root().join("foo/Cargo.toml").is_file());
60+
assert!(paths::root().join("foo/src/lib.rs").is_file());
61+
assert!(paths::root().join("foo/.git").is_dir());
62+
assert!(paths::root().join("foo/.gitignore").is_file());
63+
64+
let fp = paths::root().join("foo/.gitignore");
65+
let mut contents = String::new();
66+
File::open(&fp)
67+
.unwrap()
68+
.read_to_string(&mut contents)
69+
.unwrap();
70+
assert_eq!(
71+
contents,
72+
"/target\n\
73+
**/some.file\n\n\
74+
#Added by cargo\n\
75+
#\n\
76+
#already existing elements are commented out\n\
77+
\n\
78+
#/target\n\
79+
**/*.rs.bk\n\
80+
Cargo.lock",
81+
);
82+
83+
cargo_process("build").cwd(&paths::root().join("foo")).run();
84+
}
85+
4786
#[test]
4887
fn both_lib_and_bin() {
4988
cargo_process("init --lib --bin")

tests/testsuite/new.rs

+12
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,21 @@ fn simple_git() {
8383
assert!(paths::root().join("foo/.git").is_dir());
8484
assert!(paths::root().join("foo/.gitignore").is_file());
8585

86+
let fp = paths::root().join("foo/.gitignore");
87+
let mut contents = String::new();
88+
File::open(&fp)
89+
.unwrap()
90+
.read_to_string(&mut contents)
91+
.unwrap();
92+
assert_eq!(
93+
contents,
94+
"/target\n**/*.rs.bk\nCargo.lock",
95+
);
96+
8697
cargo_process("build").cwd(&paths::root().join("foo")).run();
8798
}
8899

100+
89101
#[test]
90102
fn no_argument() {
91103
cargo_process("new")

0 commit comments

Comments
 (0)