Skip to content

Commit

Permalink
feat: add eol::convert_to_worktree().
Browse files Browse the repository at this point in the history
It's the inverse of `eol::convert_to_git()` to re-add CRLF where there were LF only.
  • Loading branch information
Byron committed Jun 26, 2023
1 parent e45fec9 commit 1517cbc
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 171 deletions.
40 changes: 40 additions & 0 deletions gix-filter/src/eol/convert_to_worktree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::clear_and_set_capacity;
use crate::eol::{AttributesDigest, Configuration, Mode, Stats};
use bstr::{ByteSlice, ByteVec};

/// Convert all `\n` in `src` to `crlf` if `digest` and `config` indicate it, returning `true` if `buf` holds the result, or `false`
/// if no change was made after all.
pub fn convert_to_worktree(src: &[u8], digest: AttributesDigest, buf: &mut Vec<u8>, config: Configuration) -> bool {
if src.is_empty() || digest.to_eol(config) != Some(Mode::CrLf) {
return false;
}
let stats = Stats::from_bytes(src);
if !stats.will_convert_lf_to_crlf(digest, config) {
return false;
}

clear_and_set_capacity(buf, src.len() + stats.lone_lf);

let mut ofs = 0;
while let Some(pos) = src[ofs..].find_byteset(b"\r\n") {
match src[ofs + pos] {
b'\r' => {
if src.get(ofs + pos + 1) == Some(&b'\n') {
buf.push_str(&src[ofs..][..pos + 2]);
ofs += pos + 2;
} else {
buf.push_str(&src[ofs..][..pos + 1]);
ofs += pos + 1;
}
}
b'\n' => {
buf.push_str(&src[ofs..][..pos]);
buf.push_str(b"\r\n");
ofs += pos + 1;
}
_ => unreachable!("would only find one of two possible values"),
}
}
buf.push_str(&src[ofs..]);
true
}
3 changes: 3 additions & 0 deletions gix-filter/src/eol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
pub mod convert_to_git;
pub use convert_to_git::function::convert_to_git;

mod convert_to_worktree;
pub use convert_to_worktree::convert_to_worktree;

mod utils;

/// The kind of end of lines to set.
Expand Down
169 changes: 169 additions & 0 deletions gix-filter/tests/eol/convert_to_git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use bstr::{ByteSlice, ByteVec};
use gix_filter::eol;
use gix_filter::eol::AttributesDigest;
use std::path::Path;

#[test]
fn with_binary_attribute_is_never_converted() {
let mut buf = Vec::new();
let changed = eol::convert_to_git(
b"hi\r\nho",
AttributesDigest::Binary,
&mut buf,
no_call,
Default::default(),
)
.expect("no error");
assert!(!changed, "the user marked it as binary so it's never being touched");
}

#[test]
fn no_crlf_means_no_work() -> crate::Result {
let mut buf = Vec::new();
let changed = eol::convert_to_git(b"hi", AttributesDigest::TextCrlf, &mut buf, no_call, Default::default())
.expect("no error");
assert!(!changed);

let changed = eol::convert_to_git(
b"hi",
AttributesDigest::TextAutoCrlf,
&mut buf,
no_object_in_index,
Default::default(),
)
.expect("no error");
assert!(!changed, "in auto-mode, the object is queried in the index as well.");
Ok(())
}

#[test]
fn detected_as_binary() -> crate::Result {
let mut buf = Vec::new();
let changed = eol::convert_to_git(
b"hi\0zero makes it binary",
AttributesDigest::TextAuto,
&mut buf,
no_call,
Default::default(),
)
.expect("no error");
assert!(
!changed,
"in auto-mode, we have a heuristic to see if the buffer is binary"
);
Ok(())
}

#[test]
fn fast_conversion_by_stripping_cr() -> crate::Result {
let mut buf = Vec::new();
let changed = eol::convert_to_git(
b"a\r\nb\r\nc",
AttributesDigest::TextCrlf,
&mut buf,
no_call,
Default::default(),
)
.expect("no error");
assert!(changed);
assert_eq!(buf.as_bstr(), "a\nb\nc", "here carriage returns can just be stripped");
Ok(())
}

#[test]
fn slower_conversion_due_to_lone_cr() -> crate::Result {
let mut buf = Vec::new();
let changed = eol::convert_to_git(
b"\r\ra\r\nb\r\nc",
AttributesDigest::TextCrlf,
&mut buf,
no_call,
Default::default(),
)
.expect("no error");
assert!(changed);
assert_eq!(
buf.as_bstr(),
"\r\ra\nb\nc",
"here carriage returns cannot be stripped but must be handled in pairs"
);
Ok(())
}

#[test]
fn crlf_in_index_prevents_conversion_to_lf() -> crate::Result {
let mut buf = Vec::new();
let mut called = false;
let changed = eol::convert_to_git(
b"elligible\n",
AttributesDigest::TextAutoInput,
&mut buf,
|buf| {
called = true;
buf.clear();
buf.push_str("with CRLF\r\n");
Ok::<_, std::convert::Infallible>(Some(()))
},
Default::default(),
)
.expect("no error");
assert!(called, "in auto mode, the index is queried as well");
assert!(
!changed,
"we saw the CRLF is present in the index, so it's unsafe to make changes"
);
Ok(())
}

#[test]
fn round_trip_check() -> crate::Result {
let mut buf = Vec::new();
for (input, expected) in [
(&b"lone-nl\nhi\r\nho"[..], "LF would be replaced by CRLF in 'hello.txt'"),
// despite trying, I was unable to get into the other branch
(b"lone-cr\nhi\r\nho", "LF would be replaced by CRLF in 'hello.txt'"),
] {
let err = eol::convert_to_git(
input,
AttributesDigest::TextCrlf,
&mut buf,
no_call,
eol::convert_to_git::Context {
round_trip_check: Some(gix_filter::eol::convert_to_git::RoundTripCheck::Fail {
rela_path: Path::new("hello.txt"),
}),
config: Default::default(),
},
)
.unwrap_err();
assert_eq!(err.to_string(), expected);

let changed = eol::convert_to_git(
input,
AttributesDigest::TextCrlf,
&mut buf,
no_call,
eol::convert_to_git::Context {
round_trip_check: Some(gix_filter::eol::convert_to_git::RoundTripCheck::Warn {
rela_path: Path::new("hello.txt"),
}),
config: Default::default(),
},
)?;
assert!(
changed,
"in warn mode, we will get a result even though it won't round-trip"
)
}
Ok(())
}

#[allow(clippy::ptr_arg)]
fn no_call(_buf: &mut Vec<u8>) -> std::io::Result<Option<()>> {
unreachable!("index function will not be called")
}

#[allow(clippy::ptr_arg)]
fn no_object_in_index(_buf: &mut Vec<u8>) -> std::io::Result<Option<()>> {
Ok(Some(()))
}
95 changes: 95 additions & 0 deletions gix-filter/tests/eol/convert_to_worktree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use bstr::ByteSlice;
use gix_filter::eol;
use gix_filter::eol::{AttributesDigest, Configuration, Mode};

#[test]
fn no_conversion_if_attribute_digest_does_not_allow_it() {
let mut buf = Vec::new();
for digest in [
AttributesDigest::Binary,
AttributesDigest::TextInput,
AttributesDigest::TextAutoInput,
] {
let changed = eol::convert_to_worktree(b"hi\nho", digest, &mut buf, Default::default());
assert!(!changed, "the digest doesn't allow for CRLF changes");
}
}

#[test]
fn no_conversion_if_configuration_does_not_allow_it() {
let mut buf = Vec::new();
for digest in [AttributesDigest::Text, AttributesDigest::TextAuto] {
for config in [
Configuration {
auto_crlf: None,
eol: Some(Mode::CrLf),
},
Configuration {
auto_crlf: Some(false),
eol: Some(Mode::Lf),
},
] {
let changed = eol::convert_to_worktree(b"hi\nho", digest, &mut buf, config);
assert!(!changed, "the configuration doesn't allow for changes");
}
}
}

#[test]
fn no_conversion_if_nothing_to_do() {
let mut buf = Vec::new();
for (input, digest, msg) in [
(
&b"hi\r\nho"[..],
AttributesDigest::TextCrlf,
"no lone line feed to handle",
),
(
&b"binary\0linefeed\nho"[..],
AttributesDigest::TextAutoCrlf,
"binary in auto-mode is never handled",
),
(
&b"binary\nlinefeed\r\nho"[..],
AttributesDigest::TextAutoCrlf,
"mixed crlf and lf is avoided",
),
(
&b"elligible-but-disabled\nhere"[..],
AttributesDigest::Binary,
"designated binary is never handled",
),
] {
let changed = eol::convert_to_worktree(input, digest, &mut buf, Default::default());
assert!(!changed, "{msg}");
}
}

#[test]
fn each_nl_is_replaced_with_crnl() {
let mut buf = Vec::new();
let changed = eol::convert_to_worktree(
b"hi\n\nho\nend",
AttributesDigest::TextCrlf,
&mut buf,
Default::default(),
);
assert!(
changed,
"the buffer has to be changed as it is explicitly demanded and has newlines to convert"
);
assert_eq!(buf.as_bstr(), "hi\r\n\r\nho\r\nend");
}

#[test]
fn existing_crnl_are_not_replaced_for_safety_nor_are_lone_cr() {
let mut buf = Vec::new();
let changed = eol::convert_to_worktree(
b"hi\r\n\nho\r\nend\r",
AttributesDigest::TextCrlf,
&mut buf,
Default::default(),
);
assert!(changed);
assert_eq!(buf.as_bstr(), "hi\r\n\r\nho\r\nend\r");
}
Loading

0 comments on commit 1517cbc

Please sign in to comment.