// 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;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;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 { 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) } }} /// 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.", ); 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("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 { Self::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() { mut_repo.merge_local_bookmark(symbol.name, 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.Lines 1–500 of 2436. 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::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; 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; 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 { 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) } } } /// Helper struct that matches a refspec with its expected location in the /// remote it's being pushed to pub(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.", ); 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("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 { Self::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() { mut_repo.merge_local_bookmark(symbol.name, 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.