jj_lib/
commit.rs

1// Copyright 2020 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#![expect(missing_docs)]
16
17use std::cmp::Ordering;
18use std::fmt::Debug;
19use std::fmt::Error;
20use std::fmt::Formatter;
21use std::hash::Hash;
22use std::hash::Hasher;
23use std::sync::Arc;
24
25use futures::future::try_join_all;
26use itertools::Itertools as _;
27use pollster::FutureExt as _;
28
29use crate::backend;
30use crate::backend::BackendError;
31use crate::backend::BackendResult;
32use crate::backend::ChangeId;
33use crate::backend::CommitId;
34use crate::backend::Signature;
35use crate::backend::TreeId;
36use crate::conflict_labels::ConflictLabels;
37use crate::index::IndexResult;
38use crate::merge::Merge;
39use crate::merged_tree::MergedTree;
40use crate::repo::Repo;
41use crate::rewrite::merge_commit_trees;
42use crate::signing::SignResult;
43use crate::signing::Verification;
44use crate::store::Store;
45
46#[derive(Clone, serde::Serialize)]
47pub struct Commit {
48    #[serde(skip)]
49    store: Arc<Store>,
50    #[serde(rename = "commit_id")]
51    id: CommitId,
52    #[serde(flatten)]
53    data: Arc<backend::Commit>,
54}
55
56impl Debug for Commit {
57    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
58        f.debug_struct("Commit").field("id", &self.id).finish()
59        // We intentionally don't print the `data` field. You can debug-print
60        // `commit.store_commit()` to get those details.
61        //
62        // The reason is that `Commit` objects are debug-printed as part of many
63        // other data structures and in tracing.
64    }
65}
66
67impl PartialEq for Commit {
68    fn eq(&self, other: &Self) -> bool {
69        self.id == other.id
70    }
71}
72
73impl Eq for Commit {}
74
75impl Ord for Commit {
76    fn cmp(&self, other: &Self) -> Ordering {
77        self.id.cmp(&other.id)
78    }
79}
80
81impl PartialOrd for Commit {
82    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
83        Some(self.cmp(other))
84    }
85}
86
87impl Hash for Commit {
88    fn hash<H: Hasher>(&self, state: &mut H) {
89        self.id.hash(state);
90    }
91}
92
93impl Commit {
94    pub fn new(store: Arc<Store>, id: CommitId, data: Arc<backend::Commit>) -> Self {
95        Self { store, id, data }
96    }
97
98    pub fn store(&self) -> &Arc<Store> {
99        &self.store
100    }
101
102    pub fn id(&self) -> &CommitId {
103        &self.id
104    }
105
106    pub fn parent_ids(&self) -> &[CommitId] {
107        &self.data.parents
108    }
109
110    pub fn parents(&self) -> impl Iterator<Item = BackendResult<Self>> {
111        self.data.parents.iter().map(|id| self.store.get_commit(id))
112    }
113
114    pub async fn parents_async(&self) -> BackendResult<Vec<Self>> {
115        try_join_all(
116            self.data
117                .parents
118                .iter()
119                .map(|id| self.store.get_commit_async(id)),
120        )
121        .await
122    }
123
124    pub fn tree(&self) -> MergedTree {
125        MergedTree::new(
126            self.store.clone(),
127            self.data.root_tree.clone(),
128            ConflictLabels::from_merge(self.data.conflict_labels.clone()),
129        )
130    }
131
132    pub fn tree_ids(&self) -> &Merge<TreeId> {
133        &self.data.root_tree
134    }
135
136    pub fn parent_tree(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
137        self.parent_tree_async(repo).block_on()
138    }
139
140    /// Return the parent tree, merging the parent trees if there are multiple
141    /// parents.
142    pub async fn parent_tree_async(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
143        // Avoid merging parent trees if known to be empty. The index could be
144        // queried only when parents.len() > 1, but index query would be cheaper
145        // than extracting parent commit from the store.
146        if is_commit_empty_by_index(repo, &self.id)? == Some(true) {
147            return Ok(self.tree());
148        }
149        let parents: Vec<_> = self.parents_async().await?;
150        merge_commit_trees(repo, &parents).await
151    }
152
153    /// Returns whether commit's content is empty. Commit description is not
154    /// taken into consideration.
155    pub fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
156        if let Some(empty) = is_commit_empty_by_index(repo, &self.id)? {
157            return Ok(empty);
158        }
159        is_backend_commit_empty(repo, &self.store, &self.data)
160    }
161
162    pub fn has_conflict(&self) -> bool {
163        !self.tree_ids().is_resolved()
164    }
165
166    pub fn change_id(&self) -> &ChangeId {
167        &self.data.change_id
168    }
169
170    pub fn store_commit(&self) -> &Arc<backend::Commit> {
171        &self.data
172    }
173
174    pub fn description(&self) -> &str {
175        &self.data.description
176    }
177
178    pub fn author(&self) -> &Signature {
179        &self.data.author
180    }
181
182    pub fn committer(&self) -> &Signature {
183        &self.data.committer
184    }
185
186    ///  A commit is hidden if its commit id is not in the change id index.
187    pub fn is_hidden(&self, repo: &dyn Repo) -> IndexResult<bool> {
188        let maybe_targets = repo.resolve_change_id(self.change_id())?;
189        Ok(maybe_targets.is_none_or(|targets| !targets.has_visible(&self.id)))
190    }
191
192    /// A commit is discardable if it has no change from its parent, and an
193    /// empty description.
194    pub fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
195        Ok(self.description().is_empty() && self.is_empty(repo)?)
196    }
197
198    /// A quick way to just check if a signature is present.
199    pub fn is_signed(&self) -> bool {
200        self.data.secure_sig.is_some()
201    }
202
203    /// A slow (but cached) way to get the full verification.
204    pub fn verification(&self) -> SignResult<Option<Verification>> {
205        self.data
206            .secure_sig
207            .as_ref()
208            .map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig))
209            .transpose()
210    }
211
212    /// A string describing the commit to be used in conflict markers. If a
213    /// description is set, it will include the first line of the description.
214    pub fn conflict_label(&self) -> String {
215        if let Some(subject) = self.description().lines().next() {
216            // Example: nlqwxzwn 7dd24e73 "first line of description"
217            format!(
218                "{} \"{}\"",
219                self.conflict_label_short(),
220                // Control characters shouldn't be written in conflict markers, and '\0' isn't
221                // supported by the Git backend, so we just remove them. Unicode characters are
222                // supported, so we don't have to remove them.
223                subject.trim().replace(char::is_control, "")
224            )
225        } else {
226            self.conflict_label_short()
227        }
228    }
229
230    /// A short string describing the commit to be used in conflict markers.
231    /// Does not include the commit description.
232    fn conflict_label_short(&self) -> String {
233        // Example: nlqwxzwn 7dd24e73
234        format!("{:.8} {:.8}", self.change_id(), self.id())
235    }
236
237    /// A string describing the commit's parents to be used in conflict markers.
238    pub fn parents_conflict_label(&self) -> BackendResult<String> {
239        let parents: Vec<_> = self.parents().try_collect()?;
240        Ok(conflict_label_for_commits(&parents))
241    }
242}
243
244// If there is a single commit, returns the detailed conflict label for that
245// commit. If there are multiple commits, joins the short conflict labels of
246// each commit.
247pub fn conflict_label_for_commits(commits: &[Commit]) -> String {
248    if commits.len() == 1 {
249        commits[0].conflict_label()
250    } else {
251        commits.iter().map(Commit::conflict_label_short).join(", ")
252    }
253}
254
255pub(crate) fn is_backend_commit_empty(
256    repo: &dyn Repo,
257    store: &Arc<Store>,
258    commit: &backend::Commit,
259) -> BackendResult<bool> {
260    if let [parent_id] = &*commit.parents {
261        return Ok(commit.root_tree == *store.get_commit(parent_id)?.tree_ids());
262    }
263    let parents: Vec<_> = commit
264        .parents
265        .iter()
266        .map(|id| store.get_commit(id))
267        .try_collect()?;
268    let parent_tree = merge_commit_trees(repo, &parents).block_on()?;
269    Ok(commit.root_tree == *parent_tree.tree_ids())
270}
271
272fn is_commit_empty_by_index(repo: &dyn Repo, id: &CommitId) -> BackendResult<Option<bool>> {
273    let maybe_paths = repo
274        .index()
275        .changed_paths_in_commit(id)
276        // TODO: index error shouldn't be a "BackendError"
277        .map_err(|err| BackendError::Other(err.into()))?;
278    Ok(maybe_paths.map(|mut paths| paths.next().is_none()))
279}
280
281pub trait CommitIteratorExt<'c, I> {
282    fn ids(self) -> impl Iterator<Item = &'c CommitId>;
283}
284
285impl<'c, I> CommitIteratorExt<'c, I> for I
286where
287    I: Iterator<Item = &'c Commit>,
288{
289    fn ids(self) -> impl Iterator<Item = &'c CommitId> {
290        self.map(|commit| commit.id())
291    }
292}
293
294/// Wrapper to sort `Commit` by committer timestamp.
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296pub(crate) struct CommitByCommitterTimestamp(pub Commit);
297
298impl Ord for CommitByCommitterTimestamp {
299    fn cmp(&self, other: &Self) -> Ordering {
300        let self_timestamp = &self.0.committer().timestamp.timestamp;
301        let other_timestamp = &other.0.committer().timestamp.timestamp;
302        self_timestamp
303            .cmp(other_timestamp)
304            .then_with(|| self.0.cmp(&other.0)) // to comply with Eq
305    }
306}
307
308impl PartialOrd for CommitByCommitterTimestamp {
309    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
310        Some(self.cmp(other))
311    }
312}