// 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. #![allow(missing_docs)] use std::any::Any;use std::collections::HashSet;use std::fmt::{Debug, Error, Formatter};use std::io::{Cursor, Read};use std::path::{Path, PathBuf};use std::process::{Command, ExitStatus};use std::sync::{Arc, Mutex, MutexGuard};use std::time::SystemTime;use std::{fs, io, str}; use async_trait::async_trait;use gix::objs::{CommitRefIter, WriteTo};use itertools::Itertools;use prost::Message;use smallvec::SmallVec;use thiserror::Error; use crate::backend::{ make_root_commit, Backend, BackendError, BackendInitError, BackendLoadError, BackendResult, ChangeId, Commit, CommitId, Conflict, ConflictId, ConflictTerm, FileId, MergedTreeId, MillisSinceEpoch, SecureSig, Signature, SigningFn, SymlinkId, Timestamp, Tree, TreeId, TreeValue,};use crate::file_util::{IoResultExt as _, PathError};use crate::index::Index;use crate::lock::FileLock;use crate::merge::{Merge, MergeBuilder};use crate::object_id::ObjectId;use crate::repo_path::{RepoPath, RepoPathComponentBuf};use crate::settings::UserSettings;use crate::stacked_table::{ MutableTable, ReadonlyTable, TableSegment, TableStore, TableStoreError,}; const HASH_LENGTH: usize = 20;const CHANGE_ID_LENGTH: usize = 16;/// Ref namespace used only for preventing GC.const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/";const CONFLICT_SUFFIX: &str = ".jjconflict"; #[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(transparent)] Path(PathError),} impl From<Box<GitBackendInitError>> for BackendInitError { fn from(err: Box<GitBackendInitError>) -> Self { BackendInitError(err) }} #[derive(Debug, Error)]pub enum GitBackendLoadError { #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error(transparent)] Path(PathError),} impl From<Box<GitBackendLoadError>> for BackendLoadError { fn from(err: Box<GitBackendLoadError>) -> Self { BackendLoadError(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 { BackendError::Other(err.into()) }} #[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, extra_metadata_store: TableStore, cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>, /// Whether tree of imported commit should be promoted to non-legacy format. imported_commit_uses_tree_conflict_format: bool,} impl GitBackend { pub fn name() -> &'static str { "git" } fn new( base_repo: gix::ThreadSafeRepository, extra_metadata_store: TableStore, imported_commit_uses_tree_conflict_format: bool, ) -> Self { let repo = Mutex::new(base_repo.to_thread_local()); let root_commit_id = CommitId::from_bytes(&[0; HASH_LENGTH]); let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]); let empty_tree_id = TreeId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904"); GitBackend { base_repo, repo, root_commit_id, root_change_id, empty_tree_id, extra_metadata_store, cached_extra_metadata: Mutex::new(None), imported_commit_uses_tree_conflict_format, } } pub fn init_internal( settings: &UserSettings, store_path: &Path, ) -> 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::default(), gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; Self::init_with_repo(settings, store_path, git_repo_path, git_repo) } /// 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, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_workspace_root = { let path = store_path.join(workspace_root); path.canonicalize() .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::init_opts( canonical_workspace_root, gix::create::Kind::WithWorktree, gix::create::Options::default(), gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_repo_path = workspace_root.join(".git"); Self::init_with_repo(settings, store_path, &git_repo_path, git_repo) } /// 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)?; Self::init_with_repo(settings, store_path, git_repo_path, git_repo) } fn init_with_repo( settings: &UserSettings, store_path: &Path, git_repo_path: &Path, git_repo: gix::ThreadSafeRepository, ) -> 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"); 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. let git_repo_path_string = git_repo_path .components() .map(|component| component.as_os_str().to_str().unwrap().to_owned()) .join("/"); fs::write(&target_path, git_repo_path_string.as_bytes()) .context(&target_path) .map_err(GitBackendInitError::Path)?; } else { fs::write(&target_path, git_repo_path.to_str().unwrap().as_bytes()) .context(&target_path) .map_err(GitBackendInitError::Path)?; }; let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH); Ok(GitBackend::new( git_repo, extra_metadata_store, settings.use_tree_conflict_format(), )) } 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_str = fs::read_to_string(&target_path) .context(&target_path) .map_err(GitBackendLoadError::Path)?; let git_repo_path = store_path.join(git_repo_path_str); 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"), HASH_LENGTH); Ok(GitBackend::new( repo, extra_metadata_store, settings.use_tree_conflict_format(), )) } fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> { self.repo.lock().unwrap() } /// Returns new thread-local instance to access to the underlying Git repo. pub fn git_repo(&self) -> gix::Repository { self.base_repo.to_thread_local() } /// Creates new owned git repository instance. pub fn open_git_repo(&self) -> Result<git2::Repository, git2::Error> { git2::Repository::open(self.git_repo_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() } /// Path to the working directory if the repository isn't bare. pub fn git_workdir(&self) -> Option<&Path> { self.base_repo.work_dir() } 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.) tracing::debug!( heads_count = head_ids.len(), "import extra metadata entries" ); let (table, table_lock) = self.read_extra_metadata_table_locked()?; let mut mut_table = table.start_mutation(); import_extra_metadata_entries_from_heads( &locked_repo, &mut mut_table, &table_lock, &head_ids, self.imported_commit_uses_tree_conflict_format, )?; self.save_extra_metadata_table(mut_table, &table_lock) } fn read_file_sync(&self, id: &FileId) -> BackendResult<Box<dyn Read>> { let git_blob_id = validate_git_object_id(id)?; let locked_repo = self.lock_git_repo(); let mut blob = locked_repo .find_object(git_blob_id) .map_err(|err| map_not_found_err(err, id))? .try_into_blob() .map_err(|err| to_read_object_err(err, id))?; Ok(Box::new(Cursor::new(blob.take_data()))) }} /// Canonicalizes the given `path` except for the last `".git"` component.////// The last path component matters when opening a Git repo without `core.bare`/// config. This config is usually set, but the "repo" tool will set up such/// repositories and symlinks. Opening such repo with fully-canonicalized path/// would turn a colocated Git repo into a bare repo.pub fn canonicalize_git_repo_path(path: &Path) -> io::Result<PathBuf> { if path.ends_with(".git") { let workdir = path.parent().unwrap(); workdir.canonicalize().map(|dir| dir.join(".git")) } else { path.canonicalize() }} fn gix_open_opts_from_settings(settings: &UserSettings) -> gix::open::Options { let user_name = settings.user_name(); let user_email = settings.user_email(); gix::open::Options::default() .config_overrides([ // Committer has to be configured to record reflog. Author isn't // needed, but let's copy the same values. format!("author.name={user_name}"), format!("author.email={user_email}"), format!("committer.name={user_name}"), format!("committer.email={user_email}"), ]) // The git_target path should point the repository, not the working directory. .open_path_as_is(true)} fn commit_from_git_without_root_parent( id: &CommitId, git_object: &gix::Object, uses_tree_conflict_format: bool,) -> Result<Commit, BackendError> { let commit = git_object .try_to_commit_ref() .map_err(|err| to_read_object_err(err, id))?; // We reverse the bits of the commit id to create the change id. We don't want // to use the first bytes unmodified because then it would be ambiguous // if a given hash prefix refers to the commit id or the change id. It // would have been enough to pick the last 16 bytes instead of the // leading 16 bytes to address that. We also reverse the bits to make it less // likely that users depend on any relationship between the two ids. let change_id = ChangeId::new( id.as_bytes()[4..HASH_LENGTH] .iter() .rev() .map(|b| b.reverse_bits()) .collect(), ); let parents = commit .parents() .map(|oid| CommitId::from_bytes(oid.as_bytes())) .collect_vec(); let tree_id = TreeId::from_bytes(commit.tree().as_bytes()); // If this commit is a conflict, we'll update the root tree later, when we read // the extra metadata. let root_tree = if uses_tree_conflict_format { MergedTreeId::resolved(tree_id) } else { MergedTreeId::Legacy(tree_id) }; // Use lossy conversion as commit message with "mojibake" is still better than // nothing. // TODO: what should we do with commit.encoding? let description = String::from_utf8_lossy(commit.message).into_owned(); let author = signature_from_git(commit.author()); let committer = signature_from_git(commit.committer()); // If the commit is signed, extract both the signature and the signed data // (which is the commit buffer with the gpgsig header omitted). // We have to re-parse the raw commit data because gix CommitRef does not give // us the sogned data, only the signature. // Ideally, we could use try_to_commit_ref_iter at the beginning of this // function and extract everything from that. For now, this works let secure_sig = commit .extra_headers .iter() // gix does not recognize gpgsig-sha256, but prevent future footguns by checking for it too .any(|(k, _)| *k == "gpgsig" || *k == "gpgsig-sha256") .then(|| CommitRefIter::signature(&git_object.data)) .transpose() .map_err(|err| to_read_object_err(err, id))? .flatten() .map(|(sig, data)| SecureSig { data: data.to_bstring().into(), sig: sig.into_owned().into(), }); Ok(Commit { parents, predecessors: vec![], // If this commit has associated extra metadata, we may reset this later. root_tree, change_id, description, author, committer, secure_sig, })} const EMPTY_STRING_PLACEHOLDER: &str = "JJ_EMPTY_STRING"; fn signature_from_git(signature: gix::actor::SignatureRef) -> Signature { let name = signature.name;Lines 1–500 of 1969. 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. #![allow(missing_docs)] use std::any::Any; use std::collections::HashSet; use std::fmt::{Debug, Error, Formatter}; use std::io::{Cursor, Read}; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::SystemTime; use std::{fs, io, str}; use async_trait::async_trait; use gix::objs::{CommitRefIter, WriteTo}; use itertools::Itertools; use prost::Message; use smallvec::SmallVec; use thiserror::Error; use crate::backend::{ make_root_commit, Backend, BackendError, BackendInitError, BackendLoadError, BackendResult, ChangeId, Commit, CommitId, Conflict, ConflictId, ConflictTerm, FileId, MergedTreeId, MillisSinceEpoch, SecureSig, Signature, SigningFn, SymlinkId, Timestamp, Tree, TreeId, TreeValue, }; use crate::file_util::{IoResultExt as _, PathError}; use crate::index::Index; use crate::lock::FileLock; use crate::merge::{Merge, MergeBuilder}; use crate::object_id::ObjectId; use crate::repo_path::{RepoPath, RepoPathComponentBuf}; use crate::settings::UserSettings; use crate::stacked_table::{ MutableTable, ReadonlyTable, TableSegment, TableStore, TableStoreError, }; const HASH_LENGTH: usize = 20; const CHANGE_ID_LENGTH: usize = 16; /// Ref namespace used only for preventing GC. const NO_GC_REF_NAMESPACE: &str = "refs/jj/keep/"; const CONFLICT_SUFFIX: &str = ".jjconflict"; #[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(transparent)] Path(PathError), } impl From<Box<GitBackendInitError>> for BackendInitError { fn from(err: Box<GitBackendInitError>) -> Self { BackendInitError(err) } } #[derive(Debug, Error)] pub enum GitBackendLoadError { #[error("Failed to open git repository")] OpenRepository(#[source] gix::open::Error), #[error(transparent)] Path(PathError), } impl From<Box<GitBackendLoadError>> for BackendLoadError { fn from(err: Box<GitBackendLoadError>) -> Self { BackendLoadError(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 { BackendError::Other(err.into()) } } #[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, extra_metadata_store: TableStore, cached_extra_metadata: Mutex<Option<Arc<ReadonlyTable>>>, /// Whether tree of imported commit should be promoted to non-legacy format. imported_commit_uses_tree_conflict_format: bool, } impl GitBackend { pub fn name() -> &'static str { "git" } fn new( base_repo: gix::ThreadSafeRepository, extra_metadata_store: TableStore, imported_commit_uses_tree_conflict_format: bool, ) -> Self { let repo = Mutex::new(base_repo.to_thread_local()); let root_commit_id = CommitId::from_bytes(&[0; HASH_LENGTH]); let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]); let empty_tree_id = TreeId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904"); GitBackend { base_repo, repo, root_commit_id, root_change_id, empty_tree_id, extra_metadata_store, cached_extra_metadata: Mutex::new(None), imported_commit_uses_tree_conflict_format, } } pub fn init_internal( settings: &UserSettings, store_path: &Path, ) -> 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::default(), gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; Self::init_with_repo(settings, store_path, git_repo_path, git_repo) } /// 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, ) -> Result<Self, Box<GitBackendInitError>> { let canonical_workspace_root = { let path = store_path.join(workspace_root); path.canonicalize() .context(&path) .map_err(GitBackendInitError::Path)? }; let git_repo = gix::ThreadSafeRepository::init_opts( canonical_workspace_root, gix::create::Kind::WithWorktree, gix::create::Options::default(), gix_open_opts_from_settings(settings), ) .map_err(GitBackendInitError::InitRepository)?; let git_repo_path = workspace_root.join(".git"); Self::init_with_repo(settings, store_path, &git_repo_path, git_repo) } /// 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)?; Self::init_with_repo(settings, store_path, git_repo_path, git_repo) } fn init_with_repo( settings: &UserSettings, store_path: &Path, git_repo_path: &Path, git_repo: gix::ThreadSafeRepository, ) -> 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"); 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. let git_repo_path_string = git_repo_path .components() .map(|component| component.as_os_str().to_str().unwrap().to_owned()) .join("/"); fs::write(&target_path, git_repo_path_string.as_bytes()) .context(&target_path) .map_err(GitBackendInitError::Path)?; } else { fs::write(&target_path, git_repo_path.to_str().unwrap().as_bytes()) .context(&target_path) .map_err(GitBackendInitError::Path)?; }; let extra_metadata_store = TableStore::init(extra_path, HASH_LENGTH); Ok(GitBackend::new( git_repo, extra_metadata_store, settings.use_tree_conflict_format(), )) } 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_str = fs::read_to_string(&target_path) .context(&target_path) .map_err(GitBackendLoadError::Path)?; let git_repo_path = store_path.join(git_repo_path_str); 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"), HASH_LENGTH); Ok(GitBackend::new( repo, extra_metadata_store, settings.use_tree_conflict_format(), )) } fn lock_git_repo(&self) -> MutexGuard<'_, gix::Repository> { self.repo.lock().unwrap() } /// Returns new thread-local instance to access to the underlying Git repo. pub fn git_repo(&self) -> gix::Repository { self.base_repo.to_thread_local() } /// Creates new owned git repository instance. pub fn open_git_repo(&self) -> Result<git2::Repository, git2::Error> { git2::Repository::open(self.git_repo_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() } /// Path to the working directory if the repository isn't bare. pub fn git_workdir(&self) -> Option<&Path> { self.base_repo.work_dir() } 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.) tracing::debug!( heads_count = head_ids.len(), "import extra metadata entries" ); let (table, table_lock) = self.read_extra_metadata_table_locked()?; let mut mut_table = table.start_mutation(); import_extra_metadata_entries_from_heads( &locked_repo, &mut mut_table, &table_lock, &head_ids, self.imported_commit_uses_tree_conflict_format, )?; self.save_extra_metadata_table(mut_table, &table_lock) } fn read_file_sync(&self, id: &FileId) -> BackendResult<Box<dyn Read>> { let git_blob_id = validate_git_object_id(id)?; let locked_repo = self.lock_git_repo(); let mut blob = locked_repo .find_object(git_blob_id) .map_err(|err| map_not_found_err(err, id))? .try_into_blob() .map_err(|err| to_read_object_err(err, id))?; Ok(Box::new(Cursor::new(blob.take_data()))) } } /// Canonicalizes the given `path` except for the last `".git"` component. /// /// The last path component matters when opening a Git repo without `core.bare` /// config. This config is usually set, but the "repo" tool will set up such /// repositories and symlinks. Opening such repo with fully-canonicalized path /// would turn a colocated Git repo into a bare repo. pub fn canonicalize_git_repo_path(path: &Path) -> io::Result<PathBuf> { if path.ends_with(".git") { let workdir = path.parent().unwrap(); workdir.canonicalize().map(|dir| dir.join(".git")) } else { path.canonicalize() } } fn gix_open_opts_from_settings(settings: &UserSettings) -> gix::open::Options { let user_name = settings.user_name(); let user_email = settings.user_email(); gix::open::Options::default() .config_overrides([ // Committer has to be configured to record reflog. Author isn't // needed, but let's copy the same values. format!("author.name={user_name}"), format!("author.email={user_email}"), format!("committer.name={user_name}"), format!("committer.email={user_email}"), ]) // The git_target path should point the repository, not the working directory. .open_path_as_is(true) } fn commit_from_git_without_root_parent( id: &CommitId, git_object: &gix::Object, uses_tree_conflict_format: bool, ) -> Result<Commit, BackendError> { let commit = git_object .try_to_commit_ref() .map_err(|err| to_read_object_err(err, id))?; // We reverse the bits of the commit id to create the change id. We don't want // to use the first bytes unmodified because then it would be ambiguous // if a given hash prefix refers to the commit id or the change id. It // would have been enough to pick the last 16 bytes instead of the // leading 16 bytes to address that. We also reverse the bits to make it less // likely that users depend on any relationship between the two ids. let change_id = ChangeId::new( id.as_bytes()[4..HASH_LENGTH] .iter() .rev() .map(|b| b.reverse_bits()) .collect(), ); let parents = commit .parents() .map(|oid| CommitId::from_bytes(oid.as_bytes())) .collect_vec(); let tree_id = TreeId::from_bytes(commit.tree().as_bytes()); // If this commit is a conflict, we'll update the root tree later, when we read // the extra metadata. let root_tree = if uses_tree_conflict_format { MergedTreeId::resolved(tree_id) } else { MergedTreeId::Legacy(tree_id) }; // Use lossy conversion as commit message with "mojibake" is still better than // nothing. // TODO: what should we do with commit.encoding? let description = String::from_utf8_lossy(commit.message).into_owned(); let author = signature_from_git(commit.author()); let committer = signature_from_git(commit.committer()); // If the commit is signed, extract both the signature and the signed data // (which is the commit buffer with the gpgsig header omitted). // We have to re-parse the raw commit data because gix CommitRef does not give // us the sogned data, only the signature. // Ideally, we could use try_to_commit_ref_iter at the beginning of this // function and extract everything from that. For now, this works let secure_sig = commit .extra_headers .iter() // gix does not recognize gpgsig-sha256, but prevent future footguns by checking for it too .any(|(k, _)| *k == "gpgsig" || *k == "gpgsig-sha256") .then(|| CommitRefIter::signature(&git_object.data)) .transpose() .map_err(|err| to_read_object_err(err, id))? .flatten() .map(|(sig, data)| SecureSig { data: data.to_bstring().into(), sig: sig.into_owned().into(), }); Ok(Commit { parents, predecessors: vec![], // If this commit has associated extra metadata, we may reset this later. root_tree, change_id, description, author, committer, secure_sig, }) } const EMPTY_STRING_PLACEHOLDER: &str = "JJ_EMPTY_STRING"; fn signature_from_git(signature: gix::actor::SignatureRef) -> Signature { let name = signature.name;