1#![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::MergedTreeId;
35use crate::backend::Signature;
36use crate::index::IndexResult;
37use crate::merged_tree::MergedTree;
38use crate::repo::Repo;
39use crate::rewrite::merge_commit_trees;
40use crate::signing::SignResult;
41use crate::signing::Verification;
42use crate::store::Store;
43
44#[derive(Clone, serde::Serialize)]
45pub struct Commit {
46 #[serde(skip)]
47 store: Arc<Store>,
48 #[serde(rename = "commit_id")]
49 id: CommitId,
50 #[serde(flatten)]
51 data: Arc<backend::Commit>,
52}
53
54impl Debug for Commit {
55 fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
56 f.debug_struct("Commit").field("id", &self.id).finish()
57 }
63}
64
65impl PartialEq for Commit {
66 fn eq(&self, other: &Self) -> bool {
67 self.id == other.id
68 }
69}
70
71impl Eq for Commit {}
72
73impl Ord for Commit {
74 fn cmp(&self, other: &Self) -> Ordering {
75 self.id.cmp(&other.id)
76 }
77}
78
79impl PartialOrd for Commit {
80 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
81 Some(self.cmp(other))
82 }
83}
84
85impl Hash for Commit {
86 fn hash<H: Hasher>(&self, state: &mut H) {
87 self.id.hash(state);
88 }
89}
90
91impl Commit {
92 pub fn new(store: Arc<Store>, id: CommitId, data: Arc<backend::Commit>) -> Self {
93 Self { store, id, data }
94 }
95
96 pub fn store(&self) -> &Arc<Store> {
97 &self.store
98 }
99
100 pub fn id(&self) -> &CommitId {
101 &self.id
102 }
103
104 pub fn parent_ids(&self) -> &[CommitId] {
105 &self.data.parents
106 }
107
108 pub fn parents(&self) -> impl Iterator<Item = BackendResult<Self>> {
109 self.data.parents.iter().map(|id| self.store.get_commit(id))
110 }
111
112 pub async fn parents_async(&self) -> BackendResult<Vec<Self>> {
113 try_join_all(
114 self.data
115 .parents
116 .iter()
117 .map(|id| self.store.get_commit_async(id)),
118 )
119 .await
120 }
121
122 pub fn tree(&self) -> BackendResult<MergedTree> {
123 self.tree_async().block_on()
124 }
125
126 pub async fn tree_async(&self) -> BackendResult<MergedTree> {
127 self.store.get_root_tree_async(&self.data.root_tree).await
128 }
129
130 pub fn tree_id(&self) -> &MergedTreeId {
131 &self.data.root_tree
132 }
133
134 pub fn parent_tree(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
135 self.parent_tree_async(repo).block_on()
136 }
137
138 pub async fn parent_tree_async(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
141 if is_commit_empty_by_index(repo, &self.id)? == Some(true) {
145 return self.tree_async().await;
146 }
147 let parents: Vec<_> = self.parents_async().await?;
148 merge_commit_trees(repo, &parents).await
149 }
150
151 pub fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
154 if let Some(empty) = is_commit_empty_by_index(repo, &self.id)? {
155 return Ok(empty);
156 }
157 is_backend_commit_empty(repo, &self.store, &self.data)
158 }
159
160 pub fn has_conflict(&self) -> bool {
161 !self.tree_id().as_merge().is_resolved()
162 }
163
164 pub fn change_id(&self) -> &ChangeId {
165 &self.data.change_id
166 }
167
168 pub fn store_commit(&self) -> &Arc<backend::Commit> {
169 &self.data
170 }
171
172 pub fn description(&self) -> &str {
173 &self.data.description
174 }
175
176 pub fn author(&self) -> &Signature {
177 &self.data.author
178 }
179
180 pub fn committer(&self) -> &Signature {
181 &self.data.committer
182 }
183
184 pub fn is_hidden(&self, repo: &dyn Repo) -> IndexResult<bool> {
186 let maybe_entries = repo.resolve_change_id(self.change_id())?;
187 Ok(maybe_entries.is_none_or(|entries| !entries.contains(&self.id)))
188 }
189
190 pub fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
193 Ok(self.description().is_empty() && self.is_empty(repo)?)
194 }
195
196 pub fn is_signed(&self) -> bool {
198 self.data.secure_sig.is_some()
199 }
200
201 pub fn verification(&self) -> SignResult<Option<Verification>> {
203 self.data
204 .secure_sig
205 .as_ref()
206 .map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig))
207 .transpose()
208 }
209}
210
211pub(crate) fn is_backend_commit_empty(
212 repo: &dyn Repo,
213 store: &Arc<Store>,
214 commit: &backend::Commit,
215) -> BackendResult<bool> {
216 if let [parent_id] = &*commit.parents {
217 return Ok(commit.root_tree == *store.get_commit(parent_id)?.tree_id());
218 }
219 let parents: Vec<_> = commit
220 .parents
221 .iter()
222 .map(|id| store.get_commit(id))
223 .try_collect()?;
224 let parent_tree = merge_commit_trees(repo, &parents).block_on()?;
225 Ok(commit.root_tree == parent_tree.id())
226}
227
228fn is_commit_empty_by_index(repo: &dyn Repo, id: &CommitId) -> BackendResult<Option<bool>> {
229 let maybe_paths = repo
230 .index()
231 .changed_paths_in_commit(id)
232 .map_err(|err| BackendError::Other(err.into()))?;
234 Ok(maybe_paths.map(|mut paths| paths.next().is_none()))
235}
236
237pub trait CommitIteratorExt<'c, I> {
238 fn ids(self) -> impl Iterator<Item = &'c CommitId>;
239}
240
241impl<'c, I> CommitIteratorExt<'c, I> for I
242where
243 I: Iterator<Item = &'c Commit>,
244{
245 fn ids(self) -> impl Iterator<Item = &'c CommitId> {
246 self.map(|commit| commit.id())
247 }
248}
249
250#[derive(Clone, Debug, Eq, Hash, PartialEq)]
252pub(crate) struct CommitByCommitterTimestamp(pub Commit);
253
254impl Ord for CommitByCommitterTimestamp {
255 fn cmp(&self, other: &Self) -> Ordering {
256 let self_timestamp = &self.0.committer().timestamp.timestamp;
257 let other_timestamp = &other.0.committer().timestamp.timestamp;
258 self_timestamp
259 .cmp(other_timestamp)
260 .then_with(|| self.0.cmp(&other.0)) }
262}
263
264impl PartialOrd for CommitByCommitterTimestamp {
265 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
266 Some(self.cmp(other))
267 }
268}