1814 lines · 69.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::Cow;use std::collections::BTreeMap;use std::collections::HashMap;use std::collections::HashSet;use std::default::Default;use std::fmt;use std::io::Read;use std::num::NonZeroU32;use std::path::PathBuf;use std::str; use git2::Oid;use itertools::Itertools;use tempfile::NamedTempFile;use thiserror::Error; use crate::backend::BackendError;use crate::backend::CommitId;use crate::commit::Commit;use crate::git_backend::GitBackend;use crate::index::Index;use crate::object_id::ObjectId;use crate::op_store::RefTarget;use crate::op_store::RefTargetOptionExt;use crate::op_store::RemoteRef;use crate::op_store::RemoteRefState;use crate::refs;use crate::refs::BookmarkPushUpdate;use crate::repo::MutableRepo;use crate::repo::Repo;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: &str = "git";/// Ref name used as a placeholder to unset HEAD without a commit.const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root"; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)]pub enum RefName {    LocalBranch(String),    RemoteBranch { branch: String, remote: String },    Tag(String),} impl fmt::Display for RefName {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            RefName::LocalBranch(name) => write!(f, "{name}"),            RefName::RemoteBranch { branch, remote } => write!(f, "{branch}@{remote}"),            RefName::Tag(name) => write!(f, "{name}"),        }    }} pub fn parse_git_ref(ref_name: &str) -> Option<RefName> {    if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {        // Git CLI says 'HEAD' is not a valid branch name        (branch_name != "HEAD").then(|| RefName::LocalBranch(branch_name.to_string()))    } else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") {        remote_and_branch            .split_once('/')            // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch            .filter(|&(_, branch)| branch != "HEAD")            .map(|(remote, branch)| RefName::RemoteBranch {                remote: remote.to_string(),                branch: branch.to_string(),            })    } else {        ref_name            .strip_prefix("refs/tags/")            .map(|tag_name| RefName::Tag(tag_name.to_string()))    }} fn to_git_ref_name(parsed_ref: &RefName) -> Option<String> {    match parsed_ref {        RefName::LocalBranch(branch) => {            (!branch.is_empty() && branch != "HEAD").then(|| format!("refs/heads/{branch}"))        }        RefName::RemoteBranch { branch, remote } => (!branch.is_empty() && branch != "HEAD")            .then(|| format!("refs/remotes/{remote}/{branch}")),        RefName::Tag(tag) => Some(format!("refs/tags/{tag}")),    }} fn to_remote_branch<'a>(parsed_ref: &'a RefName, remote_name: &str) -> Option<&'a str> {    match parsed_ref {        RefName::RemoteBranch { branch, remote } => (remote == remote_name).then_some(branch),        RefName::LocalBranch(..) | RefName::Tag(..) => None,    }} /// Returns true if the `parsed_ref` won't be imported because its remote name/// is reserved.////// Use this as a negative `git_ref_filter` to be passed in to/// `import_some_refs()`.pub fn is_reserved_git_remote_ref(parsed_ref: &RefName) -> bool {    to_remote_branch(parsed_ref, REMOTE_NAME_FOR_LOCAL_GIT_REPO).is_some()} fn get_git_backend(store: &Store) -> Option<&GitBackend> {    store.backend_impl().downcast_ref()} fn get_git_repo(store: &Store) -> Option<gix::Repository> {    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()        .map_or(false, |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 {ref_name} is missing")]    MissingRefAncestor {        ref_name: String,        #[source]        err: BackendError,    },    #[error(        "Git remote named '{name}' is reserved for local Git repository",        name = REMOTE_NAME_FOR_LOCAL_GIT_REPO    )]    RemoteReservedForLocalGitRepo,    #[error("Unexpected backend error when importing refs")]    InternalBackend(#[source] BackendError),    #[error("Unexpected git error when importing refs")]    InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),    #[error("The repo is not backed by a Git repo")]    UnexpectedBackend,} impl GitImportError {    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {        GitImportError::InternalGitError(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 `(ref_name, (old_remote_ref, new_target))`s to be merged in to    /// the local refs.    pub changed_remote_refs: BTreeMap<RefName, (RemoteRef, RefTarget)>,} #[derive(Debug)]struct RefsToImport {    /// Git ref `(full_name, new_target)`s to be copied to the view.    changed_git_refs: Vec<(String, RefTarget)>,    /// Remote `(ref_name, (old_remote_ref, new_target))`s to be merged in to    /// the local refs.    changed_remote_refs: BTreeMap<RefName, (RemoteRef, RefTarget)>,} /// 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 branches whose git full reference name 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(&RefName) -> bool,) -> Result<GitImportStats, GitImportError> {    let store = mut_repo.store();    let git_backend = get_git_backend(store).ok_or(GitImportError::UnexpectedBackend)?;    let git_repo = git_backend.git_repo();     let RefsToImport {        changed_git_refs,        changed_remote_refs,    } = 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_refs 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 (ref_name, (_, new_target)) in &changed_remote_refs {        for id in new_target.added_ids() {            let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {                ref_name: ref_name.to_string(),                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::InternalBackend)?;     // 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 (ref_name, (old_remote_ref, new_target)) in &changed_remote_refs {        let base_target = old_remote_ref.tracking_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(ref_name, git_settings)            },        };        match ref_name {            RefName::LocalBranch(branch) => {                if new_remote_ref.is_tracking() {                    mut_repo.merge_local_bookmark(branch, base_target, &new_remote_ref.target);                }                // Update Git-tracking branch like the other remote branches.                mut_repo.set_remote_bookmark(                    branch,                    REMOTE_NAME_FOR_LOCAL_GIT_REPO,                    new_remote_ref,                );            }            RefName::RemoteBranch { branch, remote } => {                if new_remote_ref.is_tracking() {                    mut_repo.merge_local_bookmark(branch, base_target, &new_remote_ref.target);                }                // Remote-tracking branch is the last known state of the branch in the remote.                // It shouldn't diverge even if we had inconsistent view.                mut_repo.set_remote_bookmark(branch, remote, new_remote_ref);            }            RefName::Tag(name) => {                if new_remote_ref.is_tracking() {                    mut_repo.merge_tag(name, base_target, &new_remote_ref.target);                }                // TODO: If we add Git-tracking tag, it will be updated here.            }        }    }     let abandoned_commits = if git_settings.abandon_unreachable_commits {        abandon_unreachable_commits(mut_repo, &changed_remote_refs)    } else {        vec![]    };    let stats = GitImportStats {        abandoned_commits,        changed_remote_refs,    };    Ok(stats)} /// Finds commits that used to be reachable in git that no longer are reachable./// Those commits will be recorded as abandoned in the `MutableRepo`.fn abandon_unreachable_commits(    mut_repo: &mut MutableRepo,    changed_remote_refs: &BTreeMap<RefName, (RemoteRef, RefTarget)>,) -> Vec<CommitId> {    let hidable_git_heads = changed_remote_refs        .values()        .flat_map(|(old_remote_ref, _)| old_remote_ref.target.added_ids())        .cloned()        .collect_vec();    if hidable_git_heads.is_empty() {        return vec![];    }    let pinned_expression = RevsetExpression::union_all(&[        // Local refs are usually visible, no need to filter out hidden        RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),        RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))            // Hidden remote branches should not contribute to pinning            .intersection(&RevsetExpression::visible_heads().ancestors()),        RevsetExpression::root(),    ]);    let abandoned_expression = pinned_expression        .range(&RevsetExpression::commits(hidable_git_heads))        // Don't include already-abandoned commits in GitImportStats        .intersection(&RevsetExpression::visible_heads().ancestors());    let abandoned_commits = abandoned_expression        .evaluate(mut_repo)        .unwrap()        .iter()        .map(Result::unwrap) // TODO: Return error to caller        .collect_vec();    for abandoned_commit in &abandoned_commits {        mut_repo.record_abandoned_commit(abandoned_commit.clone());    }    abandoned_commits} /// Calculates diff of git refs to be imported.fn diff_refs_to_import(    view: &View,    git_repo: &gix::Repository,    git_ref_filter: impl Fn(&RefName) -> bool,) -> Result<RefsToImport, GitImportError> {    let mut known_git_refs: HashMap<&str, &RefTarget> = view        .git_refs()        .iter()        .filter_map(|(full_name, target)| {            // TODO: or clean up invalid ref in case it was stored due to historical bug?            let ref_name = parse_git_ref(full_name).expect("stored git ref should be parsable");            git_ref_filter(&ref_name).then_some((full_name.as_ref(), target))        })        .collect();    // TODO: migrate tags to the remote view, and don't destructure &RemoteRef    let mut known_remote_refs: HashMap<RefName, (&RefTarget, RemoteRefState)> = itertools::chain(        view.all_remote_bookmarks()            .map(|((branch, remote), remote_ref)| {                // TODO: want to abstract local ref as "git" tracking remote, but                // we'll probably need to refactor the git_ref_filter API first.                let ref_name = if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {                    RefName::LocalBranch(branch.to_owned())                } else {                    RefName::RemoteBranch {                        branch: branch.to_owned(),                        remote: remote.to_owned(),                    }                };                let RemoteRef { target, state } = remote_ref;                (ref_name, (target, *state))            }),        // TODO: compare to tags stored in the "git" remote view. Since tags should never        // be moved locally in jj, we can consider local tags as merge base.        view.tags().iter().map(|(name, target)| {            let ref_name = RefName::Tag(name.to_owned());            (ref_name, (target, RemoteRefState::Tracking))        }),    )    .filter(|(ref_name, _)| git_ref_filter(ref_name))    .collect();     let mut changed_git_refs = Vec::new();    let mut changed_remote_refs = BTreeMap::new();    let git_references = git_repo.references().map_err(GitImportError::from_git)?;    let chain_git_refs_iters = || -> Result<_, gix::reference::iter::init::Error> {        // Exclude uninteresting directories such as refs/jj/keep.        Ok(itertools::chain!(            git_references.local_branches()?,            git_references.remote_branches()?,            git_references.tags()?,        ))    };    for git_ref in chain_git_refs_iters().map_err(GitImportError::from_git)? {        let git_ref = git_ref.map_err(GitImportError::from_git)?;        let Ok(full_name) = str::from_utf8(git_ref.name().as_bstr()) else {            // Skip non-utf8 refs.            continue;        };        let Some(ref_name) = parse_git_ref(full_name) else {            // Skip other refs (such as notes) and symbolic refs.            continue;        };        if !git_ref_filter(&ref_name) {            continue;        }        if is_reserved_git_remote_ref(&ref_name) {            return Err(GitImportError::RemoteReservedForLocalGitRepo);        }        let old_git_target = known_git_refs.get(full_name).copied().flatten();        let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {            // Skip (or remove existing) invalid refs.            continue;        };        let new_target = RefTarget::normal(id);        known_git_refs.remove(full_name);        if new_target != *old_git_target {            changed_git_refs.push((full_name.to_owned(), new_target.clone()));        }        // TODO: Make it configurable which remotes are publishing and update public        // heads here.        let (old_remote_target, old_remote_state) = known_remote_refs            .remove(&ref_name)            .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));        if new_target != *old_remote_target {            let old_remote_ref = RemoteRef {                target: old_remote_target.clone(),                state: old_remote_state,            };            changed_remote_refs.insert(ref_name, (old_remote_ref, new_target));        }    }    for full_name in known_git_refs.into_keys() {        changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));    }    for (ref_name, (old_target, old_state)) in known_remote_refs {        let old_remote_ref = RemoteRef {            target: old_target.clone(),            state: old_state,        };        changed_remote_refs.insert(ref_name, (old_remote_ref, RefTarget::absent()));    }    Ok(RefsToImport {        changed_git_refs,        changed_remote_refs,    })} fn default_remote_ref_state_for(ref_name: &RefName, git_settings: &GitSettings) -> RemoteRefState {

Lines 1–500 of 1814.