support unapplying locked ownerships

This commit is contained in:
Nikita Galaiko 2024-01-08 15:39:22 +01:00 committed by GitButler
parent 5eb481fe20
commit aefeba8c7c
5 changed files with 354 additions and 12 deletions

View File

@ -17,13 +17,44 @@ pub struct Hunk {
pub binary: bool,
}
impl Hunk {
pub fn reverse(&self) -> Hunk {
let diff = self
.diff
.lines()
.map(|line| {
if let Some(content) = line.strip_prefix('+') {
format!("-{}", content)
} else if let Some(content) = line.strip_prefix('-') {
format!("+{}", content)
} else {
line.to_string()
}
})
.collect::<Vec<String>>()
.join("\n");
Hunk {
old_start: self.new_start,
old_lines: self.new_lines,
new_start: self.old_start,
new_lines: self.old_lines,
diff,
binary: self.binary,
}
}
}
pub struct Options {
pub reverse: bool,
pub context_lines: u32,
}
impl Default for Options {
fn default() -> Self {
Self { context_lines: 3 }
Self {
context_lines: 3,
reverse: false,
}
}
}
@ -44,7 +75,8 @@ pub fn workdir(
.show_binary(true)
.show_untracked_content(true)
.ignore_submodules(true)
.context_lines(opts.context_lines);
.context_lines(opts.context_lines)
.reverse(opts.reverse);
let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
@ -55,14 +87,17 @@ pub fn trees(
repository: &Repository,
old_tree: &git::Tree,
new_tree: &git::Tree,
opts: &Options,
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
let mut diff_opts = git2::DiffOptions::new();
diff_opts
.recurse_untracked_dirs(true)
.include_untracked(true)
.show_binary(true)
.show_untracked_content(true)
.ignore_submodules(true)
.show_untracked_content(true);
.context_lines(opts.context_lines)
.reverse(opts.reverse);
let diff =
repository.diff_tree_to_tree(Some(old_tree), Some(new_tree), Some(&mut diff_opts))?;

View File

@ -327,6 +327,7 @@ pub fn update_base_branch(
&project_repository.git_repository,
&branch_head_tree,
&branch_tree,
&diff::Options::default(),
)?;
if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged

View File

@ -34,7 +34,12 @@ pub fn list_remote_commit_files(
let parent = commit.parent(0).context("failed to get parent commit")?;
let commit_tree = commit.tree().context("failed to get commit tree")?;
let parent_tree = parent.tree().context("failed to get parent tree")?;
let diff = diff::trees(repository, &parent_tree, &commit_tree)?;
let diff = diff::trees(
repository,
&parent_tree,
&commit_tree,
&diff::Options::default(),
)?;
let files = diff
.into_iter()

View File

@ -437,7 +437,7 @@ pub fn apply_branch(
pub fn unapply_ownership(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
ownership: &Ownership,
target_ownership: &Ownership,
) -> Result<(), errors::UnapplyOwnershipError> {
if conflicts::is_resolving(project_repository) {
return Err(errors::UnapplyOwnershipError::Conflict(
@ -476,18 +476,68 @@ pub fn unapply_ownership(
)
.context("failed to get status by branch")?;
// remove the ownership from the applied branches, and write them out
// remove non locked hunks directly from the branch ownership
let branch_writer = branch::Writer::open(gb_repository);
let applied_statuses = applied_statuses
.into_iter()
.iter()
.map(|(branch, branch_files)| {
let mut branch = branch.clone();
let mut branch_files = branch_files.clone();
for file_ownership_to_take in &ownership.files {
let taken_file_ownerships = branch.ownership.take(file_ownership_to_take);
let non_commited_hunks = calculate_non_commited_diffs(
project_repository,
&branch,
&default_target,
&branch_files,
)?;
let non_locked_non_commited_hunks = non_commited_hunks
.iter()
.filter_map(|(filepath, hunks)| {
let hunks = hunks
.iter()
.filter(|hunk| {
branch_files.get(filepath).map_or(false, |hunks| {
hunks
.iter()
.any(|branch_hunk| branch_hunk.diff == hunk.diff)
})
})
.cloned()
.collect::<Vec<_>>();
if hunks.is_empty() {
None
} else {
Some((filepath.clone(), hunks))
}
})
.collect::<HashMap<_, _>>();
for target_file_ownership in &target_ownership.files {
let taken_file_ownerships = branch.ownership.take(target_file_ownership);
if taken_file_ownerships.is_empty() {
continue;
}
// only consider non locked, non commited hunks
let taken_file_ownerships = taken_file_ownerships
.iter()
.filter(|taken_file_ownership| {
non_locked_non_commited_hunks.iter().any(|hunk| {
taken_file_ownership.file_path.eq(hunk.0)
&& hunk.1.iter().any(|h| {
h.new_start == taken_file_ownership.hunks[0].start
&& h.new_start + h.new_lines
== taken_file_ownership.hunks[0].end
})
})
})
.collect::<Vec<_>>();
if taken_file_ownerships.is_empty() {
continue;
}
branch_writer.write(&mut branch)?;
branch_files = branch_files
.iter_mut()
@ -517,6 +567,67 @@ pub fn unapply_ownership(
})
.collect::<Result<Vec<_>>>()?;
let hunks_to_unapply = applied_statuses
.iter()
.map(|(branch, branch_files)| {
let non_commited_hunks = calculate_reverse_non_commited_diffs(
project_repository,
branch,
&default_target,
branch_files,
)?;
let locked_non_commited_hunks = non_commited_hunks
.iter()
.filter_map(|(filepath, hunks)| {
let hunks = hunks
.iter()
.filter(|hunk| {
branch_files.get(filepath).map_or(false, |hunks| {
!hunks
.iter()
.any(|branch_hunk| branch_hunk.diff == hunk.diff)
})
})
.cloned()
.collect::<Vec<_>>();
if hunks.is_empty() {
None
} else {
Some((filepath.clone(), hunks))
}
})
.collect::<HashMap<_, _>>();
let hunks_to_unapply = locked_non_commited_hunks
.into_iter()
.filter_map(|(file_path, hunks)| {
target_ownership
.files
.iter()
.find(|o| o.file_path == *file_path)
.map(|target_hunks| {
let hunks = hunks
.iter()
.filter(|hunk| {
target_hunks.hunks.iter().any(|target_hunk| {
target_hunk.start == hunk.old_start
&& target_hunk.end == hunk.old_start + hunk.old_lines
})
})
.cloned()
.collect::<Vec<_>>();
(file_path.clone(), hunks)
})
})
.collect::<Vec<_>>();
Ok(hunks_to_unapply)
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<HashMap<_, _>>();
let repo = &project_repository.git_repository;
let target_commit = repo
@ -527,7 +638,7 @@ pub fn unapply_ownership(
let base_tree = target_commit.tree().context("failed to get target tree")?;
// construst a new working directory tree, without the removed ownerships
let final_tree = applied_statuses.into_iter().fold(
let cumulative_tree = applied_statuses.into_iter().fold(
target_commit.tree().context("failed to get target tree"),
|final_tree, status| {
let final_tree = final_tree?;
@ -540,6 +651,12 @@ pub fn unapply_ownership(
},
)?;
let final_tree_oid =
write_tree_onto_tree(project_repository, &cumulative_tree, &hunks_to_unapply)?;
let final_tree = repo
.find_tree(final_tree_oid)
.context("failed to find tree")?;
repo.checkout_tree(&final_tree)
.force()
.remove_untracked()
@ -907,6 +1024,46 @@ fn is_requires_force(
Ok(merge_base != upstream_commit.id())
}
// returns a diff to apply to the working directory to cancel out non commited changes
pub fn calculate_reverse_non_commited_diffs(
project_repository: &project_repository::Repository,
branch: &branch::Branch,
default_target: &target::Target,
files: &HashMap<path::PathBuf, Vec<diff::Hunk>>,
) -> Result<HashMap<path::PathBuf, Vec<diff::Hunk>>> {
if default_target.sha == branch.head && !branch.applied {
return Ok(files.clone());
};
let branch_tree = if branch.applied {
let target_plus_wd_oid = write_tree(project_repository, default_target, files)?;
project_repository
.git_repository
.find_tree(target_plus_wd_oid)
} else {
project_repository.git_repository.find_tree(branch.tree)
}?;
let branch_head = project_repository
.git_repository
.find_commit(branch.head)?
.tree()?;
// do a diff between branch.head and the tree we _would_ commit
let non_commited_diff = diff::trees(
&project_repository.git_repository,
&branch_head,
&branch_tree,
&diff::Options {
reverse: true,
..Default::default()
},
)
.context("failed to diff trees")?;
Ok(non_commited_diff)
}
// given a virtual branch and it's files that are calculated off of a default target,
// return files adjusted to the branch's head commit
pub fn calculate_non_commited_diffs(
@ -938,6 +1095,7 @@ pub fn calculate_non_commited_diffs(
&project_repository.git_repository,
&branch_head,
&branch_tree,
&diff::Options::default(),
)
.context("failed to diff trees")?;
@ -979,6 +1137,7 @@ fn list_virtual_commit_files(
&project_repository.git_repository,
&parent_tree,
&commit_tree,
&diff::Options::default(),
)?;
let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff);
Ok(virtual_hunks_to_virtual_files(
@ -1594,6 +1753,7 @@ fn get_non_applied_status(
&project_repository.git_repository,
&target_tree,
&branch_tree,
&diff::Options::default(),
)?;
Ok((branch, diff))
@ -1901,11 +2061,18 @@ pub fn write_tree_onto_commit(
) -> Result<git::Oid> {
// read the base sha into an index
let git_repository = &project_repository.git_repository;
let head_commit = git_repository.find_commit(commit_oid)?;
let base_tree = head_commit.tree()?;
write_tree_onto_tree(project_repository, &base_tree, files)
}
let mut builder = git_repository.treebuilder(Some(&base_tree));
pub fn write_tree_onto_tree(
project_repository: &project_repository::Repository,
base_tree: &git::Tree,
files: &HashMap<path::PathBuf, Vec<diff::Hunk>>,
) -> Result<git::Oid> {
let git_repository = &project_repository.git_repository;
let mut builder = git_repository.treebuilder(Some(base_tree));
// now update the index with content in the working directory for each file
for (filepath, hunks) in files {
// convert this string to a Path
@ -3309,6 +3476,7 @@ pub fn create_virtual_branch_from_branch(
&project_repository.git_repository,
&merge_base_tree,
&head_commit_tree,
&diff::Options::default(),
)
.context("failed to diff trees")?;

View File

@ -5305,3 +5305,136 @@ mod create_virtual_branch_from_branch {
assert_eq!(branches[0].commits[0].description, "branch commit");
}
}
mod unapply_ownership {
use super::*;
#[tokio::test]
async fn unapply_locked_hunk() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create a commit
fs::write(repository.path().join("file.txt"), "line1\n").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap();
}
let locked_hunk_ownership = {
// change in the committed hunks leads to hunk locking
fs::write(repository.path().join("file.txt"), "line1\nline2\n").unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(branch.files[0].hunks[0].locked);
let hunk = &branch.files[0].hunks[0];
format!("{}:{}-{}", hunk.file_path.display(), hunk.start, hunk.end)
.parse()
.unwrap()
};
// unaplly locked hunk
controller
.unapply_ownership(&project_id, &locked_hunk_ownership)
.await
.unwrap();
{
// verify no changes
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.files.is_empty());
}
}
#[tokio::test]
async fn unapply_non_locked_hunk() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let hunk_ownership = {
// change in the committed hunks leads to hunk locking
fs::write(repository.path().join("file.txt"), "line1\n").unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
let hunk = &branch.files[0].hunks[0];
format!("{}:{}-{}", hunk.file_path.display(), hunk.start, hunk.end)
.parse()
.unwrap()
};
// unaplly locked hunk
controller
.unapply_ownership(&project_id, &hunk_ownership)
.await
.unwrap();
{
// verify no changes
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.files.is_empty());
}
}
}