// 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::collections::HashSet;use std::ffi::OsStr;use std::fmt::Debug;use std::fmt::Error;use std::fmt::Formatter;use std::fs;use std::io;use std::path::Path;use std::path::PathBuf;use std::pin::Pin;use std::process::Command;use std::process::ExitStatus;use std::str::Utf8Error;use std::sync::Arc;use std::sync::Mutex;use std::sync::MutexGuard;use std::time::SystemTime; use async_trait::async_trait;use futures::AsyncRead;use futures::AsyncReadExt as _;use futures::StreamExt as _;use futures::io::Cursor;use futures::stream::BoxStream;use gix::bstr::BString;use gix::objs::CommitRefIter;use gix::objs::Exists as _;use gix::objs::Write as _;use gix::objs::WriteTo as _;use itertools::Itertools as _;use once_cell::sync::OnceCell as OnceLock;use pollster::FutureExt as _;use prost::Message as _;use smallvec::SmallVec;use thiserror::Error; use crate::backend::Backend;use crate::backend::BackendError;use crate::backend::BackendInitError;use crate::backend::BackendLoadError;use crate::backend::BackendResult;use crate::backend::ChangeId;use crate::backend::Commit;use crate::backend::CommitId;use crate::backend::CopyHistory;use crate::backend::CopyId;use crate::backend::CopyRecord;use crate::backend::FileId;use crate::backend::MillisSinceEpoch;use crate::backend::RelatedCopy;use crate::backend::SecureSig;use crate::backend::Signature;use crate::backend::SigningFn;use crate::backend::SymlinkId;use crate::backend::Timestamp;use crate::backend::Tree;use crate::backend::TreeId;use crate::backend::TreeValue;use crate::backend::make_root_commit;use crate::config::ConfigGetError;use crate::file_util;use crate::file_util::BadPathEncoding;use crate::file_util::IoResultExt as _;use crate::file_util::PathError;use crate::git::GitSettings;use crate::index::Index;use crate::lock::FileLock;use crate::merge::Merge;use crate::merge::MergeBuilder;use crate::object_id::ObjectId;use crate::repo_path::RepoPath;use crate::repo_path::RepoPathBuf;use crate::repo_path::RepoPathComponentBuf;use crate::settings::UserSettings;use crate::stacked_table::MutableTable;use crate::stacked_table::ReadonlyTable;use crate::stacked_table::TableSegment as _;use crate::stacked_table::TableStore;use crate::stacked_table::TableStoreError; const CHANGE_ID_LENGTH: usize = 16;/// Ref namespace used only for preventing GC.const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/"; pub const JJ_CONFLICT_README_FILE_NAME: &str = "JJ-CONFLICT-README"; pub const JJ_TREES_COMMIT_HEADER: &str = "jj:trees";pub const JJ_CONFLICT_LABELS_COMMIT_HEADER: &str = "jj:conflict-labels";pub const CHANGE_ID_COMMIT_HEADER: &str = "change-id"; #[derive(Debug, Error)]pub enum GitBackendInitError { #[error("Failed to initialize git repository")] InitRepository(#[source] gix::init::Error), #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error("Failed to encode git repository path")] EncodeRepositoryPath(#[source] BadPathEncoding), #[error(transparent)] Config(ConfigGetError), #[error(transparent)] Path(PathError),} impl From<Box<GitBackendInitError>> for BackendInitError { fn from(err: Box<GitBackendInitError>) -> Self { Self(err) }} #[derive(Debug, Error)]pub enum GitBackendLoadError { #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error("Failed to decode git repository path")] DecodeRepositoryPath(#[source] BadPathEncoding), #[error(transparent)] Config(ConfigGetError), #[error(transparent)] Path(PathError),} impl From<Box<GitBackendLoadError>> for BackendLoadError { fn from(err: Box<GitBackendLoadError>) -> Self { Self(err) }} /// `GitBackend`-specific error that may occur after the backend is loaded.#[derive(Debug, Error)]pub enum GitBackendError { #[error("Failed to read non-git metadata")] ReadMetadata(#[source] TableStoreError), #[error("Failed to write non-git metadata")] WriteMetadata(#[source] TableStoreError),} impl From<GitBackendError> for BackendError { fn from(err: GitBackendError) -> Self { Self::Other(err.into()) }} #[derive(Debug, Error)]pub enum GitRepoAtWorkdirError { #[error("No Git repository found at {path}")] NotFound { path: PathBuf, source: gix::discover::is_git::Error, }, #[error("Unrelated Git repository found at {path}")] Unrelated { path: PathBuf }, #[error("Failed to open Git repository")] Other(#[source] Box<dyn std::error::Error + Send + Sync>),} #[derive(Debug, Error)]pub enum GitGcError { #[error("Failed to run git gc command")] GcCommand(#[source] std::io::Error), #[error("git gc command exited with an error: {0}")] GcCommandErrorStatus(ExitStatus),} pub struct GitBackend { // While gix::Repository can be created from gix::ThreadSafeRepository, it's // cheaper to cache the thread-local instance behind a mutex than creating // one for each backend method call. Our GitBackend is most likely to be // used in a single-threaded context. base_repo: gix::ThreadSafeRepository, repo: Mutex<gix::Repository>, root_commit_id: CommitId, root_change_id: ChangeId, empty_tree_id: TreeId, shallow_root_ids: OnceLock<Vec<CommitId>>, extra_metadata_store: TableStore, cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>, git_executable: PathBuf, write_change_id_header: bool,} impl GitBackend { pub fn name() -> &'static str { "git" } fn new( base_repo: gix::ThreadSafeRepository, extra_metadata_store: TableStore, git_settings: GitSettings, ) -> Self { let repo = base_repo.to_thread_local(); let root_commit_id = CommitId::from_bytes(repo.object_hash().null_ref().as_bytes()); let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]); let empty_tree_id = TreeId::from_bytes(gix::ObjectId::empty_tree(repo.object_hash()).as_bytes()); Self { base_repo, repo: Mutex::new(repo), root_commit_id, root_change_id, empty_tree_id, shallow_root_ids: OnceLock::new(), extra_metadata_store, cached_extra_metadata: Mutex::new(None), git_executable: git_settings.executable_path, write_change_id_header: git_settings.write_change_id_header, } } pub fn init_internal( settings: &UserSettings, store_path: &Path, object_hash: gix::hash::Kind, ) -> Result<Self, Box<GitBackendInitError>> { let git_repo_path = Path::new("git"); let git_repo = gix::ThreadSafeRepository::init_opts( store_path.join(git_repo_path), gix::create::Kind::Bare, gix::create::Options { object_hash: Some(object_hash), ..Default::default() }, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings) } /// Initializes backend by creating a new Git repo at the specified /// workspace path. The workspace directory must exist. pub fn init_colocated( settings: &UserSettings, store_path: &Path, workspace_root: &Path, object_hash: gix::hash::Kind, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_workspace_root = { let path = store_path.join(workspace_root); dunce::canonicalize(&path) .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::init_opts( canonical_workspace_root, gix::create::Kind::WithWorktree, gix::create::Options { object_hash: Some(object_hash), ..Default::default() }, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_repo_path = workspace_root.join(".git"); let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, &git_repo_path, git_repo, git_settings) } /// Initializes backend with an existing Git repo at the specified path. pub fn init_external( settings: &UserSettings, store_path: &Path, git_repo_path: &Path, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_git_repo_path = { let path = store_path.join(git_repo_path); canonicalize_git_repo_path(&path) .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::open_opts( canonical_git_repo_path, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::OpenRepository)?; let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings) } fn init_with_repo( store_path: &Path, git_repo_path: &Path, repo: gix::ThreadSafeRepository, git_settings: GitSettings, ) -> Result<Self, Box<GitBackendInitError>> { let extra_path = store_path.join("extra"); fs::create_dir(&extra_path) .context(&extra_path) .map_err(GitBackendInitError::Path)?; let target_path = store_path.join("git_target"); let git_repo_path = if cfg!(windows) && git_repo_path.is_relative() { // When a repository is created in Windows, format the path with *forward // slashes* and not backwards slashes. This makes it possible to use the same // repository under Windows Subsystem for Linux. // // This only works for relative paths. If the path is absolute, there's not much // we can do, and it simply won't work inside and outside WSL at the same time. file_util::slash_path(git_repo_path) } else { git_repo_path.into() }; let git_repo_path_bytes = file_util::path_to_bytes(&git_repo_path) .map_err(GitBackendInitError::EncodeRepositoryPath)?; fs::write(&target_path, git_repo_path_bytes) .context(&target_path) .map_err(GitBackendInitError::Path)?; let extra_metadata_store = TableStore::init( extra_path, repo.to_thread_local().object_hash().len_in_bytes(), ); Ok(Self::new(repo, extra_metadata_store, git_settings)) } pub fn load( settings: &UserSettings, store_path: &Path, ) -> Result<Self, Box<GitBackendLoadError>> { let git_repo_path = { let target_path = store_path.join("git_target"); let git_repo_path_bytes = fs::read(&target_path) .context(&target_path) .map_err(GitBackendLoadError::Path)?; let git_repo_path = file_util::path_from_bytes(&git_repo_path_bytes) .map_err(GitBackendLoadError::DecodeRepositoryPath)?; let git_repo_path = store_path.join(git_repo_path); canonicalize_git_repo_path(&git_repo_path) .context(&git_repo_path) .map_err(GitBackendLoadError::Path)? }; let repo = gix::ThreadSafeRepository::open_opts( git_repo_path, gix_open_opts_from_settings(settings), ) .map_err(GitBackendLoadError::OpenRepository)?; let extra_metadata_store = TableStore::load( store_path.join("extra"), repo.to_thread_local().object_hash().len_in_bytes(), ); let git_settings = GitSettings::from_settings(settings).map_err(GitBackendLoadError::Config)?; Ok(Self::new(repo, extra_metadata_store, git_settings)) } fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> { self.repo.lock().unwrap() } /// Returns a new thread-local handle for the underlying Git repository. /// /// Use [`Self::open_git_repo_at_workdir()`] for worktree operations. pub fn git_repo(&self) -> gix::Repository { self.base_repo.to_thread_local() } /// Reopens the repository at the given workspace path. Returns a new /// thread-local handle. pub fn open_git_repo_at_workdir( &self, path: &Path, ) -> Result<gix::Repository, GitRepoAtWorkdirError> { // Try the open repository first. let open_repo = self.git_repo(); if let Some(workdir) = open_repo.workdir() && (workdir == path || dunce::canonicalize(path).is_ok_and(|path| workdir == path)) { return Ok(open_repo); } // The input path doesn't include ".git". let opts = open_repo.open_options().clone().open_path_as_is(false); let work_repo = gix::ThreadSafeRepository::open_opts(path, opts) .map_err(|err| match err { gix::open::Error::NotARepository { path, source } => { GitRepoAtWorkdirError::NotFound { path, source } } err => GitRepoAtWorkdirError::Other(err.into()), })? .to_thread_local(); let canonicalize = |path: &Path| { dunce::canonicalize(path).map_err(|err| GitRepoAtWorkdirError::Other(err.into())) }; if open_repo.common_dir() == work_repo.common_dir() || canonicalize(open_repo.common_dir())? == canonicalize(work_repo.common_dir())? { // The last (path, work_repo) can be cached if needed. Ok(work_repo) } else { let path = path.to_owned(); Err(GitRepoAtWorkdirError::Unrelated { path }) } } /// Path to the `.git` directory or the repository itself if it's bare. pub fn git_repo_path(&self) -> &Path { self.base_repo.path() } fn shallow_root_ids(&self, git_repo: &gix::Repository) -> BackendResult<&[CommitId]> { // The list of shallow roots is cached by gix, but it's still expensive // to stat file on every read_object() call. Refreshing shallow roots is // also bad for consistency reasons. self.shallow_root_ids .get_or_try_init(|| { let maybe_oids = git_repo .shallow_commits() .map_err(|err| BackendError::Other(err.into()))?; let commit_ids = maybe_oids.map_or(vec![], |oids| { oids.iter() .map(|oid| CommitId::from_bytes(oid.as_bytes())) .collect() }); Ok(commit_ids) }) .map(AsRef::as_ref) } fn cached_extra_metadata_table(&self) -> BackendResult<Arc<ReadonlyTable>> { let mut locked_head = self.cached_extra_metadata.lock().unwrap(); match locked_head.as_ref() { Some(head) => Ok(head.clone()), None => { let table = self .extra_metadata_store .get_head() .map_err(GitBackendError::ReadMetadata)?; *locked_head = Some(table.clone()); Ok(table) } } } fn read_extra_metadata_table_locked(&self) -> BackendResult<(Arc<ReadonlyTable>, FileLock)> { let table = self .extra_metadata_store .get_head_locked() .map_err(GitBackendError::ReadMetadata)?; Ok(table) } fn save_extra_metadata_table( &self, mut_table: MutableTable, _table_lock: &FileLock, ) -> BackendResult<()> { let table = self .extra_metadata_store .save_table(mut_table) .map_err(GitBackendError::WriteMetadata)?; // Since the parent table was the head, saved table are likely to be new head. // If it's not, cache will be reloaded when entry can't be found. *self.cached_extra_metadata.lock().unwrap() = Some(table); Ok(()) } /// Imports the given commits and ancestors from the backing Git repo. /// /// The `head_ids` may contain commits that have already been imported, but /// the caller should filter them out to eliminate redundant I/O processing. #[tracing::instrument(skip(self, head_ids))] pub fn import_head_commits<'a>( &self, head_ids: impl IntoIterator<Item = &'a CommitId>, ) -> BackendResult<()> { let head_ids: HashSet<&CommitId> = head_ids .into_iter() .filter(|&id| *id != self.root_commit_id) .collect(); if head_ids.is_empty() { return Ok(()); } // Create no-gc ref even if known to the extras table. Concurrent GC // process might have deleted the no-gc ref. let locked_repo = self.lock_git_repo(); locked_repo .edit_references(head_ids.iter().copied().map(to_no_gc_ref_update)) .map_err(|err| BackendError::Other(Box::new(err)))?; // These commits are imported from Git. Make our change ids persist (otherwise // future write_commit() could reassign new change id.)Lines 1–500 of 2612. Load more
// 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::collections::HashSet; use std::ffi::OsStr; use std::fmt::Debug; use std::fmt::Error; use std::fmt::Formatter; use std::fs; use std::io; use std::path::Path; use std::path::PathBuf; use std::pin::Pin; use std::process::Command; use std::process::ExitStatus; use std::str::Utf8Error; use std::sync::Arc; use std::sync::Mutex; use std::sync::MutexGuard; use std::time::SystemTime; use async_trait::async_trait; use futures::AsyncRead; use futures::AsyncReadExt as _; use futures::StreamExt as _; use futures::io::Cursor; use futures::stream::BoxStream; use gix::bstr::BString; use gix::objs::CommitRefIter; use gix::objs::Exists as _; use gix::objs::Write as _; use gix::objs::WriteTo as _; use itertools::Itertools as _; use once_cell::sync::OnceCell as OnceLock; use pollster::FutureExt as _; use prost::Message as _; use smallvec::SmallVec; use thiserror::Error; use crate::backend::Backend; use crate::backend::BackendError; use crate::backend::BackendInitError; use crate::backend::BackendLoadError; use crate::backend::BackendResult; use crate::backend::ChangeId; use crate::backend::Commit; use crate::backend::CommitId; use crate::backend::CopyHistory; use crate::backend::CopyId; use crate::backend::CopyRecord; use crate::backend::FileId; use crate::backend::MillisSinceEpoch; use crate::backend::RelatedCopy; use crate::backend::SecureSig; use crate::backend::Signature; use crate::backend::SigningFn; use crate::backend::SymlinkId; use crate::backend::Timestamp; use crate::backend::Tree; use crate::backend::TreeId; use crate::backend::TreeValue; use crate::backend::make_root_commit; use crate::config::ConfigGetError; use crate::file_util; use crate::file_util::BadPathEncoding; use crate::file_util::IoResultExt as _; use crate::file_util::PathError; use crate::git::GitSettings; use crate::index::Index; use crate::lock::FileLock; use crate::merge::Merge; use crate::merge::MergeBuilder; use crate::object_id::ObjectId; use crate::repo_path::RepoPath; use crate::repo_path::RepoPathBuf; use crate::repo_path::RepoPathComponentBuf; use crate::settings::UserSettings; use crate::stacked_table::MutableTable; use crate::stacked_table::ReadonlyTable; use crate::stacked_table::TableSegment as _; use crate::stacked_table::TableStore; use crate::stacked_table::TableStoreError; const CHANGE_ID_LENGTH: usize = 16; /// Ref namespace used only for preventing GC. const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/"; pub const JJ_CONFLICT_README_FILE_NAME: &str = "JJ-CONFLICT-README"; pub const JJ_TREES_COMMIT_HEADER: &str = "jj:trees"; pub const JJ_CONFLICT_LABELS_COMMIT_HEADER: &str = "jj:conflict-labels"; pub const CHANGE_ID_COMMIT_HEADER: &str = "change-id"; #[derive(Debug, Error)] pub enum GitBackendInitError { #[error("Failed to initialize git repository")] InitRepository(#[source] gix::init::Error), #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error("Failed to encode git repository path")] EncodeRepositoryPath(#[source] BadPathEncoding), #[error(transparent)] Config(ConfigGetError), #[error(transparent)] Path(PathError), } impl From<Box<GitBackendInitError>> for BackendInitError { fn from(err: Box<GitBackendInitError>) -> Self { Self(err) } } #[derive(Debug, Error)] pub enum GitBackendLoadError { #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error("Failed to decode git repository path")] DecodeRepositoryPath(#[source] BadPathEncoding), #[error(transparent)] Config(ConfigGetError), #[error(transparent)] Path(PathError), } impl From<Box<GitBackendLoadError>> for BackendLoadError { fn from(err: Box<GitBackendLoadError>) -> Self { Self(err) } } /// `GitBackend`-specific error that may occur after the backend is loaded. #[derive(Debug, Error)] pub enum GitBackendError { #[error("Failed to read non-git metadata")] ReadMetadata(#[source] TableStoreError), #[error("Failed to write non-git metadata")] WriteMetadata(#[source] TableStoreError), } impl From<GitBackendError> for BackendError { fn from(err: GitBackendError) -> Self { Self::Other(err.into()) } } #[derive(Debug, Error)] pub enum GitRepoAtWorkdirError { #[error("No Git repository found at {path}")] NotFound { path: PathBuf, source: gix::discover::is_git::Error, }, #[error("Unrelated Git repository found at {path}")] Unrelated { path: PathBuf }, #[error("Failed to open Git repository")] Other(#[source] Box<dyn std::error::Error + Send + Sync>), } #[derive(Debug, Error)] pub enum GitGcError { #[error("Failed to run git gc command")] GcCommand(#[source] std::io::Error), #[error("git gc command exited with an error: {0}")] GcCommandErrorStatus(ExitStatus), } pub struct GitBackend { // While gix::Repository can be created from gix::ThreadSafeRepository, it's // cheaper to cache the thread-local instance behind a mutex than creating // one for each backend method call. Our GitBackend is most likely to be // used in a single-threaded context. base_repo: gix::ThreadSafeRepository, repo: Mutex<gix::Repository>, root_commit_id: CommitId, root_change_id: ChangeId, empty_tree_id: TreeId, shallow_root_ids: OnceLock<Vec<CommitId>>, extra_metadata_store: TableStore, cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>, git_executable: PathBuf, write_change_id_header: bool, } impl GitBackend { pub fn name() -> &'static str { "git" } fn new( base_repo: gix::ThreadSafeRepository, extra_metadata_store: TableStore, git_settings: GitSettings, ) -> Self { let repo = base_repo.to_thread_local(); let root_commit_id = CommitId::from_bytes(repo.object_hash().null_ref().as_bytes()); let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]); let empty_tree_id = TreeId::from_bytes(gix::ObjectId::empty_tree(repo.object_hash()).as_bytes()); Self { base_repo, repo: Mutex::new(repo), root_commit_id, root_change_id, empty_tree_id, shallow_root_ids: OnceLock::new(), extra_metadata_store, cached_extra_metadata: Mutex::new(None), git_executable: git_settings.executable_path, write_change_id_header: git_settings.write_change_id_header, } } pub fn init_internal( settings: &UserSettings, store_path: &Path, object_hash: gix::hash::Kind, ) -> Result<Self, Box<GitBackendInitError>> { let git_repo_path = Path::new("git"); let git_repo = gix::ThreadSafeRepository::init_opts( store_path.join(git_repo_path), gix::create::Kind::Bare, gix::create::Options { object_hash: Some(object_hash), ..Default::default() }, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings) } /// Initializes backend by creating a new Git repo at the specified /// workspace path. The workspace directory must exist. pub fn init_colocated( settings: &UserSettings, store_path: &Path, workspace_root: &Path, object_hash: gix::hash::Kind, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_workspace_root = { let path = store_path.join(workspace_root); dunce::canonicalize(&path) .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::init_opts( canonical_workspace_root, gix::create::Kind::WithWorktree, gix::create::Options { object_hash: Some(object_hash), ..Default::default() }, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_repo_path = workspace_root.join(".git"); let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, &git_repo_path, git_repo, git_settings) } /// Initializes backend with an existing Git repo at the specified path. pub fn init_external( settings: &UserSettings, store_path: &Path, git_repo_path: &Path, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_git_repo_path = { let path = store_path.join(git_repo_path); canonicalize_git_repo_path(&path) .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::open_opts( canonical_git_repo_path, gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::OpenRepository)?; let git_settings = GitSettings::from_settings(settings).map_err(GitBackendInitError::Config)?; Self::init_with_repo(store_path, git_repo_path, git_repo, git_settings) } fn init_with_repo( store_path: &Path, git_repo_path: &Path, repo: gix::ThreadSafeRepository, git_settings: GitSettings, ) -> Result<Self, Box<GitBackendInitError>> { let extra_path = store_path.join("extra"); fs::create_dir(&extra_path) .context(&extra_path) .map_err(GitBackendInitError::Path)?; let target_path = store_path.join("git_target"); let git_repo_path = if cfg!(windows) && git_repo_path.is_relative() { // When a repository is created in Windows, format the path with *forward // slashes* and not backwards slashes. This makes it possible to use the same // repository under Windows Subsystem for Linux. // // This only works for relative paths. If the path is absolute, there's not much // we can do, and it simply won't work inside and outside WSL at the same time. file_util::slash_path(git_repo_path) } else { git_repo_path.into() }; let git_repo_path_bytes = file_util::path_to_bytes(&git_repo_path) .map_err(GitBackendInitError::EncodeRepositoryPath)?; fs::write(&target_path, git_repo_path_bytes) .context(&target_path) .map_err(GitBackendInitError::Path)?; let extra_metadata_store = TableStore::init( extra_path, repo.to_thread_local().object_hash().len_in_bytes(), ); Ok(Self::new(repo, extra_metadata_store, git_settings)) } pub fn load( settings: &UserSettings, store_path: &Path, ) -> Result<Self, Box<GitBackendLoadError>> { let git_repo_path = { let target_path = store_path.join("git_target"); let git_repo_path_bytes = fs::read(&target_path) .context(&target_path) .map_err(GitBackendLoadError::Path)?; let git_repo_path = file_util::path_from_bytes(&git_repo_path_bytes) .map_err(GitBackendLoadError::DecodeRepositoryPath)?; let git_repo_path = store_path.join(git_repo_path); canonicalize_git_repo_path(&git_repo_path) .context(&git_repo_path) .map_err(GitBackendLoadError::Path)? }; let repo = gix::ThreadSafeRepository::open_opts( git_repo_path, gix_open_opts_from_settings(settings), ) .map_err(GitBackendLoadError::OpenRepository)?; let extra_metadata_store = TableStore::load( store_path.join("extra"), repo.to_thread_local().object_hash().len_in_bytes(), ); let git_settings = GitSettings::from_settings(settings).map_err(GitBackendLoadError::Config)?; Ok(Self::new(repo, extra_metadata_store, git_settings)) } fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> { self.repo.lock().unwrap() } /// Returns a new thread-local handle for the underlying Git repository. /// /// Use [`Self::open_git_repo_at_workdir()`] for worktree operations. pub fn git_repo(&self) -> gix::Repository { self.base_repo.to_thread_local() } /// Reopens the repository at the given workspace path. Returns a new /// thread-local handle. pub fn open_git_repo_at_workdir( &self, path: &Path, ) -> Result<gix::Repository, GitRepoAtWorkdirError> { // Try the open repository first. let open_repo = self.git_repo(); if let Some(workdir) = open_repo.workdir() && (workdir == path || dunce::canonicalize(path).is_ok_and(|path| workdir == path)) { return Ok(open_repo); } // The input path doesn't include ".git". let opts = open_repo.open_options().clone().open_path_as_is(false); let work_repo = gix::ThreadSafeRepository::open_opts(path, opts) .map_err(|err| match err { gix::open::Error::NotARepository { path, source } => { GitRepoAtWorkdirError::NotFound { path, source } } err => GitRepoAtWorkdirError::Other(err.into()), })? .to_thread_local(); let canonicalize = |path: &Path| { dunce::canonicalize(path).map_err(|err| GitRepoAtWorkdirError::Other(err.into())) }; if open_repo.common_dir() == work_repo.common_dir() || canonicalize(open_repo.common_dir())? == canonicalize(work_repo.common_dir())? { // The last (path, work_repo) can be cached if needed. Ok(work_repo) } else { let path = path.to_owned(); Err(GitRepoAtWorkdirError::Unrelated { path }) } } /// Path to the `.git` directory or the repository itself if it's bare. pub fn git_repo_path(&self) -> &Path { self.base_repo.path() } fn shallow_root_ids(&self, git_repo: &gix::Repository) -> BackendResult<&[CommitId]> { // The list of shallow roots is cached by gix, but it's still expensive // to stat file on every read_object() call. Refreshing shallow roots is // also bad for consistency reasons. self.shallow_root_ids .get_or_try_init(|| { let maybe_oids = git_repo .shallow_commits() .map_err(|err| BackendError::Other(err.into()))?; let commit_ids = maybe_oids.map_or(vec![], |oids| { oids.iter() .map(|oid| CommitId::from_bytes(oid.as_bytes())) .collect() }); Ok(commit_ids) }) .map(AsRef::as_ref) } fn cached_extra_metadata_table(&self) -> BackendResult<Arc<ReadonlyTable>> { let mut locked_head = self.cached_extra_metadata.lock().unwrap(); match locked_head.as_ref() { Some(head) => Ok(head.clone()), None => { let table = self .extra_metadata_store .get_head() .map_err(GitBackendError::ReadMetadata)?; *locked_head = Some(table.clone()); Ok(table) } } } fn read_extra_metadata_table_locked(&self) -> BackendResult<(Arc<ReadonlyTable>, FileLock)> { let table = self .extra_metadata_store .get_head_locked() .map_err(GitBackendError::ReadMetadata)?; Ok(table) } fn save_extra_metadata_table( &self, mut_table: MutableTable, _table_lock: &FileLock, ) -> BackendResult<()> { let table = self .extra_metadata_store .save_table(mut_table) .map_err(GitBackendError::WriteMetadata)?; // Since the parent table was the head, saved table are likely to be new head. // If it's not, cache will be reloaded when entry can't be found. *self.cached_extra_metadata.lock().unwrap() = Some(table); Ok(()) } /// Imports the given commits and ancestors from the backing Git repo. /// /// The `head_ids` may contain commits that have already been imported, but /// the caller should filter them out to eliminate redundant I/O processing. #[tracing::instrument(skip(self, head_ids))] pub fn import_head_commits<'a>( &self, head_ids: impl IntoIterator<Item = &'a CommitId>, ) -> BackendResult<()> { let head_ids: HashSet<&CommitId> = head_ids .into_iter() .filter(|&id| *id != self.root_commit_id) .collect(); if head_ids.is_empty() { return Ok(()); } // Create no-gc ref even if known to the extras table. Concurrent GC // process might have deleted the no-gc ref. let locked_repo = self.lock_git_repo(); locked_repo .edit_references(head_ids.iter().copied().map(to_no_gc_ref_update)) .map_err(|err| BackendError::Other(Box::new(err)))?; // These commits are imported from Git. Make our change ids persist (otherwise // future write_commit() could reassign new change id.)