Skip to content

Commit 5b0dfb2

Browse files
committed
Auto merge of #6521 - starsheriff:feature/6377-duplicates-in-ignore-files, r=dwijnand
avoid duplicates in ignore files Hi, here is my first PR for cargo. It's my take on #6377 with some minor refactoring included, mainly to avoid keeping the two different types of ignore file entries (gitignore and hg) in sync.) Basically, the contents of a ignore file are now read if the file exists and filtered out. To filter out I would propose to just comment the entries that cargo would add out, in that way it is nice to see which duplicates were found and more important _what cargo usually adds_. In that way, a user can modify his ignore file and be sure that he can keep everything that cargo would add. A new ignore file will look like this: ``` /target **/*.rs.bk Cargo.lock", ``` An existing ignore file will be modified like this: ``` /target /some/other/path #Added by cargo # #already existing elements are commented out #/target **/*.rs.bk Cargo.lock ``` Fixes #6377
2 parents dcb4360 + 99a23c0 commit 5b0dfb2

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)