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