diff --git a/src-tauri/src/virtual_branches/branch/mod.rs b/src-tauri/src/virtual_branches/branch/mod.rs index 71542599f..9ffc018f1 100644 --- a/src-tauri/src/virtual_branches/branch/mod.rs +++ b/src-tauri/src/virtual_branches/branch/mod.rs @@ -53,6 +53,16 @@ impl Ownership { 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()); @@ -272,6 +282,8 @@ mod ownership_tests { "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"), diff --git a/src-tauri/src/virtual_branches/mod.rs b/src-tauri/src/virtual_branches/mod.rs index 771411064..52b68f315 100644 --- a/src-tauri/src/virtual_branches/mod.rs +++ b/src-tauri/src/virtual_branches/mod.rs @@ -4,7 +4,7 @@ pub mod target; use std::{ collections::{HashMap, HashSet}, - path, time, vec, + ops, path, time, vec, }; use anyhow::{bail, Context, Result}; @@ -13,7 +13,6 @@ use serde::Serialize; pub use branch::Branch; pub use iterator::BranchIterator as Iterator; -use tokio::sync::Semaphore; use uuid::Uuid; use crate::{gb_repository, project_repository, reader, sessions}; @@ -442,7 +441,8 @@ pub fn move_files( }) .collect::>() } 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))? .clone(); vec![owner] @@ -483,15 +483,72 @@ pub fn move_files( Ok(()) } -fn find_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option { - let explicitly_owned_by = stack.iter().find(|b| { - b.ownership - .iter() - .filter(|o| !o.ranges.is_empty()) - .any(|o| o.contains(needle)) - }); - let implicitly_owned_by = stack.iter().find(|b| b.contains(needle)); - explicitly_owned_by.or(implicitly_owned_by).cloned() +fn distance(a: &ops::RangeInclusive, b: &ops::RangeInclusive) -> 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, + another: &ops::RangeInclusive, +) -> bool { + one.contains(another.start()) + || one.contains(another.end()) + || another.contains(one.start()) + || another.contains(one.end()) +} + +fn ranges_touching( + one: &ops::RangeInclusive, + another: &ops::RangeInclusive, + context: usize, +) -> bool { + distance(one, another) <= context || distance(another, one) <= context +} + +fn explicit_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option { + stack + .iter() + .find(|branch| { + branch + .ownership + .iter() + .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 { + 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 { + stack.iter().find(|branch| branch.contains(needle)).cloned() } fn diff_to_hunks_by_filepath( @@ -697,12 +754,10 @@ pub fn get_status_by_branch( for hunk in all_hunks { 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 hunk.file_path == "src-tauri/Cargo.lock" { - println!("explicit branch: {:?}", branch); - } - hunks_by_branch_id .entry(branch.id.clone()) .or_default() @@ -1317,6 +1372,75 @@ mod tests { 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::>(); + + 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::>(); + + 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] fn test_get_status_files_by_branch() -> Result<()> { let repository = test_repository()?; @@ -1380,7 +1504,7 @@ mod tests { let file_path = std::path::Path::new("test.txt"); std::fs::write( 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)?; @@ -1406,7 +1530,7 @@ mod tests { std::fs::write( 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); @@ -1422,7 +1546,7 @@ mod tests { })?; let branch1 = branch_reader.read(&branch1_id)?; branch_writer.write(&branch::Branch { - ownership: vec!["test.txt:8-12".try_into()?], + ownership: vec!["test.txt:11-15".try_into()?], ..branch1 })?; @@ -1451,8 +1575,6 @@ mod tests { .map(|(branch, files)| (branch.id.clone(), files)) .collect::>(); - println!("{:#?}", statuses); - assert_eq!(files_by_branch_id.len(), 2); assert_eq!(files_by_branch_id[&branch1_id].len(), 0); 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(&branch2_id)?.ownership, - vec!["test.txt:1-5,8-12".try_into()?] + vec!["test.txt".try_into()?] ); Ok(()) @@ -1643,7 +1765,7 @@ mod tests { let file_path = std::path::Path::new("test.txt"); std::fs::write( 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)?; @@ -1665,7 +1787,7 @@ mod tests { std::fs::write( 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") @@ -1698,6 +1820,8 @@ mod tests { .map(|(branch, files)| (branch.id.clone(), files)) .collect::>(); + println!("{:#?}", statuses); + 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][0].hunks.len(), 1); @@ -1709,7 +1833,7 @@ mod tests { let branch_reader = branch::Reader::new(¤t_session_reader); assert_eq!( branch_reader.read(&branch1_id)?.ownership, - vec!["test.txt:8-12".try_into()?] + vec!["test.txt:12-16".try_into()?] ); assert_eq!( branch_reader.read(&branch2_id)?.ownership, @@ -1720,7 +1844,7 @@ mod tests { } #[test] - fn test_move_hunks_partial_implicity() -> Result<()> { + fn test_move_hunks_partial_implicity_owned() -> Result<()> { let repository = test_repository()?; let project = projects::Project::try_from(&repository)?; 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"); std::fs::write( 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)?; @@ -1754,7 +1878,7 @@ mod tests { std::fs::write( 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") @@ -1792,6 +1916,8 @@ mod tests { let statuses = get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status"); + println!("{:#?}", statuses); + let files_by_branch_id = statuses .iter() .map(|(branch, files)| (branch.id.clone(), files)) diff --git a/src-tauri/src/virtual_branches/target/reader.rs b/src-tauri/src/virtual_branches/target/reader.rs index ee6eaeb67..c58bb7658 100644 --- a/src-tauri/src/virtual_branches/target/reader.rs +++ b/src-tauri/src/virtual_branches/target/reader.rs @@ -56,10 +56,7 @@ mod tests { use crate::{ gb_repository, projects, sessions, storage, users, - virtual_branches::{ - branch, - target::{self, writer::TargetWriter}, - }, + virtual_branches::{branch, target::writer::TargetWriter}, }; use super::*; @@ -111,21 +108,6 @@ mod tests { 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] fn test_read_not_found() -> Result<()> { let repository = test_repository()?;