Skip to content

Commit

Permalink
view: move merging of views up to repo level (#111)
Browse files Browse the repository at this point in the history
I want to make it so when we apply a repo-level change that removes a
head, we rebase descendants of that head onto the closes visible
ancestor, or onto its successor if the head has been rewritten (see
#111 for details). The view itself doesn't have enough information for
that; we need repo-level information (to figure out relationships
between commits).

The view doesn't have enough information to do the.
  • Loading branch information
martinvonz committed Mar 24, 2022
1 parent 03aad3a commit 5efda2c
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 104 deletions.
107 changes: 104 additions & 3 deletions lib/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,12 +693,113 @@ impl MutableRepo {
self.index.merge_in(other_repo.index());

self.enforce_view_invariants();
self.view
.get_mut()
.merge(self.index.as_index_ref(), &base_repo.view, &other_repo.view);
self.merge_view(&base_repo.view, &other_repo.view);
self.view_dirty = true;
}

fn merge_view(&mut self, base: &View, other: &View) {
// Merge checkouts. If there's a conflict, we keep the self side.
for (workspace_id, base_checkout) in base.checkouts() {
let self_checkout = self.view().get_checkout(workspace_id);
let other_checkout = other.get_checkout(workspace_id);
if other_checkout == Some(base_checkout) || other_checkout == self_checkout {
// The other side didn't change or both sides changed in the
// same way.
} else if let Some(other_checkout) = other_checkout {
if self_checkout == Some(base_checkout) {
self.view_mut()
.set_checkout(workspace_id.clone(), other_checkout.clone());
}
} else {
// The other side removed the workspace. We want to remove it even if the self
// side changed the checkout.
self.view_mut().remove_checkout(workspace_id);
}
}
for (workspace_id, other_checkout) in other.checkouts() {
if self.view().get_checkout(workspace_id).is_none()
&& base.get_checkout(workspace_id).is_none()
{
// The other side added the workspace.
self.view_mut()
.set_checkout(workspace_id.clone(), other_checkout.clone());
}
}

for removed_head in base.public_heads().difference(other.public_heads()) {
self.view_mut().remove_public_head(removed_head);
}
for added_head in other.public_heads().difference(base.public_heads()) {
self.view_mut().add_public_head(added_head);
}

for removed_head in base.heads().difference(other.heads()) {
self.view_mut().remove_head(removed_head);
}
for added_head in other.heads().difference(base.heads()) {
self.view_mut().add_head(added_head);
}
// TODO: Should it be considered a conflict if a commit-head is removed on one
// side while a child or successor is created on another side? Maybe a
// warning?

let mut maybe_changed_ref_names = HashSet::new();

let base_branches: HashSet<_> = base.branches().keys().cloned().collect();
let other_branches: HashSet<_> = other.branches().keys().cloned().collect();
for branch_name in base_branches.union(&other_branches) {
let base_branch = base.branches().get(branch_name);
let other_branch = other.branches().get(branch_name);
if other_branch == base_branch {
// Unchanged on other side
continue;
}

maybe_changed_ref_names.insert(RefName::LocalBranch(branch_name.clone()));
if let Some(branch) = base_branch {
for remote in branch.remote_targets.keys() {
maybe_changed_ref_names.insert(RefName::RemoteBranch {
branch: branch_name.clone(),
remote: remote.clone(),
});
}
}
if let Some(branch) = other_branch {
for remote in branch.remote_targets.keys() {
maybe_changed_ref_names.insert(RefName::RemoteBranch {
branch: branch_name.clone(),
remote: remote.clone(),
});
}
}
}

for tag_name in base.tags().keys() {
maybe_changed_ref_names.insert(RefName::Tag(tag_name.clone()));
}
for tag_name in other.tags().keys() {
maybe_changed_ref_names.insert(RefName::Tag(tag_name.clone()));
}

for git_ref_name in base.git_refs().keys() {
maybe_changed_ref_names.insert(RefName::GitRef(git_ref_name.clone()));
}
for git_ref_name in other.git_refs().keys() {
maybe_changed_ref_names.insert(RefName::GitRef(git_ref_name.clone()));
}

for ref_name in maybe_changed_ref_names {
let base_target = base.get_ref(&ref_name);
let other_target = other.get_ref(&ref_name);
self.view.get_mut().merge_single_ref(
self.index.as_index_ref(),
&ref_name,
base_target.as_ref(),
other_target.as_ref(),
);
}
}

pub fn merge_single_ref(
&mut self,
ref_name: &RefName,
Expand Down
101 changes: 0 additions & 101 deletions lib/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,107 +236,6 @@ impl View {
&mut self.data
}

pub fn merge(&mut self, index: IndexRef, base: &View, other: &View) {
// Merge checkouts. If there's a conflict, we keep the self side.
for (workspace_id, base_checkout) in base.checkouts() {
let self_checkout = self.get_checkout(workspace_id);
let other_checkout = other.get_checkout(workspace_id);
if other_checkout == Some(base_checkout) || other_checkout == self_checkout {
// The other side didn't change or both sides changed in the
// same way.
} else if let Some(other_checkout) = other_checkout {
if self_checkout == Some(base_checkout) {
self.set_checkout(workspace_id.clone(), other_checkout.clone());
}
} else {
// The other side removed the workspace. We want to remove it even if the self
// side changed the checkout.
self.remove_checkout(workspace_id);
}
}
for (workspace_id, other_checkout) in other.checkouts() {
if self.get_checkout(workspace_id).is_none()
&& base.get_checkout(workspace_id).is_none()
{
// The other side added the workspace.
self.set_checkout(workspace_id.clone(), other_checkout.clone());
}
}

for removed_head in base.public_heads().difference(other.public_heads()) {
self.remove_public_head(removed_head);
}
for added_head in other.public_heads().difference(base.public_heads()) {
self.add_public_head(added_head);
}

for removed_head in base.heads().difference(other.heads()) {
self.remove_head(removed_head);
}
for added_head in other.heads().difference(base.heads()) {
self.add_head(added_head);
}
// TODO: Should it be considered a conflict if a commit-head is removed on one
// side while a child or successor is created on another side? Maybe a
// warning?

let mut maybe_changed_ref_names = HashSet::new();

let base_branches: HashSet<_> = base.branches().keys().cloned().collect();
let other_branches: HashSet<_> = other.branches().keys().cloned().collect();
for branch_name in base_branches.union(&other_branches) {
let base_branch = base.branches().get(branch_name);
let other_branch = other.branches().get(branch_name);
if other_branch == base_branch {
// Unchanged on other side
continue;
}

maybe_changed_ref_names.insert(RefName::LocalBranch(branch_name.clone()));
if let Some(branch) = base_branch {
for remote in branch.remote_targets.keys() {
maybe_changed_ref_names.insert(RefName::RemoteBranch {
branch: branch_name.clone(),
remote: remote.clone(),
});
}
}
if let Some(branch) = other_branch {
for remote in branch.remote_targets.keys() {
maybe_changed_ref_names.insert(RefName::RemoteBranch {
branch: branch_name.clone(),
remote: remote.clone(),
});
}
}
}

for tag_name in base.tags().keys() {
maybe_changed_ref_names.insert(RefName::Tag(tag_name.clone()));
}
for tag_name in other.tags().keys() {
maybe_changed_ref_names.insert(RefName::Tag(tag_name.clone()));
}

for git_ref_name in base.git_refs().keys() {
maybe_changed_ref_names.insert(RefName::GitRef(git_ref_name.clone()));
}
for git_ref_name in other.git_refs().keys() {
maybe_changed_ref_names.insert(RefName::GitRef(git_ref_name.clone()));
}

for ref_name in maybe_changed_ref_names {
let base_target = base.get_ref(&ref_name);
let other_target = other.get_ref(&ref_name);
self.merge_single_ref(
index,
&ref_name,
base_target.as_ref(),
other_target.as_ref(),
);
}
}

pub fn merge_single_ref(
&mut self,
index: IndexRef,
Expand Down

0 comments on commit 5efda2c

Please sign in to comment.