mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-23 09:33:01 +03:00
separate hunks type
This commit is contained in:
parent
f6c6b6c51d
commit
daad336602
94
src-tauri/src/virtual_branches/branch/hunk.rs
Normal file
94
src-tauri/src/virtual_branches/branch/hunk.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::{fmt::Display, ops::RangeInclusive};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
static CONTEXT: usize = 3; // default git diff context
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Hunk {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl From<RangeInclusive<usize>> for Hunk {
|
||||
fn from(range: RangeInclusive<usize>) -> Self {
|
||||
Hunk {
|
||||
start: *range.start(),
|
||||
end: *range.end(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Hunk {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
let mut range = s.split('-');
|
||||
if range.clone().count() != 2 {
|
||||
return Err(anyhow!("invalid range: {}", s));
|
||||
}
|
||||
let start = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse start of range: {}", s))?;
|
||||
let end = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse end of range: {}", s))?;
|
||||
if start > end {
|
||||
Err(anyhow!("invalid range: {}", s))
|
||||
} else {
|
||||
Ok(Hunk { start, end })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Hunk {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}-{}", self.start, self.end,)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hunk {
|
||||
pub fn start(&self) -> &usize {
|
||||
&self.start
|
||||
}
|
||||
|
||||
pub fn contains(&self, line: &usize) -> bool {
|
||||
self.start <= *line && self.end >= *line
|
||||
}
|
||||
|
||||
pub fn distance(&self, another: &Hunk) -> usize {
|
||||
if self.start > another.end {
|
||||
self.start - another.end
|
||||
} else if another.start > self.end {
|
||||
another.start - self.end
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touches(&self, another: &Hunk) -> bool {
|
||||
self.distance(another) <= CONTEXT * 2
|
||||
}
|
||||
|
||||
pub fn intersects(&self, another: &Hunk) -> bool {
|
||||
self.contains(&another.start)
|
||||
|| self.contains(&another.end)
|
||||
|| another.contains(&self.start)
|
||||
|| another.contains(&self.end)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_from_string() {
|
||||
let hunk = Hunk::from(1..=2);
|
||||
assert_eq!(hunk, Hunk::try_from(hunk.to_string().as_str()).unwrap());
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
mod hunk;
|
||||
mod ownership;
|
||||
mod reader;
|
||||
mod writer;
|
||||
|
||||
@ -5,404 +7,10 @@ pub use reader::BranchReader as Reader;
|
||||
pub use writer::BranchWriter as Writer;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, ops, path, vec};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Ownership {
|
||||
pub file_path: path::PathBuf,
|
||||
pub ranges: Vec<ops::RangeInclusive<usize>>,
|
||||
}
|
||||
|
||||
impl TryFrom<&String> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &String) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ownership {
|
||||
pub fn normalize(&self) -> Ownership {
|
||||
let mut ranges = self.ranges.clone();
|
||||
ranges.sort_by(|a, b| a.start().cmp(b.start()));
|
||||
ranges.dedup();
|
||||
Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
ranges,
|
||||
}
|
||||
}
|
||||
|
||||
// return a copy of self, with another ranges added
|
||||
pub fn plus(&self, another: &Ownership) -> Ownership {
|
||||
if !self.file_path.eq(&another.file_path) {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
if self.ranges.is_empty() {
|
||||
// full ownership + partial ownership = full ownership
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
if another.ranges.is_empty() {
|
||||
// partial ownership + full ownership = full ownership
|
||||
return another.clone();
|
||||
}
|
||||
|
||||
let mut ranges = self.ranges.clone();
|
||||
ranges.extend(another.ranges.clone());
|
||||
|
||||
Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
ranges,
|
||||
}
|
||||
.normalize()
|
||||
}
|
||||
|
||||
// returns a copy of self, with another ranges removed
|
||||
// if all of the ranges are removed, return None
|
||||
pub fn minus(&self, another: &Ownership) -> Option<Ownership> {
|
||||
if !self.file_path.eq(&another.file_path) {
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
if another.ranges.is_empty() {
|
||||
// any ownership - full ownership = empty ownership
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.ranges.is_empty() {
|
||||
// full ownership - partial ownership = full ownership, since we don't know all the
|
||||
// hunks.
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
let mut ranges = self.ranges.clone();
|
||||
for range in &another.ranges {
|
||||
ranges = ranges
|
||||
.iter()
|
||||
.flat_map(
|
||||
|r: &ops::RangeInclusive<usize>| -> Vec<ops::RangeInclusive<usize>> {
|
||||
if r.eq(range) {
|
||||
vec![]
|
||||
} else {
|
||||
vec![r.clone()]
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
}
|
||||
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, another: &Ownership) -> bool {
|
||||
if self.file_path != another.file_path {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.ranges.is_empty() {
|
||||
// full ownership
|
||||
return true;
|
||||
}
|
||||
|
||||
if another.ranges.is_empty() {
|
||||
// empty ownership
|
||||
return false;
|
||||
}
|
||||
|
||||
another
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| self.ranges.iter().find(|r| r.eq(&range)))
|
||||
.all(|x| x.is_some())
|
||||
}
|
||||
|
||||
fn parse_range(s: &str) -> Result<ops::RangeInclusive<usize>> {
|
||||
let mut range = s.split('-');
|
||||
if range.clone().count() != 2 {
|
||||
return Err(anyhow!("invalid range: {}", s));
|
||||
}
|
||||
let start = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse start of range: {}", s))?;
|
||||
let end = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse end of range: {}", s))?;
|
||||
if start > end {
|
||||
Err(anyhow!("invalid range: {}", s))
|
||||
} else {
|
||||
Ok(start..=end)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_string(s: &str) -> Result<Self> {
|
||||
let mut parts = s.split(':');
|
||||
let file_path = parts.next().unwrap();
|
||||
let ranges = match parts.next() {
|
||||
Some(raw_ranges) => raw_ranges
|
||||
.split(',')
|
||||
.map(Self::parse_range)
|
||||
.collect::<Result<Vec<ops::RangeInclusive<usize>>>>(),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
.context(format!("failed to parse ownership ranges: {}", s))?;
|
||||
Ok(Self {
|
||||
file_path: path::PathBuf::from(file_path),
|
||||
ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ownership_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_ownership() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![1..=2, 4..=5]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_no_ranges() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_invalid_range() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5-6");
|
||||
assert!(ownership.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_invalid_range_2() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,6-5");
|
||||
assert!(ownership.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![1..=2, 4..=5],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs:1-2,4-5".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string_no_ranges() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize() {
|
||||
vec![
|
||||
("file.txt:1-10", "file.txt:1-10"),
|
||||
("file.txt:1-10,15-16", "file.txt:1-10,15-16"),
|
||||
("file.txt:1-10,10-15,15-16", "file.txt:1-10,10-15,15-16"),
|
||||
("file.txt:1-10,5-12", "file.txt:1-10,5-12"),
|
||||
("file.txt:15-16,1-10", "file.txt:1-10,15-16"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(expected).unwrap(),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, expected)| {
|
||||
let got = a.normalize();
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"normalize {} expected {}, got {}",
|
||||
a, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus() {
|
||||
vec![
|
||||
("file.txt:1-10", "another.txt:1-5", "file.txt:1-10"),
|
||||
("file.txt:1-10", "file.txt:1-5", "file.txt:1-10,1-5"),
|
||||
("file.txt:1-10", "file.txt:12-15", "file.txt:1-10,12-15"),
|
||||
(
|
||||
"file.txt:1-10",
|
||||
"file.txt:8-15,20-25",
|
||||
"file.txt:1-10,8-15,20-25",
|
||||
),
|
||||
("file.txt:1-10", "file.txt", "file.txt"),
|
||||
("file.txt", "file.txt:1-10", "file.txt"),
|
||||
("file.txt:1-10", "file.txt:10-15", "file.txt:1-10,10-15"),
|
||||
("file.txt:5-10", "file.txt:1-5", "file.txt:1-5,5-10"),
|
||||
("file.txt:1-10", "file.txt:1-10", "file.txt:1-10"),
|
||||
("file.txt:5-10", "file.txt:2-7", "file.txt:2-7,5-10"),
|
||||
("file.txt:5-10", "file.txt:7-12", "file.txt:5-10,7-12"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
Ownership::parse_string(expected).unwrap(),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
let got = a.plus(&b);
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"{} plus {}, expected {}, got {}",
|
||||
a, b, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minus() {
|
||||
vec![
|
||||
("file.txt:1-10", "another.txt:1-5", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:1-5", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:11-15", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:1-10", None),
|
||||
("file.txt:1-10", "file.txt", None),
|
||||
("file.txt", "file.txt", None),
|
||||
("file.txt", "file.txt:1-10", Some("file.txt")),
|
||||
(
|
||||
"file.txt:1-10,11-15",
|
||||
"file.txt:11-15",
|
||||
Some("file.txt:1-10"),
|
||||
),
|
||||
(
|
||||
"file.txt:1-10,11-15,15-17",
|
||||
"file.txt:1-10,15-17",
|
||||
Some("file.txt:11-15"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
expected.map(|s| Ownership::parse_string(s).unwrap()),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
let got = a.minus(&b);
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"{} minus {}, expected {:?}, got {:?}",
|
||||
a, b, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
vec![
|
||||
("file.txt", "another.txt", false),
|
||||
("file.txt", "file.txt", true),
|
||||
("file.txt", "file.txt:1-10", true),
|
||||
("file.txt:1-10", "file.txt", false),
|
||||
("file.txt:1-10", "file.txt:11-20", false),
|
||||
("file.txt:1-10", "file.txt:1-5", false),
|
||||
("file.txt:1-10", "file.txt:5-10", false),
|
||||
("file.txt:1-10", "file.txt:1-10", true),
|
||||
("file.txt:1-10", "file.txt:2-7", false),
|
||||
("file.txt:3-5", "file.txt:1-10", false),
|
||||
("file.txt:1-10", "another.txt:1-10", false),
|
||||
("file.txt:1-10", "file.txt:1-10,20-25", false),
|
||||
("file.txt:1-10,11-15", "file.txt:11-15", true),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
expected,
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
assert_eq!(
|
||||
a.contains(&b),
|
||||
expected,
|
||||
"{} contains {}, expected {}",
|
||||
a,
|
||||
b,
|
||||
expected
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ownership {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.ranges.is_empty() {
|
||||
write!(f, "{}", self.file_path.to_str().unwrap())
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}",
|
||||
self.file_path.to_str().unwrap(),
|
||||
self.ranges
|
||||
.iter()
|
||||
.map(|r| format!("{}-{}", r.start(), r.end()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use ownership::Ownership;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Branch {
|
||||
|
376
src-tauri/src/virtual_branches/branch/ownership.rs
Normal file
376
src-tauri/src/virtual_branches/branch/ownership.rs
Normal file
@ -0,0 +1,376 @@
|
||||
use std::{fmt, path, vec};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use super::hunk::Hunk;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Ownership {
|
||||
pub file_path: path::PathBuf,
|
||||
pub hunks: Vec<Hunk>,
|
||||
}
|
||||
|
||||
impl TryFrom<&String> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &String) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Ownership {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
|
||||
Self::parse_string(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ownership {
|
||||
pub fn normalize(&self) -> Ownership {
|
||||
let mut ranges = self.hunks.clone();
|
||||
ranges.sort_by(|a, b| a.start().cmp(b.start()));
|
||||
ranges.dedup();
|
||||
Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
hunks: ranges,
|
||||
}
|
||||
}
|
||||
|
||||
// return a copy of self, with another ranges added
|
||||
pub fn plus(&self, another: &Ownership) -> Ownership {
|
||||
if !self.file_path.eq(&another.file_path) {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
if self.hunks.is_empty() {
|
||||
// full ownership + partial ownership = full ownership
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
if another.hunks.is_empty() {
|
||||
// partial ownership + full ownership = full ownership
|
||||
return another.clone();
|
||||
}
|
||||
|
||||
let mut ranges = self.hunks.clone();
|
||||
ranges.extend(another.hunks.clone());
|
||||
|
||||
Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
hunks: ranges,
|
||||
}
|
||||
.normalize()
|
||||
}
|
||||
|
||||
// returns a copy of self, with another ranges removed
|
||||
// if all of the ranges are removed, return None
|
||||
pub fn minus(&self, another: &Ownership) -> Option<Ownership> {
|
||||
if !self.file_path.eq(&another.file_path) {
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
if another.hunks.is_empty() {
|
||||
// any ownership - full ownership = empty ownership
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.hunks.is_empty() {
|
||||
// full ownership - partial ownership = full ownership, since we don't know all the
|
||||
// hunks.
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
let mut ranges = self.hunks.clone();
|
||||
for range in &another.hunks {
|
||||
ranges = ranges
|
||||
.iter()
|
||||
.flat_map(|r: &Hunk| -> Vec<Hunk> {
|
||||
if r.eq(range) {
|
||||
vec![]
|
||||
} else {
|
||||
vec![r.clone()]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ownership {
|
||||
file_path: self.file_path.clone(),
|
||||
hunks: ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, another: &Ownership) -> bool {
|
||||
if self.file_path != another.file_path {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.hunks.is_empty() {
|
||||
// full ownership
|
||||
return true;
|
||||
}
|
||||
|
||||
if another.hunks.is_empty() {
|
||||
// empty ownership
|
||||
return false;
|
||||
}
|
||||
|
||||
another
|
||||
.hunks
|
||||
.iter()
|
||||
.map(|range| self.hunks.iter().find(|r| r.eq(&range)))
|
||||
.all(|x| x.is_some())
|
||||
}
|
||||
|
||||
pub fn parse_string(s: &str) -> Result<Self> {
|
||||
let mut parts = s.split(':');
|
||||
let file_path = parts.next().unwrap();
|
||||
let ranges = match parts.next() {
|
||||
Some(raw_ranges) => raw_ranges
|
||||
.split(',')
|
||||
.map(Hunk::try_from)
|
||||
.collect::<Result<Vec<Hunk>>>(),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
.context(format!("failed to parse ownership ranges: {}", s))?;
|
||||
Ok(Self {
|
||||
file_path: path::PathBuf::from(file_path),
|
||||
hunks: ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_ownership() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
hunks: vec![(1..=2).into(), (4..=5).into()]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_no_ranges() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
hunks: vec![]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_invalid_range() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5-6");
|
||||
assert!(ownership.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_invalid_range_2() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,6-5");
|
||||
assert!(ownership.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
hunks: vec![(1..=2).into(), (4..=5).into()],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs:1-2,4-5".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string_no_ranges() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
hunks: vec![],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize() {
|
||||
vec![
|
||||
("file.txt:1-10", "file.txt:1-10"),
|
||||
("file.txt:1-10,15-16", "file.txt:1-10,15-16"),
|
||||
("file.txt:1-10,10-15,15-16", "file.txt:1-10,10-15,15-16"),
|
||||
("file.txt:1-10,5-12", "file.txt:1-10,5-12"),
|
||||
("file.txt:15-16,1-10", "file.txt:1-10,15-16"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(expected).unwrap(),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, expected)| {
|
||||
let got = a.normalize();
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"normalize {} expected {}, got {}",
|
||||
a, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus() {
|
||||
vec![
|
||||
("file.txt:1-10", "another.txt:1-5", "file.txt:1-10"),
|
||||
("file.txt:1-10", "file.txt:1-5", "file.txt:1-10,1-5"),
|
||||
("file.txt:1-10", "file.txt:12-15", "file.txt:1-10,12-15"),
|
||||
(
|
||||
"file.txt:1-10",
|
||||
"file.txt:8-15,20-25",
|
||||
"file.txt:1-10,8-15,20-25",
|
||||
),
|
||||
("file.txt:1-10", "file.txt", "file.txt"),
|
||||
("file.txt", "file.txt:1-10", "file.txt"),
|
||||
("file.txt:1-10", "file.txt:10-15", "file.txt:1-10,10-15"),
|
||||
("file.txt:5-10", "file.txt:1-5", "file.txt:1-5,5-10"),
|
||||
("file.txt:1-10", "file.txt:1-10", "file.txt:1-10"),
|
||||
("file.txt:5-10", "file.txt:2-7", "file.txt:2-7,5-10"),
|
||||
("file.txt:5-10", "file.txt:7-12", "file.txt:5-10,7-12"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
Ownership::parse_string(expected).unwrap(),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
let got = a.plus(&b);
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"{} plus {}, expected {}, got {}",
|
||||
a, b, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minus() {
|
||||
vec![
|
||||
("file.txt:1-10", "another.txt:1-5", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:1-5", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:11-15", Some("file.txt:1-10")),
|
||||
("file.txt:1-10", "file.txt:1-10", None),
|
||||
("file.txt:1-10", "file.txt", None),
|
||||
("file.txt", "file.txt", None),
|
||||
("file.txt", "file.txt:1-10", Some("file.txt")),
|
||||
(
|
||||
"file.txt:1-10,11-15",
|
||||
"file.txt:11-15",
|
||||
Some("file.txt:1-10"),
|
||||
),
|
||||
(
|
||||
"file.txt:1-10,11-15,15-17",
|
||||
"file.txt:1-10,15-17",
|
||||
Some("file.txt:11-15"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
expected.map(|s| Ownership::parse_string(s).unwrap()),
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
let got = a.minus(&b);
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"{} minus {}, expected {:?}, got {:?}",
|
||||
a, b, expected, got
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
vec![
|
||||
("file.txt", "another.txt", false),
|
||||
("file.txt", "file.txt", true),
|
||||
("file.txt", "file.txt:1-10", true),
|
||||
("file.txt:1-10", "file.txt", false),
|
||||
("file.txt:1-10", "file.txt:11-20", false),
|
||||
("file.txt:1-10", "file.txt:1-5", false),
|
||||
("file.txt:1-10", "file.txt:5-10", false),
|
||||
("file.txt:1-10", "file.txt:1-10", true),
|
||||
("file.txt:1-10", "file.txt:2-7", false),
|
||||
("file.txt:3-5", "file.txt:1-10", false),
|
||||
("file.txt:1-10", "another.txt:1-10", false),
|
||||
("file.txt:1-10", "file.txt:1-10,20-25", false),
|
||||
("file.txt:1-10,11-15", "file.txt:11-15", true),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(a, b, expected)| {
|
||||
(
|
||||
Ownership::parse_string(a).unwrap(),
|
||||
Ownership::parse_string(b).unwrap(),
|
||||
expected,
|
||||
)
|
||||
})
|
||||
.for_each(|(a, b, expected)| {
|
||||
assert_eq!(
|
||||
a.contains(&b),
|
||||
expected,
|
||||
"{} contains {}, expected {}",
|
||||
a,
|
||||
b,
|
||||
expected
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ownership {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.hunks.is_empty() {
|
||||
write!(f, "{}", self.file_path.to_str().unwrap())
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}",
|
||||
self.file_path.to_str().unwrap(),
|
||||
self.hunks
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
ownership: vec![Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ pub mod target;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops, path, time, vec,
|
||||
path, time, vec,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
@ -429,7 +429,7 @@ pub fn move_files(
|
||||
};
|
||||
|
||||
for ownership in to_move {
|
||||
let source_branches = if ownership.ranges.is_empty() {
|
||||
let source_branches = if ownership.hunks.is_empty() {
|
||||
// find all branches that own any part of the file
|
||||
virtual_branches
|
||||
.clone()
|
||||
@ -483,34 +483,6 @@ pub fn move_files(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn distance(a: &ops::RangeInclusive<usize>, b: &ops::RangeInclusive<usize>) -> usize {
|
||||
if a.start() > b.end() {
|
||||
a.start() - b.end()
|
||||
} else if b.start() > a.end() {
|
||||
b.start() - a.end()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn ranges_intersect(
|
||||
one: &ops::RangeInclusive<usize>,
|
||||
another: &ops::RangeInclusive<usize>,
|
||||
) -> bool {
|
||||
one.contains(another.start())
|
||||
|| one.contains(another.end())
|
||||
|| another.contains(one.start())
|
||||
|| another.contains(one.end())
|
||||
}
|
||||
|
||||
fn ranges_touching(
|
||||
one: &ops::RangeInclusive<usize>,
|
||||
another: &ops::RangeInclusive<usize>,
|
||||
context: usize,
|
||||
) -> bool {
|
||||
distance(one, another) <= context || distance(another, one) <= context
|
||||
}
|
||||
|
||||
fn explicit_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option<branch::Branch> {
|
||||
stack
|
||||
.iter()
|
||||
@ -518,7 +490,7 @@ fn explicit_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Optio
|
||||
branch
|
||||
.ownership
|
||||
.iter()
|
||||
.filter(|ownership| !ownership.ranges.is_empty()) // only consider explicit ownership
|
||||
.filter(|ownership| !ownership.hunks.is_empty()) // only consider explicit ownership
|
||||
.any(|ownership| ownership.contains(needle))
|
||||
})
|
||||
.cloned()
|
||||
@ -534,13 +506,13 @@ fn owned_by_proximity(
|
||||
branch
|
||||
.ownership
|
||||
.iter()
|
||||
.filter(|ownership| !ownership.ranges.is_empty()) // only consider explicit ownership
|
||||
.filter(|ownership| !ownership.hunks.is_empty()) // only consider explicit ownership
|
||||
.any(|ownership| {
|
||||
ownership.ranges.iter().any(|range| {
|
||||
ownership.hunks.iter().any(|range| {
|
||||
needle
|
||||
.ranges
|
||||
.hunks
|
||||
.iter()
|
||||
.any(|r| ranges_touching(r, range, 6) || ranges_intersect(r, range))
|
||||
.any(|r| r.touches(range) || r.intersects(range))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -86,7 +86,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ fn test_branch() -> virtual_branches::branch::Branch {
|
||||
.unwrap(),
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
hunks: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user