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 _;
27
28use crate::backend;
29use crate::backend::BackendError;
30use crate::backend::BackendResult;
31use crate::backend::ChangeId;
32use crate::backend::CommitId;
33use crate::backend::Signature;
34use crate::backend::TreeId;
35use crate::conflict_labels::ConflictLabels;
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::rewrite::merge_commit_trees_no_resolve;
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 }
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 async fn parents(&self) -> BackendResult<Vec<Self>> {
111 try_join_all(
112 self.data
113 .parents
114 .iter()
115 .map(|id| self.store.get_commit_async(id)),
116 )
117 .await
118 }
119
120 pub fn tree(&self) -> MergedTree {
121 MergedTree::new(
122 self.store.clone(),
123 self.data.root_tree.clone(),
124 ConflictLabels::from_merge(self.data.conflict_labels.clone()),
125 )
126 }
127
128 pub fn tree_ids(&self) -> &Merge<TreeId> {
129 &self.data.root_tree
130 }
131
132 pub async fn parent_tree(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
135 if is_commit_empty_by_index(repo, &self.id)? == Some(true) {
139 return Ok(self.tree());
140 }
141 let parents = self.parents().await?;
142 merge_commit_trees(repo, &parents).await
143 }
144
145 pub async fn parent_tree_no_resolve(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
148 if is_commit_empty_by_index(repo, &self.id)? == Some(true) {
152 return Ok(self.tree());
153 }
154 let parents = self.parents().await?;
155 merge_commit_trees_no_resolve(repo, &parents).await
156 }
157
158 pub async fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
161 if let Some(empty) = is_commit_empty_by_index(repo, &self.id)? {
162 return Ok(empty);
163 }
164 is_backend_commit_empty(repo, &self.store, &self.data).await
165 }
166
167 pub fn has_conflict(&self) -> bool {
168 !self.tree_ids().is_resolved()
169 }
170
171 pub fn change_id(&self) -> &ChangeId {
172 &self.data.change_id
173 }
174
175 pub fn store_commit(&self) -> &Arc<backend::Commit> {
176 &self.data
177 }
178
179 pub fn description(&self) -> &str {
180 &self.data.description
181 }
182
183 pub fn author(&self) -> &Signature {
184 &self.data.author
185 }
186
187 pub fn committer(&self) -> &Signature {
188 &self.data.committer
189 }
190
191 pub fn is_hidden(&self, repo: &dyn Repo) -> IndexResult<bool> {
193 let maybe_targets = repo.resolve_change_id(self.change_id())?;
194 Ok(maybe_targets.is_none_or(|targets| !targets.has_visible(&self.id)))
195 }
196
197 pub async fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
200 Ok(self.description().is_empty() && self.is_empty(repo).await?)
201 }
202
203 pub fn is_signed(&self) -> bool {
205 self.data.secure_sig.is_some()
206 }
207
208 pub fn verification(&self) -> SignResult<Option<Verification>> {
210 self.data
211 .secure_sig
212 .as_ref()
213 .map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig))
214 .transpose()
215 }
216
217 pub fn conflict_label(&self) -> String {
220 if let Some(subject) = self.description().lines().next() {
221 format!(
223 "{} \"{}\"",
224 self.conflict_label_short(),
225 subject.trim().replace(char::is_control, "")
229 )
230 } else {
231 self.conflict_label_short()
232 }
233 }
234
235 fn conflict_label_short(&self) -> String {
238 format!("{:.8} {:.8}", self.change_id(), self.id())
240 }
241
242 pub async fn parents_conflict_label(&self) -> BackendResult<String> {
244 let parents = self.parents().await?;
245 Ok(conflict_label_for_commits(&parents))
246 }
247}
248
249pub fn conflict_label_for_commits(commits: &[Commit]) -> String {
253 if commits.len() == 1 {
254 commits[0].conflict_label()
255 } else {
256 commits.iter().map(Commit::conflict_label_short).join(", ")
257 }
258}
259
260pub(crate) async fn is_backend_commit_empty(
261 repo: &dyn Repo,
262 store: &Arc<Store>,
263 commit: &backend::Commit,
264) -> BackendResult<bool> {
265 if let [parent_id] = &*commit.parents {
266 return Ok(commit.root_tree == *store.get_commit_async(parent_id).await?.tree_ids());
267 }
268 let parents = try_join_all(commit.parents.iter().map(|id| store.get_commit_async(id))).await?;
269 let parent_tree = merge_commit_trees(repo, &parents).await?;
270 Ok(commit.root_tree == *parent_tree.tree_ids())
271}
272
273fn is_commit_empty_by_index(repo: &dyn Repo, id: &CommitId) -> BackendResult<Option<bool>> {
274 let maybe_paths = repo
275 .index()
276 .changed_paths_in_commit(id)
277 .map_err(|err| BackendError::Other(err.into()))?;
279 Ok(maybe_paths.map(|mut paths| paths.next().is_none()))
280}
281
282pub trait CommitIteratorExt<'c, I> {
283 fn ids(self) -> impl Iterator<Item = &'c CommitId>;
284}
285
286impl<'c, I> CommitIteratorExt<'c, I> for I
287where
288 I: Iterator<Item = &'c Commit>,
289{
290 fn ids(self) -> impl Iterator<Item = &'c CommitId> {
291 self.map(|commit| commit.id())
292 }
293}
294
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
297pub(crate) struct CommitByCommitterTimestamp(pub Commit);
298
299impl Ord for CommitByCommitterTimestamp {
300 fn cmp(&self, other: &Self) -> Ordering {
301 let self_timestamp = &self.0.committer().timestamp.timestamp;
302 let other_timestamp = &other.0.committer().timestamp.timestamp;
303 self_timestamp
304 .cmp(other_timestamp)
305 .then_with(|| self.0.cmp(&other.0)) }
307}
308
309impl PartialOrd for CommitByCommitterTimestamp {
310 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
311 Some(self.cmp(other))
312 }
313}