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