Ilya GrigorievNightly clippy fixes96bf1902 years agoHistory
1969 lines · 74.4 KB · Rust
RawDownload
// 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.