diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index f98894987d5..06fb7c42beb 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1310,7 +1310,9 @@ impl WorkspaceCommandHelper { &mut self, ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> { let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?; - if wc_commit.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids() { + if wc_commit.tree().tree_ids_and_labels() + != locked_ws.locked_wc().old_tree().tree_ids_and_labels() + { return Err(user_error("Concurrent working copy operation. Try again.")); } Ok((locked_ws, wc_commit)) @@ -1924,7 +1926,7 @@ to the current parents may contain changes from multiple commits. .block_on() .map_err(snapshot_command_error)? }; - if new_tree.tree_ids() != wc_commit.tree_ids() { + if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() { let mut tx = start_repo_transaction(&self.user_repo.repo, self.env.command.string_args()); tx.set_is_snapshot(true); diff --git a/cli/src/commands/bookmark/list.rs b/cli/src/commands/bookmark/list.rs index 1d162561598..c91f1e890e5 100644 --- a/cli/src/commands/bookmark/list.rs +++ b/cli/src/commands/bookmark/list.rs @@ -451,6 +451,7 @@ mod tests { use jj_lib::backend::Signature; use jj_lib::backend::Timestamp; use jj_lib::backend::TreeId; + use jj_lib::conflict_labels::ConflictLabels; use jj_lib::merge::Merge; use jj_lib::op_store::RefTarget; @@ -461,6 +462,7 @@ mod tests { parents: vec![], predecessors: vec![], root_tree: Merge::resolved(TreeId::new(vec![])), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::new(vec![]), description: String::new(), author, diff --git a/cli/src/commands/revert.rs b/cli/src/commands/revert.rs index 6902736ff81..dc3200ce69f 100644 --- a/cli/src/commands/revert.rs +++ b/cli/src/commands/revert.rs @@ -146,7 +146,9 @@ pub(crate) fn cmd_revert( { let old_base_tree = commit_to_revert.parent_tree(tx.repo())?; let old_tree = commit_to_revert.tree(); - let new_tree = new_base_tree.merge(old_tree, old_base_tree).block_on()?; + let new_tree = new_base_tree + .merge_unlabeled(old_tree, old_base_tree) + .block_on()?; let new_parent_ids = parent_ids.clone(); let new_commit = tx .repo_mut() diff --git a/cli/src/commands/split.rs b/cli/src/commands/split.rs index 0fbd52ecc35..087ac945b59 100644 --- a/cli/src/commands/split.rs +++ b/cli/src/commands/split.rs @@ -288,7 +288,7 @@ pub(crate) fn cmd_split( // containing the user selected changes as the base for the merge. // This results in a tree with the changes the user didn't select. target_tree - .merge(target.selected_tree.clone(), target.parent_tree.clone()) + .merge_unlabeled(target.selected_tree.clone(), target.parent_tree.clone()) .block_on()? } else { target_tree diff --git a/cli/src/merge_tools/builtin.rs b/cli/src/merge_tools/builtin.rs index a5c37984023..2ed6eb4543e 100644 --- a/cli/src/merge_tools/builtin.rs +++ b/cli/src/merge_tools/builtin.rs @@ -1519,7 +1519,7 @@ mod tests { let base = testutils::create_single_tree(&test_repo.repo, &[(file_path, "")]); let left = testutils::create_single_tree(&test_repo.repo, &[(file_path, "1\n")]); let right = testutils::create_single_tree(&test_repo.repo, &[(file_path, "2\n")]); - MergedTree::new( + MergedTree::unlabeled( store.clone(), Merge::from_vec(vec![ left.id().clone(), diff --git a/cli/tests/test_file_chmod_command.rs b/cli/tests/test_file_chmod_command.rs index 43f1d7b0e23..30d1a4715d9 100644 --- a/cli/tests/test_file_chmod_command.rs +++ b/cli/tests/test_file_chmod_command.rs @@ -280,7 +280,7 @@ fn test_chmod_exec_bit_settings() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_timestamp), @r#" Current operation: OperationId("8c58a72d1118aa7d8b1295949a7fa8c6fcda63a3c89813faf2b8ca599ceebf8adcfcbeb8f0bbb6439c86b47dd68b9cf85074c9e57214c3fb4b632e0c9e87ad65") - Current tree: MergedTree { tree_ids: Resolved(TreeId("6d5f482d15035cdd7733b1b551d1fead28d22592")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("6d5f482d15035cdd7733b1b551d1fead28d22592")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(false) } 5 None "file" [EOF] "#); // in-repo: false, on-disk: false (1/4) @@ -293,7 +293,7 @@ fn test_chmod_exec_bit_settings() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_timestamp), @r#" Current operation: OperationId("3a6a78820e6892164ed55680b92fa679fbb4d6acd4135c7413d1b815bedcd2c24c85ac8f4f96c96f76012f33d31ffbf50473b938feadf36fcd9c92997789aeca") - Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(true) } 5 None "file" [EOF] "#); // in-repo: true, on-disk: true (2/4) @@ -315,7 +315,7 @@ fn test_chmod_exec_bit_settings() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_timestamp), @r#" Current operation: OperationId("cab1801e16b54d5b413f638bdf74388520b51232c88db6b314ef64b054607ab82ae6ef0b1f707b52aa8d2131511f6f48f8ca52e465621ff38c442b0ec893f309") - Current tree: MergedTree { tree_ids: Resolved(TreeId("6d5f482d15035cdd7733b1b551d1fead28d22592")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("6d5f482d15035cdd7733b1b551d1fead28d22592")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(true) } 5 None "file" [EOF] "#); // in-repo: false, on-disk: true (3/4) @@ -326,7 +326,7 @@ fn test_chmod_exec_bit_settings() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_timestamp), @r#" Current operation: OperationId("def8ce6211dcff6d2784d5309d36079c1cb6eeb70821ae144982c76d38ed76fedc8b84e4daddaac70f6a0aae1c301ff5b60e1baa6ac371dabd77cec3537d2c39") - Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(false) } 5 None "file" [EOF] "#); // in-repo: true, on-disk: false (4/4) Yay! We've observed all possible states! @@ -359,7 +359,7 @@ fn test_chmod_exec_bit_settings() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_timestamp), @r#" Current operation: OperationId("0cce4e44f0b47cc4404f74fe164536aa57f67b8981726ce6ec88c39d79e266a2586a79d51a065906b6d8b284b39fe0ab023547f65571d1b61a97916f7f7cf4d8") - Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("5201dbafb66dc1b28b029a262e1b206f6f93df1e")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(true) } 5 None "file" [EOF] "#); diff --git a/cli/tests/test_operations.rs b/cli/tests/test_operations.rs index 657b3102f24..fb9ef870441 100644 --- a/cli/tests/test_operations.rs +++ b/cli/tests/test_operations.rs @@ -678,7 +678,7 @@ fn test_op_abandon_ancestors() { "); insta::assert_snapshot!(work_dir.run_jj(["debug", "local-working-copy", "--ignore-working-copy"]), @r#" Current operation: OperationId("1675333b7de89b5da012c696d797345bad2a6ce55a4b605e85c3897f818f05e11e8c53de19d34c2fee38a36528dc95bd2a378f72ac0877f8bec2513a68043253") - Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), labels: Unlabeled, .. } [EOF] "#); insta::assert_snapshot!(work_dir.run_jj(["op", "log"]), @r" @@ -739,7 +739,7 @@ fn test_op_abandon_ancestors() { "); insta::assert_snapshot!(work_dir.run_jj(["debug", "local-working-copy", "--ignore-working-copy"]), @r#" Current operation: OperationId("ce6a0300b7346109e75a6dcc97e3ff9e1488ce43a4073dd9eb81afb7f463b4543d3f15cf9a42a9864a4aaf6daab900b6b037dbdcb95f87422e891f7e884641aa") - Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), labels: Unlabeled, .. } [EOF] "#); insta::assert_snapshot!(work_dir.run_jj(["op", "log"]), @r" @@ -787,7 +787,7 @@ fn test_op_abandon_without_updating_working_copy() { "); insta::assert_snapshot!(work_dir.run_jj(["debug", "local-working-copy", "--ignore-working-copy"]), @r#" Current operation: OperationId("0d4bb8e4a2babc4c216be0f9bde32aeef888abebde0062aeb1c204dde5e1f476fa951fcbeceb2263cf505008ba87a834849469dede30dfc589f37d5073aedfbe") - Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), labels: Unlabeled, .. } [EOF] "#); insta::assert_snapshot!(work_dir.run_jj(["op", "log", "-n1", "--ignore-working-copy"]), @r" @@ -809,7 +809,7 @@ fn test_op_abandon_without_updating_working_copy() { "); insta::assert_snapshot!(work_dir.run_jj(["debug", "local-working-copy", "--ignore-working-copy"]), @r#" Current operation: OperationId("0d4bb8e4a2babc4c216be0f9bde32aeef888abebde0062aeb1c204dde5e1f476fa951fcbeceb2263cf505008ba87a834849469dede30dfc589f37d5073aedfbe") - Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("4b825dc642cb6eb9a060e54bf8d69288fbee4904")), labels: Unlabeled, .. } [EOF] "#); insta::assert_snapshot!(work_dir.run_jj(["op", "log", "-n1", "--ignore-working-copy"]), @r" diff --git a/cli/tests/test_working_copy.rs b/cli/tests/test_working_copy.rs index 48e84df7d40..93477dfe0ce 100644 --- a/cli/tests/test_working_copy.rs +++ b/cli/tests/test_working_copy.rs @@ -361,7 +361,7 @@ fn test_conflict_marker_length_stored_in_working_copy() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_output), @r#" Current operation: OperationId("53f0bb27f3ac96896abd48a5fb0fdfcf4e61389a70290468a2e1ec1db5389661fc6e4aec6c1d03f1bd7db07cafa9f6a802dc5e78c936c09e7dbd9aea1b4ea2fa") - Current tree: MergedTree { tree_ids: Conflicted([TreeId("381273b50cf73f8c81b3f1502ee89e9bbd6c1518"), TreeId("771f3d31c4588ea40a8864b2a981749888e596c2"), TreeId("f56b8223da0dab22b03b8323ced4946329aeb4e0")]), .. } + Current tree: MergedTree { tree_ids: Conflicted([TreeId("381273b50cf73f8c81b3f1502ee89e9bbd6c1518"), TreeId("771f3d31c4588ea40a8864b2a981749888e596c2"), TreeId("f56b8223da0dab22b03b8323ced4946329aeb4e0")]), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(false) } 249 Some(MaterializedConflictData { conflict_marker_len: 11 }) "file" [EOF] "#); @@ -424,7 +424,7 @@ fn test_conflict_marker_length_stored_in_working_copy() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_output), @r#" Current operation: OperationId("d9ace267ba6a972d19d345fadcfa2177f42e4ea6c11ff4a44e70c034322c233372edfe6169e14292f0cc5d43d43c42329a7a0fa8231b524a43a73fba96b8f114") - Current tree: MergedTree { tree_ids: Conflicted([TreeId("381273b50cf73f8c81b3f1502ee89e9bbd6c1518"), TreeId("771f3d31c4588ea40a8864b2a981749888e596c2"), TreeId("3329c18c95f7b7a55c278c2259e9c4ce711fae59")]), .. } + Current tree: MergedTree { tree_ids: Conflicted([TreeId("381273b50cf73f8c81b3f1502ee89e9bbd6c1518"), TreeId("771f3d31c4588ea40a8864b2a981749888e596c2"), TreeId("3329c18c95f7b7a55c278c2259e9c4ce711fae59")]), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(false) } 289 Some(MaterializedConflictData { conflict_marker_len: 11 }) "file" [EOF] "#); @@ -459,7 +459,7 @@ fn test_conflict_marker_length_stored_in_working_copy() { let output = work_dir.run_jj(["debug", "local-working-copy"]); insta::assert_snapshot!(output.normalize_stdout_with(redact_output), @r#" Current operation: OperationId("77a5650168d075d4b3171483acafab04e2822ae3ff1d30d2cdb1ae4bcb6d7739439847891752cc9408a356438e0762a8acc4ae9473c0a9c06619ea71d60984a0") - Current tree: MergedTree { tree_ids: Resolved(TreeId("6120567b3cb2472d549753ed3e4b84183d52a650")), .. } + Current tree: MergedTree { tree_ids: Resolved(TreeId("6120567b3cb2472d549753ed3e4b84183d52a650")), labels: Unlabeled, .. } Normal { exec_bit: ExecBit(false) } 130 None "file" [EOF] "#); diff --git a/lib/src/absorb.rs b/lib/src/absorb.rs index 61d4ef21830..f1283894664 100644 --- a/lib/src/absorb.rs +++ b/lib/src/absorb.rs @@ -317,7 +317,7 @@ pub fn absorb_hunks( let commit_builder = rewriter.rebase().await?; let destination_tree = commit_builder.tree(); let new_tree = destination_tree - .merge(source.parent_tree.clone(), selected_tree) + .merge_unlabeled(source.parent_tree.clone(), selected_tree) .block_on()?; let mut predecessors = commit_builder.predecessors().to_vec(); predecessors.push(source.commit.id().clone()); diff --git a/lib/src/backend.rs b/lib/src/backend.rs index 4a650753aa6..d291b969760 100644 --- a/lib/src/backend.rs +++ b/lib/src/backend.rs @@ -26,6 +26,7 @@ use futures::stream::BoxStream; use thiserror::Error; use tokio::io::AsyncRead; +use crate::conflict_labels::ConflictLabels; use crate::content_hash::ContentHash; use crate::hex_util; use crate::index::Index; @@ -161,6 +162,8 @@ pub struct Commit { pub predecessors: Vec, #[serde(skip)] // TODO: should be exposed? pub root_tree: Merge, + #[serde(skip)] + pub conflict_labels: ConflictLabels, pub change_id: ChangeId, pub description: String, pub author: Signature, @@ -397,6 +400,7 @@ pub fn make_root_commit(root_change_id: ChangeId, empty_tree_id: TreeId) -> Comm parents: vec![], predecessors: vec![], root_tree: Merge::resolved(empty_tree_id), + conflict_labels: ConflictLabels::unlabeled(), change_id: root_change_id, description: String::new(), author: signature.clone(), diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 6dc7ac65955..067eccb6ec7 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -121,7 +121,11 @@ impl Commit { } pub fn tree(&self) -> MergedTree { - MergedTree::new(self.store.clone(), self.data.root_tree.clone()) + MergedTree::new( + self.store.clone(), + self.data.root_tree.clone(), + self.data.conflict_labels.clone(), + ) } pub fn tree_ids(&self) -> &Merge { diff --git a/lib/src/commit_builder.rs b/lib/src/commit_builder.rs index d35e1359723..16fef786e9e 100644 --- a/lib/src/commit_builder.rs +++ b/lib/src/commit_builder.rs @@ -194,11 +194,13 @@ impl DetachedCommitBuilder { let signature = settings.signature(); assert!(!parents.is_empty()); let rng = settings.get_rng(); + let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels(); let change_id = rng.new_change_id(store.change_id_length()); let commit = backend::Commit { parents, predecessors: vec![], - root_tree: tree.into_tree_ids(), + root_tree, + conflict_labels, change_id, description: String::new(), author: signature.clone(), @@ -303,7 +305,11 @@ impl DetachedCommitBuilder { } pub fn tree(&self) -> MergedTree { - MergedTree::new(self.store.clone(), self.commit.root_tree.clone()) + MergedTree::new( + self.store.clone(), + self.commit.root_tree.clone(), + self.commit.conflict_labels.clone(), + ) } pub fn tree_ids(&self) -> &Merge { @@ -312,7 +318,7 @@ impl DetachedCommitBuilder { pub fn set_tree(&mut self, tree: MergedTree) -> &mut Self { assert!(Arc::ptr_eq(tree.store(), &self.store)); - self.commit.root_tree = tree.into_tree_ids(); + (self.commit.root_tree, self.commit.conflict_labels) = tree.into_tree_ids_and_labels(); self } diff --git a/lib/src/conflict_labels.rs b/lib/src/conflict_labels.rs new file mode 100644 index 00000000000..9ec9bddc969 --- /dev/null +++ b/lib/src/conflict_labels.rs @@ -0,0 +1,184 @@ +// Copyright 2025 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Labels for conflicted trees. + +use std::fmt; +use std::sync::Arc; + +use crate::content_hash::ContentHash; +use crate::merge::Merge; + +/// Optionally contains a set of labels for the terms of a conflict. Resolved +/// merges cannot be labeled. The conflict labels are reference-counted to make +/// them more efficient to clone. +#[derive(ContentHash, PartialEq, Eq, Clone)] +pub struct ConflictLabels { + labels: Option>>, +} + +impl ConflictLabels { + /// Create a `ConflictLabels` with no labels. + pub const fn unlabeled() -> Self { + Self { labels: None } + } + + /// Create a `ConflictLabels` from a `Merge`. If the merge is + /// resolved or if any label is empty, the labels will be discarded, since + /// resolved merges cannot have labels, and labels cannot be empty. + pub fn new(labels: Merge) -> Self { + if labels.is_resolved() || labels.iter().any(|label| label.is_empty()) { + Self::unlabeled() + } else { + Self { + labels: Some(Arc::new(labels)), + } + } + } + + /// Create a `ConflictLabels` from a `Vec`, with an empty vec + /// representing no labels. + pub fn from_vec(labels: Vec) -> Self { + if labels.is_empty() { + Self::unlabeled() + } else { + Self::new(Merge::from_vec(labels)) + } + } + + /// Returns true if there are labels present. + pub fn is_present(&self) -> bool { + self.labels.is_some() + } + + /// Returns the number of labeled sides, or `None` if unlabeled. + pub fn num_sides(&self) -> Option { + self.labels.as_ref().map(|labels| labels.num_sides()) + } + + /// Returns the underlying labels as an `Option<&Merge>`. + pub fn as_merge(&self) -> Option<&Merge> { + self.labels.as_ref().map(Arc::as_ref) + } + + /// Returns the underlying labels as an `Option>`, cloning if + /// necessary. + pub fn into_merge(self) -> Option> { + self.labels.map(Arc::unwrap_or_clone) + } + + /// Returns the conflict labels as a slice. If there are no labels, returns + /// an empty slice. + pub fn as_slice(&self) -> &[String] { + self.as_merge().map_or(&[], |labels| labels.as_slice()) + } + + /// Get the label for a side at an index. + pub fn get_add(&self, add_index: usize) -> Option<&str> { + self.as_merge() + .and_then(|merge| merge.get_add(add_index).map(String::as_str)) + } + + /// Get the label for a base at an index. + pub fn get_remove(&self, remove_index: usize) -> Option<&str> { + self.as_merge() + .and_then(|merge| merge.get_remove(remove_index).map(String::as_str)) + } + + /// Simplify a merge with the same number of sides while preserving the + /// conflict labels corresponding to each side of the merge. + pub fn simplify_with(&self, merge: &Merge) -> (Self, Merge) { + if let Some(labels) = self.as_merge() { + let (labels, simplified) = labels + .as_ref() + .zip(merge.as_ref()) + .simplify_by(|&(_label, item)| item) + .unzip(); + (Self::new(labels.cloned()), simplified.cloned()) + } else { + let simplified = merge.simplify(); + (Self::unlabeled(), simplified) + } + } +} + +impl From> for ConflictLabels { + fn from(value: Merge) -> Self { + Self::new(value) + } +} + +impl From> for ConflictLabels { + fn from(value: Merge<&str>) -> Self { + Self::new(value.map(|&label| label.to_owned())) + } +} + +impl From> for ConflictLabels +where + T: Into, +{ + fn from(value: Option) -> Self { + value.map_or_else(Self::unlabeled, T::into) + } +} + +impl fmt::Debug for ConflictLabels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(labels) = self.as_merge() { + f.debug_tuple("Labeled").field(&labels.as_slice()).finish() + } else { + write!(f, "Unlabeled") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_conflict_labels_from_vec() { + // From empty vec for unlabeled + assert_eq!( + ConflictLabels::from_vec(vec![]), + ConflictLabels::unlabeled() + ); + // From non-empty vec of terms + assert_eq!( + ConflictLabels::from_vec(vec![ + String::from("left"), + String::from("base"), + String::from("right") + ]), + ConflictLabels::from(Some(Merge::from_vec(vec!["left", "base", "right"]))) + ); + } + + #[test] + fn test_conflict_labels_as_slice() { + // Empty slice for unlabeled + let empty: &[String] = &[]; + assert_eq!(ConflictLabels::unlabeled().as_slice(), empty); + // Slice of terms for labeled + assert_eq!( + ConflictLabels::from(Some(Merge::from_vec(vec!["left", "base", "right"]))).as_slice(), + &[ + String::from("left"), + String::from("base"), + String::from("right") + ] + ); + } +} diff --git a/lib/src/content_hash.rs b/lib/src/content_hash.rs index 0137ac009c9..06a09727829 100644 --- a/lib/src/content_hash.rs +++ b/lib/src/content_hash.rs @@ -1,5 +1,7 @@ //! Portable, stable hashing suitable for identifying values +use std::sync::Arc; + use blake2::Blake2b512; // Re-export DigestUpdate so that the ContentHash proc macro can be used in // external crates without directly depending on the digest crate. @@ -128,6 +130,12 @@ impl ContentHash for Option { } } +impl ContentHash for Arc { + fn hash(&self, state: &mut impl DigestUpdate) { + self.as_ref().hash(state); + } +} + impl ContentHash for std::collections::HashMap where K: ContentHash + Ord, diff --git a/lib/src/git_backend.rs b/lib/src/git_backend.rs index 92376cc3efe..3aa3af917c2 100644 --- a/lib/src/git_backend.rs +++ b/lib/src/git_backend.rs @@ -71,6 +71,7 @@ use crate::backend::TreeId; use crate::backend::TreeValue; use crate::backend::make_root_commit; use crate::config::ConfigGetError; +use crate::conflict_labels::ConflictLabels; use crate::file_util; use crate::file_util::BadPathEncoding; use crate::file_util::IoResultExt as _; @@ -97,6 +98,7 @@ const CHANGE_ID_LENGTH: usize = 16; const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/"; pub const JJ_TREES_COMMIT_HEADER: &str = "jj:trees"; +pub const JJ_CONFLICT_LABELS_COMMIT_HEADER: &str = "jj:conflict-labels"; pub const CHANGE_ID_COMMIT_HEADER: &str = "change-id"; #[derive(Debug, Error)] @@ -577,6 +579,19 @@ fn commit_from_git_without_root_parent( .map(|oid| CommitId::from_bytes(oid.as_bytes())) .collect_vec() }; + // If the commit is a conflict, the conflict labels are stored in a commit + // header separately from the trees. + let conflict_labels: ConflictLabels = commit + .extra_headers() + .find(JJ_CONFLICT_LABELS_COMMIT_HEADER) + .map(|header| { + str::from_utf8(header) + .expect("labels should be valid utf8") + .split_terminator('\n') + .collect::>() + .build() + }) + .into(); // Conflicted commits written before we started using the `jj:trees` header // (~March 2024) may have the root trees stored in the extra metadata table // instead. For such commits, we'll update the root tree later when we read the @@ -623,6 +638,7 @@ fn commit_from_git_without_root_parent( predecessors: vec![], // If this commit has associated extra metadata, we may reset this later. root_tree, + conflict_labels, change_id, description, author, @@ -1249,6 +1265,14 @@ impl Backend for GitBackend { } } let mut extra_headers: Vec<(BString, BString)> = vec![]; + if let Some(conflict_labels) = contents.conflict_labels.as_merge() { + // Labels cannot contain '\n' since we use it as a separator in the header. + assert!(conflict_labels.iter().all(|label| !label.contains('\n'))); + extra_headers.push(( + JJ_CONFLICT_LABELS_COMMIT_HEADER.into(), + conflict_labels.iter().join("\n").into(), + )); + } if !tree_ids.is_resolved() { let value = tree_ids.iter().map(|id| id.hex()).join(" "); extra_headers.push((JJ_TREES_COMMIT_HEADER.into(), value.into())); @@ -1898,6 +1922,7 @@ mod tests { parents: vec![backend.root_commit_id().clone()], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: original_change_id.clone(), description: "initial".to_string(), author: create_signature(), @@ -1989,6 +2014,7 @@ mod tests { parents: vec![], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::from_hex("abc123"), description: "".to_string(), author: create_signature(), @@ -2075,6 +2101,7 @@ mod tests { parents: vec![backend.root_commit_id().clone()], predecessors: vec![], root_tree: root_tree.clone(), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::from_hex("abc123"), description: "".to_string(), author: create_signature(), @@ -2175,6 +2202,7 @@ mod tests { parents: vec![backend.root_commit_id().clone()], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::new(vec![42; 16]), description: "initial".to_string(), author: signature.clone(), @@ -2255,6 +2283,7 @@ mod tests { parents: vec![backend.root_commit_id().clone()], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::from_hex("7f0a7ce70354b22efcccf7bf144017c4"), description: "initial".to_string(), author: create_signature(), @@ -2297,6 +2326,7 @@ mod tests { parents: vec![backend.root_commit_id().clone()], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::new(vec![42; 16]), description: "initial".to_string(), author: create_signature(), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3fcc088a6e6..aed05c51c77 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -36,6 +36,7 @@ pub mod commit; pub mod commit_builder; pub mod config; mod config_resolver; +pub mod conflict_labels; pub mod conflicts; pub mod copies; pub mod dag_walk; diff --git a/lib/src/local_working_copy.rs b/lib/src/local_working_copy.rs index f452f6536eb..ce890b3a78c 100644 --- a/lib/src/local_working_copy.rs +++ b/lib/src/local_working_copy.rs @@ -66,6 +66,7 @@ use crate::backend::TreeId; use crate::backend::TreeValue; use crate::commit::Commit; use crate::config::ConfigGetError; +use crate::conflict_labels::ConflictLabels; use crate::conflicts; use crate::conflicts::ConflictMarkerStyle; use crate::conflicts::ConflictMaterializeOptions; @@ -1081,7 +1082,11 @@ impl TreeState { .iter() .map(|id| TreeId::new(id.clone())) .collect(); - self.tree = MergedTree::new(self.store.clone(), tree_ids_builder.build()); + self.tree = MergedTree::new( + self.store.clone(), + tree_ids_builder.build(), + ConflictLabels::from_vec(proto.conflict_labels), + ); } self.file_states = FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted); @@ -1099,6 +1104,7 @@ impl TreeState { .iter() .map(|id| id.to_bytes()) .collect(); + proto.conflict_labels = self.tree.labels().as_slice().to_owned(); proto.file_states = self.file_states.data.clone(); // `FileStatesMap` is guaranteed to be sorted. proto.is_file_states_sorted = true; @@ -1275,7 +1281,7 @@ impl TreeState { }); trace_span!("write tree").in_scope(|| -> Result<(), BackendError> { let new_tree = tree_builder.write_tree()?; - is_dirty |= new_tree.tree_ids() != self.tree.tree_ids(); + is_dirty |= new_tree.tree_ids_and_labels() != self.tree.tree_ids_and_labels(); self.tree = new_tree.clone(); Ok(()) })?; @@ -2602,7 +2608,7 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { // continue an interrupted update if we find such a file. let new_tree = commit.tree(); let tree_state = self.wc.tree_state_mut()?; - if tree_state.tree.tree_ids() != new_tree.tree_ids() { + if tree_state.tree.tree_ids_and_labels() != new_tree.tree_ids_and_labels() { let stats = tree_state.check_out(&new_tree)?; self.tree_state_dirty = true; Ok(stats) @@ -2652,7 +2658,10 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy { mut self: Box, operation_id: OperationId, ) -> Result, WorkingCopyStateError> { - assert!(self.tree_state_dirty || self.old_tree.tree_ids() == self.wc.tree()?.tree_ids()); + assert!( + self.tree_state_dirty + || self.old_tree.tree_ids_and_labels() == self.wc.tree()?.tree_ids_and_labels() + ); if self.tree_state_dirty { self.wc .tree_state_mut()? diff --git a/lib/src/merge.rs b/lib/src/merge.rs index 38554812f9c..4b242b35bb9 100644 --- a/lib/src/merge.rs +++ b/lib/src/merge.rs @@ -29,6 +29,7 @@ use std::sync::Arc; use futures::future::try_join_all; use itertools::Itertools as _; use smallvec::SmallVec; +use smallvec::smallvec; use smallvec::smallvec_inline; use crate::backend::BackendResult; @@ -216,6 +217,16 @@ impl Merge { } } + /// Creates a `Merge` by repeating a single value. + pub fn repeated(value: T, num_sides: usize) -> Self + where + T: Clone, + { + Self { + values: smallvec![value; num_sides * 2 - 1], + } + } + /// Create a `Merge` from a `removes` and `adds`, padding with `None` to /// make sure that there is exactly one more `adds` than `removes`. pub fn from_legacy_form( @@ -334,14 +345,12 @@ impl Merge { simplified_to_original_indices } - /// Simplify the merge by joining diffs like A->B and B->C into A->C. - /// Also drops trivial diffs like A->A. + /// Apply the mapping returned by [`Self::get_simplified_mapping`]. #[must_use] - pub fn simplify(&self) -> Self + fn apply_simplified_mapping(&self, mapping: &[usize]) -> Self where - T: PartialEq + Clone, + T: Clone, { - let mapping = self.get_simplified_mapping(); // Reorder values based on their new indices in the simplified merge. let values = mapping .iter() @@ -350,6 +359,28 @@ impl Merge { Self { values } } + /// Simplify the merge by joining diffs like A->B and B->C into A->C. + /// Also drops trivial diffs like A->A. + #[must_use] + pub fn simplify(&self) -> Self + where + T: PartialEq + Clone, + { + let mapping = self.get_simplified_mapping(); + self.apply_simplified_mapping(&mapping) + } + + /// Simplify the merge, using a function to choose which values to compare. + #[must_use] + pub fn simplify_by<'a, U>(&'a self, f: impl FnMut(&'a T) -> U) -> Self + where + T: Clone, + U: PartialEq, + { + let mapping = self.map(f).get_simplified_mapping(); + self.apply_simplified_mapping(&mapping) + } + /// Updates the merge based on the given simplified merge. pub fn update_from_simplified(mut self, simplified: Self) -> Self where @@ -434,6 +465,38 @@ impl Merge { values: values.into(), }) } + + /// Converts a `&Merge` into a `Merge<&T>`. + pub fn as_ref(&self) -> Merge<&T> { + let values = self.values.iter().collect(); + Merge { values } + } + + /// Zip two merges which have the same number of terms. Panics if the merges + /// don't have the same number of terms. + pub fn zip(self, other: Merge) -> Merge<(T, U)> { + assert_eq!(self.values.len(), other.values.len()); + let values = self.values.into_iter().zip(other.values).collect(); + Merge { values } + } +} + +impl Merge<(T, U)> { + /// Unzips a merge of pairs into a pair of merges. + pub fn unzip(self) -> (Merge, Merge) { + let (left, right) = self.values.into_iter().unzip(); + (Merge { values: left }, Merge { values: right }) + } +} + +impl Merge<&'_ T> { + /// Convert a `Merge<&T>` into a `Merge` by cloning each term. + pub fn cloned(&self) -> Merge + where + T: Clone, + { + self.map(|&term| term.clone()) + } } /// Helper for consuming items from an iterator and then creating a `Merge`. @@ -1139,6 +1202,46 @@ mod tests { assert_eq!(c(&[0, 1, 2, 3, 4, 5, 1]).simplify(), c(&[0, 3, 4, 5, 2])); } + #[test] + fn test_simplify_by() { + fn enumerate_and_simplify_by(merge: Merge) -> Merge<(usize, i32)> { + let enumerated = Merge::from_vec(merge.iter().copied().enumerate().collect_vec()); + enumerated.simplify_by(|&(_index, value)| value) + } + + // 1-way merge + assert_eq!(enumerate_and_simplify_by(c(&[0])), c(&[(0, 0)])); + // 3-way merge + assert_eq!(enumerate_and_simplify_by(c(&[1, 0, 0])), c(&[(0, 1)])); + assert_eq!( + enumerate_and_simplify_by(c(&[1, 0, 2])), + c(&[(0, 1), (1, 0), (2, 2)]) + ); + // 5-way merge + assert_eq!(enumerate_and_simplify_by(c(&[0, 0, 0, 0, 0])), c(&[(4, 0)])); + assert_eq!(enumerate_and_simplify_by(c(&[0, 0, 0, 0, 1])), c(&[(4, 1)])); + assert_eq!( + enumerate_and_simplify_by(c(&[0, 0, 0, 1, 2])), + c(&[(2, 0), (3, 1), (4, 2)]) + ); + assert_eq!( + enumerate_and_simplify_by(c(&[0, 1, 2, 2, 0])), + c(&[(0, 0), (1, 1), (4, 0)]) + ); + assert_eq!( + enumerate_and_simplify_by(c(&[0, 1, 2, 2, 2])), + c(&[(0, 0), (1, 1), (4, 2)]) + ); + assert_eq!( + enumerate_and_simplify_by(c(&[0, 1, 2, 2, 3])), + c(&[(0, 0), (1, 1), (4, 3)]) + ); + assert_eq!( + enumerate_and_simplify_by(c(&[0, 1, 2, 3, 4])), + c(&[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]) + ); + } + #[test] fn test_update_from_simplified() { // 1-way merge @@ -1321,4 +1424,26 @@ mod tests { c(&[0, 1, 2, 5, 4, 3, 6, 7, 8]) ); } + + #[test] + fn test_zip() { + // Zip of 1-way merges + assert_eq!(c(&[1]).zip(c(&[2])), c(&[(1, 2)])); + // Zip of 3-way merges + assert_eq!( + c(&[1, 2, 3]).zip(c(&[4, 5, 6])), + c(&[(1, 4), (2, 5), (3, 6)]) + ); + } + + #[test] + fn test_unzip() { + // 1-way merge + assert_eq!(c(&[(1, 2)]).unzip(), (c(&[1]), c(&[2]))); + // 3-way merge + assert_eq!( + c(&[(1, 4), (2, 5), (3, 6)]).unzip(), + (c(&[1, 2, 3]), c(&[4, 5, 6])) + ); + } } diff --git a/lib/src/merged_tree.rs b/lib/src/merged_tree.rs index 9f3f5dbdb56..d4f056e9ffd 100644 --- a/lib/src/merged_tree.rs +++ b/lib/src/merged_tree.rs @@ -38,6 +38,7 @@ use pollster::FutureExt as _; use crate::backend::BackendResult; use crate::backend::TreeId; use crate::backend::TreeValue; +use crate::conflict_labels::ConflictLabels; use crate::copies::CopiesTreeDiffEntry; use crate::copies::CopiesTreeDiffStream; use crate::copies::CopyRecords; @@ -56,19 +57,20 @@ use crate::tree::Tree; use crate::tree_builder::TreeBuilder; use crate::tree_merge::merge_trees; -/// Presents a view of a merged set of trees at the root directory. In the -/// future, this may store additional metadata like conflict labels, so tree IDs -/// should be compared instead when checking for file changes. +/// Presents a view of a merged set of trees at the root directory, as well as +/// conflict labels. #[derive(Clone)] pub struct MergedTree { store: Arc, tree_ids: Merge, + labels: ConflictLabels, } impl fmt::Debug for MergedTree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MergedTree") .field("tree_ids", &self.tree_ids) + .field("labels", &self.labels) .finish_non_exhaustive() } } @@ -76,12 +78,28 @@ impl fmt::Debug for MergedTree { impl MergedTree { /// Creates a `MergedTree` with the given resolved tree ID. pub fn resolved(store: Arc, tree_id: TreeId) -> Self { - Self::new(store, Merge::resolved(tree_id)) + Self::unlabeled(store, Merge::resolved(tree_id)) + } + + /// Creates a `MergedTree` with the given tree IDs, without conflict labels. + pub fn unlabeled(store: Arc, tree_ids: Merge) -> Self { + Self { + store, + tree_ids, + labels: ConflictLabels::unlabeled(), + } } /// Creates a `MergedTree` with the given tree IDs. - pub fn new(store: Arc, tree_ids: Merge) -> Self { - Self { store, tree_ids } + pub fn new(store: Arc, tree_ids: Merge, labels: ConflictLabels) -> Self { + if let Some(num_sides) = labels.num_sides() { + assert_eq!(tree_ids.num_sides(), num_sides); + } + Self { + store, + tree_ids, + labels, + } } /// The `Store` associated with this tree. @@ -95,11 +113,29 @@ impl MergedTree { &self.tree_ids } - /// Extracts the underlying tree IDs for this `MergedTree`. + /// Extracts the underlying tree IDs for this `MergedTree`, discarding any + /// conflict labels. pub fn into_tree_ids(self) -> Merge { self.tree_ids } + /// Returns this merge's conflict labels, if any. + pub fn labels(&self) -> &ConflictLabels { + &self.labels + } + + /// Returns both the underlying tree IDs and any conflict labels. This can + /// be used to check whether there are changes in files to be materialized + /// in the working copy. + pub fn tree_ids_and_labels(&self) -> (&Merge, &ConflictLabels) { + (&self.tree_ids, &self.labels) + } + + /// Extracts the underlying tree IDs and conflict labels. + pub fn into_tree_ids_and_labels(self) -> (Merge, ConflictLabels) { + (self.tree_ids, self.labels) + } + /// Reads the merge of tree objects represented by this `MergedTree`. pub fn trees(&self) -> BackendResult> { self.trees_async().block_on() @@ -112,6 +148,24 @@ impl MergedTree { .await } + /// Returns a label for each term in a merge. If the merge is resolved, + /// returns `resolved_label` instead. Missing labels are indicated by + /// empty strings. + pub fn labels_by_term<'a>(&'a self, resolved_label: &'a str) -> Merge<&'a str> { + if self.tree_ids.is_resolved() { + assert!(!self.labels.is_present()); + Merge::resolved(resolved_label) + } else { + self.labels.as_merge().map_or_else( + || Merge::repeated("", self.tree_ids.num_sides()), + |labels| { + assert_eq!(labels.num_sides(), self.tree_ids.num_sides()); + labels.map(|label| label.as_str()) + }, + ) + } + } + /// Tries to resolve any conflicts, resolving any conflicts that can be /// automatically resolved and leaving the rest unresolved. pub async fn resolve(self) -> BackendResult { @@ -120,7 +174,11 @@ impl MergedTree { // a resolved merge. However, that function will always preserve the arity of // conflicts it cannot resolve. So we simplify the conflict again // here to possibly reduce a complex conflict to a simpler one. - let simplified = merged.simplify(); + let (simplified_labels, simplified) = if merged.is_resolved() { + (ConflictLabels::unlabeled(), merged) + } else { + self.labels.simplify_with(&merged) + }; // If debug assertions are enabled, check that the merge was idempotent. In // particular, that this last simplification doesn't enable further automatic // resolutions @@ -128,7 +186,11 @@ impl MergedTree { let re_merged = merge_trees(&self.store, simplified.clone()).await.unwrap(); debug_assert_eq!(re_merged, simplified); } - Ok(Self::new(self.store, simplified)) + Ok(Self { + store: self.store, + tree_ids: simplified, + labels: simplified_labels, + }) } /// An iterator over the conflicts in this tree, including subtrees. @@ -251,18 +313,50 @@ impl MergedTree { } /// Merges this tree with `other`, using `base` as base. Any conflicts will - /// be resolved recursively if possible. - pub async fn merge(self, base: Self, other: Self) -> BackendResult { - self.merge_no_resolve(base, other).resolve().await + /// be resolved recursively if possible. Does not add conflict labels. + pub async fn merge_unlabeled(self, base: Self, other: Self) -> BackendResult { + Self::merge(Merge::from_vec(vec![ + (self, String::new()), + (base, String::new()), + (other, String::new()), + ])) + .await + } + + /// Merges the provided trees into a single `MergedTree`. Any conflicts will + /// be resolved recursively if possible. The provided labels are used if a + /// conflict arises. However, if one of the input trees is already + /// conflicted, the corresponding label will be ignored, and its existing + /// labels will be used instead (or if any conflicted tree is unlabeled, + /// then the entire result will also be unlabeled). + pub async fn merge(merge: Merge<(Self, String)>) -> BackendResult { + Self::merge_no_resolve(merge).resolve().await } - /// Merges this tree with `other`, using `base` as base, without attempting + /// Merges the provided trees into a single `MergedTree`, without attempting /// to resolve file conflicts. - pub fn merge_no_resolve(self, base: Self, other: Self) -> Self { - debug_assert!(Arc::ptr_eq(&base.store, &self.store)); - debug_assert!(Arc::ptr_eq(&other.store, &self.store)); - let nested = Merge::from_vec(vec![self.tree_ids, base.tree_ids, other.tree_ids]); - Self::new(self.store, nested.flatten().simplify()) + pub fn merge_no_resolve(merge: Merge<(Self, String)>) -> Self { + debug_assert!( + merge + .iter() + .map(|(tree, _)| Arc::as_ptr(tree.store())) + .all_equal() + ); + let store = merge.first().0.store().clone(); + let flattened_labels: ConflictLabels = merge + .map(|(tree, label)| tree.labels_by_term(label)) + .flatten() + .into(); + + let flattened_tree_ids: Merge = merge + .into_iter() + .map(|(tree, _label)| tree.into_tree_ids()) + .collect::>() + .build() + .flatten(); + + let (labels, tree_ids) = flattened_labels.simplify_with(&flattened_tree_ids); + Self::new(store, tree_ids, labels) } } @@ -918,11 +1012,12 @@ impl MergedTreeBuilder { /// Create new tree(s) from the base tree(s) and overrides. pub fn write_tree(self) -> BackendResult { let store = self.base_tree.store.clone(); + let labels = self.base_tree.labels().clone(); let new_tree_ids = self.write_merged_trees()?; match new_tree_ids.simplify().into_resolved() { Ok(single_tree_id) => Ok(MergedTree::resolved(store, single_tree_id)), Err(tree_ids) => { - let tree = MergedTree::new(store, tree_ids); + let tree = MergedTree::new(store, tree_ids, labels); tree.resolve().block_on() } } diff --git a/lib/src/protos/local_working_copy.proto b/lib/src/protos/local_working_copy.proto index ce167613e91..713a6f63f5b 100644 --- a/lib/src/protos/local_working_copy.proto +++ b/lib/src/protos/local_working_copy.proto @@ -51,6 +51,8 @@ message TreeState { // Alternating positive and negative terms if there's a conflict, otherwise a // single (positive) value repeated bytes tree_ids = 5; + // Labels for the terms of a conflict. + repeated string conflict_labels = 7; repeated FileStateEntry file_states = 2; bool is_file_states_sorted = 6; SparsePatterns sparse_patterns = 3; diff --git a/lib/src/protos/local_working_copy.rs b/lib/src/protos/local_working_copy.rs index 4d552d2dfcc..a9dd9f4d9dc 100644 --- a/lib/src/protos/local_working_copy.rs +++ b/lib/src/protos/local_working_copy.rs @@ -37,6 +37,9 @@ pub struct TreeState { /// single (positive) value #[prost(bytes = "vec", repeated, tag = "5")] pub tree_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// Labels for the terms of a conflict. + #[prost(string, repeated, tag = "7")] + pub conflict_labels: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag = "2")] pub file_states: ::prost::alloc::vec::Vec, #[prost(bool, tag = "6")] diff --git a/lib/src/protos/simple_store.proto b/lib/src/protos/simple_store.proto index 24b4fa63604..a7a6a4e5bff 100644 --- a/lib/src/protos/simple_store.proto +++ b/lib/src/protos/simple_store.proto @@ -44,6 +44,8 @@ message Commit { repeated bytes predecessors = 2; // Alternating positive and negative terms repeated bytes root_tree = 3; + // Labels for the terms of a conflict. + repeated string conflict_labels = 10; bytes change_id = 4; string description = 5; diff --git a/lib/src/protos/simple_store.rs b/lib/src/protos/simple_store.rs index fc6d1fb924d..8f1fdd5652b 100644 --- a/lib/src/protos/simple_store.rs +++ b/lib/src/protos/simple_store.rs @@ -49,6 +49,9 @@ pub struct Commit { /// Alternating positive and negative terms #[prost(bytes = "vec", repeated, tag = "3")] pub root_tree: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// Labels for the terms of a conflict. + #[prost(string, repeated, tag = "10")] + pub conflict_labels: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(bytes = "vec", tag = "4")] pub change_id: ::prost::alloc::vec::Vec, #[prost(string, tag = "5")] diff --git a/lib/src/rewrite.rs b/lib/src/rewrite.rs index 90e456dbfac..4e256101743 100644 --- a/lib/src/rewrite.rs +++ b/lib/src/rewrite.rs @@ -81,7 +81,7 @@ pub async fn merge_commit_trees_no_resolve_without_repo( Ok::<_, BackendError>(commit.tree_ids().clone()) }) .await?; - Ok(MergedTree::new( + Ok(MergedTree::unlabeled( store.clone(), tree_id_merge.flatten().simplify(), )) @@ -284,7 +284,9 @@ impl<'repo> CommitRewriter<'repo> { let (old_base_tree, new_base_tree) = try_join!(old_base_tree_fut, new_base_tree_fut)?; ( old_base_tree.tree_ids() == self.old_commit.tree_ids(), - new_base_tree.merge(old_base_tree, old_tree).await?, + new_base_tree + .merge_unlabeled(old_base_tree, old_tree) + .await?, ) }; // Ensure we don't abandon commits with multiple parents (merge commits), even @@ -381,7 +383,7 @@ pub fn rebase_to_dest_parent( let source_parent_tree = source.parent_tree(repo)?; let source_tree = source.tree(); destination_tree - .merge(source_parent_tree, source_tree) + .merge_unlabeled(source_parent_tree, source_tree) .block_on() }, ) @@ -1201,7 +1203,7 @@ pub fn squash_commits<'repo>( let source_tree = source.commit.commit.tree(); // Apply the reverse of the selected changes onto the source let new_source_tree = source_tree - .merge( + .merge_unlabeled( source.commit.selected_tree.clone(), source.commit.parent_tree.clone(), ) @@ -1239,7 +1241,7 @@ pub fn squash_commits<'repo>( let mut destination_tree = rewritten_destination.tree(); for source in &source_commits { destination_tree = destination_tree - .merge( + .merge_unlabeled( source.commit.parent_tree.clone(), source.commit.selected_tree.clone(), ) diff --git a/lib/src/simple_backend.rs b/lib/src/simple_backend.rs index 87d73257cc7..4af14939c18 100644 --- a/lib/src/simple_backend.rs +++ b/lib/src/simple_backend.rs @@ -56,6 +56,7 @@ use crate::backend::Tree; use crate::backend::TreeId; use crate::backend::TreeValue; use crate::backend::make_root_commit; +use crate::conflict_labels::ConflictLabels; use crate::content_hash::blake2b_hash; use crate::file_util::persist_content_addressed_temp_file; use crate::index::Index; @@ -359,6 +360,7 @@ pub fn commit_to_proto(commit: &Commit) -> crate::protos::simple_store::Commit { proto.predecessors.push(predecessor.to_bytes()); } proto.root_tree = commit.root_tree.iter().map(|id| id.to_bytes()).collect(); + proto.conflict_labels = commit.conflict_labels.as_slice().to_owned(); proto.change_id = commit.change_id.to_bytes(); proto.description = commit.description.clone(); proto.author = Some(signature_to_proto(&commit.author)); @@ -378,11 +380,13 @@ fn commit_from_proto(mut proto: crate::protos::simple_store::Commit) -> Commit { let predecessors = proto.predecessors.into_iter().map(CommitId::new).collect(); let merge_builder: MergeBuilder<_> = proto.root_tree.into_iter().map(TreeId::new).collect(); let root_tree = merge_builder.build(); + let conflict_labels = ConflictLabels::from_vec(proto.conflict_labels); let change_id = ChangeId::new(proto.change_id); Commit { parents, predecessors, root_tree, + conflict_labels, change_id, description: proto.description, author: signature_from_proto(proto.author.unwrap_or_default()), @@ -515,6 +519,7 @@ mod tests { parents: vec![], predecessors: vec![], root_tree: Merge::resolved(backend.empty_tree_id().clone()), + conflict_labels: ConflictLabels::unlabeled(), change_id: ChangeId::from_hex("abc123"), description: "".to_string(), author: create_signature(), diff --git a/lib/tests/test_commit_builder.rs b/lib/tests/test_commit_builder.rs index 2d2a07e4602..22cf29036b9 100644 --- a/lib/tests/test_commit_builder.rs +++ b/lib/tests/test_commit_builder.rs @@ -186,7 +186,7 @@ fn test_rewrite(backend: TestRepoBackend) { let store = repo.store(); // We have a new store instance, so we need to associate the old tree with the // new store instance. - let rewritten_tree = MergedTree::new(store.clone(), rewritten_tree.into_tree_ids()); + let rewritten_tree = MergedTree::unlabeled(store.clone(), rewritten_tree.into_tree_ids()); let initial_commit = store.get_commit(initial_commit.id()).unwrap(); let mut tx = repo.start_transaction(); let rewritten_commit = tx diff --git a/lib/tests/test_git_backend.rs b/lib/tests/test_git_backend.rs index 4e30396ef91..655163bb1f8 100644 --- a/lib/tests/test_git_backend.rs +++ b/lib/tests/test_git_backend.rs @@ -23,8 +23,10 @@ use futures::executor::block_on_stream; use jj_lib::backend::CommitId; use jj_lib::backend::CopyRecord; use jj_lib::commit::Commit; +use jj_lib::conflict_labels::ConflictLabels; use jj_lib::git_backend::GitBackend; use jj_lib::git_backend::JJ_TREES_COMMIT_HEADER; +use jj_lib::merge::Merge; use jj_lib::merged_tree::MergedTree; use jj_lib::object_id::ObjectId as _; use jj_lib::repo::ReadonlyRepo; @@ -369,3 +371,30 @@ fn test_jj_trees_header_with_one_tree() { ) "#); } + +#[test] +fn test_conflict_headers_roundtrip() { + let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); + let repo = test_repo.repo; + + let tree_1 = create_single_tree(&repo, &[(repo_path("file"), "aaa")]); + let tree_2 = create_single_tree(&repo, &[(repo_path("file"), "bbb")]); + let tree_3 = create_single_tree(&repo, &[(repo_path("file"), "ccc")]); + + let merged_tree = MergedTree::new( + repo.store().clone(), + Merge::from_vec(vec![ + tree_1.id().clone(), + tree_2.id().clone(), + tree_3.id().clone(), + ]), + ConflictLabels::from_vec(vec!["side 1".into(), "base".into(), "side 2".into()]), + ); + + // Create a commit with the conflicted tree. + let commit = commit_with_tree(repo.store(), merged_tree); + // Clear cached commit to ensure it is re-read. + repo.store().clear_caches(); + // Conflict trees and labels should be preserved on read. + assert_eq!(repo.store().get_commit(commit.id()).unwrap(), commit); +} diff --git a/lib/tests/test_id_prefix.rs b/lib/tests/test_id_prefix.rs index f86880a43e7..1fbd32a2fdf 100644 --- a/lib/tests/test_id_prefix.rs +++ b/lib/tests/test_id_prefix.rs @@ -549,7 +549,7 @@ fn test_id_prefix_shadowed_by_ref() { let commit_id_sym = commit.id().to_string(); let change_id_sym = commit.change_id().to_string(); - insta::assert_snapshot!(commit_id_sym, @"38b5c5aebe81a8441470"); + insta::assert_snapshot!(commit_id_sym, @"673f04bd4087fba5f703"); insta::assert_snapshot!(change_id_sym, @"sryyqqkqmuumyrlruupspprvnulvovzm"); let context = IdPrefixContext::default(); diff --git a/lib/tests/test_local_working_copy.rs b/lib/tests/test_local_working_copy.rs index 8e9cdb3baa8..8759c08bf0e 100644 --- a/lib/tests/test_local_working_copy.rs +++ b/lib/tests/test_local_working_copy.rs @@ -422,7 +422,7 @@ fn test_conflict_subdirectory() { let tree1 = create_tree(repo, &[(path, "0")]); let commit1 = commit_with_tree(repo.store(), tree1.clone()); let tree2 = create_tree(repo, &[(path, "1")]); - let merged_tree = tree1.merge(empty_tree, tree2).block_on().unwrap(); + let merged_tree = tree1.merge_unlabeled(empty_tree, tree2).block_on().unwrap(); let merged_commit = commit_with_tree(repo.store(), merged_tree); let repo = &test_workspace.repo; let ws = &mut test_workspace.workspace; @@ -859,10 +859,10 @@ fn test_materialize_snapshot_conflicted_files() { let base2_tree = create_tree(repo, &[(file1_path, "b\n"), (file2_path, "3\n")]); let side3_tree = create_tree(repo, &[(file1_path, "c\n"), (file2_path, "3\n")]); let merged_tree = side1_tree - .merge(base1_tree, side2_tree) + .merge_unlabeled(base1_tree, side2_tree) .block_on() .unwrap() - .merge(base2_tree, side3_tree) + .merge_unlabeled(base2_tree, side3_tree) .block_on() .unwrap(); let commit = commit_with_tree(repo.store(), merged_tree.clone()); @@ -990,7 +990,10 @@ fn test_materialize_snapshot_unchanged_conflicts() { let base_tree = create_tree(repo, &[(file_path, base_content)]); let left_tree = create_tree(repo, &[(file_path, left_content)]); let right_tree = create_tree(repo, &[(file_path, right_content)]); - let merged_tree = left_tree.merge(base_tree, right_tree).block_on().unwrap(); + let merged_tree = left_tree + .merge_unlabeled(base_tree, right_tree) + .block_on() + .unwrap(); let commit = commit_with_tree(repo.store(), merged_tree.clone()); test_workspace diff --git a/lib/tests/test_merged_tree.rs b/lib/tests/test_merged_tree.rs index 53efdf93e09..375cb6117e5 100644 --- a/lib/tests/test_merged_tree.rs +++ b/lib/tests/test_merged_tree.rs @@ -18,6 +18,7 @@ use jj_lib::backend::CommitId; use jj_lib::backend::CopyRecord; use jj_lib::backend::FileId; use jj_lib::backend::TreeValue; +use jj_lib::conflict_labels::ConflictLabels; use jj_lib::copies::CopiesTreeDiffEntryPath; use jj_lib::copies::CopyOperation; use jj_lib::copies::CopyRecords; @@ -78,7 +79,7 @@ fn test_merged_tree_builder_resolves_conflict() { let tree2 = create_single_tree(repo, &[(path1, "bar")]); let tree3 = create_single_tree(repo, &[(path1, "bar")]); - let base_tree = MergedTree::new( + let base_tree = MergedTree::unlabeled( store.clone(), Merge::from_removes_adds( [tree1.id().clone()], @@ -130,7 +131,7 @@ fn test_path_value_and_entries() { (file_dir_conflict_sub_path, "1"), ], ); - let merged_tree = MergedTree::new( + let merged_tree = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![tree1.id().clone()], @@ -284,10 +285,12 @@ fn test_resolve_success() { let tree = MergedTree::new( repo.store().clone(), - Merge::from_removes_adds( - vec![base1.id().clone()], - vec![side1.id().clone(), side2.id().clone()], - ), + Merge::from_vec(vec![ + side1.id().clone(), + base1.id().clone(), + side2.id().clone(), + ]), + ConflictLabels::from_vec(vec!["left".into(), "base".into(), "right".into()]), ); let resolved_tree = tree.resolve().block_on().unwrap(); assert!(resolved_tree.tree_ids().is_resolved()); @@ -306,7 +309,7 @@ fn test_resolve_root_becomes_empty() { let side1 = create_single_tree(repo, &[(path2, "base1")]); let side2 = create_single_tree(repo, &[(path1, "base1")]); - let tree = MergedTree::new( + let tree = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![base1.id().clone()], @@ -326,29 +329,50 @@ fn test_resolve_with_conflict() { // cannot) let trivial_path = repo_path("dir1/trivial"); let conflict_path = repo_path("dir2/file_conflict"); - let base1 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "base1")]); - let side1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]); - let side2 = create_single_tree(repo, &[(trivial_path, "base1"), (conflict_path, "side2")]); - let expected_base1 = - create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "base1")]); + + // We start with a 3-sided conflict: + let side1 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "base")]); + let base1 = create_single_tree(repo, &[(trivial_path, "base"), (conflict_path, "base")]); + let side2 = create_single_tree(repo, &[(trivial_path, "base"), (conflict_path, "side2")]); + let base2 = create_single_tree(repo, &[(trivial_path, "base"), (conflict_path, "base")]); + let side3 = create_single_tree(repo, &[(trivial_path, "base"), (conflict_path, "side3")]); + + // This should be reduced to a 2-sided conflict after "trivial" is resolved: let expected_side1 = - create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side1")]); - let expected_side2 = create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side2")]); + let expected_base1 = + create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "base")]); + let expected_side2 = + create_single_tree(repo, &[(trivial_path, "side1"), (conflict_path, "side3")]); let tree = MergedTree::new( repo.store().clone(), - Merge::from_removes_adds( - vec![base1.id().clone()], - vec![side1.id().clone(), side2.id().clone()], - ), + Merge::from_vec(vec![ + side1.id().clone(), + base1.id().clone(), + side2.id().clone(), + base2.id().clone(), + side3.id().clone(), + ]), + ConflictLabels::from_vec(vec![ + "side 1".into(), + "base 1".into(), + "side 2".into(), + "base 2".into(), + "side 3".into(), + ]), ); let resolved_tree = tree.resolve().block_on().unwrap(); - assert_eq!( - resolved_tree.tree_ids(), - &Merge::from_removes_adds( - vec![expected_base1.id().clone()], - vec![expected_side1.id().clone(), expected_side2.id().clone()] + assert_tree_eq!( + resolved_tree, + MergedTree::new( + repo.store().clone(), + Merge::from_vec(vec![ + expected_side1.id().clone(), + expected_base1.id().clone(), + expected_side2.id().clone() + ]), + ConflictLabels::from_vec(vec!["side 2".into(), "base 2".into(), "side 3".into()]), ) ); } @@ -367,10 +391,12 @@ fn test_resolve_with_conflict_containing_empty_subtree() { let tree = MergedTree::new( repo.store().clone(), - Merge::from_removes_adds( - vec![base1.id().clone()], - vec![side1.id().clone(), side2.id().clone()], - ), + Merge::from_vec(vec![ + side1.id().clone(), + base1.id().clone(), + side2.id().clone(), + ]), + ConflictLabels::from_vec(vec!["left".into(), "base".into(), "right".into()]), ); let resolved_tree = tree.clone().resolve().block_on().unwrap(); assert_tree_eq!(resolved_tree, tree); @@ -443,7 +469,7 @@ fn test_conflict_iterator() { ], ); - let tree = MergedTree::new( + let tree = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![base1.id().clone()], @@ -539,7 +565,7 @@ fn test_conflict_iterator_higher_arity() { &[(two_sided_path, "side3"), (three_sided_path, "side3")], ); - let tree = MergedTree::new( + let tree = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![base1.id().clone(), base2.id().clone()], @@ -877,14 +903,14 @@ fn test_diff_conflicted() { (path4, "right-side2"), ], ); - let left_merged = MergedTree::new( + let left_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![left_base.id().clone()], vec![left_side1.id().clone(), left_side2.id().clone()], ), ); - let right_merged = MergedTree::new( + let right_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![right_base.id().clone()], @@ -1018,14 +1044,14 @@ fn test_diff_dir_file() { (&path6.join(file), "right"), ], ); - let left_merged = MergedTree::new( + let left_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![left_base.id().clone()], vec![left_side1.id().clone(), left_side2.id().clone()], ), ); - let right_merged = MergedTree::new( + let right_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![right_base.id().clone()], @@ -1214,7 +1240,7 @@ fn test_merge_simple() { let expected_merged = MergedTree::resolved(repo.store().clone(), expected.id().clone()); let merged = side1_merged - .merge(base1_merged, side2_merged) + .merge_unlabeled(base1_merged, side2_merged) .block_on() .unwrap(); assert_tree_eq!(merged, expected_merged); @@ -1238,7 +1264,7 @@ fn test_merge_partial_resolution() { let base1_merged = MergedTree::resolved(repo.store().clone(), base1.id().clone()); let side1_merged = MergedTree::resolved(repo.store().clone(), side1.id().clone()); let side2_merged = MergedTree::resolved(repo.store().clone(), side2.id().clone()); - let expected_merged = MergedTree::new( + let expected_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![expected_base1.id().clone()], @@ -1247,7 +1273,7 @@ fn test_merge_partial_resolution() { ); let merged = side1_merged - .merge(base1_merged, side2_merged) + .merge_unlabeled(base1_merged, side2_merged) .block_on() .unwrap(); assert_tree_eq!(merged, expected_merged); @@ -1267,21 +1293,21 @@ fn test_merge_simplify_only() { let tree4 = create_single_tree(repo, &[(path, "4")]); let tree5 = create_single_tree(repo, &[(path, "5")]); let expected = tree5.clone(); - let base1_merged = MergedTree::new( + let base1_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![tree1.id().clone()], vec![tree2.id().clone(), tree3.id().clone()], ), ); - let side1_merged = MergedTree::new( + let side1_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![tree1.id().clone()], vec![tree4.id().clone(), tree2.id().clone()], ), ); - let side2_merged = MergedTree::new( + let side2_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![tree4.id().clone()], @@ -1291,7 +1317,7 @@ fn test_merge_simplify_only() { let expected_merged = MergedTree::resolved(repo.store().clone(), expected.id().clone()); let merged = side1_merged - .merge(base1_merged, side2_merged) + .merge_unlabeled(base1_merged, side2_merged) .block_on() .unwrap(); assert_tree_eq!(merged, expected_merged); @@ -1308,35 +1334,110 @@ fn test_merge_simplify_result() { // The conflict in path1 cannot be resolved, but the conflict in path2 can. let path1 = repo_path("dir1/file"); let path2 = repo_path("dir2/file"); - let tree1 = create_single_tree(repo, &[(path1, "1"), (path2, "1")]); - let tree2 = create_single_tree(repo, &[(path1, "2"), (path2, "2")]); - let tree3 = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); - let tree4 = create_single_tree(repo, &[(path1, "4"), (path2, "2")]); - let tree5 = create_single_tree(repo, &[(path1, "4"), (path2, "1")]); - let expected_base1 = create_single_tree(repo, &[(path1, "1"), (path2, "3")]); + let side1_left = create_single_tree(repo, &[(path1, "2"), (path2, "2")]); + let side1_base = create_single_tree(repo, &[(path1, "1"), (path2, "1")]); + let side1_right = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); + let base1 = create_single_tree(repo, &[(path1, "4"), (path2, "2")]); + let side2 = create_single_tree(repo, &[(path1, "4"), (path2, "1")]); let expected_side1 = create_single_tree(repo, &[(path1, "2"), (path2, "3")]); + let expected_base1 = create_single_tree(repo, &[(path1, "1"), (path2, "3")]); let expected_side2 = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); let side1_merged = MergedTree::new( repo.store().clone(), - Merge::from_removes_adds( - vec![tree1.id().clone()], - vec![tree2.id().clone(), tree3.id().clone()], - ), + Merge::from_vec(vec![ + side1_left.id().clone(), + side1_base.id().clone(), + side1_right.id().clone(), + ]), + ConflictLabels::from_vec(vec![ + "side 1 left".into(), + "side 1 base".into(), + "side 1 right".into(), + ]), ); - let base1_merged = MergedTree::resolved(repo.store().clone(), tree4.id().clone()); - let side2_merged = MergedTree::resolved(repo.store().clone(), tree5.id().clone()); + let base1_merged = MergedTree::resolved(repo.store().clone(), base1.id().clone()); + let side2_merged = MergedTree::resolved(repo.store().clone(), side2.id().clone()); let expected_merged = MergedTree::new( repo.store().clone(), - Merge::from_removes_adds( - vec![expected_base1.id().clone()], - vec![expected_side1.id().clone(), expected_side2.id().clone()], - ), - ); + Merge::from_vec(vec![ + expected_side1.id().clone(), + expected_base1.id().clone(), + expected_side2.id().clone(), + ]), + ConflictLabels::from_vec(vec![ + "side 1 left".into(), + "side 1 base".into(), + "side 1 right".into(), + ]), + ); + + // Although we pass labels here, they don't appear in the final result. The + // "side 1" label is ignored because that side is already conflicted. The "base + // 1" and "side 2" labels are used, but then those sides are removed after + // resolving and simplifying. + let merged = MergedTree::merge(Merge::from_vec(vec![ + (side1_merged, "side 1".into()), + (base1_merged, "base 1".into()), + (side2_merged, "side 2".into()), + ])) + .block_on() + .unwrap(); + assert_tree_eq!(merged, expected_merged); +} - let merged = side1_merged - .merge(base1_merged, side2_merged) - .block_on() - .unwrap(); +/// Test that resolved trees take their labels from `MergeLabels`. +#[test] +fn test_merge_simplify_result_with_resolved_labels() { + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + + // The conflict in path1 cannot be resolved, but the conflict in path2 can. + let path1 = repo_path("dir1/file"); + let path2 = repo_path("dir2/file"); + let side1 = create_single_tree(repo, &[(path1, "2"), (path2, "2")]); + let base1 = create_single_tree(repo, &[(path1, "1"), (path2, "1")]); + let side2_left = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); + let side2_base = create_single_tree(repo, &[(path1, "4"), (path2, "2")]); + let side2_right = create_single_tree(repo, &[(path1, "4"), (path2, "1")]); + let expected_side1 = create_single_tree(repo, &[(path1, "2"), (path2, "3")]); + let expected_base1 = create_single_tree(repo, &[(path1, "1"), (path2, "3")]); + let expected_side2 = create_single_tree(repo, &[(path1, "3"), (path2, "3")]); + let side1_merged = MergedTree::resolved(repo.store().clone(), side1.id().clone()); + let base1_merged = MergedTree::resolved(repo.store().clone(), base1.id().clone()); + let side2_merged = MergedTree::new( + repo.store().clone(), + Merge::from_vec(vec![ + side2_left.id().clone(), + side2_base.id().clone(), + side2_right.id().clone(), + ]), + ConflictLabels::from_vec(vec![ + "side 2 left".into(), + "side 2 base".into(), + "side 2 right".into(), + ]), + ); + let expected_merged = MergedTree::new( + repo.store().clone(), + Merge::from_vec(vec![ + expected_side1.id().clone(), + expected_base1.id().clone(), + expected_side2.id().clone(), + ]), + ConflictLabels::from_vec(vec!["side 1".into(), "base 1".into(), "side 2 left".into()]), + ); + + // Since side 1 and base 1 are resolved, they will use the provided "side 1" and + // "base 1" labels. Since side 2 is conflicted, its existing labels are used + // instead of the provided "side 2" label. Two of the terms from side 2 will be + // removed after resolving and simplifying. + let merged = MergedTree::merge(Merge::from_vec(vec![ + (side1_merged, "side 1".into()), + (base1_merged, "base 1".into()), + (side2_merged, "side 2".into()), + ])) + .block_on() + .unwrap(); assert_tree_eq!(merged, expected_merged); } @@ -1410,7 +1511,7 @@ fn test_merge_simplify_file_conflict() { let parent_base = create_single_tree(repo, &[(conflict_path, &parent_base_text)]); let parent_left = create_single_tree(repo, &[(conflict_path, &parent_left_text)]); let parent_right = create_single_tree(repo, &[(conflict_path, &parent_right_text)]); - let parent_merged = MergedTree::new( + let parent_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![parent_base.id().clone()], @@ -1431,7 +1532,7 @@ fn test_merge_simplify_file_conflict() { repo, &[(other_path, "child1"), (conflict_path, &child1_right_text)], ); - let child1_merged = MergedTree::new( + let child1_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![child1_base.id().clone()], @@ -1451,7 +1552,7 @@ fn test_merge_simplify_file_conflict() { let expected_merged = MergedTree::resolved(repo.store().clone(), expected.id().clone()); let merged = child1_merged - .merge(parent_merged, child2_merged) + .merge_unlabeled(parent_merged, child2_merged) .block_on() .unwrap(); assert_tree_eq!(merged, expected_merged); @@ -1495,7 +1596,7 @@ fn test_merge_simplify_file_conflict_with_absent() { let child2_right = create_single_tree(repo, &[(child2_path, ""), (conflict_path, "0\n2\n")]); let child1_merged = MergedTree::resolved(repo.store().clone(), child1.id().clone()); let parent_merged = MergedTree::resolved(repo.store().clone(), parent.id().clone()); - let child2_merged = MergedTree::new( + let child2_merged = MergedTree::unlabeled( repo.store().clone(), Merge::from_removes_adds( vec![child2_base.id().clone()], @@ -1507,7 +1608,7 @@ fn test_merge_simplify_file_conflict_with_absent() { let expected_merged = MergedTree::resolved(repo.store().clone(), expected.id().clone()); let merged = child1_merged - .merge(parent_merged, child2_merged) + .merge_unlabeled(parent_merged, child2_merged) .block_on() .unwrap(); assert_tree_eq!(merged, expected_merged); diff --git a/lib/tests/test_mut_repo.rs b/lib/tests/test_mut_repo.rs index 2d50243de6b..4914cacb8d5 100644 --- a/lib/tests/test_mut_repo.rs +++ b/lib/tests/test_mut_repo.rs @@ -153,7 +153,7 @@ fn test_edit_previous_empty_merge() { let empty_tree = repo.store().root_commit().tree(); let old_parent_tree = old_parent1 .tree() - .merge(empty_tree, old_parent2.tree()) + .merge_unlabeled(empty_tree, old_parent2.tree()) .block_on() .unwrap(); let old_wc_commit = mut_repo diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index d1ec418ce43..493445e92ea 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -4560,7 +4560,7 @@ fn test_evaluate_expression_diff_contains_conflict(indexed: bool) { let commit1 = create_commit(vec![repo.store().root_commit_id().clone()], tree1.clone()); let tree2 = create_tree(&repo, &[(file_path, "0\n2\n")]); let tree3 = create_tree(&repo, &[(file_path, "0\n3\n")]); - let tree4 = tree2.merge(tree1, tree3).block_on().unwrap(); + let tree4 = tree2.merge_unlabeled(tree1, tree3).block_on().unwrap(); let commit2 = create_commit(vec![commit1.id().clone()], tree4); assert_eq!( @@ -4672,7 +4672,7 @@ fn test_evaluate_expression_conflict() { let tree3 = create_tree(repo, &[(file_path1, "3"), (file_path2, "1")]); let commit3 = create_commit(vec![commit2.id().clone()], tree3.clone()); let tree4 = tree2 - .merge(tree1.clone(), tree3.clone()) + .merge_unlabeled(tree1.clone(), tree3.clone()) .block_on() .unwrap(); let commit4 = create_commit(vec![commit3.id().clone()], tree4); diff --git a/lib/tests/test_revset_optimized.rs b/lib/tests/test_revset_optimized.rs index 5114e26d800..4bc109a69f3 100644 --- a/lib/tests/test_revset_optimized.rs +++ b/lib/tests/test_revset_optimized.rs @@ -196,15 +196,15 @@ fn test_mostly_linear() { insta::assert_snapshot!( commits.iter().map(|c| format!("{:<2} {}\n", c.description(), c.id())).join(""), @r" 00000000000000000000 - 1 0481b93947ec320582da - 2 cf00f20ba8d03cfe27d7 - 3 db1fb816b776ac1257a1 - 4 3fa336059698229e1869 - 5 f1d81483ce728dac9c5c - 6 c0620144c8381147c3eb - 7 f1355db0abd492eb4df4 - 8 2aa8feb562b8426f485f - 9 fee08f51862cdd69739d + 1 9347727a23ac136d026b + 2 5c4534eb232b7932ac27 + 3 10bec2cf4b4a75515fc5 + 4 33eb7f9e9ef8bf8546b4 + 5 d0d3647d53753b0b1c67 + 6 a54f0de3bb7821fcb258 + 7 0b007c84e16879cafd60 + 8 2eac20af222a3aa9608d + 9 2c8000b6ebc655b72b39 "); let commit_ids = commits.iter().map(|c| c.id().clone()).collect_vec(); @@ -250,14 +250,14 @@ fn test_weird_merges() { insta::assert_snapshot!( commits.iter().map(|c| format!("{:<2} {}\n", c.description(), c.id())).join(""), @r" 00000000000000000000 - 1 0481b93947ec320582da - 2 cf00f20ba8d03cfe27d7 - 3 eae7b745114ccd1e7c2b - 4 8ff5f4ecfd6e51f7fb46 - 5 0701d1a6ff427cc5d1c1 - 6 8b2aa399c528813d0bfc - 7 abd7903b1690685bb9c8 - 8 0f916c2bef3fe0aa2f54 + 1 9347727a23ac136d026b + 2 5c4534eb232b7932ac27 + 3 00b1f75480b18e77fc98 + 4 7085f958ae1f042e9c00 + 5 bcc333f855632686364c + 6 d28da0fda414d06342c7 + 7 5067c973b6522d2fe22b + 8 bcd39d54b0a57f8e0bc4 "); let commit_ids = commits.iter().map(|c| c.id().clone()).collect_vec(); @@ -326,15 +326,15 @@ fn test_feature_branches() { insta::assert_snapshot!( commits.iter().map(|c| format!("{:<2} {}\n", c.description(), c.id())).join(""), @r" 00000000000000000000 - 1 0481b93947ec320582da - 2 dfc04cc2cdd8ddb7b55b - 3 eae7b745114ccd1e7c2b - 4 8e272dcf1dfef181cb22 - 5 62bb52a8eb37e1dffcb1 - 6 900d6e697d7e53fe31c5 - 7 74bcf8cf11c54a565c0c - 8 01c9367ecaaa7c0bfc47 - 9 60b2edccd2b633d4b164 + 1 9347727a23ac136d026b + 2 93699472db1201082f52 + 3 00b1f75480b18e77fc98 + 4 46dfe619ac49062d18bf + 5 0fec13c6709efa261757 + 6 778679be2d9356bc5b83 + 7 085e9e6c69d725474f90 + 8 86477d8ee9c71753cf68 + 9 91cb695c790da9ff2a55 "); let commit_ids = commits.iter().map(|c| c.id().clone()).collect_vec(); @@ -396,15 +396,15 @@ fn test_rewritten() { insta::assert_snapshot!( commits.iter().map(|c| format!("{:<2} {}\n", c.description(), c.id())).join(""), @r" 00000000000000000000 - 1 0481b93947ec320582da - 2 cf00f20ba8d03cfe27d7 - 3 0ae48179bdee2dc5cbee - 4 01e8f78ec985350a98f5 - 5 2586f14733ac1c4f2b89 - 2b 4dc5572169ee6230a7fc - 3 9efc19868da6032ea4f1 - 5 ea4b6ce59436a2ccfa67 - 5 31f5e38f8d68c839c6f6 + 1 9347727a23ac136d026b + 2 5c4534eb232b7932ac27 + 3 6595a1e42c74951a4086 + 4 49810d5cb6c34127feb0 + 5 ce2ab1dc315839bd495b + 2b 2d5a81f7fbabc5d4f17d + 3 03bff596bac4c9fc4f18 + 5 2c3d504a53680b4b2a3d + 5 c8098708ea0cd0ad52bd "); let commit_ids = commits.iter().map(|c| c.id().clone()).collect_vec(); diff --git a/lib/testutils/src/lib.rs b/lib/testutils/src/lib.rs index 912dada1b25..633d1fbc060 100644 --- a/lib/testutils/src/lib.rs +++ b/lib/testutils/src/lib.rs @@ -543,10 +543,12 @@ pub fn commit_with_tree(store: &Arc, tree: MergedTree) -> Commit { tz_offset: 0, }, }; + let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels(); let commit = backend::Commit { parents: vec![store.root_commit_id().clone()], predecessors: vec![], - root_tree: tree.into_tree_ids(), + root_tree, + conflict_labels, change_id: ChangeId::from_hex("abcd"), description: "description".to_string(), author: signature.clone(), @@ -599,8 +601,8 @@ macro_rules! assert_tree_eq { let left_tree: &::jj_lib::merged_tree::MergedTree = &$left_tree; let right_tree: &::jj_lib::merged_tree::MergedTree = &$right_tree; assert_eq!( - left_tree.tree_ids(), - right_tree.tree_ids(), + left_tree.tree_ids_and_labels(), + right_tree.tree_ids_and_labels(), "{}:\n left: {}\nright: {}", format_args!($($args)*), $crate::dump_tree(left_tree),