3616 lines · 134.0 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. #![expect(missing_docs)] use std::borrow::Borrow;use std::borrow::Cow;use std::collections::HashMap;use std::collections::HashSet;use std::default::Default;use std::ffi::OsString;use std::fs::File;use std::iter;use std::num::NonZeroU32;use std::path::Path;use std::path::PathBuf;use std::sync::Arc; use bstr::BStr;use bstr::BString;use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::stream;use gix::refspec::Instruction;use itertools::Itertools as _;use thiserror::Error; use crate::backend::BackendError;use crate::backend::ChangeId;use crate::backend::CommitId;use crate::backend::TreeValue;use crate::commit::Commit;use crate::config::ConfigGetError;use crate::file_util::IoResultExt as _;use crate::file_util::PathError;use crate::git_backend::GitBackend;use crate::git_subprocess::GitFetchStatus;pub use crate::git_subprocess::GitProgress;pub use crate::git_subprocess::GitSidebandLineTerminator;pub use crate::git_subprocess::GitSubprocessCallback;use crate::git_subprocess::GitSubprocessContext;use crate::git_subprocess::GitSubprocessError;use crate::index::IndexError;use crate::matchers::EverythingMatcher;use crate::merge::Diff;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;use crate::ref_name::WorkspaceName;use crate::repo::MutableRepo;use crate::repo::Repo;use crate::repo_path::RepoPath;use crate::revset::ResolvedRevsetExpression;use crate::revset::RevsetEvaluationError;use crate::revset::RevsetExpression;use crate::revset::RevsetStreamExt as _;use crate::settings::UserSettings;use crate::store::Store;use crate::str_util::StringExpression;use crate::str_util::StringMatcher;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/";/// Git ref prefix where remote bookmarks are stored.const REMOTE_BOOKMARK_REF_NAMESPACE: &str = "refs/remotes/";/// Git ref prefix where remote tags will be temporarily fetched.const REMOTE_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";/// 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(Clone, Debug)]pub struct GitSettings {    pub abandon_unreachable_commits: bool,    pub executable_path: PathBuf,    pub record_synthetic_predecessors: bool,    pub write_change_id_header: bool,} impl GitSettings {    pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {        Ok(Self {            abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,            executable_path: settings.get("git.executable-path")?,            record_synthetic_predecessors: settings                .get_bool("git.record-synthetic-predecessors")?,            write_change_id_header: settings.get("git.write-change-id-header")?,        })    }     pub fn to_subprocess_options(&self) -> GitSubprocessOptions {        GitSubprocessOptions {            executable_path: self.executable_path.clone(),            environment: HashMap::new(),        }    }} /// Configuration for a Git subprocess#[derive(Clone, Debug)]pub struct GitSubprocessOptions {    pub executable_path: PathBuf,    /// Used by consumers of jj-lib to set environment variables like    /// GIT_ASKPASS (for authentication callbacks) or GIT_TRACE (for debugging).    /// Setting per-subcommand environment variables avoids the need for unsafe    /// code and process-wide state.    pub environment: HashMap<OsString, OsString>,} impl GitSubprocessOptions {    pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {        Ok(Self {            executable_path: settings.get("git.executable-path")?,            environment: HashMap::new(),        })    }} #[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),    #[error("Invalid Git remote name")]    InvalidName(#[from] gix::remote::name::Error),} fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {    gix::remote::name::validated(name.as_str())?;    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(())    }} /// Converts [`CommitId`] of valid length to [`gix::oid`].fn oid_from_commit_id(id: &CommitId) -> &gix::oid {    gix::oid::from_bytes_unchecked(id.as_bytes())} /// Converts [`CommitId`] of valid length to [`gix::ObjectId`].fn owned_oid_from_commit_id(id: &CommitId) -> gix::ObjectId {    gix::ObjectId::from_bytes_or_panic(id.as_bytes())} /// 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(Debug, Default)]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>)>,    /// remote bookmarks that couldn't be exported to local Git repo    pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,} impl GitPushStats {    pub fn all_ok(&self) -> bool {        self.rejected.is_empty()            && self.remote_rejected.is_empty()            && self.unexported_bookmarks.is_empty()    }     /// Returns true if there are at least one bookmark that was successfully    /// pushed to the remote and exported to the local Git repo.    pub fn some_exported(&self) -> bool {        self.pushed.len() > self.unexported_bookmarks.len()    }} /// 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 {        Self {            forced: true,            source: Some(source.into()),            destination: destination.into(),        }    }     fn delete(destination: impl Into<String>) -> Self {        // We don't force push on branch deletion        Self {            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)        }    }} /// Representation of a negative Git refspec#[derive(Debug)]#[repr(transparent)]pub(crate) struct NegativeRefSpec {    source: String,} impl NegativeRefSpec {    fn new(source: impl Into<String>) -> Self {        Self {            source: source.into(),        }    }     pub(crate) fn to_git_format(&self) -> String {        format!("^{}", self.source)    }} /// 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 gix::oid>,} impl<'a> RefToPush<'a> {    fn new(        refspec: &'a RefSpec,        expected_locations: &'a HashMap<&GitRefName, Option<&gix::oid>>,    ) -> 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.",            );         Self {            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(REMOTE_BOOKMARK_REF_NAMESPACE)    {        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 parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {    let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;    let (remote, name) = remote_and_name.split_once('/')?;    if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {        return None;    }    let name = RefName::new(name);    let remote = RemoteName::new(remote);    Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))} 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!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())            }        }        GitRefKind::Tag => {            // Only local tags are mapped. Remote tags don't exist in Git world.            (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())        }    }} fn to_git_or_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> GitRefNameBuf {    let RemoteRefSymbol { name, remote } = symbol;    let name = name.as_str();    let remote = remote.as_str();    if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {        format!("refs/tags/{name}").into()    } else {        format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{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().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_commit_oid` (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_commit_oid: Option<&gix::oid>,) -> Option<gix::ObjectId> {    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(known_oid) = known_commit_oid {        let raw_ref = &git_ref.inner;        if let Some(oid) = raw_ref.target.try_id()            && oid == known_oid        {            return Some(oid.to_owned());        }        if let Some(oid) = raw_ref.peeled            && oid == known_oid        {            // Perhaps an annotated tag stored in packed-refs file, and pointing to the            // already known target commit.            return Some(oid);        }        // 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()) {                let oid = oid.detach();                if oid == known_oid {                    // An annotated tag pointing to the already known target commit.                    return Some(oid);                }                // 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);            }        }    }     // 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_some(peeled_id.detach())} #[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,    },

Lines 1–500 of 3616.