diff --git a/src/cargo/ops/lockfile.rs b/src/cargo/ops/lockfile.rs index 2160b6f0154..e5f279dc985 100644 --- a/src/cargo/ops/lockfile.rs +++ b/src/cargo/ops/lockfile.rs @@ -112,7 +112,12 @@ fn resolve_to_string_orig( fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String { let toml = toml::Table::try_from(resolve).unwrap(); - let mut out = String::new(); + let use_crlf = if let Some(orig) = orig { + has_crlf_line_endings(orig) + } else { + false + }; + let mut out = Emitter::new(use_crlf); // At the start of the file we notify the reader that the file is generated. // Specifically Phabricator ignores files containing "@generated", so we use that. @@ -184,11 +189,9 @@ fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String { // file doesn't contain any trailing newlines so trim out the extra if // necessary. if resolve.version() >= ResolveVersion::V2 { - while out.ends_with("\n\n") { - out.pop(); - } + out.remove_trailing_whitespace(); } - out + out.underlying } fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool { @@ -209,7 +212,7 @@ fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool { orig.lines().eq(current.lines()) } -fn emit_package(dep: &toml::Table, out: &mut String) { +fn emit_package(dep: &toml::Table, out: &mut Emitter) { out.push_str(&format!("name = {}\n", &dep["name"])); out.push_str(&format!("version = {}\n", &dep["version"])); @@ -245,3 +248,61 @@ fn lock_root(ws: &Workspace<'_>) -> Filesystem { Filesystem::new(ws.root().to_owned()) } } + +struct Emitter { + underlying: String, + use_crlf: bool, +} + +impl Emitter { + pub fn new(use_crlf: bool) -> Emitter { + Emitter { + underlying: String::new(), + use_crlf, + } + } + + pub fn push_str(&mut self, string: &str) { + if self.use_crlf { + let mut iterator = string.lines().peekable(); + + while let Some(line) = iterator.next() { + self.underlying.push_str(line); + if iterator.peek().is_some() || string.ends_with('\n') { + self.underlying.push_str("\r\n"); + } + } + } else { + self.underlying.push_str(string); + } + } + + pub fn push(&mut self, ch: char) { + if ch == '\n' && self.use_crlf { + self.underlying.push('\r'); + } + self.underlying.push(ch); + } + + pub fn remove_trailing_whitespace(&mut self) { + if self.use_crlf { + while self.underlying.ends_with("\n\r\n") { + self.underlying.pop(); + self.underlying.pop(); + } + } else { + while self.underlying.ends_with("\n\n") { + self.underlying.pop(); + } + } + } +} + +fn has_crlf_line_endings(s: &str) -> bool { + // Only check the first line. + if let Some(lf) = s.find('\n') { + s[..lf].ends_with('\r') + } else { + false + } +} diff --git a/tests/testsuite/generate_lockfile.rs b/tests/testsuite/generate_lockfile.rs index ed282fc192d..c6965d55b51 100644 --- a/tests/testsuite/generate_lockfile.rs +++ b/tests/testsuite/generate_lockfile.rs @@ -171,6 +171,37 @@ fn cargo_update_generate_lockfile() { assert!(lockfile.is_file()); } +#[cargo_test] +fn cargo_update_generate_lockfile_crlf() { + let p = project() + .file("src/main.rs", "fn main() {}") + .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1")) + .file("bar/src/lib.rs", "") + .build(); + + let lockfile = p.root().join("Cargo.lock"); + p.cargo("generate-lockfile").run(); + assert!(lockfile.is_file()); + p.cargo("generate-lockfile").run(); + + let lock0 = p.read_lockfile(); + + assert!(lock0.starts_with("# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n")); + let lock_expected = lock0.replace("\n", "\r\n"); + let lock_edited = lock_expected.replace( + "# This file is automatically @generated by Cargo", + "\r\n\r\n# This is a change to force a rewrite of the file which is otherwise skipped.", + ); + p.change_file("Cargo.lock", &lock_edited); + + p.cargo("generate-lockfile").run(); + + let lock2 = p.read_lockfile(); + + assert!(lock2.starts_with("# This file is automatically @generated by Cargo.\r\n# It is not intended for manual editing.\r\n"), "{:?}", lock2); + assert_eq!(lock2, lock_expected); +} + #[cargo_test] fn duplicate_entries_in_lockfile() { let _a = ProjectBuilder::new(paths::root().join("a"))