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::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 }
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 pub async fn parent_tree_async(&self, repo: &dyn Repo) -> BackendResult<MergedTree> {
143 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 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 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 pub fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
195 Ok(self.description().is_empty() && self.is_empty(repo)?)
196 }
197
198 pub fn is_signed(&self) -> bool {
200 self.data.secure_sig.is_some()
201 }
202
203 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 pub fn conflict_label(&self) -> String {
215 if let Some(subject) = self.description().lines().next() {
216 format!(
218 "{} \"{}\"",
219 self.conflict_label_short(),
220 subject.trim().replace(char::is_control, "")
224 )
225 } else {
226 self.conflict_label_short()
227 }
228 }
229
230 fn conflict_label_short(&self) -> String {
233 format!("{:.8} {:.8}", self.change_id(), self.id())
235 }
236
237 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
244pub 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 .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#[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)) }
306}
307
308impl PartialOrd for CommitByCommitterTimestamp {
309 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
310 Some(self.cmp(other))
311 }
312}