616 lines · 23.9 KB · Rust
RawDownload
// Copyright 2023 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::collections::HashMap;use std::collections::HashSet;use std::fs;use std::io;use std::io::Write as _;use std::path::Path;use std::path::PathBuf;use std::slice;use std::sync::Arc; use itertools::Itertools as _;use pollster::FutureExt as _;use prost::Message as _;use tempfile::NamedTempFile;use thiserror::Error; use super::changed_path::ChangedPathIndexSegmentId;use super::changed_path::CompositeChangedPathIndex;use super::changed_path::collect_changed_paths;use super::composite::AsCompositeIndex as _;use super::composite::CommitIndexSegmentId;use super::entry::GlobalCommitPosition;use super::mutable::DefaultMutableIndex;use super::readonly::DefaultReadonlyIndex;use super::readonly::FieldLengths;use super::readonly::ReadonlyCommitIndexSegment;use super::readonly::ReadonlyIndexLoadError;use crate::backend::BackendError;use crate::backend::BackendInitError;use crate::backend::CommitId;use crate::commit::CommitByCommitterTimestamp;use crate::dag_walk;use crate::file_util;use crate::file_util::IoResultExt as _;use crate::file_util::PathError;use crate::file_util::persist_temp_file;use crate::index::IndexStore;use crate::index::IndexStoreError;use crate::index::IndexStoreResult;use crate::index::MutableIndex;use crate::index::ReadonlyIndex;use crate::object_id::ObjectId as _;use crate::op_store::OpStoreError;use crate::op_store::OperationId;use crate::op_walk;use crate::operation::Operation;use crate::store::Store; // BLAKE2b-512 hash length in hex stringconst SEGMENT_FILE_NAME_LENGTH: usize = 64 * 2; /// Error that may occur during `DefaultIndexStore` initialization.#[derive(Debug, Error)]#[error("Failed to initialize index store")]pub struct DefaultIndexStoreInitError(#[from] pub PathError); impl From<DefaultIndexStoreInitError> for BackendInitError {    fn from(err: DefaultIndexStoreInitError) -> Self {        Self(err.into())    }} #[derive(Debug, Error)]pub enum DefaultIndexStoreError {    #[error("Failed to associate index files with an operation {op_id}")]    AssociateIndex {        op_id: OperationId,        source: PathError,    },    #[error("Failed to load associated index file names")]    LoadAssociation(#[source] PathError),    #[error(transparent)]    LoadIndex(ReadonlyIndexLoadError),    #[error("Failed to write index file")]    SaveIndex(#[source] PathError),    #[error("Failed to index commits at operation {op_id}")]    IndexCommits {        op_id: OperationId,        source: BackendError,    },    #[error(transparent)]    OpStore(#[from] OpStoreError),} #[derive(Debug)]pub struct DefaultIndexStore {    dir: PathBuf,} impl DefaultIndexStore {    pub fn name() -> &'static str {        "default"    }     pub fn init(dir: &Path) -> Result<Self, DefaultIndexStoreInitError> {        let store = Self {            dir: dir.to_owned(),        };        store.ensure_base_dirs()?;        Ok(store)    }     pub fn load(dir: &Path) -> Self {        Self {            dir: dir.to_owned(),        }    }     pub fn reinit(&self) -> Result<(), DefaultIndexStoreInitError> {        // Create base directories in case the store was initialized by old jj.        self.ensure_base_dirs()?;        // Remove all operation links to trigger rebuilding.        file_util::remove_dir_contents(&self.op_links_dir())?;        file_util::remove_dir_contents(&self.legacy_operations_dir())?;        // Remove index segments to save disk space. If raced, new segment file        // will be created by the other process.        file_util::remove_dir_contents(&self.commit_segments_dir())?;        file_util::remove_dir_contents(&self.changed_path_segments_dir())?;        // jj <= 0.14 created segment files in the top directory        for entry in self.dir.read_dir().context(&self.dir)? {            let entry = entry.context(&self.dir)?;            let path = entry.path();            if path.file_name().unwrap().len() != SEGMENT_FILE_NAME_LENGTH {                // Skip "type" file, "operations" directory, etc.                continue;            }            fs::remove_file(&path).context(&path)?;        }        Ok(())    }     fn ensure_base_dirs(&self) -> Result<(), PathError> {        for dir in [            self.op_links_dir(),            self.legacy_operations_dir(),            self.commit_segments_dir(),            self.changed_path_segments_dir(),        ] {            file_util::create_or_reuse_dir(&dir).context(&dir)?;        }        Ok(())    }     /// Directory for mapping from operations to segments. (jj >= 0.33)    fn op_links_dir(&self) -> PathBuf {        self.dir.join("op_links")    }     /// Directory for mapping from operations to commit segments. (jj < 0.33)    fn legacy_operations_dir(&self) -> PathBuf {        self.dir.join("operations")    }     /// Directory for commit segment files.    fn commit_segments_dir(&self) -> PathBuf {        self.dir.join("segments")    }     /// Directory for changed-path segment files.    fn changed_path_segments_dir(&self) -> PathBuf {        self.dir.join("changed_paths")    }     fn load_index_at_operation(        &self,        op_id: &OperationId,        lengths: FieldLengths,    ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {        let commit_segment_id;        let changed_path_start_commit_pos;        let changed_path_segment_ids;        let op_link_file = self.op_links_dir().join(op_id.hex());        match fs::read(&op_link_file).context(&op_link_file) {            Ok(data) => {                let proto = crate::protos::default_index::SegmentControl::decode(&*data)                    .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))                    .context(&op_link_file)                    .map_err(DefaultIndexStoreError::LoadAssociation)?;                commit_segment_id = CommitIndexSegmentId::new(proto.commit_segment_id);                changed_path_start_commit_pos = proto                    .changed_path_start_commit_pos                    .map(GlobalCommitPosition);                changed_path_segment_ids = proto                    .changed_path_segment_ids                    .into_iter()                    .map(ChangedPathIndexSegmentId::new)                    .collect_vec();            }            // TODO: drop support for legacy operation link file in jj 0.39 or so            Err(PathError { source: error, .. }) if error.kind() == io::ErrorKind::NotFound => {                let op_id_file = self.legacy_operations_dir().join(op_id.hex());                let data = fs::read(&op_id_file)                    .context(&op_id_file)                    .map_err(DefaultIndexStoreError::LoadAssociation)?;                commit_segment_id = CommitIndexSegmentId::try_from_hex(&data)                    .ok_or_else(|| {                        io::Error::new(io::ErrorKind::InvalidData, "file name is not valid hex")                    })                    .context(&op_id_file)                    .map_err(DefaultIndexStoreError::LoadAssociation)?;                changed_path_start_commit_pos = None;                changed_path_segment_ids = vec![];            }            Err(err) => return Err(DefaultIndexStoreError::LoadAssociation(err)),        };         let commits = ReadonlyCommitIndexSegment::load(            &self.commit_segments_dir(),            commit_segment_id,            lengths,        )        .map_err(DefaultIndexStoreError::LoadIndex)?;        // TODO: lazy load or mmap?        let changed_paths = if let Some(start_commit_pos) = changed_path_start_commit_pos {            CompositeChangedPathIndex::load(                &self.changed_path_segments_dir(),                start_commit_pos,                &changed_path_segment_ids,            )            .map_err(DefaultIndexStoreError::LoadIndex)?        } else {            CompositeChangedPathIndex::null()        };        Ok(DefaultReadonlyIndex::from_segment(commits, changed_paths))    }     /// Rebuilds index for the given `operation`.    ///    /// The index to be built will be calculated from one of the ancestor    /// operations if exists. Use `reinit()` to rebuild index from scratch.    #[tracing::instrument(skip(self, store))]    pub async fn build_index_at_operation(        &self,        operation: &Operation,        store: &Arc<Store>,    ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {        tracing::info!("scanning operations to index");        let op_links_dir = self.op_links_dir();        let legacy_operations_dir = self.legacy_operations_dir();        let field_lengths = FieldLengths {            commit_id: store.commit_id_length(),            change_id: store.change_id_length(),        };        // Pick the latest existing ancestor operation as the parent segment.        let mut unindexed_ops = Vec::new();        let mut parent_op = None;        for op in op_walk::walk_ancestors(slice::from_ref(operation)) {            let op = op?;            if op_links_dir.join(op.id().hex()).is_file()                || legacy_operations_dir.join(op.id().hex()).is_file()            {                parent_op = Some(op);                break;            } else {                unindexed_ops.push(op);            }        }        let ops_to_visit = if let Some(op) = &parent_op {            // There may be concurrent ops, so revisit from the head. The parent            // op is usually shallow if existed.            op_walk::walk_ancestors_range(slice::from_ref(operation), slice::from_ref(op))                .try_collect()?        } else {            unindexed_ops        };        tracing::info!(            ops_count = ops_to_visit.len(),            "collecting head commits to index"        );        let mut historical_heads: HashMap<CommitId, OperationId> = HashMap::new();        for op in &ops_to_visit {            for commit_id in itertools::chain(                op.all_referenced_commit_ids(),                op.view()?.all_referenced_commit_ids(),            ) {                if !historical_heads.contains_key(commit_id) {                    historical_heads.insert(commit_id.clone(), op.id().clone());                }            }        }        let mut mutable_index;        let maybe_parent_index;        match &parent_op {            None => {                mutable_index = DefaultMutableIndex::full(field_lengths);                maybe_parent_index = None;            }            Some(op) => {                let parent_index = self.load_index_at_operation(op.id(), field_lengths)?;                mutable_index = parent_index.start_modification();                maybe_parent_index = Some(parent_index);            }        }         tracing::info!(            ?maybe_parent_index,            heads_count = historical_heads.len(),            "indexing commits reachable from historical heads"        );        // Build a list of ancestors of heads where parents come after the        // commit itself.        let parent_index_has_id = |id: &CommitId| {            maybe_parent_index                .as_ref()                .is_some_and(|index| index.has_id_impl(id))        };        let get_commit_with_op = |commit_id: &CommitId, op_id: &OperationId| {            let op_id = op_id.clone();            match store.get_commit(commit_id) {                // Propagate head's op_id to report possible source of an error.                // The op_id doesn't have to be included in the sort key, but                // that wouldn't matter since the commit should be unique.                Ok(commit) => Ok((CommitByCommitterTimestamp(commit), op_id)),                Err(source) => Err(DefaultIndexStoreError::IndexCommits { op_id, source }),            }        };        // Retain immediate predecessors if legacy operation exists. Some        // commands (e.g. squash into grandparent) may leave transitive        // predecessors, which aren't visible to any views.        // TODO: delete this workaround with commit.predecessors.        let commits_to_keep_immediate_predecessors = if ops_to_visit            .iter()            .any(|op| !op.stores_commit_predecessors())        {            let mut ancestors = HashSet::new();            let mut work = historical_heads.keys().cloned().collect_vec();            while let Some(commit_id) = work.pop() {                if ancestors.contains(&commit_id) || parent_index_has_id(&commit_id) {                    continue;                }                if let Ok(commit) = store.get_commit(&commit_id) {                    work.extend(commit.parent_ids().iter().cloned());                }                ancestors.insert(commit_id);            }            ancestors        } else {            HashSet::new()        };        let commits = dag_walk::topo_order_reverse_ord_ok(            historical_heads                .iter()                .filter(|&(commit_id, _)| !parent_index_has_id(commit_id))                .map(|(commit_id, op_id)| get_commit_with_op(commit_id, op_id)),            |(CommitByCommitterTimestamp(commit), _)| commit.id().clone(),            |(CommitByCommitterTimestamp(commit), op_id)| {                let keep_predecessors =                    commits_to_keep_immediate_predecessors.contains(commit.id());                itertools::chain(                    commit.parent_ids(),                    keep_predecessors                        .then_some(&commit.store_commit().predecessors)                        .into_iter()                        .flatten(),                )                .filter(|&id| !parent_index_has_id(id))                .map(|commit_id| get_commit_with_op(commit_id, op_id))                .collect_vec()            },            |_| panic!("graph has cycle"),        )?;        for (CommitByCommitterTimestamp(commit), op_id) in commits.iter().rev() {            mutable_index.add_commit(commit).await.map_err(|source| {                DefaultIndexStoreError::IndexCommits {                    op_id: op_id.clone(),                    source,                }            })?;        }         let index = self.save_mutable_index(mutable_index, operation.id())?;        tracing::info!(?index, commits_count = commits.len(), "saved new index");         Ok(index)    }     /// Builds changed-path index for the specified operation.    ///    /// At most `max_commits` number of commits will be scanned from the latest    /// unindexed commit.    #[tracing::instrument(skip(self, store))]    pub async fn build_changed_path_index_at_operation(        &self,        op_id: &OperationId,        store: &Arc<Store>,        max_commits: u32,        // TODO: add progress callback?    ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {        // Create directories in case the store was initialized by jj < 0.33.        self.ensure_base_dirs()            .map_err(DefaultIndexStoreError::SaveIndex)?;        let field_lengths = FieldLengths {            commit_id: store.commit_id_length(),            change_id: store.change_id_length(),        };        let index = self.load_index_at_operation(op_id, field_lengths)?;        let old_changed_paths = index.changed_paths();         // Distribute max_commits to contiguous pre/post ranges:        //   ..|pre|old_changed_paths|post|        //   (where pre.len() + post.len() <= max_commits)        let pre_start;        let pre_end;        let post_start;        let post_end;        if let Some(GlobalCommitPosition(pos)) = old_changed_paths.start_commit_pos() {            post_start = pos + old_changed_paths.num_commits();            assert!(post_start <= index.num_commits());            post_end = u32::saturating_add(post_start, max_commits).min(index.num_commits());            pre_start = u32::saturating_sub(pos, max_commits - (post_end - post_start));            pre_end = pos;        } else {            pre_start = u32::saturating_sub(index.num_commits(), max_commits);            pre_end = index.num_commits();            post_start = pre_end;            post_end = pre_end;        }         let to_index_err = |source| DefaultIndexStoreError::IndexCommits {            op_id: op_id.clone(),            source,        };        let index_commit = async |changed_paths: &mut CompositeChangedPathIndex,                                  pos: GlobalCommitPosition| {            assert_eq!(changed_paths.next_mutable_commit_pos(), Some(pos));            let commit_id = index.as_composite().commits().entry_by_pos(pos).commit_id();            let commit = store.get_commit_async(&commit_id).await?;            let paths = collect_changed_paths(&index, &commit).await?;            changed_paths.add_changed_paths(paths);            Ok(())        };         // Index pre range        let mut new_changed_paths =            CompositeChangedPathIndex::empty(GlobalCommitPosition(pre_start));        new_changed_paths.make_mutable();        tracing::info!(?pre_start, ?pre_end, "indexing changed paths in commits");        for pos in (pre_start..pre_end).map(GlobalCommitPosition) {            index_commit(&mut new_changed_paths, pos)                .await                .map_err(to_index_err)?;        }        new_changed_paths            .save_in(&self.changed_path_segments_dir())            .map_err(DefaultIndexStoreError::SaveIndex)?;         // Copy previously-indexed segments        new_changed_paths.append_segments(old_changed_paths);         // Index post range, which is usually empty        new_changed_paths.make_mutable();        tracing::info!(?post_start, ?post_end, "indexing changed paths in commits");        for pos in (post_start..post_end).map(GlobalCommitPosition) {            index_commit(&mut new_changed_paths, pos)                .await                .map_err(to_index_err)?;        }        new_changed_paths.maybe_squash_with_ancestors();        new_changed_paths            .save_in(&self.changed_path_segments_dir())            .map_err(DefaultIndexStoreError::SaveIndex)?;         // Update the operation link to point to the new segments        let commits = index.readonly_commits().clone();        let index = DefaultReadonlyIndex::from_segment(commits, new_changed_paths);        self.associate_index_with_operation(&index, op_id)            .map_err(|source| DefaultIndexStoreError::AssociateIndex {                op_id: op_id.to_owned(),                source,            })?;        Ok(index)    }     fn save_mutable_index(        &self,        index: DefaultMutableIndex,        op_id: &OperationId,    ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {        // Create directories in case the store was initialized by jj < 0.33.        self.ensure_base_dirs()            .map_err(DefaultIndexStoreError::SaveIndex)?;        let (commits, mut changed_paths) = index.into_segment();        let commits = commits            .maybe_squash_with_ancestors()

Lines 1–500 of 616.