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::index::IndexResult;
37use crate::merge::Merge;
38use crate::merged_tree::MergedTree;
39use crate::repo::Repo;
40use crate::rewrite::merge_commit_trees;
41use crate::signing::SignResult;
42use crate::signing::Verification;
43use crate::store::Store;
44
45#[derive(Clone, serde::Serialize)]
46pub struct Commit {
47    #[serde(skip)]
48    store: Arc<Store>,
49    #[serde(rename = "commit_id")]
50    id: CommitId,
51    #[serde(flatten)]
52    data: Arc<backend::Commit>,
53}
54
55impl Debug for Commit {
56    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
57        f.debug_struct("Commit").field("id", &self.id).finish()
58        // We intentionally don't print the `data` field. You can debug-print
59        // `commit.store_commit()` to get those details.
60        //
61        // The reason is that `Commit` objects are debug-printed as part of many
62        // other data structures and in tracing.
63    }
64}
65
66impl PartialEq for Commit {
67    fn eq(&self, other: &Self) -> bool {
68        self.id == other.id
69    }
70}
71
72impl Eq for Commit {}
73
74impl Ord for Commit {
75    fn cmp(&self, other: &Self) -> Ordering {
76        self.id.cmp(&other.id)
77    }
78}
79
80impl PartialOrd for Commit {
81    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
82        Some(self.cmp(other))
83    }
84}
85
86impl Hash for Commit {
87    fn hash<H: Hasher>(&self, state: &mut H) {
88        self.id.hash(state);
89    }
90}
91
92impl Commit {
93    pub fn new(store: Arc<Store>, id: CommitId, data: Arc<backend::Commit>) -> Self {
94        Self { store, id, data }
95    }
96
97    pub fn store(&self) -> &Arc<Store> {
98        &self.store
99    }
100
101    pub fn id(&self) -> &CommitId {
102        &self.id
103    }
104
105    pub fn parent_ids(&self) -> &[CommitId] {
106        &self.data.parents
107    }
108
109    pub fn parents(&self) -> impl Iterator<Item = BackendResult<Self>> {
110        self.data.parents.iter().map(|id| self.store.get_commit(id))
111    }
112
113    pub async fn parents_async(&self) -> BackendResult<Vec<Self>> {
114        try_join_all(
115            self.data
116                .parents
117                .iter()
118                .map(|id| self.store.get_commit_async(id)),
119        )
120        .await
121    }
122
123    pub fn tree(&self) -> MergedTree {
124        MergedTree::new(self.store.clone(), self.data.root_tree.clone())
125    }
126
127    pub fn tree_ids(&self) -> &Merge<TreeId> {
128        &self.data.root_tree
129    }
130
131    pub fn parent_tree(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
132        self.parent_tree_async(repo).block_on()
133    }
134
135    /// Return the parent tree, merging the parent trees if there are multiple
136    /// parents.
137    pub async fn parent_tree_async(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
138        // Avoid merging parent trees if known to be empty. The index could be
139        // queried only when parents.len() > 1, but index query would be cheaper
140        // than extracting parent commit from the store.
141        if is_commit_empty_by_index(repo, &self.id)? == Some(true) {
142            return Ok(self.tree());
143        }
144        let parents: Vec<_> = self.parents_async().await?;
145        merge_commit_trees(repo, &parents).await
146    }
147
148    /// Returns whether commit's content is empty. Commit description is not
149    /// taken into consideration.
150    pub fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
151        if let Some(empty) = is_commit_empty_by_index(repo, &self.id)? {
152            return Ok(empty);
153        }
154        is_backend_commit_empty(repo, &self.store, &self.data)
155    }
156
157    pub fn has_conflict(&self) -> bool {
158        !self.tree_ids().is_resolved()
159    }
160
161    pub fn change_id(&self) -> &ChangeId {
162        &self.data.change_id
163    }
164
165    pub fn store_commit(&self) -> &Arc<backend::Commit> {
166        &self.data
167    }
168
169    pub fn description(&self) -> &str {
170        &self.data.description
171    }
172
173    pub fn author(&self) -> &Signature {
174        &self.data.author
175    }
176
177    pub fn committer(&self) -> &Signature {
178        &self.data.committer
179    }
180
181    ///  A commit is hidden if its commit id is not in the change id index.
182    pub fn is_hidden(&self, repo: &dyn Repo) -> IndexResult<bool> {
183        let maybe_entries = repo.resolve_change_id(self.change_id())?;
184        Ok(maybe_entries.is_none_or(|entries| !entries.contains(&self.id)))
185    }
186
187    /// A commit is discardable if it has no change from its parent, and an
188    /// empty description.
189    pub fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
190        Ok(self.description().is_empty() && self.is_empty(repo)?)
191    }
192
193    /// A quick way to just check if a signature is present.
194    pub fn is_signed(&self) -> bool {
195        self.data.secure_sig.is_some()
196    }
197
198    /// A slow (but cached) way to get the full verification.
199    pub fn verification(&self) -> SignResult<Option<Verification>> {
200        self.data
201            .secure_sig
202            .as_ref()
203            .map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig))
204            .transpose()
205    }
206}
207
208pub(crate) fn is_backend_commit_empty(
209    repo: &dyn Repo,
210    store: &Arc<Store>,
211    commit: &backend::Commit,
212) -> BackendResult<bool> {
213    if let [parent_id] = &*commit.parents {
214        return Ok(commit.root_tree == *store.get_commit(parent_id)?.tree_ids());
215    }
216    let parents: Vec<_> = commit
217        .parents
218        .iter()
219        .map(|id| store.get_commit(id))
220        .try_collect()?;
221    let parent_tree = merge_commit_trees(repo, &parents).block_on()?;
222    Ok(commit.root_tree == *parent_tree.tree_ids())
223}
224
225fn is_commit_empty_by_index(repo: &dyn Repo, id: &CommitId) -> BackendResult<Option<bool>> {
226    let maybe_paths = repo
227        .index()
228        .changed_paths_in_commit(id)
229        // TODO: index error shouldn't be a "BackendError"
230        .map_err(|err| BackendError::Other(err.into()))?;
231    Ok(maybe_paths.map(|mut paths| paths.next().is_none()))
232}
233
234pub trait CommitIteratorExt<'c, I> {
235    fn ids(self) -> impl Iterator<Item = &'c CommitId>;
236}
237
238impl<'c, I> CommitIteratorExt<'c, I> for I
239where
240    I: Iterator<Item = &'c Commit>,
241{
242    fn ids(self) -> impl Iterator<Item = &'c CommitId> {
243        self.map(|commit| commit.id())
244    }
245}
246
247/// Wrapper to sort `Commit` by committer timestamp.
248#[derive(Clone, Debug, Eq, Hash, PartialEq)]
249pub(crate) struct CommitByCommitterTimestamp(pub Commit);
250
251impl Ord for CommitByCommitterTimestamp {
252    fn cmp(&self, other: &Self) -> Ordering {
253        let self_timestamp = &self.0.committer().timestamp.timestamp;
254        let other_timestamp = &other.0.committer().timestamp.timestamp;
255        self_timestamp
256            .cmp(other_timestamp)
257            .then_with(|| self.0.cmp(&other.0)) // to comply with Eq
258    }
259}
260
261impl PartialOrd for CommitByCommitterTimestamp {
262    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
263        Some(self.cmp(other))
264    }
265}