// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![expect(missing_docs)] use std::cmp::Ordering; use std::fmt::Debug; use std::fmt::Error; use std::fmt::Formatter; use std::hash::Hash; use std::hash::Hasher; use std::sync::Arc; use futures::future::try_join_all; use itertools::Itertools as _; use crate::backend; use crate::backend::BackendError; use crate::backend::BackendResult; use crate::backend::ChangeId; use crate::backend::CommitId; use crate::backend::Signature; use crate::backend::TreeId; use crate::conflict_labels::ConflictLabels; use crate::index::IndexResult; use crate::merge::Merge; use crate::merged_tree::MergedTree; use crate::repo::Repo; use crate::rewrite::merge_commit_trees; use crate::signing::SignResult; use crate::signing::Verification; use crate::store::Store; #[derive(Clone, serde::Serialize)] pub struct Commit { #[serde(skip)] store: Arc, #[serde(rename = "commit_id")] id: CommitId, #[serde(flatten)] data: Arc, } impl Debug for Commit { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { f.debug_struct("Commit").field("id", &self.id).finish() // We intentionally don't print the `data` field. You can debug-print // `commit.store_commit()` to get those details. // // The reason is that `Commit` objects are debug-printed as part of many // other data structures and in tracing. } } impl PartialEq for Commit { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Commit {} impl Ord for Commit { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } impl PartialOrd for Commit { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Hash for Commit { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl Commit { pub fn new(store: Arc, id: CommitId, data: Arc) -> Self { Self { store, id, data } } pub fn store(&self) -> &Arc { &self.store } pub fn id(&self) -> &CommitId { &self.id } pub fn parent_ids(&self) -> &[CommitId] { &self.data.parents } pub async fn parents(&self) -> BackendResult> { try_join_all( self.data .parents .iter() .map(|id| self.store.get_commit_async(id)), ) .await } pub fn tree(&self) -> MergedTree { MergedTree::new( self.store.clone(), self.data.root_tree.clone(), ConflictLabels::from_merge(self.data.conflict_labels.clone()), ) } pub fn tree_ids(&self) -> &Merge { &self.data.root_tree } /// Return the parent tree, merging the parent trees if there are multiple /// parents. pub async fn parent_tree(&self, repo: &dyn Repo) -> BackendResult { // Avoid merging parent trees if known to be empty. The index could be // queried only when parents.len() > 1, but index query would be cheaper // than extracting parent commit from the store. if is_commit_empty_by_index(repo, &self.id)? == Some(true) { return Ok(self.tree()); } let parents = self.parents().await?; merge_commit_trees(repo, &parents).await } /// Returns whether commit's content is empty. Commit description is not /// taken into consideration. pub async fn is_empty(&self, repo: &dyn Repo) -> BackendResult { if let Some(empty) = is_commit_empty_by_index(repo, &self.id)? { return Ok(empty); } is_backend_commit_empty(repo, &self.store, &self.data).await } pub fn has_conflict(&self) -> bool { !self.tree_ids().is_resolved() } pub fn change_id(&self) -> &ChangeId { &self.data.change_id } pub fn store_commit(&self) -> &Arc { &self.data } pub fn description(&self) -> &str { &self.data.description } pub fn author(&self) -> &Signature { &self.data.author } pub fn committer(&self) -> &Signature { &self.data.committer } /// A commit is hidden if its commit id is not in the change id index. pub fn is_hidden(&self, repo: &dyn Repo) -> IndexResult { let maybe_targets = repo.resolve_change_id(self.change_id())?; Ok(maybe_targets.is_none_or(|targets| !targets.has_visible(&self.id))) } /// A commit is discardable if it has no change from its parent, and an /// empty description. pub async fn is_discardable(&self, repo: &dyn Repo) -> BackendResult { Ok(self.description().is_empty() && self.is_empty(repo).await?) } /// A quick way to just check if a signature is present. pub fn is_signed(&self) -> bool { self.data.secure_sig.is_some() } /// A slow (but cached) way to get the full verification. pub fn verification(&self) -> SignResult> { self.data .secure_sig .as_ref() .map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig)) .transpose() } /// A string describing the commit to be used in conflict markers. If a /// description is set, it will include the first line of the description. pub fn conflict_label(&self) -> String { if let Some(subject) = self.description().lines().next() { // Example: nlqwxzwn 7dd24e73 "first line of description" format!( "{} \"{}\"", self.conflict_label_short(), // Control characters shouldn't be written in conflict markers, and '\0' isn't // supported by the Git backend, so we just remove them. Unicode characters are // supported, so we don't have to remove them. subject.trim().replace(char::is_control, "") ) } else { self.conflict_label_short() } } /// A short string describing the commit to be used in conflict markers. /// Does not include the commit description. fn conflict_label_short(&self) -> String { // Example: nlqwxzwn 7dd24e73 format!("{:.8} {:.8}", self.change_id(), self.id()) } /// A string describing the commit's parents to be used in conflict markers. pub async fn parents_conflict_label(&self) -> BackendResult { let parents = self.parents().await?; Ok(conflict_label_for_commits(&parents)) } } // If there is a single commit, returns the detailed conflict label for that // commit. If there are multiple commits, joins the short conflict labels of // each commit. pub fn conflict_label_for_commits(commits: &[Commit]) -> String { if commits.len() == 1 { commits[0].conflict_label() } else { commits.iter().map(Commit::conflict_label_short).join(", ") } } pub(crate) async fn is_backend_commit_empty( repo: &dyn Repo, store: &Arc, commit: &backend::Commit, ) -> BackendResult { if let [parent_id] = &*commit.parents { return Ok(commit.root_tree == *store.get_commit_async(parent_id).await?.tree_ids()); } let parents = try_join_all(commit.parents.iter().map(|id| store.get_commit_async(id))).await?; let parent_tree = merge_commit_trees(repo, &parents).await?; Ok(commit.root_tree == *parent_tree.tree_ids()) } fn is_commit_empty_by_index(repo: &dyn Repo, id: &CommitId) -> BackendResult> { let maybe_paths = repo .index() .changed_paths_in_commit(id) // TODO: index error shouldn't be a "BackendError" .map_err(|err| BackendError::Other(err.into()))?; Ok(maybe_paths.map(|mut paths| paths.next().is_none())) } pub trait CommitIteratorExt<'c, I> { fn ids(self) -> impl Iterator; } impl<'c, I> CommitIteratorExt<'c, I> for I where I: Iterator, { fn ids(self) -> impl Iterator { self.map(|commit| commit.id()) } } /// Wrapper to sort `Commit` by committer timestamp. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub(crate) struct CommitByCommitterTimestamp(pub Commit); impl Ord for CommitByCommitterTimestamp { fn cmp(&self, other: &Self) -> Ordering { let self_timestamp = &self.0.committer().timestamp.timestamp; let other_timestamp = &other.0.committer().timestamp.timestamp; self_timestamp .cmp(other_timestamp) .then_with(|| self.0.cmp(&other.0)) // to comply with Eq } } impl PartialOrd for CommitByCommitterTimestamp { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } }