2929 lines · 108.9 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::borrow::Borrow;use std::borrow::Cow;use std::collections::HashMap;use std::collections::HashSet;use std::default::Default;use std::fs::File;use std::num::NonZeroU32;use std::path::PathBuf;use std::str;use std::sync::Arc; use bstr::BStr;use bstr::BString;use futures::StreamExt as _;use itertools::Itertools as _;use pollster::FutureExt as _;use thiserror::Error; use crate::backend::BackendError;use crate::backend::BackendResult;use crate::backend::CommitId;use crate::backend::TreeValue;use crate::commit::Commit;use crate::file_util::IoResultExt as _;use crate::file_util::PathError;use crate::git_backend::GitBackend;use crate::git_subprocess::GitSubprocessContext;use crate::git_subprocess::GitSubprocessError;#[cfg(feature = "git2")]use crate::index::Index;use crate::matchers::EverythingMatcher;use crate::merged_tree::MergedTree;use crate::merged_tree::TreeDiffEntry;use crate::object_id::ObjectId as _;use crate::op_store::RefTarget;use crate::op_store::RefTargetOptionExt as _;use crate::op_store::RemoteRef;use crate::op_store::RemoteRefState;use crate::ref_name::GitRefName;use crate::ref_name::GitRefNameBuf;use crate::ref_name::RefName;use crate::ref_name::RefNameBuf;use crate::ref_name::RemoteName;use crate::ref_name::RemoteNameBuf;use crate::ref_name::RemoteRefSymbol;use crate::ref_name::RemoteRefSymbolBuf;#[cfg(feature = "git2")]use crate::refs;use crate::refs::BookmarkPushUpdate;use crate::repo::MutableRepo;use crate::repo::Repo;use crate::repo_path::RepoPath;use crate::revset::RevsetExpression;use crate::settings::GitSettings;use crate::store::Store;use crate::str_util::StringPattern;use crate::view::View; /// Reserved remote name for the backing Git repo.pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");/// Git ref prefix that would conflict with the reserved "git" remote.pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";/// Ref name used as a placeholder to unset HEAD without a commit.const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";/// Dummy file to be added to the index to indicate that the user is editing a/// commit with a conflict that isn't represented in the Git index.const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict"; #[derive(Debug, Error)]pub enum GitRemoteNameError {    #[error(        "Git remote named '{name}' is reserved for local Git repository",        name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()    )]    ReservedForLocalGitRepo,    #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]    WithSlash(RemoteNameBuf),} fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {    if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {        Err(GitRemoteNameError::ReservedForLocalGitRepo)    } else if name.as_str().contains("/") {        Err(GitRemoteNameError::WithSlash(name.to_owned()))    } else {        Ok(())    }} /// Type of Git ref to be imported or exported.#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]pub enum GitRefKind {    Bookmark,    Tag,} /// Stats from a git push#[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct GitPushStats {    /// reference accepted by the remote    pub pushed: Vec<GitRefNameBuf>,    /// rejected reference, due to lease failure, with an optional reason    pub rejected: Vec<(GitRefNameBuf, Option<String>)>,    /// reference rejected by the remote, with an optional reason    pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,} impl GitPushStats {    pub fn all_ok(&self) -> bool {        self.rejected.is_empty() && self.remote_rejected.is_empty()    }} /// Newtype to look up `HashMap` entry by key of shorter lifetime.////// https://users.rust-lang.org/t/unexpected-lifetime-issue-with-hashmap-remove/113961/6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]struct RemoteRefKey<'a>(RemoteRefSymbol<'a>); impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {    fn borrow(&self) -> &RemoteRefSymbol<'b> {        &self.0    }} /// Representation of a Git refspec////// It is often the case that we need only parts of the refspec,/// Passing strings around and repeatedly parsing them is sub-optimal, confusing/// and error prone#[derive(Debug, Hash, PartialEq, Eq)]pub(crate) struct RefSpec {    forced: bool,    // Source and destination may be fully-qualified ref name, glob pattern, or    // object ID. The GitRefNameBuf type shouldn't be used.    source: Option<String>,    destination: String,} impl RefSpec {    fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {        RefSpec {            forced: true,            source: Some(source.into()),            destination: destination.into(),        }    }     fn delete(destination: impl Into<String>) -> Self {        // We don't force push on branch deletion        RefSpec {            forced: false,            source: None,            destination: destination.into(),        }    }     pub(crate) fn to_git_format(&self) -> String {        format!(            "{}{}",            if self.forced { "+" } else { "" },            self.to_git_format_not_forced()        )    }     /// Format git refspec without the leading force flag '+'    ///    /// When independently setting --force-with-lease, having the    /// leading flag overrides the lease, so we need to print it    /// without it    pub(crate) fn to_git_format_not_forced(&self) -> String {        if let Some(s) = &self.source {            format!("{}:{}", s, self.destination)        } else {            format!(":{}", self.destination)        }    }} /// Helper struct that matches a refspec with its expected location in the/// remote it's being pushed topub(crate) struct RefToPush<'a> {    pub(crate) refspec: &'a RefSpec,    pub(crate) expected_location: Option<&'a CommitId>,} impl<'a> RefToPush<'a> {    fn new(        refspec: &'a RefSpec,        expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,    ) -> Self {        let expected_location = *expected_locations            .get(GitRefName::new(&refspec.destination))            .expect(                "The refspecs and the expected locations were both constructed from the same \                 source of truth. This means the lookup should always work.",            );         RefToPush {            refspec,            expected_location,        }    }     pub(crate) fn to_git_lease(&self) -> String {        format!(            "{}:{}",            self.refspec.destination,            self.expected_location                .map(|x| x.to_string())                .as_deref()                .unwrap_or("")        )    }} /// Translates Git ref name to jj's `name@remote` symbol. Returns `None` if the/// ref cannot be represented in jj.pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {    if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {        // Git CLI says 'HEAD' is not a valid branch name        if name == "HEAD" {            return None;        }        let name = RefName::new(name);        let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;        Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))    } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {        let (remote, name) = remote_and_name.split_once('/')?;        // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch        if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {            return None;        }        let name = RefName::new(name);        let remote = RemoteName::new(remote);        Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))    } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {        let name = RefName::new(name);        let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;        Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))    } else {        None    }} fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {    let RemoteRefSymbol { name, remote } = symbol;    let name = name.as_str();    let remote = remote.as_str();    if name.is_empty() || remote.is_empty() {        return None;    }    match kind {        GitRefKind::Bookmark => {            if name == "HEAD" {                return None;            }            if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {                Some(format!("refs/heads/{name}").into())            } else {                Some(format!("refs/remotes/{remote}/{name}").into())            }        }        GitRefKind::Tag => {            (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())        }    }} #[derive(Debug, Error)]#[error("The repo is not backed by a Git repo")]pub struct UnexpectedGitBackendError; /// Returns the underlying `GitBackend` implementation.pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {    store        .backend_impl()        .downcast_ref()        .ok_or(UnexpectedGitBackendError)} /// Returns new thread-local instance to access to the underlying Git repo.pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {    get_git_backend(store).map(|backend| backend.git_repo())} /// Checks if `git_ref` points to a Git commit object, and returns its id.////// If the ref points to the previously `known_target` (i.e. unchanged), this/// should be faster than `git_ref.into_fully_peeled_id()`.fn resolve_git_ref_to_commit_id(    git_ref: &gix::Reference,    known_target: &RefTarget,) -> Option<CommitId> {    let mut peeling_ref = Cow::Borrowed(git_ref);     // Try fast path if we have a candidate id which is known to be a commit object.    if let Some(id) = known_target.as_normal() {        let raw_ref = &git_ref.inner;        if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {            return Some(id.clone());        }        if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {            // Perhaps an annotated tag stored in packed-refs file, and pointing to the            // already known target commit.            return Some(id.clone());        }        // A tag (according to ref name.) Try to peel one more level. This is slightly        // faster than recurse into into_fully_peeled_id(). If we recorded a tag oid, we        // could skip this at all.        if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {            let maybe_tag = git_ref                .try_id()                .and_then(|id| id.object().ok())                .and_then(|object| object.try_into_tag().ok());            if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {                if oid.as_bytes() == id.as_bytes() {                    // An annotated tag pointing to the already known target commit.                    return Some(id.clone());                }                // Unknown id. Recurse from the current state. A tag may point to                // non-commit object.                peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());            }        }    }     // Alternatively, we might want to inline the first half of the peeling    // loop. into_fully_peeled_id() looks up the target object to see if it's    // a tag or not, and we need to check if it's a commit object.    let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;    let is_commit = peeled_id        .object()        .is_ok_and(|object| object.kind.is_commit());    is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))} #[derive(Error, Debug)]pub enum GitImportError {    #[error("Failed to read Git HEAD target commit {id}")]    MissingHeadTarget {        id: CommitId,        #[source]        err: BackendError,    },    #[error("Ancestor of Git ref {symbol} is missing")]    MissingRefAncestor {        symbol: RemoteRefSymbolBuf,        #[source]        err: BackendError,    },    #[error(transparent)]    Backend(BackendError),    #[error(transparent)]    Git(Box<dyn std::error::Error + Send + Sync>),    #[error(transparent)]    UnexpectedBackend(#[from] UnexpectedGitBackendError),} impl GitImportError {    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {        GitImportError::Git(source.into())    }} /// Describes changes made by `import_refs()` or `fetch()`.#[derive(Clone, Debug, Eq, PartialEq, Default)]pub struct GitImportStats {    /// Commits superseded by newly imported commits.    pub abandoned_commits: Vec<CommitId>,    /// Remote bookmark `(symbol, (old_remote_ref, new_target))`s to be merged    /// in to the local bookmarks, sorted by `symbol`.    pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,    /// Remote tag `(symbol, (old_remote_ref, new_target))`s to be merged in to    /// the local tags, sorted by `symbol`.    pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,    /// Git ref names that couldn't be imported, sorted by name.    ///    /// This list doesn't include refs that are supposed to be ignored, such as    /// refs pointing to non-commit objects.    pub failed_ref_names: Vec<BString>,} #[derive(Debug)]struct RefsToImport {    /// Git ref `(full_name, new_target)`s to be copied to the view, sorted by    /// `full_name`.    changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,    /// Remote bookmark `(symbol, (old_remote_ref, new_target))`s to be merged    /// in to the local bookmarks, sorted by `symbol`.    changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,    /// Remote tag `(symbol, (old_remote_ref, new_target))`s to be merged in to    /// the local tags, sorted by `symbol`.    changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,    /// Git ref names that couldn't be imported, sorted by name.    failed_ref_names: Vec<BString>,} /// Reflect changes made in the underlying Git repo in the Jujutsu repo.////// This function detects conflicts (if both Git and JJ modified a bookmark) and/// records them in JJ's view.pub fn import_refs(    mut_repo: &mut MutableRepo,    git_settings: &GitSettings,) -> Result<GitImportStats, GitImportError> {    import_some_refs(mut_repo, git_settings, |_, _| true)} /// Reflect changes made in the underlying Git repo in the Jujutsu repo.////// Only bookmarks and tags whose remote symbol pass the filter will be/// considered for addition, update, or deletion.pub fn import_some_refs(    mut_repo: &mut MutableRepo,    git_settings: &GitSettings,    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,) -> Result<GitImportStats, GitImportError> {    let store = mut_repo.store();    let git_backend = get_git_backend(store)?;    let git_repo = git_backend.git_repo();     let RefsToImport {        changed_git_refs,        changed_remote_bookmarks,        changed_remote_tags,        failed_ref_names,    } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;     // Bulk-import all reachable Git commits to the backend to reduce overhead    // of table merging and ref updates.    //    // changed_remote_bookmarks/tags might contain new_targets that are not in    // changed_git_refs, but such targets should have already been imported to    // the backend.    let index = mut_repo.index();    let missing_head_ids = changed_git_refs        .iter()        .flat_map(|(_, new_target)| new_target.added_ids())        .filter(|&id| !index.has_id(id));    let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();     // Import new remote heads    let mut head_commits = Vec::new();    let get_commit = |id| {        // If bulk-import failed, try again to find bad head or ref.        if !heads_imported && !index.has_id(id) {            git_backend.import_head_commits([id])?;        }        store.get_commit(id)    };    for (symbol, (_, new_target)) in        itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)    {        for id in new_target.added_ids() {            let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {                symbol: symbol.clone(),                err,            })?;            head_commits.push(commit);        }    }    // It's unlikely the imported commits were missing, but I/O-related error    // can still occur.    mut_repo        .add_heads(&head_commits)        .map_err(GitImportError::Backend)?;     // Apply the change that happened in git since last time we imported refs.    for (full_name, new_target) in changed_git_refs {        mut_repo.set_git_ref_target(&full_name, new_target);    }    for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {        let symbol = symbol.as_ref();        let base_target = old_remote_ref.tracked_target();        let new_remote_ref = RemoteRef {            target: new_target.clone(),            state: if old_remote_ref.is_present() {                old_remote_ref.state            } else {                default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)            },        };        if new_remote_ref.is_tracked() {

Lines 1–500 of 2929.