Skip to main content

jj_lib/
conflict_labels.rs

1// Copyright 2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Labels for conflicted trees.
16
17use std::fmt;
18
19use crate::merge::Merge;
20
21/// Optionally contains a set of labels for the terms of a conflict. Resolved
22/// merges cannot be labeled.
23#[derive(PartialEq, Eq, Clone)]
24pub struct ConflictLabels {
25    // If the merge is resolved, the label must be empty.
26    labels: Merge<String>,
27}
28
29impl ConflictLabels {
30    /// Create a `ConflictLabels` with no labels.
31    pub const fn unlabeled() -> Self {
32        Self {
33            labels: Merge::resolved(String::new()),
34        }
35    }
36
37    /// Create a `ConflictLabels` from a `Merge<String>`. If the merge is
38    /// resolved, the labels will be discarded since resolved merges cannot have
39    /// labels.
40    pub fn from_merge(labels: Merge<String>) -> Self {
41        if labels.is_resolved() || labels.iter().all(|label| label.is_empty()) {
42            Self::unlabeled()
43        } else {
44            Self { labels }
45        }
46    }
47
48    /// Create a `ConflictLabels` from a `Vec<String>`, with an empty vec
49    /// representing no labels.
50    pub fn from_vec(labels: Vec<String>) -> Self {
51        if labels.is_empty() {
52            Self::unlabeled()
53        } else {
54            Self::from_merge(Merge::from_vec(labels))
55        }
56    }
57
58    /// Returns true if there are labels present.
59    pub fn has_labels(&self) -> bool {
60        !self.labels.is_resolved()
61    }
62
63    /// Returns the number of sides of the underlying merge if any terms have
64    /// labels, or `None` if there are no labels.
65    pub fn num_sides(&self) -> Option<usize> {
66        self.has_labels().then_some(self.labels.num_sides())
67    }
68
69    /// Returns the underlying `Merge<String>`.
70    pub fn as_merge(&self) -> &Merge<String> {
71        &self.labels
72    }
73
74    /// Extracts the underlying `Merge<String>`.
75    pub fn into_merge(self) -> Merge<String> {
76        self.labels
77    }
78
79    /// Returns the conflict labels as a slice. If there are no labels, returns
80    /// an empty slice.
81    pub fn as_slice(&self) -> &[String] {
82        if self.has_labels() {
83            self.labels.as_slice()
84        } else {
85            &[]
86        }
87    }
88
89    /// Get the label for a side at an index.
90    pub fn get_add(&self, add_index: usize) -> Option<&str> {
91        self.labels
92            .get_add(add_index)
93            .filter(|label| !label.is_empty())
94            .map(String::as_str)
95    }
96
97    /// Get the label for a base at an index.
98    pub fn get_remove(&self, remove_index: usize) -> Option<&str> {
99        self.labels
100            .get_remove(remove_index)
101            .filter(|label| !label.is_empty())
102            .map(String::as_str)
103    }
104
105    /// Simplify a merge with the same number of sides while preserving the
106    /// conflict labels corresponding to each side of the merge.
107    pub fn simplify_with<T: PartialEq + Clone>(&self, merge: &Merge<T>) -> (Self, Merge<T>) {
108        if self.has_labels() {
109            let (labels, simplified) = self
110                .labels
111                .as_ref()
112                .zip(merge.as_ref())
113                .simplify_by(|&(_label, item)| item)
114                .unzip();
115            (Self::from_merge(labels.cloned()), simplified.cloned())
116        } else {
117            let simplified = merge.simplify();
118            (Self::unlabeled(), simplified)
119        }
120    }
121}
122
123impl fmt::Debug for ConflictLabels {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        if self.has_labels() {
126            f.debug_tuple("Labeled")
127                .field(&self.labels.as_slice())
128                .finish()
129        } else {
130            write!(f, "Unlabeled")
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_conflict_labels_from_vec() {
141        // From empty vec for unlabeled
142        assert_eq!(
143            ConflictLabels::from_vec(vec![]),
144            ConflictLabels::unlabeled()
145        );
146        // From non-empty vec of terms
147        assert_eq!(
148            ConflictLabels::from_vec(vec![
149                String::from("left"),
150                String::from("base"),
151                String::from("right")
152            ]),
153            ConflictLabels::from_merge(Merge::from_vec(vec![
154                String::from("left"),
155                String::from("base"),
156                String::from("right")
157            ]))
158        );
159    }
160
161    #[test]
162    fn test_conflict_labels_as_slice() {
163        // Empty slice for unlabeled
164        let empty: &[String] = &[];
165        assert_eq!(ConflictLabels::unlabeled().as_slice(), empty);
166        // Slice of terms for labeled
167        assert_eq!(
168            ConflictLabels::from_merge(Merge::from_vec(vec![
169                String::from("left"),
170                String::from("base"),
171                String::from("right")
172            ]))
173            .as_slice(),
174            &[
175                String::from("left"),
176                String::from("base"),
177                String::from("right")
178            ]
179        );
180    }
181}