mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-23 09:33:01 +03:00
detect changed hunks
This commit is contained in:
parent
0a45945738
commit
c30623f15a
@ -53,6 +53,16 @@ impl Ownership {
|
|||||||
return self.clone();
|
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();
|
let mut ranges = self.ranges.clone();
|
||||||
ranges.extend(another.ranges.clone());
|
ranges.extend(another.ranges.clone());
|
||||||
|
|
||||||
@ -272,6 +282,8 @@ mod ownership_tests {
|
|||||||
"file.txt:8-15,20-25",
|
"file.txt:8-15,20-25",
|
||||||
"file.txt:1-10,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: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: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:1-10", "file.txt:1-10", "file.txt:1-10"),
|
||||||
|
@ -4,7 +4,7 @@ pub mod target;
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path, time, vec,
|
ops, path, time, vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
@ -13,7 +13,6 @@ use serde::Serialize;
|
|||||||
|
|
||||||
pub use branch::Branch;
|
pub use branch::Branch;
|
||||||
pub use iterator::BranchIterator as Iterator;
|
pub use iterator::BranchIterator as Iterator;
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{gb_repository, project_repository, reader, sessions};
|
use crate::{gb_repository, project_repository, reader, sessions};
|
||||||
@ -442,7 +441,8 @@ pub fn move_files(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
let owner = find_owner(&virtual_branches, ownership)
|
let owner = explicit_owner(&virtual_branches, ownership)
|
||||||
|
.or_else(|| implicit_owner(&virtual_branches, ownership))
|
||||||
.context(format!("failed to find owner branch for {}", ownership))?
|
.context(format!("failed to find owner branch for {}", ownership))?
|
||||||
.clone();
|
.clone();
|
||||||
vec![owner]
|
vec![owner]
|
||||||
@ -483,15 +483,72 @@ pub fn move_files(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option<branch::Branch> {
|
fn distance(a: &ops::RangeInclusive<usize>, b: &ops::RangeInclusive<usize>) -> usize {
|
||||||
let explicitly_owned_by = stack.iter().find(|b| {
|
if a.start() > b.end() {
|
||||||
b.ownership
|
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()
|
.iter()
|
||||||
.filter(|o| !o.ranges.is_empty())
|
.find(|branch| {
|
||||||
.any(|o| o.contains(needle))
|
branch
|
||||||
});
|
.ownership
|
||||||
let implicitly_owned_by = stack.iter().find(|b| b.contains(needle));
|
.iter()
|
||||||
explicitly_owned_by.or(implicitly_owned_by).cloned()
|
.filter(|ownership| !ownership.ranges.is_empty()) // only consider explicit ownership
|
||||||
|
.any(|ownership| ownership.contains(needle))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owned_by_proximity(
|
||||||
|
stack: &[branch::Branch],
|
||||||
|
needle: &branch::Ownership,
|
||||||
|
) -> Option<branch::Branch> {
|
||||||
|
stack
|
||||||
|
.iter()
|
||||||
|
.find(|branch| {
|
||||||
|
branch
|
||||||
|
.ownership
|
||||||
|
.iter()
|
||||||
|
.filter(|ownership| !ownership.ranges.is_empty()) // only consider explicit ownership
|
||||||
|
.any(|ownership| {
|
||||||
|
ownership.ranges.iter().any(|range| {
|
||||||
|
needle
|
||||||
|
.ranges
|
||||||
|
.iter()
|
||||||
|
.any(|r| ranges_touching(r, range, 6) || ranges_intersect(r, range))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn implicit_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option<branch::Branch> {
|
||||||
|
stack.iter().find(|branch| branch.contains(needle)).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_to_hunks_by_filepath(
|
fn diff_to_hunks_by_filepath(
|
||||||
@ -697,12 +754,10 @@ pub fn get_status_by_branch(
|
|||||||
for hunk in all_hunks {
|
for hunk in all_hunks {
|
||||||
let hunk_ownership = Ownership::try_from(&hunk.id)?;
|
let hunk_ownership = Ownership::try_from(&hunk.id)?;
|
||||||
|
|
||||||
let owned_by = find_owner(&virtual_branches, &hunk_ownership);
|
let owned_by = explicit_owner(&virtual_branches, &hunk_ownership)
|
||||||
|
.or_else(|| implicit_owner(&virtual_branches, &hunk_ownership))
|
||||||
|
.or_else(|| owned_by_proximity(&virtual_branches, &hunk_ownership));
|
||||||
if let Some(branch) = owned_by {
|
if let Some(branch) = owned_by {
|
||||||
if hunk.file_path == "src-tauri/Cargo.lock" {
|
|
||||||
println!("explicit branch: {:?}", branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
hunks_by_branch_id
|
hunks_by_branch_id
|
||||||
.entry(branch.id.clone())
|
.entry(branch.id.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
@ -1317,6 +1372,75 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hunk_expantion() -> Result<()> {
|
||||||
|
let repository = test_repository()?;
|
||||||
|
let project = projects::Project::try_from(&repository)?;
|
||||||
|
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
||||||
|
let storage = storage::Storage::from_path(tempdir()?.path());
|
||||||
|
let user_store = users::Storage::new(storage.clone());
|
||||||
|
let project_store = projects::Storage::new(storage);
|
||||||
|
project_store.add_project(&project)?;
|
||||||
|
let gb_repo = gb_repository::Repository::open(
|
||||||
|
gb_repo_path,
|
||||||
|
project.id.clone(),
|
||||||
|
project_store,
|
||||||
|
user_store,
|
||||||
|
)?;
|
||||||
|
let project_repository = project_repository::Repository::open(&project)?;
|
||||||
|
|
||||||
|
target::Writer::new(&gb_repo).write_default(&target::Target {
|
||||||
|
name: "origin".to_string(),
|
||||||
|
remote: "origin".to_string(),
|
||||||
|
sha: repository.head().unwrap().target().unwrap(),
|
||||||
|
behind: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let file_path = std::path::Path::new("test.txt");
|
||||||
|
std::fs::write(
|
||||||
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
|
"line1\nline2\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
||||||
|
.expect("failed to create virtual branch");
|
||||||
|
let branch2_id = create_virtual_branch(&gb_repo, "test_branch2")
|
||||||
|
.expect("failed to create virtual branch");
|
||||||
|
branch::Writer::new(&gb_repo).write_selected(&Some(branch1_id.clone()))?;
|
||||||
|
|
||||||
|
let statuses =
|
||||||
|
get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status");
|
||||||
|
let files_by_branch_id = statuses
|
||||||
|
.iter()
|
||||||
|
.map(|(branch, files)| (branch.id.clone(), files))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
assert_eq!(files_by_branch_id.len(), 2);
|
||||||
|
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
|
||||||
|
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
|
||||||
|
|
||||||
|
// even though selected branch has changed
|
||||||
|
branch::Writer::new(&gb_repo).write_selected(&Some(branch2_id.clone()))?;
|
||||||
|
// a slightly different hunk should still go to the same branch
|
||||||
|
std::fs::write(
|
||||||
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
|
"line1\nline2\nline3\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let statuses =
|
||||||
|
get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status");
|
||||||
|
let files_by_branch_id = statuses
|
||||||
|
.iter()
|
||||||
|
.map(|(branch, files)| (branch.id.clone(), files))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
assert_eq!(files_by_branch_id.len(), 2);
|
||||||
|
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
|
||||||
|
assert_eq!(files_by_branch_id[&branch2_id].len(), 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_status_files_by_branch() -> Result<()> {
|
fn test_get_status_files_by_branch() -> Result<()> {
|
||||||
let repository = test_repository()?;
|
let repository = test_repository()?;
|
||||||
@ -1380,7 +1504,7 @@ mod tests {
|
|||||||
let file_path = std::path::Path::new("test.txt");
|
let file_path = std::path::Path::new("test.txt");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\n",
|
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
commit_all(&repository)?;
|
commit_all(&repository)?;
|
||||||
|
|
||||||
@ -1406,7 +1530,7 @@ mod tests {
|
|||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n",
|
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branch_writer = branch::Writer::new(&gb_repo);
|
let branch_writer = branch::Writer::new(&gb_repo);
|
||||||
@ -1422,7 +1546,7 @@ mod tests {
|
|||||||
})?;
|
})?;
|
||||||
let branch1 = branch_reader.read(&branch1_id)?;
|
let branch1 = branch_reader.read(&branch1_id)?;
|
||||||
branch_writer.write(&branch::Branch {
|
branch_writer.write(&branch::Branch {
|
||||||
ownership: vec!["test.txt:8-12".try_into()?],
|
ownership: vec!["test.txt:11-15".try_into()?],
|
||||||
..branch1
|
..branch1
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -1451,8 +1575,6 @@ mod tests {
|
|||||||
.map(|(branch, files)| (branch.id.clone(), files))
|
.map(|(branch, files)| (branch.id.clone(), files))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
println!("{:#?}", statuses);
|
|
||||||
|
|
||||||
assert_eq!(files_by_branch_id.len(), 2);
|
assert_eq!(files_by_branch_id.len(), 2);
|
||||||
assert_eq!(files_by_branch_id[&branch1_id].len(), 0);
|
assert_eq!(files_by_branch_id[&branch1_id].len(), 0);
|
||||||
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
|
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
|
||||||
@ -1462,7 +1584,7 @@ mod tests {
|
|||||||
assert_eq!(branch_reader.read(&branch1_id)?.ownership, vec![]);
|
assert_eq!(branch_reader.read(&branch1_id)?.ownership, vec![]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
branch_reader.read(&branch2_id)?.ownership,
|
branch_reader.read(&branch2_id)?.ownership,
|
||||||
vec!["test.txt:1-5,8-12".try_into()?]
|
vec!["test.txt".try_into()?]
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -1643,7 +1765,7 @@ mod tests {
|
|||||||
let file_path = std::path::Path::new("test.txt");
|
let file_path = std::path::Path::new("test.txt");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\n",
|
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\n",
|
||||||
)?;
|
)?;
|
||||||
commit_all(&repository)?;
|
commit_all(&repository)?;
|
||||||
|
|
||||||
@ -1665,7 +1787,7 @@ mod tests {
|
|||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n",
|
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
||||||
@ -1698,6 +1820,8 @@ mod tests {
|
|||||||
.map(|(branch, files)| (branch.id.clone(), files))
|
.map(|(branch, files)| (branch.id.clone(), files))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
println!("{:#?}", statuses);
|
||||||
|
|
||||||
assert_eq!(files_by_branch_id.len(), 2);
|
assert_eq!(files_by_branch_id.len(), 2);
|
||||||
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
|
assert_eq!(files_by_branch_id[&branch1_id].len(), 1);
|
||||||
assert_eq!(files_by_branch_id[&branch1_id][0].hunks.len(), 1);
|
assert_eq!(files_by_branch_id[&branch1_id][0].hunks.len(), 1);
|
||||||
@ -1709,7 +1833,7 @@ mod tests {
|
|||||||
let branch_reader = branch::Reader::new(¤t_session_reader);
|
let branch_reader = branch::Reader::new(¤t_session_reader);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
branch_reader.read(&branch1_id)?.ownership,
|
branch_reader.read(&branch1_id)?.ownership,
|
||||||
vec!["test.txt:8-12".try_into()?]
|
vec!["test.txt:12-16".try_into()?]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
branch_reader.read(&branch2_id)?.ownership,
|
branch_reader.read(&branch2_id)?.ownership,
|
||||||
@ -1720,7 +1844,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_move_hunks_partial_implicity() -> Result<()> {
|
fn test_move_hunks_partial_implicity_owned() -> Result<()> {
|
||||||
let repository = test_repository()?;
|
let repository = test_repository()?;
|
||||||
let project = projects::Project::try_from(&repository)?;
|
let project = projects::Project::try_from(&repository)?;
|
||||||
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
|
||||||
@ -1732,7 +1856,7 @@ mod tests {
|
|||||||
let file_path = std::path::Path::new("test.txt");
|
let file_path = std::path::Path::new("test.txt");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\n",
|
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
commit_all(&repository)?;
|
commit_all(&repository)?;
|
||||||
|
|
||||||
@ -1754,7 +1878,7 @@ mod tests {
|
|||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
std::path::Path::new(&project.path).join(file_path),
|
std::path::Path::new(&project.path).join(file_path),
|
||||||
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n",
|
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
||||||
@ -1792,6 +1916,8 @@ mod tests {
|
|||||||
let statuses =
|
let statuses =
|
||||||
get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status");
|
get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status");
|
||||||
|
|
||||||
|
println!("{:#?}", statuses);
|
||||||
|
|
||||||
let files_by_branch_id = statuses
|
let files_by_branch_id = statuses
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(branch, files)| (branch.id.clone(), files))
|
.map(|(branch, files)| (branch.id.clone(), files))
|
||||||
|
@ -56,10 +56,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
gb_repository, projects, sessions, storage, users,
|
gb_repository, projects, sessions, storage, users,
|
||||||
virtual_branches::{
|
virtual_branches::{branch, target::writer::TargetWriter},
|
||||||
branch,
|
|
||||||
target::{self, writer::TargetWriter},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -111,21 +108,6 @@ mod tests {
|
|||||||
Ok(repository)
|
Ok(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
static mut TEST_TARGET_INDEX: usize = 0;
|
|
||||||
|
|
||||||
fn test_target() -> Target {
|
|
||||||
Target {
|
|
||||||
name: format!("target_name_{}", unsafe { TEST_TARGET_INDEX }),
|
|
||||||
remote: format!("remote_{}", unsafe { TEST_TARGET_INDEX }),
|
|
||||||
sha: git2::Oid::from_str(&format!(
|
|
||||||
"0123456789abcdef0123456789abcdef0123456{}",
|
|
||||||
unsafe { TEST_TARGET_INDEX }
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
behind: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_read_not_found() -> Result<()> {
|
fn test_read_not_found() -> Result<()> {
|
||||||
let repository = test_repository()?;
|
let repository = test_repository()?;
|
||||||
|
Loading…
Reference in New Issue
Block a user