use anyhow::Result;
use regex::Regex;
use similar::{
ChangeTag,
TextDiff,
};
pub fn apply_version_hunks(
head_content: &str,
working_content: &str,
old_version: &str,
new_version: &str,
) -> Result<String> {
let diff = TextDiff::from_lines(head_content, working_content);
let mut result = Vec::new();
for change in diff.iter_all_changes() {
let line = change.value();
let is_version_related =
line.contains("version") || line.contains(old_version) || line.contains(new_version);
match change.tag() {
ChangeTag::Equal => {
result.push(line);
}
ChangeTag::Delete => {
if is_version_related {
} else {
result.push(line);
}
}
ChangeTag::Insert => {
if is_version_related {
result.push(line);
} else {
}
}
}
}
Ok(result.join(""))
}
pub fn has_non_version_changes(
head_content: &str,
working_content: &str,
old_version: &str,
new_version: &str,
) -> bool {
let diff = TextDiff::from_lines(head_content, working_content);
for change in diff.iter_all_changes() {
if matches!(change.tag(), ChangeTag::Delete | ChangeTag::Insert) {
let line = change.value();
let is_version_related = line.contains("version")
|| line.contains(old_version)
|| line.contains(new_version);
if !is_version_related {
return true;
}
}
}
false
}
fn is_readme_version_line(
line: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> bool {
let hyphenated = crate_name.replace('_', "-");
let underscored = crate_name.replace('-', "_");
for name in [&hyphenated, &underscored] {
for version in [old_version, new_version] {
let pattern = format!(
r#"{}(\s|[-_])?\s*=\s*"{}""#,
regex::escape(name),
regex::escape(version)
);
if let Ok(re) = Regex::new(&pattern)
&& re.is_match(line)
{
return true;
}
}
}
false
}
pub fn apply_readme_version_hunks(
head_content: &str,
working_content: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> Result<String> {
let diff = TextDiff::from_lines(head_content, working_content);
let mut result = Vec::new();
for change in diff.iter_all_changes() {
let line = change.value();
let is_version_related = is_readme_version_line(line, crate_name, old_version, new_version);
match change.tag() {
ChangeTag::Equal => {
result.push(line);
}
ChangeTag::Delete => {
if is_version_related {
} else {
result.push(line);
}
}
ChangeTag::Insert => {
if is_version_related {
result.push(line);
} else {
}
}
}
}
Ok(result.join(""))
}
pub fn has_non_readme_version_changes(
head_content: &str,
working_content: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> bool {
let diff = TextDiff::from_lines(head_content, working_content);
for change in diff.iter_all_changes() {
if matches!(change.tag(), ChangeTag::Delete | ChangeTag::Insert) {
let line = change.value();
let is_version_related =
is_readme_version_line(line, crate_name, old_version, new_version);
if !is_version_related {
return true;
}
}
}
false
}
fn is_cargo_lock_version_line(
line: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> bool {
let name_pattern = format!(r#"name\s*=\s*"{}""#, regex::escape(crate_name));
if let Ok(re) = Regex::new(&name_pattern)
&& re.is_match(line)
{
return true;
}
for version in [old_version, new_version] {
let version_pattern = format!(r#"version\s*=\s*"{}""#, regex::escape(version));
if let Ok(re) = Regex::new(&version_pattern)
&& re.is_match(line)
{
return true;
}
}
false
}
pub fn apply_cargo_lock_version_hunks(
head_content: &str,
working_content: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> Result<String> {
let diff = TextDiff::from_lines(head_content, working_content);
let mut result = Vec::new();
let changes: Vec<_> = diff.iter_all_changes().collect();
let mut current_hunk: Vec<_> = Vec::new();
let mut hunks: Vec<Vec<_>> = Vec::new();
for change in &changes {
match change.tag() {
ChangeTag::Equal => {
if !current_hunk.is_empty() {
hunks.push(current_hunk.clone());
current_hunk.clear();
}
result.push(change.value());
}
ChangeTag::Delete | ChangeTag::Insert => {
current_hunk.push(change);
}
}
}
if !current_hunk.is_empty() {
hunks.push(current_hunk);
}
result.clear();
for change in &changes {
let line = change.value();
match change.tag() {
ChangeTag::Equal => {
result.push(line);
}
ChangeTag::Delete => {
let is_our_version =
is_cargo_lock_version_line(line, crate_name, old_version, new_version);
if is_our_version {
} else {
result.push(line);
}
}
ChangeTag::Insert => {
let is_our_version =
is_cargo_lock_version_line(line, crate_name, old_version, new_version);
if is_our_version {
result.push(line);
} else {
}
}
}
}
Ok(result.join(""))
}
pub fn has_non_cargo_lock_version_changes(
head_content: &str,
working_content: &str,
crate_name: &str,
old_version: &str,
new_version: &str,
) -> bool {
let diff = TextDiff::from_lines(head_content, working_content);
for change in diff.iter_all_changes() {
if matches!(change.tag(), ChangeTag::Delete | ChangeTag::Insert) {
let line = change.value();
let is_our_version =
is_cargo_lock_version_line(line, crate_name, old_version, new_version);
if !is_our_version {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_version_hunks_only_version_change() {
let head = "[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n";
let working = "[package]\nname = \"test\"\nversion = \"0.2.0\"\nedition = \"2021\"\n";
let staged = apply_version_hunks(head, working, "0.1.0", "0.2.0").unwrap();
assert!(staged.contains("version = \"0.2.0\""));
assert!(!staged.contains("0.1.0"));
}
#[test]
fn test_apply_version_hunks_mixed_changes() {
let head = "[package]\nname = \"test\"\nversion = \"0.1.0\"\ndescription = \"old desc\"\n";
let working =
"[package]\nname = \"test\"\nversion = \"0.2.0\"\ndescription = \"new desc\"\n";
let staged = apply_version_hunks(head, working, "0.1.0", "0.2.0").unwrap();
assert!(staged.contains("version = \"0.2.0\""));
assert!(staged.contains("description = \"old desc\""));
assert!(!staged.contains("description = \"new desc\""));
}
#[test]
fn test_has_non_version_changes_true() {
let head = "[package]\nname = \"test\"\nversion = \"0.1.0\"\n";
let working = "[package]\nname = \"test-renamed\"\nversion = \"0.2.0\"\n";
assert!(has_non_version_changes(head, working, "0.1.0", "0.2.0"));
}
#[test]
fn test_has_non_version_changes_false() {
let head = "[package]\nname = \"test\"\nversion = \"0.1.0\"\n";
let working = "[package]\nname = \"test\"\nversion = \"0.2.0\"\n";
assert!(!has_non_version_changes(head, working, "0.1.0", "0.2.0"));
}
#[test]
fn test_apply_version_hunks_multiple_version_fields() {
let head =
"[package]\nversion = \"1.0.0\"\n[dependencies]\ncrate-a = { version = \"1.0.0\" }\n";
let working =
"[package]\nversion = \"2.0.0\"\n[dependencies]\ncrate-a = { version = \"2.0.0\" }\n";
let staged = apply_version_hunks(head, working, "1.0.0", "2.0.0").unwrap();
assert!(staged.contains("version = \"2.0.0\""));
assert!(!staged.contains("1.0.0"));
}
#[test]
fn test_apply_readme_version_hunks_only_version_change() {
let head = r#"# My Crate
Add to Cargo.toml:
```toml
my-crate = "0.1.0"
```
"#;
let working = r#"# My Crate
Add to Cargo.toml:
```toml
my-crate = "0.2.0"
```
"#;
let staged =
apply_readme_version_hunks(head, working, "my-crate", "0.1.0", "0.2.0").unwrap();
assert!(staged.contains(r#"my-crate = "0.2.0""#));
assert!(!staged.contains(r#"my-crate = "0.1.0""#));
}
#[test]
fn test_apply_readme_version_hunks_mixed_changes() {
let head = r#"# My Crate
Old description.
```toml
my-crate = "0.1.0"
```
"#;
let working = r#"# My Crate
New description with more details.
```toml
my-crate = "0.2.0"
```
"#;
let staged =
apply_readme_version_hunks(head, working, "my-crate", "0.1.0", "0.2.0").unwrap();
assert!(staged.contains(r#"my-crate = "0.2.0""#));
assert!(staged.contains("Old description."));
assert!(!staged.contains("New description"));
}
#[test]
fn test_apply_readme_version_hunks_underscored_name() {
let head = r#"my_crate = "1.0.0""#;
let working = r#"my_crate = "1.1.0""#;
let staged =
apply_readme_version_hunks(head, working, "my-crate", "1.0.0", "1.1.0").unwrap();
assert!(staged.contains(r#"my_crate = "1.1.0""#));
}
#[test]
fn test_has_non_readme_version_changes_true() {
let head = "# Readme\nmy-crate = \"0.1.0\"\n";
let working = "# Updated Readme\nmy-crate = \"0.2.0\"\n";
assert!(has_non_readme_version_changes(
head, working, "my-crate", "0.1.0", "0.2.0"
));
}
#[test]
fn test_has_non_readme_version_changes_false() {
let head = "# Readme\nmy-crate = \"0.1.0\"\n";
let working = "# Readme\nmy-crate = \"0.2.0\"\n";
assert!(!has_non_readme_version_changes(
head, working, "my-crate", "0.1.0", "0.2.0"
));
}
#[test]
fn test_apply_cargo_lock_version_hunks_only_our_crate() {
let head = r#"[[package]]
name = "my-crate"
version = "0.1.0"
[[package]]
name = "other-crate"
version = "1.0.0"
"#;
let working = r#"[[package]]
name = "my-crate"
version = "0.2.0"
[[package]]
name = "other-crate"
version = "1.0.0"
"#;
let staged =
apply_cargo_lock_version_hunks(head, working, "my-crate", "0.1.0", "0.2.0").unwrap();
assert!(staged.contains(r#"version = "0.2.0""#));
assert!(!staged.contains(r#"version = "0.1.0""#));
}
#[test]
fn test_apply_cargo_lock_version_hunks_mixed_changes() {
let head = r#"[[package]]
name = "my-crate"
version = "0.1.0"
[[package]]
name = "other-crate"
version = "1.0.0"
"#;
let working = r#"[[package]]
name = "my-crate"
version = "0.2.0"
[[package]]
name = "other-crate"
version = "2.0.0"
"#;
let staged =
apply_cargo_lock_version_hunks(head, working, "my-crate", "0.1.0", "0.2.0").unwrap();
assert!(staged.contains(r#"name = "my-crate""#));
assert!(
staged.contains(r#"version = "0.2.0""#),
"Our crate's new version should be included"
);
assert!(staged.contains(r#"name = "other-crate""#));
assert!(
staged.contains(r#"version = "1.0.0""#),
"Other crate's version should stay at 1.0.0"
);
assert!(
!staged.matches(r#"version = "2.0.0""#).count() > 0
|| staged.matches(r#"version = "0.2.0""#).count() == 1,
"Only our crate should have updated version"
);
}
#[test]
fn test_has_non_cargo_lock_version_changes_true() {
let head = r#"[[package]]
name = "my-crate"
version = "0.1.0"
[[package]]
name = "other-crate"
version = "1.0.0"
"#;
let working = r#"[[package]]
name = "my-crate"
version = "0.2.0"
[[package]]
name = "other-crate"
version = "2.0.0"
"#;
assert!(has_non_cargo_lock_version_changes(
head, working, "my-crate", "0.1.0", "0.2.0"
));
}
#[test]
fn test_has_non_cargo_lock_version_changes_false() {
let head = r#"[[package]]
name = "my-crate"
version = "0.1.0"
"#;
let working = r#"[[package]]
name = "my-crate"
version = "0.2.0"
"#;
assert!(!has_non_cargo_lock_version_changes(
head, working, "my-crate", "0.1.0", "0.2.0"
));
}
}