// 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::PathBuf;use std::sync::Arc; use bstr::BStr;use bstr::BString;use futures::StreamExt as _;use futures::TryStreamExt as _;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::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, }, #[error(transparent)] Backend(#[from] BackendError), #[error(transparent)]Lines 1–500 of 3527. 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. #![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::PathBuf; use std::sync::Arc; use bstr::BStr; use bstr::BString; use futures::StreamExt as _; use futures::TryStreamExt as _; 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::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 to pub(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, }, #[error(transparent)] Backend(#[from] BackendError), #[error(transparent)]