just playing with tangled
at globpattern 2715 lines 104 kB view raw
1// Copyright 2020 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15#![allow(missing_docs)] 16 17use std::borrow::Borrow; 18use std::borrow::Cow; 19use std::collections::BTreeMap; 20use std::collections::HashMap; 21#[cfg(feature = "git2")] 22use std::collections::HashSet; 23use std::default::Default; 24use std::fmt; 25use std::fs::File; 26use std::num::NonZeroU32; 27use std::path::PathBuf; 28use std::str; 29 30use bstr::BStr; 31use bstr::BString; 32use itertools::Itertools as _; 33use thiserror::Error; 34 35use crate::backend::BackendError; 36use crate::backend::BackendResult; 37use crate::backend::CommitId; 38use crate::backend::TreeValue; 39use crate::commit::Commit; 40use crate::file_util::IoResultExt as _; 41use crate::file_util::PathError; 42use crate::git_backend::GitBackend; 43use crate::git_subprocess::GitSubprocessContext; 44use crate::git_subprocess::GitSubprocessError; 45#[cfg(feature = "git2")] 46use crate::index::Index; 47use crate::merged_tree::MergedTree; 48use crate::object_id::ObjectId as _; 49use crate::op_store::RefTarget; 50use crate::op_store::RefTargetOptionExt as _; 51use crate::op_store::RemoteRef; 52use crate::op_store::RemoteRefState; 53#[cfg(feature = "git2")] 54use crate::refs; 55use crate::refs::BookmarkPushUpdate; 56use crate::refs::RemoteRefSymbol; 57use crate::refs::RemoteRefSymbolBuf; 58use crate::repo::MutableRepo; 59use crate::repo::Repo; 60use crate::repo_path::RepoPath; 61use crate::revset::RevsetExpression; 62use crate::settings::GitSettings; 63use crate::store::Store; 64use crate::str_util::StringPattern; 65use crate::view::View; 66 67/// Reserved remote name for the backing Git repo. 68pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &str = "git"; 69/// Git ref prefix that would conflict with the reserved "git" remote. 70pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/"; 71/// Ref name used as a placeholder to unset HEAD without a commit. 72const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root"; 73/// Dummy file to be added to the index to indicate that the user is editing a 74/// commit with a conflict that isn't represented in the Git index. 75const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict"; 76 77#[derive(Debug, Error)] 78pub enum GitRemoteNameError { 79 #[error( 80 "Git remote named '{name}' is reserved for local Git repository", 81 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO 82 )] 83 ReservedForLocalGitRepo, 84 #[error("Git remotes with slashes are incompatible with jj: {0}")] 85 WithSlash(String), 86} 87 88fn validate_remote_name(name: &str) -> Result<(), GitRemoteNameError> { 89 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO { 90 Err(GitRemoteNameError::ReservedForLocalGitRepo) 91 } else if name.contains("/") { 92 Err(GitRemoteNameError::WithSlash(name.to_owned())) 93 } else { 94 Ok(()) 95 } 96} 97 98/// Type of Git ref to be imported or exported. 99#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 100pub enum GitRefKind { 101 Bookmark, 102 Tag, 103} 104 105/// Newtype to look up `HashMap` entry by key of shorter lifetime. 106/// 107/// https://users.rust-lang.org/t/unexpected-lifetime-issue-with-hashmap-remove/113961/6 108#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 109struct RemoteRefKey<'a>((GitRefKind, RemoteRefSymbol<'a>)); 110 111impl<'a: 'b, 'b> Borrow<(GitRefKind, RemoteRefSymbol<'b>)> for RemoteRefKey<'a> { 112 fn borrow(&self) -> &(GitRefKind, RemoteRefSymbol<'b>) { 113 &self.0 114 } 115} 116 117// TODO: will be replaced with (GitRefKind, RemoteRefSymbol) 118#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] 119pub enum RefName { 120 LocalBranch(String), 121 RemoteBranch(RemoteRefSymbolBuf), 122 Tag(String), 123} 124 125impl fmt::Display for RefName { 126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 match self { 128 RefName::LocalBranch(name) => write!(f, "{name}"), 129 RefName::RemoteBranch(symbol) => write!(f, "{symbol}"), 130 RefName::Tag(name) => write!(f, "{name}"), 131 } 132 } 133} 134 135/// Representation of a Git refspec 136/// 137/// It is often the case that we need only parts of the refspec, 138/// Passing strings around and repeatedly parsing them is sub-optimal, confusing 139/// and error prone 140#[derive(Debug, Hash, PartialEq, Eq)] 141pub(crate) struct RefSpec { 142 forced: bool, 143 source: Option<String>, 144 destination: String, 145} 146 147impl RefSpec { 148 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self { 149 RefSpec { 150 forced: true, 151 source: Some(source.into()), 152 destination: destination.into(), 153 } 154 } 155 156 fn delete(destination: impl Into<String>) -> Self { 157 // We don't force push on branch deletion 158 RefSpec { 159 forced: false, 160 source: None, 161 destination: destination.into(), 162 } 163 } 164 165 pub(crate) fn to_git_format(&self) -> String { 166 format!( 167 "{}{}", 168 if self.forced { "+" } else { "" }, 169 self.to_git_format_not_forced() 170 ) 171 } 172 173 /// Format git refspec without the leading force flag '+' 174 /// 175 /// When independently setting --force-with-lease, having the 176 /// leading flag overrides the lease, so we need to print it 177 /// without it 178 pub(crate) fn to_git_format_not_forced(&self) -> String { 179 if let Some(s) = &self.source { 180 format!("{}:{}", s, self.destination) 181 } else { 182 format!(":{}", self.destination) 183 } 184 } 185} 186 187/// Helper struct that matches a refspec with its expected location in the 188/// remote it's being pushed to 189pub(crate) struct RefToPush<'a> { 190 pub(crate) refspec: &'a RefSpec, 191 pub(crate) expected_location: Option<&'a CommitId>, 192} 193 194impl<'a> RefToPush<'a> { 195 fn new(refspec: &'a RefSpec, expected_locations: &'a HashMap<&str, Option<&CommitId>>) -> Self { 196 let expected_location = *expected_locations.get(refspec.destination.as_str()).expect( 197 "The refspecs and the expected locations were both constructed from the same source \ 198 of truth. This means the lookup should always work.", 199 ); 200 201 RefToPush { 202 refspec, 203 expected_location, 204 } 205 } 206 207 pub(crate) fn to_git_lease(&self) -> String { 208 format!( 209 "{}:{}", 210 self.refspec.destination, 211 self.expected_location 212 .map(|x| x.to_string()) 213 .as_deref() 214 .unwrap_or("") 215 ) 216 } 217} 218 219pub fn parse_git_ref(full_name: &str) -> Option<RefName> { 220 let (kind, symbol) = parse_git_ref_inner(full_name)?; 221 Some(to_legacy_ref_name(kind, symbol)) 222} 223 224/// Translates Git ref name to jj's `name@remote` symbol. Returns `None` if the 225/// ref cannot be represented in jj. 226// TODO: replace parse_git_ref() 227fn parse_git_ref_inner(full_name: &str) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> { 228 if let Some(name) = full_name.strip_prefix("refs/heads/") { 229 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO; 230 // Git CLI says 'HEAD' is not a valid branch name 231 (name != "HEAD").then_some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote })) 232 } else if let Some(remote_and_name) = full_name.strip_prefix("refs/remotes/") { 233 let (remote, name) = remote_and_name.split_once('/')?; 234 // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch 235 (remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO && name != "HEAD") 236 .then_some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote })) 237 } else if let Some(name) = full_name.strip_prefix("refs/tags/") { 238 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO; 239 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote })) 240 } else { 241 None 242 } 243} 244 245fn to_legacy_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> RefName { 246 match kind { 247 GitRefKind::Bookmark => { 248 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO { 249 RefName::LocalBranch(symbol.name.to_owned()) 250 } else { 251 RefName::RemoteBranch(symbol.to_owned()) 252 } 253 } 254 GitRefKind::Tag => { 255 assert_eq!(symbol.remote, REMOTE_NAME_FOR_LOCAL_GIT_REPO); 256 RefName::Tag(symbol.name.to_owned()) 257 } 258 } 259} 260 261fn to_git_ref_name(parsed_ref: &RefName) -> Option<String> { 262 match parsed_ref { 263 RefName::LocalBranch(branch) => { 264 (!branch.is_empty() && branch != "HEAD").then(|| format!("refs/heads/{branch}")) 265 } 266 RefName::RemoteBranch(RemoteRefSymbolBuf { name, remote }) => { 267 (!name.is_empty() && name != "HEAD").then(|| format!("refs/remotes/{remote}/{name}")) 268 } 269 RefName::Tag(tag) => Some(format!("refs/tags/{tag}")), 270 } 271} 272 273#[derive(Debug, Error)] 274#[error("The repo is not backed by a Git repo")] 275pub struct UnexpectedGitBackendError; 276 277/// Returns the underlying `GitBackend` implementation. 278pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> { 279 store 280 .backend_impl() 281 .downcast_ref() 282 .ok_or(UnexpectedGitBackendError) 283} 284 285/// Returns new thread-local instance to access to the underlying Git repo. 286pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> { 287 get_git_backend(store).map(|backend| backend.git_repo()) 288} 289 290/// Checks if `git_ref` points to a Git commit object, and returns its id. 291/// 292/// If the ref points to the previously `known_target` (i.e. unchanged), this 293/// should be faster than `git_ref.into_fully_peeled_id()`. 294fn resolve_git_ref_to_commit_id( 295 git_ref: &gix::Reference, 296 known_target: &RefTarget, 297) -> Option<CommitId> { 298 let mut peeling_ref = Cow::Borrowed(git_ref); 299 300 // Try fast path if we have a candidate id which is known to be a commit object. 301 if let Some(id) = known_target.as_normal() { 302 let raw_ref = &git_ref.inner; 303 if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) { 304 return Some(id.clone()); 305 } 306 if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) { 307 // Perhaps an annotated tag stored in packed-refs file, and pointing to the 308 // already known target commit. 309 return Some(id.clone()); 310 } 311 // A tag (according to ref name.) Try to peel one more level. This is slightly 312 // faster than recurse into into_fully_peeled_id(). If we recorded a tag oid, we 313 // could skip this at all. 314 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") { 315 let maybe_tag = git_ref 316 .try_id() 317 .and_then(|id| id.object().ok()) 318 .and_then(|object| object.try_into_tag().ok()); 319 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) { 320 if oid.as_bytes() == id.as_bytes() { 321 // An annotated tag pointing to the already known target commit. 322 return Some(id.clone()); 323 } 324 // Unknown id. Recurse from the current state. A tag may point to 325 // non-commit object. 326 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach()); 327 } 328 } 329 } 330 331 // Alternatively, we might want to inline the first half of the peeling 332 // loop. into_fully_peeled_id() looks up the target object to see if it's 333 // a tag or not, and we need to check if it's a commit object. 334 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?; 335 let is_commit = peeled_id 336 .object() 337 .is_ok_and(|object| object.kind.is_commit()); 338 is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes())) 339} 340 341#[derive(Error, Debug)] 342pub enum GitImportError { 343 #[error("Failed to read Git HEAD target commit {id}")] 344 MissingHeadTarget { 345 id: CommitId, 346 #[source] 347 err: BackendError, 348 }, 349 #[error("Ancestor of Git ref {symbol} is missing")] 350 MissingRefAncestor { 351 symbol: RemoteRefSymbolBuf, 352 #[source] 353 err: BackendError, 354 }, 355 #[error("Unexpected backend error when importing refs")] 356 InternalBackend(#[source] BackendError), 357 #[error("Unexpected git error when importing refs")] 358 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>), 359 #[error(transparent)] 360 UnexpectedBackend(#[from] UnexpectedGitBackendError), 361} 362 363impl GitImportError { 364 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self { 365 GitImportError::InternalGitError(source.into()) 366 } 367} 368 369/// Describes changes made by `import_refs()` or `fetch()`. 370#[derive(Clone, Debug, Eq, PartialEq, Default)] 371pub struct GitImportStats { 372 /// Commits superseded by newly imported commits. 373 pub abandoned_commits: Vec<CommitId>, 374 /// Remote `((kind, symbol), (old_remote_ref, new_target))`s to be merged in 375 /// to the local refs. 376 pub changed_remote_refs: BTreeMap<(GitRefKind, RemoteRefSymbolBuf), (RemoteRef, RefTarget)>, 377 /// Git ref names that couldn't be imported. 378 /// 379 /// This list doesn't include refs that are supposed to be ignored, such as 380 /// refs pointing to non-commit objects. 381 pub failed_ref_names: Vec<BString>, 382} 383 384#[derive(Debug)] 385struct RefsToImport { 386 /// Git ref `(full_name, new_target)`s to be copied to the view. 387 changed_git_refs: Vec<(String, RefTarget)>, 388 /// Remote `((kind, symbol), (old_remote_ref, new_target))`s to be merged in 389 /// to the local refs. 390 changed_remote_refs: BTreeMap<(GitRefKind, RemoteRefSymbolBuf), (RemoteRef, RefTarget)>, 391 /// Git ref names that couldn't be imported. 392 failed_ref_names: Vec<BString>, 393} 394 395/// Reflect changes made in the underlying Git repo in the Jujutsu repo. 396/// 397/// This function detects conflicts (if both Git and JJ modified a bookmark) and 398/// records them in JJ's view. 399pub fn import_refs( 400 mut_repo: &mut MutableRepo, 401 git_settings: &GitSettings, 402) -> Result<GitImportStats, GitImportError> { 403 import_some_refs(mut_repo, git_settings, |_, _| true) 404} 405 406/// Reflect changes made in the underlying Git repo in the Jujutsu repo. 407/// 408/// Only bookmarks and tags whose remote symbol pass the filter will be 409/// considered for addition, update, or deletion. 410pub fn import_some_refs( 411 mut_repo: &mut MutableRepo, 412 git_settings: &GitSettings, 413 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool, 414) -> Result<GitImportStats, GitImportError> { 415 let store = mut_repo.store(); 416 let git_backend = get_git_backend(store)?; 417 let git_repo = git_backend.git_repo(); 418 419 let RefsToImport { 420 changed_git_refs, 421 changed_remote_refs, 422 failed_ref_names, 423 } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?; 424 425 // Bulk-import all reachable Git commits to the backend to reduce overhead 426 // of table merging and ref updates. 427 // 428 // changed_remote_refs might contain new_targets that are not in 429 // changed_git_refs, but such targets should have already been imported to 430 // the backend. 431 let index = mut_repo.index(); 432 let missing_head_ids = changed_git_refs 433 .iter() 434 .flat_map(|(_, new_target)| new_target.added_ids()) 435 .filter(|&id| !index.has_id(id)); 436 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok(); 437 438 // Import new remote heads 439 let mut head_commits = Vec::new(); 440 let get_commit = |id| { 441 // If bulk-import failed, try again to find bad head or ref. 442 if !heads_imported && !index.has_id(id) { 443 git_backend.import_head_commits([id])?; 444 } 445 store.get_commit(id) 446 }; 447 for ((_, symbol), (_, new_target)) in &changed_remote_refs { 448 for id in new_target.added_ids() { 449 let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor { 450 symbol: symbol.clone(), 451 err, 452 })?; 453 head_commits.push(commit); 454 } 455 } 456 // It's unlikely the imported commits were missing, but I/O-related error 457 // can still occur. 458 mut_repo 459 .add_heads(&head_commits) 460 .map_err(GitImportError::InternalBackend)?; 461 462 // Apply the change that happened in git since last time we imported refs. 463 for (full_name, new_target) in changed_git_refs { 464 mut_repo.set_git_ref_target(&full_name, new_target); 465 } 466 for ((kind, symbol), (old_remote_ref, new_target)) in &changed_remote_refs { 467 let symbol = symbol.as_ref(); 468 let base_target = old_remote_ref.tracking_target(); 469 let new_remote_ref = RemoteRef { 470 target: new_target.clone(), 471 state: if old_remote_ref.is_present() { 472 old_remote_ref.state 473 } else { 474 default_remote_ref_state_for(*kind, symbol, git_settings) 475 }, 476 }; 477 match kind { 478 GitRefKind::Bookmark => { 479 if new_remote_ref.is_tracking() { 480 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target); 481 } 482 // Remote-tracking branch is the last known state of the branch in the remote. 483 // It shouldn't diverge even if we had inconsistent view. 484 mut_repo.set_remote_bookmark(symbol, new_remote_ref); 485 } 486 GitRefKind::Tag => { 487 if new_remote_ref.is_tracking() { 488 mut_repo.merge_tag(symbol.name, base_target, &new_remote_ref.target); 489 } 490 // TODO: If we add Git-tracking tag, it will be updated here. 491 } 492 } 493 } 494 495 let abandoned_commits = if git_settings.abandon_unreachable_commits { 496 abandon_unreachable_commits(mut_repo, &changed_remote_refs) 497 .map_err(GitImportError::InternalBackend)? 498 } else { 499 vec![] 500 }; 501 let stats = GitImportStats { 502 abandoned_commits, 503 changed_remote_refs, 504 failed_ref_names, 505 }; 506 Ok(stats) 507} 508 509/// Finds commits that used to be reachable in git that no longer are reachable. 510/// Those commits will be recorded as abandoned in the `MutableRepo`. 511fn abandon_unreachable_commits( 512 mut_repo: &mut MutableRepo, 513 changed_remote_refs: &BTreeMap<(GitRefKind, RemoteRefSymbolBuf), (RemoteRef, RefTarget)>, 514) -> BackendResult<Vec<CommitId>> { 515 let hidable_git_heads = changed_remote_refs 516 .values() 517 .flat_map(|(old_remote_ref, _)| old_remote_ref.target.added_ids()) 518 .cloned() 519 .collect_vec(); 520 if hidable_git_heads.is_empty() { 521 return Ok(vec![]); 522 } 523 let pinned_expression = RevsetExpression::union_all(&[ 524 // Local refs are usually visible, no need to filter out hidden 525 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())), 526 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view())) 527 // Hidden remote branches should not contribute to pinning 528 .intersection(&RevsetExpression::visible_heads().ancestors()), 529 RevsetExpression::root(), 530 ]); 531 let abandoned_expression = pinned_expression 532 .range(&RevsetExpression::commits(hidable_git_heads)) 533 // Don't include already-abandoned commits in GitImportStats 534 .intersection(&RevsetExpression::visible_heads().ancestors()); 535 let abandoned_commit_ids: Vec<_> = abandoned_expression 536 .evaluate(mut_repo) 537 .map_err(|err| err.expect_backend_error())? 538 .iter() 539 .try_collect() 540 .map_err(|err| err.expect_backend_error())?; 541 for id in &abandoned_commit_ids { 542 let commit = mut_repo.store().get_commit(id)?; 543 mut_repo.record_abandoned_commit(&commit); 544 } 545 Ok(abandoned_commit_ids) 546} 547 548/// Calculates diff of git refs to be imported. 549fn diff_refs_to_import( 550 view: &View, 551 git_repo: &gix::Repository, 552 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool, 553) -> Result<RefsToImport, GitImportError> { 554 let mut known_git_refs: HashMap<&str, &RefTarget> = view 555 .git_refs() 556 .iter() 557 .filter_map(|(full_name, target)| { 558 // TODO: or clean up invalid ref in case it was stored due to historical bug? 559 let (kind, symbol) = 560 parse_git_ref_inner(full_name).expect("stored git ref should be parsable"); 561 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target)) 562 }) 563 .collect(); 564 // TODO: migrate tags to the remote view, and don't destructure &RemoteRef 565 let mut known_remote_refs: HashMap<RemoteRefKey, (&RefTarget, RemoteRefState)> = 566 itertools::chain( 567 view.all_remote_bookmarks().map(|(symbol, remote_ref)| { 568 let RemoteRef { target, state } = remote_ref; 569 ((GitRefKind::Bookmark, symbol), (target, *state)) 570 }), 571 // TODO: compare to tags stored in the "git" remote view. Since tags should never 572 // be moved locally in jj, we can consider local tags as merge base. 573 view.tags().iter().map(|(name, target)| { 574 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO; 575 let symbol = RemoteRefSymbol { name, remote }; 576 let state = RemoteRefState::Tracking; 577 ((GitRefKind::Tag, symbol), (target, state)) 578 }), 579 ) 580 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol)) 581 .map(|((kind, symbol), remote_ref)| (RemoteRefKey((kind, symbol)), remote_ref)) 582 .collect(); 583 584 let mut changed_git_refs = Vec::new(); 585 let mut changed_remote_refs = BTreeMap::new(); 586 let mut failed_ref_names = Vec::new(); 587 let git_references = git_repo.references().map_err(GitImportError::from_git)?; 588 let chain_git_refs_iters = || -> Result<_, gix::reference::iter::init::Error> { 589 // Exclude uninteresting directories such as refs/jj/keep. 590 Ok(itertools::chain!( 591 git_references.local_branches()?, 592 git_references.remote_branches()?, 593 git_references.tags()?, 594 )) 595 }; 596 for git_ref in chain_git_refs_iters().map_err(GitImportError::from_git)? { 597 let git_ref = git_ref.map_err(GitImportError::from_git)?; 598 let full_name_bytes = git_ref.name().as_bstr(); 599 let Ok(full_name) = str::from_utf8(full_name_bytes) else { 600 // Non-utf8 refs cannot be imported. 601 failed_ref_names.push(full_name_bytes.to_owned()); 602 continue; 603 }; 604 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) { 605 failed_ref_names.push(full_name_bytes.to_owned()); 606 continue; 607 } 608 let Some((kind, symbol)) = parse_git_ref_inner(full_name) else { 609 // Skip special refs such as refs/remotes/*/HEAD. 610 continue; 611 }; 612 if !git_ref_filter(kind, symbol) { 613 continue; 614 } 615 let old_git_target = known_git_refs.get(full_name).copied().flatten(); 616 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else { 617 // Skip (or remove existing) invalid refs. 618 continue; 619 }; 620 let new_target = RefTarget::normal(id); 621 known_git_refs.remove(full_name); 622 if new_target != *old_git_target { 623 changed_git_refs.push((full_name.to_owned(), new_target.clone())); 624 } 625 // TODO: Make it configurable which remotes are publishing and update public 626 // heads here. 627 let (old_remote_target, old_remote_state) = known_remote_refs 628 .remove(&(kind, symbol)) 629 .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New)); 630 if new_target != *old_remote_target { 631 let old_remote_ref = RemoteRef { 632 target: old_remote_target.clone(), 633 state: old_remote_state, 634 }; 635 changed_remote_refs.insert((kind, symbol.to_owned()), (old_remote_ref, new_target)); 636 } 637 } 638 for full_name in known_git_refs.into_keys() { 639 changed_git_refs.push((full_name.to_owned(), RefTarget::absent())); 640 } 641 for (RemoteRefKey((kind, symbol)), (old_target, old_state)) in known_remote_refs { 642 let old_remote_ref = RemoteRef { 643 target: old_target.clone(), 644 state: old_state, 645 }; 646 changed_remote_refs.insert( 647 (kind, symbol.to_owned()), 648 (old_remote_ref, RefTarget::absent()), 649 ); 650 } 651 652 // Stabilize output 653 failed_ref_names.sort_unstable(); 654 Ok(RefsToImport { 655 changed_git_refs, 656 changed_remote_refs, 657 failed_ref_names, 658 }) 659} 660 661fn default_remote_ref_state_for( 662 kind: GitRefKind, 663 symbol: RemoteRefSymbol<'_>, 664 git_settings: &GitSettings, 665) -> RemoteRefState { 666 match kind { 667 GitRefKind::Bookmark => { 668 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark { 669 RemoteRefState::Tracking 670 } else { 671 RemoteRefState::New 672 } 673 } 674 GitRefKind::Tag => RemoteRefState::Tracking, 675 } 676} 677 678/// Commits referenced by local branches or tags. 679/// 680/// On `import_refs()`, this is similar to collecting commits referenced by 681/// `view.git_refs()`. Main difference is that local branches can be moved by 682/// tracking remotes, and such mutation isn't applied to `view.git_refs()` yet. 683fn pinned_commit_ids(view: &View) -> Vec<CommitId> { 684 itertools::chain( 685 view.local_bookmarks().map(|(_, target)| target), 686 view.tags().values(), 687 ) 688 .flat_map(|target| target.added_ids()) 689 .cloned() 690 .collect() 691} 692 693/// Commits referenced by untracked remote branches including hidden ones. 694/// 695/// Tracked remote branches aren't included because they should have been merged 696/// into the local counterparts, and the changes pulled from one remote should 697/// propagate to the other remotes on later push. OTOH, untracked remote 698/// branches are considered independent refs. 699fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> { 700 view.all_remote_bookmarks() 701 .filter(|(_, remote_ref)| !remote_ref.is_tracking()) 702 .map(|(_, remote_ref)| &remote_ref.target) 703 .flat_map(|target| target.added_ids()) 704 .cloned() 705 .collect() 706} 707 708/// Imports HEAD from the underlying Git repo. 709/// 710/// Unlike `import_refs()`, the old HEAD branch is not abandoned because HEAD 711/// move doesn't always mean the old HEAD branch has been rewritten. 712/// 713/// Unlike `reset_head()`, this function doesn't move the working-copy commit to 714/// the child of the new HEAD revision. 715pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> { 716 let store = mut_repo.store(); 717 let git_backend = get_git_backend(store)?; 718 let git_repo = git_backend.git_repo(); 719 720 let old_git_head = mut_repo.view().git_head(); 721 let new_git_head_id = if let Ok(oid) = git_repo.head_id() { 722 Some(CommitId::from_bytes(oid.as_bytes())) 723 } else { 724 None 725 }; 726 if old_git_head.as_resolved() == Some(&new_git_head_id) { 727 return Ok(()); 728 } 729 730 // Import new head 731 if let Some(head_id) = &new_git_head_id { 732 let index = mut_repo.index(); 733 if !index.has_id(head_id) { 734 git_backend.import_head_commits([head_id]).map_err(|err| { 735 GitImportError::MissingHeadTarget { 736 id: head_id.clone(), 737 err, 738 } 739 })?; 740 } 741 // It's unlikely the imported commits were missing, but I/O-related 742 // error can still occur. 743 store 744 .get_commit(head_id) 745 .and_then(|commit| mut_repo.add_head(&commit)) 746 .map_err(GitImportError::InternalBackend)?; 747 } 748 749 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id)); 750 Ok(()) 751} 752 753#[derive(Error, Debug)] 754pub enum GitExportError { 755 #[error("Git error")] 756 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>), 757 #[error(transparent)] 758 UnexpectedBackend(#[from] UnexpectedGitBackendError), 759 #[error(transparent)] 760 Backend(#[from] BackendError), 761} 762 763impl GitExportError { 764 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self { 765 GitExportError::InternalGitError(source.into()) 766 } 767} 768 769/// A ref we failed to export to Git, along with the reason it failed. 770#[derive(Debug)] 771pub struct FailedRefExport { 772 pub name: RefName, 773 pub reason: FailedRefExportReason, 774} 775 776/// The reason we failed to export a ref to Git. 777#[derive(Debug, Error)] 778pub enum FailedRefExportReason { 779 /// The name is not allowed in Git. 780 #[error("Name is not allowed in Git")] 781 InvalidGitName, 782 /// The ref was in a conflicted state from the last import. A re-import 783 /// should fix it. 784 #[error("Ref was in a conflicted state from the last import")] 785 ConflictedOldState, 786 /// The branch points to the root commit, which Git doesn't have 787 #[error("Ref cannot point to the root commit in Git")] 788 OnRootCommit, 789 /// We wanted to delete it, but it had been modified in Git. 790 #[error("Deleted ref had been modified in Git")] 791 DeletedInJjModifiedInGit, 792 /// We wanted to add it, but Git had added it with a different target 793 #[error("Added ref had been added with a different target in Git")] 794 AddedInJjAddedInGit, 795 /// We wanted to modify it, but Git had deleted it 796 #[error("Modified ref had been deleted in Git")] 797 ModifiedInJjDeletedInGit, 798 /// Failed to delete the ref from the Git repo 799 #[error("Failed to delete")] 800 FailedToDelete(#[source] Box<gix::reference::edit::Error>), 801 /// Failed to set the ref in the Git repo 802 #[error("Failed to set")] 803 FailedToSet(#[source] Box<gix::reference::edit::Error>), 804} 805 806#[derive(Debug)] 807struct RefsToExport { 808 branches_to_update: BTreeMap<RefName, (Option<gix::ObjectId>, gix::ObjectId)>, 809 branches_to_delete: BTreeMap<RefName, gix::ObjectId>, 810 failed_branches: HashMap<RefName, FailedRefExportReason>, 811} 812 813/// Export changes to branches made in the Jujutsu repo compared to our last 814/// seen view of the Git repo in `mut_repo.view().git_refs()`. Returns a list of 815/// refs that failed to export. 816/// 817/// We ignore changed branches that are conflicted (were also changed in the Git 818/// repo compared to our last remembered view of the Git repo). These will be 819/// marked conflicted by the next `jj git import`. 820/// 821/// We do not export tags and other refs at the moment, since these aren't 822/// supposed to be modified by JJ. For them, the Git state is considered 823/// authoritative. 824pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<Vec<FailedRefExport>, GitExportError> { 825 export_some_refs(mut_repo, |_, _| true) 826} 827 828pub fn export_some_refs( 829 mut_repo: &mut MutableRepo, 830 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool, 831) -> Result<Vec<FailedRefExport>, GitExportError> { 832 let git_repo = get_git_repo(mut_repo.store())?; 833 834 let RefsToExport { 835 branches_to_update, 836 branches_to_delete, 837 mut failed_branches, 838 } = diff_refs_to_export( 839 mut_repo.view(), 840 mut_repo.store().root_commit_id(), 841 &git_ref_filter, 842 ); 843 844 // TODO: Also check other worktrees' HEAD. 845 if let Ok(head_ref) = git_repo.find_reference("HEAD") { 846 if let Some(parsed_ref) = head_ref 847 .target() 848 .try_name() 849 .and_then(|name| str::from_utf8(name.as_bstr()).ok()) 850 .and_then(parse_git_ref) 851 { 852 let old_target = head_ref.inner.target.clone(); 853 let current_oid = match head_ref.into_fully_peeled_id() { 854 Ok(id) => Some(id.detach()), 855 Err(gix::reference::peel::Error::ToId( 856 gix::refs::peel::to_id::Error::FollowToObject( 857 gix::refs::peel::to_object::Error::Follow( 858 gix::refs::file::find::existing::Error::NotFound { .. }, 859 ), 860 ), 861 )) => None, // Unborn ref should be considered absent 862 Err(err) => return Err(GitExportError::from_git(err)), 863 }; 864 let new_oid = if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) { 865 Some(new_oid) 866 } else if branches_to_delete.contains_key(&parsed_ref) { 867 None 868 } else { 869 current_oid.as_ref() 870 }; 871 if new_oid != current_oid.as_ref() { 872 update_git_head( 873 &git_repo, 874 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target), 875 current_oid, 876 )?; 877 } 878 } 879 } 880 for (parsed_ref_name, old_oid) in branches_to_delete { 881 let Some(git_ref_name) = to_git_ref_name(&parsed_ref_name) else { 882 failed_branches.insert(parsed_ref_name, FailedRefExportReason::InvalidGitName); 883 continue; 884 }; 885 if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) { 886 failed_branches.insert(parsed_ref_name, reason); 887 } else { 888 let new_target = RefTarget::absent(); 889 mut_repo.set_git_ref_target(&git_ref_name, new_target); 890 } 891 } 892 for (parsed_ref_name, (old_oid, new_oid)) in branches_to_update { 893 let Some(git_ref_name) = to_git_ref_name(&parsed_ref_name) else { 894 failed_branches.insert(parsed_ref_name, FailedRefExportReason::InvalidGitName); 895 continue; 896 }; 897 if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) { 898 failed_branches.insert(parsed_ref_name, reason); 899 } else { 900 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes())); 901 mut_repo.set_git_ref_target(&git_ref_name, new_target); 902 } 903 } 904 905 copy_exportable_local_branches_to_remote_view( 906 mut_repo, 907 REMOTE_NAME_FOR_LOCAL_GIT_REPO, 908 |name| { 909 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO; 910 let symbol = RemoteRefSymbol { name, remote }; 911 git_ref_filter(GitRefKind::Bookmark, symbol) 912 && !failed_branches.contains_key(&RefName::LocalBranch(name.to_owned())) 913 }, 914 ); 915 916 let failed_branches = failed_branches 917 .into_iter() 918 .map(|(name, reason)| FailedRefExport { name, reason }) 919 .sorted_unstable_by(|a, b| a.name.cmp(&b.name)) 920 .collect(); 921 Ok(failed_branches) 922} 923 924fn copy_exportable_local_branches_to_remote_view( 925 mut_repo: &mut MutableRepo, 926 remote: &str, 927 name_filter: impl Fn(&str) -> bool, 928) { 929 let new_local_branches = mut_repo 930 .view() 931 .local_remote_bookmarks(remote) 932 .filter_map(|(name, targets)| { 933 // TODO: filter out untracked branches (if we add support for untracked @git 934 // branches) 935 let old_target = &targets.remote_ref.target; 936 let new_target = targets.local_target; 937 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target)) 938 }) 939 .filter(|&(name, _)| name_filter(name)) 940 .map(|(name, new_target)| (name.to_owned(), new_target.clone())) 941 .collect_vec(); 942 for (ref name, new_target) in new_local_branches { 943 let symbol = RemoteRefSymbol { name, remote }; 944 let new_remote_ref = RemoteRef { 945 target: new_target, 946 state: RemoteRefState::Tracking, 947 }; 948 mut_repo.set_remote_bookmark(symbol, new_remote_ref); 949 } 950} 951 952/// Calculates diff of branches to be exported. 953fn diff_refs_to_export( 954 view: &View, 955 root_commit_id: &CommitId, 956 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool, 957) -> RefsToExport { 958 // Local targets will be copied to the "git" remote if successfully exported. So 959 // the local branches are considered to be the new "git" remote branches. 960 let mut all_branch_targets: HashMap<RefName, (&RefTarget, &RefTarget)> = itertools::chain( 961 view.local_bookmarks().map(|(name, target)| { 962 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO; 963 (RemoteRefSymbol { name, remote }, target) 964 }), 965 view.all_remote_bookmarks() 966 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO) 967 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)), 968 ) 969 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol)) 970 .map(|(symbol, new_target)| { 971 let ref_name = to_legacy_ref_name(GitRefKind::Bookmark, symbol); 972 (ref_name, (RefTarget::absent_ref(), new_target)) 973 }) 974 .collect(); 975 let known_git_refs = view 976 .git_refs() 977 .iter() 978 .map(|(full_name, target)| { 979 let (kind, symbol) = 980 parse_git_ref_inner(full_name).expect("stored git ref should be parsable"); 981 ((kind, symbol), target) 982 }) 983 .filter(|&((kind, symbol), _)| { 984 // There are two situations where remote-tracking branches get out of sync: 985 // 1. `jj branch forget` 986 // 2. `jj op undo`/`restore` in colocated repo 987 kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol) 988 }) 989 .map(|((kind, symbol), target)| { 990 let ref_name = to_legacy_ref_name(kind, symbol); 991 (ref_name, target) 992 }); 993 for (ref_name, target) in known_git_refs { 994 all_branch_targets 995 .entry(ref_name) 996 .and_modify(|(old_target, _)| *old_target = target) 997 .or_insert((target, RefTarget::absent_ref())); 998 } 999 1000 let mut branches_to_update = BTreeMap::new(); 1001 let mut branches_to_delete = BTreeMap::new(); 1002 let mut failed_branches = HashMap::new(); 1003 let root_commit_target = RefTarget::normal(root_commit_id.clone()); 1004 for (ref_name, (old_target, new_target)) in all_branch_targets { 1005 if new_target == old_target { 1006 continue; 1007 } 1008 if *new_target == root_commit_target { 1009 // Git doesn't have a root commit 1010 failed_branches.insert(ref_name, FailedRefExportReason::OnRootCommit); 1011 continue; 1012 } 1013 let old_oid = if let Some(id) = old_target.as_normal() { 1014 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes())) 1015 } else if old_target.has_conflict() { 1016 // The old git ref should only be a conflict if there were concurrent import 1017 // operations while the value changed. Don't overwrite these values. 1018 failed_branches.insert(ref_name, FailedRefExportReason::ConflictedOldState); 1019 continue; 1020 } else { 1021 assert!(old_target.is_absent()); 1022 None 1023 }; 1024 if let Some(id) = new_target.as_normal() { 1025 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes()); 1026 branches_to_update.insert(ref_name, (old_oid, new_oid)); 1027 } else if new_target.has_conflict() { 1028 // Skip conflicts and leave the old value in git_refs 1029 continue; 1030 } else { 1031 assert!(new_target.is_absent()); 1032 branches_to_delete.insert(ref_name, old_oid.unwrap()); 1033 } 1034 } 1035 1036 RefsToExport { 1037 branches_to_update, 1038 branches_to_delete, 1039 failed_branches, 1040 } 1041} 1042 1043fn delete_git_ref( 1044 git_repo: &gix::Repository, 1045 git_ref_name: &str, 1046 old_oid: &gix::oid, 1047) -> Result<(), FailedRefExportReason> { 1048 if let Ok(git_ref) = git_repo.find_reference(git_ref_name) { 1049 if git_ref.inner.target.try_id() == Some(old_oid) { 1050 // The branch has not been updated by git, so go ahead and delete it 1051 git_ref 1052 .delete() 1053 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?; 1054 } else { 1055 // The branch was updated by git 1056 return Err(FailedRefExportReason::DeletedInJjModifiedInGit); 1057 } 1058 } else { 1059 // The branch is already deleted 1060 } 1061 Ok(()) 1062} 1063 1064fn update_git_ref( 1065 git_repo: &gix::Repository, 1066 git_ref_name: &str, 1067 old_oid: Option<gix::ObjectId>, 1068 new_oid: gix::ObjectId, 1069) -> Result<(), FailedRefExportReason> { 1070 match old_oid { 1071 None => { 1072 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name) { 1073 // The branch was added in jj and in git. We're good if and only if git 1074 // pointed it to our desired target. 1075 if git_repo_ref.inner.target.try_id() != Some(&new_oid) { 1076 return Err(FailedRefExportReason::AddedInJjAddedInGit); 1077 } 1078 } else { 1079 // The branch was added in jj but still doesn't exist in git, so add it 1080 git_repo 1081 .reference( 1082 git_ref_name, 1083 new_oid, 1084 gix::refs::transaction::PreviousValue::MustNotExist, 1085 "export from jj", 1086 ) 1087 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?; 1088 } 1089 } 1090 Some(old_oid) => { 1091 // The branch was modified in jj. We can use gix API for updating under a lock. 1092 if let Err(err) = git_repo.reference( 1093 git_ref_name, 1094 new_oid, 1095 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()), 1096 "export from jj", 1097 ) { 1098 // The reference was probably updated in git 1099 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name) { 1100 // We still consider this a success if it was updated to our desired target 1101 if git_repo_ref.inner.target.try_id() != Some(&new_oid) { 1102 return Err(FailedRefExportReason::FailedToSet(err.into())); 1103 } 1104 } else { 1105 // The reference was deleted in git and moved in jj 1106 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit); 1107 } 1108 } else { 1109 // Successfully updated from old_oid to new_oid (unchanged in 1110 // git) 1111 } 1112 } 1113 } 1114 Ok(()) 1115} 1116 1117/// Ensures Git HEAD is detached and pointing to the `new_oid`. If `new_oid` 1118/// is `None` (meaning absent), dummy placeholder ref will be set. 1119fn update_git_head( 1120 git_repo: &gix::Repository, 1121 expected_ref: gix::refs::transaction::PreviousValue, 1122 new_oid: Option<gix::ObjectId>, 1123) -> Result<(), GitExportError> { 1124 let mut ref_edits = Vec::new(); 1125 let new_target = if let Some(oid) = new_oid { 1126 gix::refs::Target::Object(oid) 1127 } else { 1128 // Can't detach HEAD without a commit. Use placeholder ref to nullify 1129 // the HEAD. The placeholder ref isn't a normal branch ref. Git CLI 1130 // appears to deal with that, and can move the placeholder ref. So we 1131 // need to ensure that the ref doesn't exist. 1132 ref_edits.push(gix::refs::transaction::RefEdit { 1133 change: gix::refs::transaction::Change::Delete { 1134 expected: gix::refs::transaction::PreviousValue::Any, 1135 log: gix::refs::transaction::RefLog::AndReference, 1136 }, 1137 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(), 1138 deref: false, 1139 }); 1140 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap()) 1141 }; 1142 ref_edits.push(gix::refs::transaction::RefEdit { 1143 change: gix::refs::transaction::Change::Update { 1144 log: gix::refs::transaction::LogChange { 1145 message: "export from jj".into(), 1146 ..Default::default() 1147 }, 1148 expected: expected_ref, 1149 new: new_target, 1150 }, 1151 name: "HEAD".try_into().unwrap(), 1152 deref: false, 1153 }); 1154 git_repo 1155 .edit_references(ref_edits) 1156 .map_err(GitExportError::from_git)?; 1157 Ok(()) 1158} 1159 1160/// Sets Git HEAD to the parent of the given working-copy commit and resets 1161/// the Git index. 1162pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitExportError> { 1163 let git_repo = get_git_repo(mut_repo.store())?; 1164 1165 let first_parent_id = &wc_commit.parent_ids()[0]; 1166 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() { 1167 RefTarget::normal(first_parent_id.clone()) 1168 } else { 1169 RefTarget::absent() 1170 }; 1171 1172 // If the first parent of the working copy has changed, reset the Git HEAD. 1173 let old_head_target = mut_repo.git_head(); 1174 if old_head_target != new_head_target { 1175 let expected_ref = if let Some(id) = old_head_target.as_normal() { 1176 // We have to check the actual HEAD state because we don't record a 1177 // symbolic ref as such. 1178 let actual_head = git_repo.head().map_err(GitExportError::from_git)?; 1179 if actual_head.is_detached() { 1180 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes()); 1181 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into()) 1182 } else { 1183 // Just overwrite symbolic ref, which is unusual. Alternatively, 1184 // maybe we can test the target ref by issuing noop edit. 1185 gix::refs::transaction::PreviousValue::MustExist 1186 } 1187 } else { 1188 // Just overwrite if unborn (or conflict), which is also unusual. 1189 gix::refs::transaction::PreviousValue::MustExist 1190 }; 1191 let new_oid = new_head_target 1192 .as_normal() 1193 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes())); 1194 update_git_head(&git_repo, expected_ref, new_oid)?; 1195 mut_repo.set_git_head_target(new_head_target); 1196 } 1197 1198 // If there is an ongoing operation (merge, rebase, etc.), we need to clean it 1199 // up. 1200 // 1201 // TODO: Polish and upstream this to `gix`. 1202 if git_repo.state().is_some() { 1203 // Based on the files `git2::Repository::cleanup_state` deletes; when 1204 // upstreaming this logic should probably become more elaborate to match 1205 // `git(1)` behaviour. 1206 const STATE_FILE_NAMES: &[&str] = &[ 1207 "MERGE_HEAD", 1208 "MERGE_MODE", 1209 "MERGE_MSG", 1210 "REVERT_HEAD", 1211 "CHERRY_PICK_HEAD", 1212 "BISECT_LOG", 1213 ]; 1214 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"]; 1215 let handle_err = |err: PathError| match err.error.kind() { 1216 std::io::ErrorKind::NotFound => Ok(()), 1217 _ => Err(GitExportError::from_git(err)), 1218 }; 1219 for file_name in STATE_FILE_NAMES { 1220 let path = git_repo.path().join(file_name); 1221 std::fs::remove_file(&path) 1222 .context(&path) 1223 .or_else(handle_err)?; 1224 } 1225 for dir_name in STATE_DIR_NAMES { 1226 let path = git_repo.path().join(dir_name); 1227 std::fs::remove_dir_all(&path) 1228 .context(&path) 1229 .or_else(handle_err)?; 1230 } 1231 } 1232 1233 let parent_tree = wc_commit.parent_tree(mut_repo)?; 1234 1235 // Use the merged parent tree as the Git index, allowing `git diff` to show the 1236 // same changes as `jj diff`. If the merged parent tree has conflicts, then the 1237 // Git index will also be conflicted. 1238 let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() { 1239 if tree.id() == mut_repo.store().empty_tree_id() { 1240 // If the tree is empty, gix can fail to load the object (since Git doesn't 1241 // require the empty tree to actually be present in the object database), so we 1242 // just use an empty index directly. 1243 gix::index::File::from_state( 1244 gix::index::State::new(git_repo.object_hash()), 1245 git_repo.index_path(), 1246 ) 1247 } else { 1248 // If the parent tree is resolved, we can use gix's `index_from_tree` method. 1249 // This is more efficient than iterating over the tree and adding each entry. 1250 git_repo 1251 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes())) 1252 .map_err(GitExportError::from_git)? 1253 } 1254 } else { 1255 build_index_from_merged_tree(&git_repo, parent_tree)? 1256 }; 1257 1258 // Match entries in the new index with entries in the old index, and copy stat 1259 // information if the entry didn't change. 1260 if let Some(old_index) = git_repo.try_index().map_err(GitExportError::from_git)? { 1261 index 1262 .entries_mut_with_paths() 1263 .merge_join_by(old_index.entries(), |(entry, path), old_entry| { 1264 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index)) 1265 .then_with(|| entry.stage().cmp(&old_entry.stage())) 1266 }) 1267 .filter_map(|merged| merged.both()) 1268 .map(|((entry, _), old_entry)| (entry, old_entry)) 1269 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode) 1270 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat); 1271 } 1272 1273 debug_assert!(index.verify_entries().is_ok()); 1274 1275 index 1276 .write(gix::index::write::Options::default()) 1277 .map_err(GitExportError::from_git)?; 1278 1279 Ok(()) 1280} 1281 1282fn build_index_from_merged_tree( 1283 git_repo: &gix::Repository, 1284 merged_tree: MergedTree, 1285) -> Result<gix::index::File, GitExportError> { 1286 let mut index = gix::index::File::from_state( 1287 gix::index::State::new(git_repo.object_hash()), 1288 git_repo.index_path(), 1289 ); 1290 1291 let mut push_index_entry = 1292 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| { 1293 let Some(entry) = maybe_entry else { 1294 return; 1295 }; 1296 1297 let (id, mode) = match entry { 1298 TreeValue::File { id, executable } => { 1299 if *executable { 1300 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE) 1301 } else { 1302 (id.as_bytes(), gix::index::entry::Mode::FILE) 1303 } 1304 } 1305 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK), 1306 TreeValue::Tree(_) => { 1307 // This case is only possible if there is a file-directory conflict, since 1308 // `MergedTree::entries` handles the recursion otherwise. We only materialize a 1309 // file in the working copy for file-directory conflicts, so we don't add the 1310 // tree to the index here either. 1311 return; 1312 } 1313 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT), 1314 TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"), 1315 }; 1316 1317 let path = BStr::new(path.as_internal_file_string()); 1318 1319 // It is safe to push the entry because we ensure that we only add each path to 1320 // a stage once, and we sort the entries after we finish adding them. 1321 index.dangerously_push_entry( 1322 gix::index::entry::Stat::default(), 1323 gix::ObjectId::from_bytes_or_panic(id), 1324 gix::index::entry::Flags::from_stage(stage), 1325 mode, 1326 path, 1327 ); 1328 }; 1329 1330 let mut has_many_sided_conflict = false; 1331 1332 for (path, entry) in merged_tree.entries() { 1333 let entry = entry?; 1334 if let Some(resolved) = entry.as_resolved() { 1335 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted); 1336 continue; 1337 } 1338 1339 let conflict = entry.simplify(); 1340 if let [left, base, right] = conflict.as_slice() { 1341 // 2-sided conflicts can be represented in the Git index 1342 push_index_entry(&path, left, gix::index::entry::Stage::Ours); 1343 push_index_entry(&path, base, gix::index::entry::Stage::Base); 1344 push_index_entry(&path, right, gix::index::entry::Stage::Theirs); 1345 } else { 1346 // We can't represent many-sided conflicts in the Git index, so just add the 1347 // first side as staged. This is preferable to adding the first 2 sides as a 1348 // conflict, since some tools rely on being able to resolve conflicts using the 1349 // index, which could lead to an incorrect conflict resolution if the index 1350 // didn't contain all of the conflict sides. Instead, we add a dummy conflict of 1351 // a file named ".jj-do-not-resolve-this-conflict" to prevent the user from 1352 // accidentally committing the conflict markers. 1353 has_many_sided_conflict = true; 1354 push_index_entry( 1355 &path, 1356 conflict.first(), 1357 gix::index::entry::Stage::Unconflicted, 1358 ); 1359 } 1360 } 1361 1362 // Required after `dangerously_push_entry` for correctness. We use do a lookup 1363 // in the index after this, so it must be sorted before we do the lookup. 1364 index.sort_entries(); 1365 1366 // If the conflict had an unrepresentable conflict and the dummy file path isn't 1367 // already added in the index, add a dummy file as a conflict. 1368 if has_many_sided_conflict 1369 && index 1370 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into()) 1371 .is_err() 1372 { 1373 let file_blob = git_repo 1374 .write_blob( 1375 b"The working copy commit contains conflicts which cannot be resolved using Git.\n", 1376 ) 1377 .map_err(GitExportError::from_git)?; 1378 index.dangerously_push_entry( 1379 gix::index::entry::Stat::default(), 1380 file_blob.detach(), 1381 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours), 1382 gix::index::entry::Mode::FILE, 1383 INDEX_DUMMY_CONFLICT_FILE.into(), 1384 ); 1385 // We need to sort again for correctness before writing the index file since we 1386 // added a new entry. 1387 index.sort_entries(); 1388 } 1389 1390 Ok(index) 1391} 1392 1393#[derive(Debug, Error)] 1394pub enum GitRemoteManagementError { 1395 #[error("No git remote named '{0}'")] 1396 NoSuchRemote(String), 1397 #[error("Git remote named '{0}' already exists")] 1398 RemoteAlreadyExists(String), 1399 #[error(transparent)] 1400 RemoteName(#[from] GitRemoteNameError), 1401 #[error("Git remote named '{0}' has nonstandard configuration")] 1402 NonstandardConfiguration(String), 1403 #[error("Error saving Git configuration")] 1404 GitConfigSaveError(#[source] std::io::Error), 1405 #[error("Unexpected Git error when managing remotes")] 1406 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>), 1407 #[error(transparent)] 1408 UnexpectedBackend(#[from] UnexpectedGitBackendError), 1409} 1410 1411impl GitRemoteManagementError { 1412 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self { 1413 GitRemoteManagementError::InternalGitError(source.into()) 1414 } 1415} 1416 1417#[cfg(feature = "git2")] 1418fn is_remote_not_found_err(err: &git2::Error) -> bool { 1419 matches!( 1420 (err.class(), err.code()), 1421 ( 1422 git2::ErrorClass::Config, 1423 git2::ErrorCode::NotFound | git2::ErrorCode::InvalidSpec 1424 ) 1425 ) 1426} 1427 1428/// Determine, by its name, if a remote refers to the special local-only "git" 1429/// remote that is used in the Git backend. 1430/// 1431/// This function always returns false if the "git" feature is not enabled. 1432pub fn is_special_git_remote(remote: &str) -> bool { 1433 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO 1434} 1435 1436fn add_ref( 1437 name: gix::refs::FullName, 1438 target: gix::refs::Target, 1439 message: BString, 1440) -> gix::refs::transaction::RefEdit { 1441 gix::refs::transaction::RefEdit { 1442 change: gix::refs::transaction::Change::Update { 1443 log: gix::refs::transaction::LogChange { 1444 mode: gix::refs::transaction::RefLog::AndReference, 1445 force_create_reflog: false, 1446 message, 1447 }, 1448 expected: gix::refs::transaction::PreviousValue::MustNotExist, 1449 new: target, 1450 }, 1451 name, 1452 deref: false, 1453 } 1454} 1455 1456fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit { 1457 gix::refs::transaction::RefEdit { 1458 change: gix::refs::transaction::Change::Delete { 1459 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch( 1460 reference.target().into_owned(), 1461 ), 1462 log: gix::refs::transaction::RefLog::AndReference, 1463 }, 1464 name: reference.name().to_owned(), 1465 deref: false, 1466 } 1467} 1468 1469/// Save an edited [`gix::config::File`] to its original location on disk. 1470/// 1471/// Note that the resulting configuration changes are *not* persisted to the 1472/// originating [`gix::Repository`]! The repository must be reloaded with the 1473/// new configuration if necessary. 1474fn save_git_config(config: &gix::config::File) -> std::io::Result<()> { 1475 let mut config_file = File::create( 1476 config 1477 .meta() 1478 .path 1479 .as_ref() 1480 .expect("Git repository to have a config file"), 1481 )?; 1482 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta()) 1483} 1484 1485fn git_config_branch_section_ids_by_remote( 1486 config: &gix::config::File, 1487 remote_name: &str, 1488) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> { 1489 config 1490 .sections_by_name("branch") 1491 .into_iter() 1492 .flatten() 1493 .filter_map(|section| { 1494 let remote_values = section.values("remote"); 1495 let push_remote_values = section.values("pushRemote"); 1496 if !remote_values 1497 .iter() 1498 .chain(push_remote_values.iter()) 1499 .any(|branch_remote_name| **branch_remote_name == remote_name.as_bytes()) 1500 { 1501 return None; 1502 } 1503 if remote_values.len() > 1 1504 || push_remote_values.len() > 1 1505 || section.value_names().any(|name| { 1506 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge") 1507 }) 1508 { 1509 return Some(Err(GitRemoteManagementError::NonstandardConfiguration( 1510 remote_name.to_owned(), 1511 ))); 1512 } 1513 Some(Ok(section.id())) 1514 }) 1515 .collect() 1516} 1517 1518fn rename_remote_in_git_branch_config_sections( 1519 config: &mut gix::config::File, 1520 old_remote_name: &str, 1521 new_remote_name: &str, 1522) -> Result<(), GitRemoteManagementError> { 1523 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? { 1524 config 1525 .section_mut_by_id(id) 1526 .expect("found section to exist") 1527 .set( 1528 "remote" 1529 .try_into() 1530 .expect("'remote' to be a valid value name"), 1531 BStr::new(new_remote_name), 1532 ); 1533 } 1534 Ok(()) 1535} 1536 1537fn remove_remote_git_branch_config_sections( 1538 config: &mut gix::config::File, 1539 remote_name: &str, 1540) -> Result<(), GitRemoteManagementError> { 1541 for id in git_config_branch_section_ids_by_remote(config, remote_name)? { 1542 config 1543 .remove_section_by_id(id) 1544 .expect("removed section to exist"); 1545 } 1546 Ok(()) 1547} 1548 1549fn remove_remote_git_config_sections( 1550 config: &mut gix::config::File, 1551 remote_name: &str, 1552) -> Result<(), GitRemoteManagementError> { 1553 let section_ids_to_remove: Vec<_> = config 1554 .sections_by_name("remote") 1555 .into_iter() 1556 .flatten() 1557 .filter(|section| section.header().subsection_name() == Some(BStr::new(remote_name))) 1558 .map(|section| { 1559 if section.value_names().any(|name| { 1560 !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch") 1561 }) { 1562 return Err(GitRemoteManagementError::NonstandardConfiguration( 1563 remote_name.to_owned(), 1564 )); 1565 } 1566 Ok(section.id()) 1567 }) 1568 .try_collect()?; 1569 for id in section_ids_to_remove { 1570 config 1571 .remove_section_by_id(id) 1572 .expect("removed section to exist"); 1573 } 1574 Ok(()) 1575} 1576 1577/// Returns a sorted list of configured remote names. 1578pub fn get_all_remote_names(store: &Store) -> Result<Vec<String>, UnexpectedGitBackendError> { 1579 let git_repo = get_git_repo(store)?; 1580 let names = git_repo 1581 .remote_names() 1582 .into_iter() 1583 // exclude empty [remote "<name>"] section 1584 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some()) 1585 // ignore non-UTF-8 remote names which we don't support 1586 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok()) 1587 .collect(); 1588 Ok(names) 1589} 1590 1591pub fn add_remote( 1592 store: &Store, 1593 remote_name: &str, 1594 url: &str, 1595) -> Result<(), GitRemoteManagementError> { 1596 let git_repo = get_git_repo(store)?; 1597 1598 validate_remote_name(remote_name)?; 1599 1600 if git_repo.try_find_remote(remote_name).is_some() { 1601 return Err(GitRemoteManagementError::RemoteAlreadyExists( 1602 remote_name.to_owned(), 1603 )); 1604 } 1605 1606 let mut remote = git_repo 1607 .remote_at(url) 1608 .map_err(GitRemoteManagementError::from_git)? 1609 .with_refspecs( 1610 [format!("+refs/heads/*:refs/remotes/{remote_name}/*").as_bytes()], 1611 gix::remote::Direction::Fetch, 1612 ) 1613 .expect("default refspec to be valid"); 1614 1615 let mut config = git_repo.config_snapshot().clone(); 1616 remote 1617 .save_as_to(remote_name, &mut config) 1618 .map_err(GitRemoteManagementError::from_git)?; 1619 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?; 1620 1621 Ok(()) 1622} 1623 1624pub fn remove_remote( 1625 mut_repo: &mut MutableRepo, 1626 remote_name: &str, 1627) -> Result<(), GitRemoteManagementError> { 1628 let mut git_repo = get_git_repo(mut_repo.store())?; 1629 1630 if git_repo.try_find_remote(remote_name).is_none() { 1631 return Err(GitRemoteManagementError::NoSuchRemote( 1632 remote_name.to_owned(), 1633 )); 1634 }; 1635 1636 let mut config = git_repo.config_snapshot().clone(); 1637 remove_remote_git_branch_config_sections(&mut config, remote_name)?; 1638 remove_remote_git_config_sections(&mut config, remote_name)?; 1639 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?; 1640 1641 remove_remote_git_refs(&mut git_repo, remote_name) 1642 .map_err(GitRemoteManagementError::from_git)?; 1643 1644 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO { 1645 remove_remote_refs(mut_repo, remote_name); 1646 } 1647 1648 Ok(()) 1649} 1650 1651fn remove_remote_git_refs( 1652 git_repo: &mut gix::Repository, 1653 remote_name: &str, 1654) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { 1655 let edits: Vec<_> = git_repo 1656 .references()? 1657 .prefixed(format!("refs/remotes/{remote_name}/"))? 1658 .map_ok(remove_ref) 1659 .try_collect()?; 1660 git_repo.edit_references(edits)?; 1661 Ok(()) 1662} 1663 1664fn remove_remote_refs(mut_repo: &mut MutableRepo, remote_name: &str) { 1665 mut_repo.remove_remote(remote_name); 1666 let prefix = format!("refs/remotes/{remote_name}/"); 1667 let git_refs_to_delete = mut_repo 1668 .view() 1669 .git_refs() 1670 .keys() 1671 .filter(|&r| r.starts_with(&prefix)) 1672 .cloned() 1673 .collect_vec(); 1674 for git_ref in git_refs_to_delete { 1675 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent()); 1676 } 1677} 1678 1679pub fn rename_remote( 1680 mut_repo: &mut MutableRepo, 1681 old_remote_name: &str, 1682 new_remote_name: &str, 1683) -> Result<(), GitRemoteManagementError> { 1684 let mut git_repo = get_git_repo(mut_repo.store())?; 1685 1686 validate_remote_name(new_remote_name)?; 1687 1688 let Some(result) = git_repo.try_find_remote(old_remote_name) else { 1689 return Err(GitRemoteManagementError::NoSuchRemote( 1690 old_remote_name.to_owned(), 1691 )); 1692 }; 1693 let mut remote = result.map_err(GitRemoteManagementError::from_git)?; 1694 1695 if git_repo.try_find_remote(new_remote_name).is_some() { 1696 return Err(GitRemoteManagementError::RemoteAlreadyExists( 1697 new_remote_name.to_owned(), 1698 )); 1699 } 1700 1701 match ( 1702 remote.refspecs(gix::remote::Direction::Fetch), 1703 remote.refspecs(gix::remote::Direction::Push), 1704 ) { 1705 ([refspec], []) 1706 if refspec.to_ref().to_bstring() 1707 == format!("+refs/heads/*:refs/remotes/{old_remote_name}/*").as_bytes() => {} 1708 _ => { 1709 return Err(GitRemoteManagementError::NonstandardConfiguration( 1710 old_remote_name.to_owned(), 1711 )) 1712 } 1713 } 1714 1715 remote 1716 .replace_refspecs( 1717 [format!("+refs/heads/*:refs/remotes/{new_remote_name}/*").as_bytes()], 1718 gix::remote::Direction::Fetch, 1719 ) 1720 .expect("default refspec to be valid"); 1721 1722 let mut config = git_repo.config_snapshot().clone(); 1723 remote 1724 .save_as_to(new_remote_name, &mut config) 1725 .map_err(GitRemoteManagementError::from_git)?; 1726 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?; 1727 remove_remote_git_config_sections(&mut config, old_remote_name)?; 1728 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?; 1729 1730 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name) 1731 .map_err(GitRemoteManagementError::from_git)?; 1732 1733 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO { 1734 rename_remote_refs(mut_repo, old_remote_name, new_remote_name); 1735 } 1736 1737 Ok(()) 1738} 1739 1740fn rename_remote_git_refs( 1741 git_repo: &mut gix::Repository, 1742 old_remote_name: &str, 1743 new_remote_name: &str, 1744) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { 1745 let old_prefix = format!("refs/remotes/{old_remote_name}/"); 1746 let new_prefix = format!("refs/remotes/{new_remote_name}/"); 1747 let ref_log_message = BString::from(format!( 1748 "renamed remote {old_remote_name} to {new_remote_name}" 1749 )); 1750 1751 let edits: Vec<_> = git_repo 1752 .references()? 1753 .prefixed(old_prefix.clone())? 1754 .map_ok(|old_ref| { 1755 let new_name = BString::new( 1756 [ 1757 new_prefix.as_bytes(), 1758 &old_ref.name().as_bstr()[old_prefix.len()..], 1759 ] 1760 .concat(), 1761 ); 1762 [ 1763 add_ref( 1764 new_name.try_into().expect("new ref name to be valid"), 1765 old_ref.target().into_owned(), 1766 ref_log_message.clone(), 1767 ), 1768 remove_ref(old_ref), 1769 ] 1770 }) 1771 .flatten_ok() 1772 .try_collect()?; 1773 git_repo.edit_references(edits)?; 1774 Ok(()) 1775} 1776 1777/// Set the `url` to be used when fetching data from a remote. 1778/// 1779/// Shim for the missing `gix::Remote::fetch_url` API. 1780/// 1781/// **TODO:** Upstream an implementation of this to `gix`. 1782fn gix_remote_with_fetch_url<Url, E>( 1783 remote: gix::Remote, 1784 url: Url, 1785) -> Result<gix::Remote, gix::remote::init::Error> 1786where 1787 Url: TryInto<gix::Url, Error = E>, 1788 gix::url::parse::Error: From<E>, 1789{ 1790 let mut new_remote = remote.repo().remote_at(url)?; 1791 // Copy the existing data from `remote`. 1792 // 1793 // We don’t copy the push URL, as there does not seem to be any way to reliably 1794 // detect whether one is present with the current API, and `jj git remote 1795 // set-url` refuses to work with them anyway. 1796 new_remote = new_remote.with_fetch_tags(remote.fetch_tags()); 1797 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] { 1798 new_remote 1799 .replace_refspecs( 1800 remote 1801 .refspecs(direction) 1802 .iter() 1803 .map(|refspec| refspec.to_ref().to_bstring()), 1804 direction, 1805 ) 1806 .expect("existing refspecs to be valid"); 1807 } 1808 Ok(new_remote) 1809} 1810 1811pub fn set_remote_url( 1812 store: &Store, 1813 remote_name: &str, 1814 new_remote_url: &str, 1815) -> Result<(), GitRemoteManagementError> { 1816 let git_repo = get_git_repo(store)?; 1817 1818 validate_remote_name(remote_name)?; 1819 1820 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name) else { 1821 return Err(GitRemoteManagementError::NoSuchRemote( 1822 remote_name.to_owned(), 1823 )); 1824 }; 1825 let mut remote = result.map_err(GitRemoteManagementError::from_git)?; 1826 1827 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) { 1828 return Err(GitRemoteManagementError::NonstandardConfiguration( 1829 remote_name.to_owned(), 1830 )); 1831 } 1832 1833 remote = gix_remote_with_fetch_url(remote, new_remote_url) 1834 .map_err(GitRemoteManagementError::from_git)?; 1835 1836 let mut config = git_repo.config_snapshot().clone(); 1837 remote 1838 .save_as_to(remote_name, &mut config) 1839 .map_err(GitRemoteManagementError::from_git)?; 1840 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?; 1841 1842 Ok(()) 1843} 1844 1845fn rename_remote_refs(mut_repo: &mut MutableRepo, old_remote_name: &str, new_remote_name: &str) { 1846 mut_repo.rename_remote(old_remote_name, new_remote_name); 1847 let prefix = format!("refs/remotes/{old_remote_name}/"); 1848 let git_refs = mut_repo 1849 .view() 1850 .git_refs() 1851 .iter() 1852 .filter_map(|(r, target)| { 1853 r.strip_prefix(&prefix).map(|p| { 1854 ( 1855 r.clone(), 1856 format!("refs/remotes/{new_remote_name}/{p}"), 1857 target.clone(), 1858 ) 1859 }) 1860 }) 1861 .collect_vec(); 1862 for (old, new, target) in git_refs { 1863 mut_repo.set_git_ref_target(&old, RefTarget::absent()); 1864 mut_repo.set_git_ref_target(&new, target); 1865 } 1866} 1867 1868const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']']; 1869 1870#[derive(Error, Debug)] 1871pub enum GitFetchError { 1872 #[error("No git remote named '{0}'")] 1873 NoSuchRemote(String), 1874 #[error( 1875 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`", 1876 chars = INVALID_REFSPEC_CHARS.iter().join("`, `") 1877 )] 1878 InvalidBranchPattern(StringPattern), 1879 #[error(transparent)] 1880 RemoteName(#[from] GitRemoteNameError), 1881 // TODO: I'm sure there are other errors possible, such as transport-level errors. 1882 #[cfg(feature = "git2")] 1883 #[error("Unexpected git error when fetching")] 1884 InternalGitError(#[from] git2::Error), 1885 #[error(transparent)] 1886 Subprocess(#[from] GitSubprocessError), 1887} 1888 1889// TODO: If Git2 implementation is removed, this can be replaced with 1890// UnexpectedGitBackendError. 1891#[derive(Debug, Error)] 1892pub enum GitFetchPrepareError { 1893 #[cfg(feature = "git2")] 1894 #[error(transparent)] 1895 Git2(#[from] git2::Error), 1896 #[error(transparent)] 1897 UnexpectedBackend(#[from] UnexpectedGitBackendError), 1898} 1899 1900#[cfg(feature = "git2")] 1901fn git2_fetch_options( 1902 mut callbacks: RemoteCallbacks<'_>, 1903 depth: Option<NonZeroU32>, 1904) -> git2::FetchOptions<'_> { 1905 let mut proxy_options = git2::ProxyOptions::new(); 1906 proxy_options.auto(); 1907 1908 let mut fetch_options = git2::FetchOptions::new(); 1909 fetch_options.proxy_options(proxy_options); 1910 // git2 doesn't provide API to set "no-progress" protocol option. If 1911 // sideband callback were enabled, remote progress messages would be written 1912 // no matter if the process was attached to a tty or not. 1913 if callbacks.progress.is_none() { 1914 callbacks.sideband_progress = None; 1915 } 1916 fetch_options.remote_callbacks(callbacks.into_git()); 1917 if let Some(depth) = depth { 1918 fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX)); 1919 } 1920 1921 fetch_options 1922} 1923 1924struct FetchedBranches { 1925 remote: String, 1926 branches: Vec<StringPattern>, 1927} 1928 1929/// Helper struct to execute multiple `git fetch` operations 1930pub struct GitFetch<'a> { 1931 mut_repo: &'a mut MutableRepo, 1932 fetch_impl: GitFetchImpl<'a>, 1933 git_settings: &'a GitSettings, 1934 fetched: Vec<FetchedBranches>, 1935} 1936 1937impl<'a> GitFetch<'a> { 1938 pub fn new( 1939 mut_repo: &'a mut MutableRepo, 1940 git_settings: &'a GitSettings, 1941 ) -> Result<Self, GitFetchPrepareError> { 1942 let fetch_impl = GitFetchImpl::new(mut_repo.store(), git_settings)?; 1943 Ok(GitFetch { 1944 mut_repo, 1945 fetch_impl, 1946 git_settings, 1947 fetched: vec![], 1948 }) 1949 } 1950 1951 /// Perform a `git fetch` on the local git repo, updating the 1952 /// remote-tracking branches in the git repo. 1953 /// 1954 /// Keeps track of the {branch_names, remote_name} pair the refs can be 1955 /// subsequently imported into the `jj` repo by calling `import_refs()`. 1956 #[tracing::instrument(skip(self, callbacks))] 1957 pub fn fetch( 1958 &mut self, 1959 remote_name: &str, 1960 branch_names: &[StringPattern], 1961 callbacks: RemoteCallbacks<'_>, 1962 depth: Option<NonZeroU32>, 1963 ) -> Result<(), GitFetchError> { 1964 validate_remote_name(remote_name)?; 1965 self.fetch_impl 1966 .fetch(remote_name, branch_names, callbacks, depth)?; 1967 self.fetched.push(FetchedBranches { 1968 remote: remote_name.to_string(), 1969 branches: branch_names.to_vec(), 1970 }); 1971 Ok(()) 1972 } 1973 1974 /// Queries remote for the default branch name. 1975 #[tracing::instrument(skip(self, callbacks))] 1976 pub fn get_default_branch( 1977 &self, 1978 remote_name: &str, 1979 callbacks: RemoteCallbacks<'_>, 1980 ) -> Result<Option<String>, GitFetchError> { 1981 self.fetch_impl.get_default_branch(remote_name, callbacks) 1982 } 1983 1984 /// Import the previously fetched remote-tracking branches into the jj repo 1985 /// and update jj's local branches. We also import local tags since remote 1986 /// tags should have been merged by Git. 1987 /// 1988 /// Clears all yet-to-be-imported {branch_names, remote_name} pairs after 1989 /// the import. If `fetch()` has not been called since the last time 1990 /// `import_refs()` was called then this will be a no-op. 1991 #[tracing::instrument(skip(self))] 1992 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> { 1993 tracing::debug!("import_refs"); 1994 let import_stats = 1995 import_some_refs( 1996 self.mut_repo, 1997 self.git_settings, 1998 |kind, symbol| match kind { 1999 GitRefKind::Bookmark => self 2000 .fetched 2001 .iter() 2002 .filter(|fetched| fetched.remote == symbol.remote) 2003 .any(|fetched| { 2004 fetched 2005 .branches 2006 .iter() 2007 .any(|pattern| pattern.matches(symbol.name)) 2008 }), 2009 GitRefKind::Tag => true, 2010 }, 2011 )?; 2012 2013 self.fetched.clear(); 2014 2015 Ok(import_stats) 2016 } 2017} 2018 2019fn expand_fetch_refspecs( 2020 remote_name: &str, 2021 branch_names: &[StringPattern], 2022) -> Result<Vec<RefSpec>, GitFetchError> { 2023 branch_names 2024 .iter() 2025 .map(|pattern| { 2026 pattern 2027 .to_glob() 2028 .filter( 2029 /* This triggered by non-glob `*`s in addition to INVALID_REFSPEC_CHARS 2030 * because `to_glob()` escapes such `*`s as `[*]`. */ 2031 |glob| !glob.contains(INVALID_REFSPEC_CHARS), 2032 ) 2033 .map(|glob| { 2034 RefSpec::forced( 2035 format!("refs/heads/{glob}"), 2036 format!("refs/remotes/{remote_name}/{glob}"), 2037 ) 2038 }) 2039 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone())) 2040 }) 2041 .collect() 2042} 2043 2044enum GitFetchImpl<'a> { 2045 #[cfg(feature = "git2")] 2046 Git2 { git_repo: git2::Repository }, 2047 Subprocess { 2048 git_repo: Box<gix::Repository>, 2049 git_ctx: GitSubprocessContext<'a>, 2050 }, 2051} 2052 2053impl<'a> GitFetchImpl<'a> { 2054 fn new(store: &Store, git_settings: &'a GitSettings) -> Result<Self, GitFetchPrepareError> { 2055 let git_backend = get_git_backend(store)?; 2056 #[cfg(feature = "git2")] 2057 if !git_settings.subprocess { 2058 let git_repo = git2::Repository::open(git_backend.git_repo_path())?; 2059 return Ok(GitFetchImpl::Git2 { git_repo }); 2060 } 2061 let git_repo = Box::new(git_backend.git_repo()); 2062 let git_ctx = 2063 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path); 2064 Ok(GitFetchImpl::Subprocess { git_repo, git_ctx }) 2065 } 2066 2067 fn fetch( 2068 &self, 2069 remote_name: &str, 2070 branch_names: &[StringPattern], 2071 callbacks: RemoteCallbacks<'_>, 2072 depth: Option<NonZeroU32>, 2073 ) -> Result<(), GitFetchError> { 2074 match self { 2075 #[cfg(feature = "git2")] 2076 GitFetchImpl::Git2 { git_repo } => { 2077 git2_fetch(git_repo, remote_name, branch_names, callbacks, depth) 2078 } 2079 GitFetchImpl::Subprocess { git_repo, git_ctx } => subprocess_fetch( 2080 git_repo, 2081 git_ctx, 2082 remote_name, 2083 branch_names, 2084 callbacks, 2085 depth, 2086 ), 2087 } 2088 } 2089 2090 fn get_default_branch( 2091 &self, 2092 remote_name: &str, 2093 callbacks: RemoteCallbacks<'_>, 2094 ) -> Result<Option<String>, GitFetchError> { 2095 match self { 2096 #[cfg(feature = "git2")] 2097 GitFetchImpl::Git2 { git_repo } => { 2098 git2_get_default_branch(git_repo, remote_name, callbacks) 2099 } 2100 GitFetchImpl::Subprocess { git_repo, git_ctx } => { 2101 subprocess_get_default_branch(git_repo, git_ctx, remote_name, callbacks) 2102 } 2103 } 2104 } 2105} 2106 2107#[cfg(feature = "git2")] 2108fn git2_fetch( 2109 git_repo: &git2::Repository, 2110 remote_name: &str, 2111 branch_names: &[StringPattern], 2112 callbacks: RemoteCallbacks<'_>, 2113 depth: Option<NonZeroU32>, 2114) -> Result<(), GitFetchError> { 2115 let mut remote = git_repo.find_remote(remote_name).map_err(|err| { 2116 if is_remote_not_found_err(&err) { 2117 GitFetchError::NoSuchRemote(remote_name.to_string()) 2118 } else { 2119 GitFetchError::InternalGitError(err) 2120 } 2121 })?; 2122 // At this point, we are only updating Git's remote tracking branches, not the 2123 // local branches. 2124 let refspecs: Vec<String> = expand_fetch_refspecs(remote_name, branch_names)? 2125 .iter() 2126 .map(|refspec| refspec.to_git_format()) 2127 .collect(); 2128 2129 if refspecs.is_empty() { 2130 // Don't fall back to the base refspecs. 2131 return Ok(()); 2132 } 2133 2134 tracing::debug!("remote.download"); 2135 remote.download(&refspecs, Some(&mut git2_fetch_options(callbacks, depth)))?; 2136 tracing::debug!("remote.prune"); 2137 remote.prune(None)?; 2138 tracing::debug!("remote.update_tips"); 2139 remote.update_tips( 2140 None, 2141 git2::RemoteUpdateFlags::empty(), 2142 git2::AutotagOption::Unspecified, 2143 None, 2144 )?; 2145 tracing::debug!("remote.disconnect"); 2146 remote.disconnect()?; 2147 Ok(()) 2148} 2149 2150#[cfg(feature = "git2")] 2151fn git2_get_default_branch( 2152 git_repo: &git2::Repository, 2153 remote_name: &str, 2154 callbacks: RemoteCallbacks<'_>, 2155) -> Result<Option<String>, GitFetchError> { 2156 let mut remote = git_repo.find_remote(remote_name).map_err(|err| { 2157 if is_remote_not_found_err(&err) { 2158 GitFetchError::NoSuchRemote(remote_name.to_string()) 2159 } else { 2160 GitFetchError::InternalGitError(err) 2161 } 2162 })?; 2163 // Unlike .download(), connect_auth() returns RAII object. 2164 tracing::debug!("remote.connect"); 2165 let connection = { 2166 let mut proxy_options = git2::ProxyOptions::new(); 2167 proxy_options.auto(); 2168 remote.connect_auth( 2169 git2::Direction::Fetch, 2170 Some(callbacks.into_git()), 2171 Some(proxy_options), 2172 )? 2173 }; 2174 let mut default_branch = None; 2175 tracing::debug!("remote.default_branch"); 2176 if let Ok(default_ref_buf) = connection.default_branch() { 2177 if let Some(default_ref) = default_ref_buf.as_str() { 2178 // LocalBranch here is the local branch on the remote, so it's really the remote 2179 // branch 2180 if let Some(RefName::LocalBranch(branch_name)) = parse_git_ref(default_ref) { 2181 tracing::debug!(default_branch = branch_name); 2182 default_branch = Some(branch_name); 2183 } 2184 } 2185 } 2186 Ok(default_branch) 2187} 2188 2189fn subprocess_fetch( 2190 git_repo: &gix::Repository, 2191 git_ctx: &GitSubprocessContext, 2192 remote_name: &str, 2193 branch_names: &[StringPattern], 2194 mut callbacks: RemoteCallbacks<'_>, 2195 depth: Option<NonZeroU32>, 2196) -> Result<(), GitFetchError> { 2197 // check the remote exists 2198 if git_repo.try_find_remote(remote_name).is_none() { 2199 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned())); 2200 } 2201 // At this point, we are only updating Git's remote tracking branches, not the 2202 // local branches. 2203 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?; 2204 if remaining_refspecs.is_empty() { 2205 // Don't fall back to the base refspecs. 2206 return Ok(()); 2207 } 2208 2209 let mut branches_to_prune = Vec::new(); 2210 // git unfortunately errors out if one of the many refspecs is not found 2211 // 2212 // our approach is to filter out failures and retry, 2213 // until either all have failed or an attempt has succeeded 2214 // 2215 // even more unfortunately, git errors out one refspec at a time, 2216 // meaning that the below cycle runs in O(#failed refspecs) 2217 while let Some(failing_refspec) = 2218 git_ctx.spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)? 2219 { 2220 tracing::debug!(failing_refspec, "failed to fetch ref"); 2221 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec)); 2222 2223 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") { 2224 branches_to_prune.push(format!("{remote_name}/{branch_name}")); 2225 } 2226 } 2227 2228 // Even if git fetch has --prune, if a branch is not found it will not be 2229 // pruned on fetch 2230 git_ctx.spawn_branch_prune(&branches_to_prune)?; 2231 Ok(()) 2232} 2233 2234fn subprocess_get_default_branch( 2235 git_repo: &gix::Repository, 2236 git_ctx: &GitSubprocessContext, 2237 remote_name: &str, 2238 _callbacks: RemoteCallbacks<'_>, 2239) -> Result<Option<String>, GitFetchError> { 2240 if git_repo.try_find_remote(remote_name).is_none() { 2241 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned())); 2242 } 2243 let default_branch = git_ctx.spawn_remote_show(remote_name)?; 2244 tracing::debug!(default_branch = default_branch); 2245 Ok(default_branch) 2246} 2247 2248#[derive(Error, Debug)] 2249pub enum GitPushError { 2250 #[error("No git remote named '{0}'")] 2251 NoSuchRemote(String), 2252 #[error(transparent)] 2253 RemoteName(#[from] GitRemoteNameError), 2254 #[error("Refs in unexpected location: {0:?}")] 2255 RefInUnexpectedLocation(Vec<String>), 2256 #[error("Remote rejected the update of some refs (do you have permission to push to {0:?}?)")] 2257 RefUpdateRejected(Vec<String>), 2258 // TODO: I'm sure there are other errors possible, such as transport-level errors, 2259 // and errors caused by the remote rejecting the push. 2260 #[cfg(feature = "git2")] 2261 #[error("Unexpected git error when pushing")] 2262 InternalGitError(#[from] git2::Error), 2263 #[error(transparent)] 2264 Subprocess(#[from] GitSubprocessError), 2265 #[error(transparent)] 2266 UnexpectedBackend(#[from] UnexpectedGitBackendError), 2267} 2268 2269#[derive(Clone, Debug)] 2270pub struct GitBranchPushTargets { 2271 pub branch_updates: Vec<(String, BookmarkPushUpdate)>, 2272} 2273 2274pub struct GitRefUpdate { 2275 pub qualified_name: String, 2276 /// Expected position on the remote or None if we expect the ref to not 2277 /// exist on the remote 2278 /// 2279 /// This is sourced from the local remote-tracking branch. 2280 pub expected_current_target: Option<CommitId>, 2281 pub new_target: Option<CommitId>, 2282} 2283 2284/// Pushes the specified branches and updates the repo view accordingly. 2285pub fn push_branches( 2286 mut_repo: &mut MutableRepo, 2287 git_settings: &GitSettings, 2288 remote: &str, 2289 targets: &GitBranchPushTargets, 2290 callbacks: RemoteCallbacks<'_>, 2291) -> Result<(), GitPushError> { 2292 validate_remote_name(remote)?; 2293 2294 let ref_updates = targets 2295 .branch_updates 2296 .iter() 2297 .map(|(name, update)| GitRefUpdate { 2298 qualified_name: format!("refs/heads/{name}"), 2299 expected_current_target: update.old_target.clone(), 2300 new_target: update.new_target.clone(), 2301 }) 2302 .collect_vec(); 2303 push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?; 2304 2305 // TODO: add support for partially pushed refs? we could update the view 2306 // excluding rejected refs, but the transaction would be aborted anyway 2307 // if we returned an Err. 2308 for (name, update) in &targets.branch_updates { 2309 let remote_symbol = RemoteRefSymbol { name, remote }; 2310 let git_ref_name = format!("refs/remotes/{remote}/{name}"); 2311 let new_remote_ref = RemoteRef { 2312 target: RefTarget::resolved(update.new_target.clone()), 2313 state: RemoteRefState::Tracking, 2314 }; 2315 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone()); 2316 mut_repo.set_remote_bookmark(remote_symbol, new_remote_ref); 2317 } 2318 2319 Ok(()) 2320} 2321 2322/// Pushes the specified Git refs without updating the repo view. 2323pub fn push_updates( 2324 repo: &dyn Repo, 2325 git_settings: &GitSettings, 2326 remote_name: &str, 2327 updates: &[GitRefUpdate], 2328 callbacks: RemoteCallbacks<'_>, 2329) -> Result<(), GitPushError> { 2330 let mut qualified_remote_refs_expected_locations = HashMap::new(); 2331 let mut refspecs = vec![]; 2332 for update in updates { 2333 qualified_remote_refs_expected_locations.insert( 2334 update.qualified_name.as_str(), 2335 update.expected_current_target.as_ref(), 2336 ); 2337 if let Some(new_target) = &update.new_target { 2338 // We always force-push. We use the push_negotiation callback in 2339 // `push_refs` to check that the refs did not unexpectedly move on 2340 // the remote. 2341 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name)); 2342 } else { 2343 // Prefixing this with `+` to force-push or not should make no 2344 // difference. The push negotiation happens regardless, and wouldn't 2345 // allow creating a branch if it's not a fast-forward. 2346 refspecs.push(RefSpec::delete(&update.qualified_name)); 2347 } 2348 } 2349 // TODO(ilyagr): `push_refs`, or parts of it, should probably be inlined. This 2350 // requires adjusting some tests. 2351 2352 let git_backend = get_git_backend(repo.store())?; 2353 #[cfg(feature = "git2")] 2354 if !git_settings.subprocess { 2355 let git_repo = git2::Repository::open(git_backend.git_repo_path())?; 2356 let refspecs: Vec<String> = refspecs.iter().map(RefSpec::to_git_format).collect(); 2357 return git2_push_refs( 2358 repo, 2359 &git_repo, 2360 remote_name, 2361 &qualified_remote_refs_expected_locations, 2362 &refspecs, 2363 callbacks, 2364 ); 2365 } 2366 let git_repo = git_backend.git_repo(); 2367 let git_ctx = 2368 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path); 2369 subprocess_push_refs( 2370 &git_repo, 2371 &git_ctx, 2372 remote_name, 2373 &qualified_remote_refs_expected_locations, 2374 &refspecs, 2375 callbacks, 2376 ) 2377} 2378 2379#[cfg(feature = "git2")] 2380fn git2_push_refs( 2381 repo: &dyn Repo, 2382 git_repo: &git2::Repository, 2383 remote_name: &str, 2384 qualified_remote_refs_expected_locations: &HashMap<&str, Option<&CommitId>>, 2385 refspecs: &[String], 2386 callbacks: RemoteCallbacks<'_>, 2387) -> Result<(), GitPushError> { 2388 let mut remote = git_repo.find_remote(remote_name).map_err(|err| { 2389 if is_remote_not_found_err(&err) { 2390 GitPushError::NoSuchRemote(remote_name.to_string()) 2391 } else { 2392 GitPushError::InternalGitError(err) 2393 } 2394 })?; 2395 let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs_expected_locations 2396 .keys() 2397 .copied() 2398 .collect(); 2399 let mut failed_push_negotiations = vec![]; 2400 let push_result = { 2401 let mut push_options = git2::PushOptions::new(); 2402 let mut proxy_options = git2::ProxyOptions::new(); 2403 proxy_options.auto(); 2404 push_options.proxy_options(proxy_options); 2405 let mut callbacks = callbacks.into_git(); 2406 callbacks.push_negotiation(|updates| { 2407 for update in updates { 2408 let dst_refname = update 2409 .dst_refname() 2410 .expect("Expect reference name to be valid UTF-8"); 2411 let expected_remote_location = *qualified_remote_refs_expected_locations 2412 .get(dst_refname) 2413 .expect("Push is trying to move a ref it wasn't asked to move"); 2414 let oid_to_maybe_commitid = 2415 |oid: git2::Oid| (!oid.is_zero()).then(|| CommitId::from_bytes(oid.as_bytes())); 2416 let actual_remote_location = oid_to_maybe_commitid(update.src()); 2417 let local_location = oid_to_maybe_commitid(update.dst()); 2418 2419 match allow_push( 2420 repo.index(), 2421 actual_remote_location.as_ref(), 2422 expected_remote_location, 2423 local_location.as_ref(), 2424 ) { 2425 Ok(PushAllowReason::NormalMatch) => {} 2426 Ok(PushAllowReason::UnexpectedNoop) => { 2427 tracing::info!( 2428 "The push of {dst_refname} is unexpectedly a no-op, the remote branch \ 2429 is already at {actual_remote_location:?}. We expected it to be at \ 2430 {expected_remote_location:?}. We don't consider this an error.", 2431 ); 2432 } 2433 Ok(PushAllowReason::ExceptionalFastforward) => { 2434 // TODO(ilyagr): We could consider printing a user-facing message at 2435 // this point. 2436 tracing::info!( 2437 "We allow the push of {dst_refname} to {local_location:?}, even \ 2438 though it is unexpectedly at {actual_remote_location:?} on the \ 2439 server rather than the expected {expected_remote_location:?}. The \ 2440 desired location is a descendant of the actual location, and the \ 2441 actual location is a descendant of the expected location.", 2442 ); 2443 } 2444 Err(()) => { 2445 // While we show debug info in the message with `--debug`, 2446 // there's probably no need to show the detailed commit 2447 // locations to the user normally. They should do a `jj git 2448 // fetch`, and the resulting branch conflicts should contain 2449 // all the information they need. 2450 tracing::info!( 2451 "Cannot push {dst_refname} to {local_location:?}; it is at \ 2452 unexpectedly at {actual_remote_location:?} on the server as opposed \ 2453 to the expected {expected_remote_location:?}", 2454 ); 2455 failed_push_negotiations.push(dst_refname.to_string()); 2456 } 2457 } 2458 } 2459 if failed_push_negotiations.is_empty() { 2460 Ok(()) 2461 } else { 2462 Err(git2::Error::from_str("failed push negotiation")) 2463 } 2464 }); 2465 callbacks.push_update_reference(|refname, status| { 2466 // The status is Some if the ref update was rejected 2467 if status.is_none() { 2468 remaining_remote_refs.remove(refname); 2469 } 2470 Ok(()) 2471 }); 2472 push_options.remote_callbacks(callbacks); 2473 remote.push(refspecs, Some(&mut push_options)) 2474 }; 2475 if !failed_push_negotiations.is_empty() { 2476 // If the push negotiation returned an error, `remote.push` would not 2477 // have pushed anything and would have returned an error, as expected. 2478 // However, the error it returns is not necessarily the error we'd 2479 // expect. It also depends on the exact versions of `libgit2` and 2480 // `git2.rs`. So, we cannot rely on it containing any useful 2481 // information. See https://github.com/rust-lang/git2-rs/issues/1042. 2482 assert!(push_result.is_err()); 2483 failed_push_negotiations.sort(); 2484 Err(GitPushError::RefInUnexpectedLocation( 2485 failed_push_negotiations, 2486 )) 2487 } else { 2488 push_result?; 2489 if remaining_remote_refs.is_empty() { 2490 Ok(()) 2491 } else { 2492 // remote rejected refs because of remote config (e.g., no push on main) 2493 Err(GitPushError::RefUpdateRejected( 2494 remaining_remote_refs 2495 .iter() 2496 .sorted() 2497 .map(|name| name.to_string()) 2498 .collect(), 2499 )) 2500 } 2501 } 2502} 2503 2504fn subprocess_push_refs( 2505 git_repo: &gix::Repository, 2506 git_ctx: &GitSubprocessContext, 2507 remote_name: &str, 2508 qualified_remote_refs_expected_locations: &HashMap<&str, Option<&CommitId>>, 2509 refspecs: &[RefSpec], 2510 mut callbacks: RemoteCallbacks<'_>, 2511) -> Result<(), GitPushError> { 2512 // check the remote exists 2513 if git_repo.try_find_remote(remote_name).is_none() { 2514 return Err(GitPushError::NoSuchRemote(remote_name.to_owned())); 2515 } 2516 2517 let refs_to_push: Vec<RefToPush> = refspecs 2518 .iter() 2519 .map(|full_refspec| RefToPush::new(full_refspec, qualified_remote_refs_expected_locations)) 2520 .collect(); 2521 2522 let push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?; 2523 tracing::debug!(?push_stats); 2524 2525 if !push_stats.rejected.is_empty() { 2526 let mut refs_in_unexpected_locations = push_stats.rejected; 2527 refs_in_unexpected_locations.sort(); 2528 Err(GitPushError::RefInUnexpectedLocation( 2529 refs_in_unexpected_locations, 2530 )) 2531 } else if !push_stats.remote_rejected.is_empty() { 2532 let mut rejected_refs = push_stats.remote_rejected; 2533 rejected_refs.sort(); 2534 // remote rejected refs because of remote config (e.g., no push on main) 2535 Err(GitPushError::RefUpdateRejected(rejected_refs)) 2536 } else { 2537 Ok(()) 2538 } 2539} 2540 2541#[cfg(feature = "git2")] 2542#[derive(Debug, Clone, PartialEq, Eq)] 2543enum PushAllowReason { 2544 NormalMatch, 2545 ExceptionalFastforward, 2546 UnexpectedNoop, 2547} 2548 2549#[cfg(feature = "git2")] 2550fn allow_push( 2551 index: &dyn Index, 2552 actual_remote_location: Option<&CommitId>, 2553 expected_remote_location: Option<&CommitId>, 2554 destination_location: Option<&CommitId>, 2555) -> Result<PushAllowReason, ()> { 2556 if actual_remote_location == expected_remote_location { 2557 return Ok(PushAllowReason::NormalMatch); 2558 } 2559 2560 // If the remote ref is in an unexpected location, we still allow some 2561 // pushes, based on whether `jj git fetch` would result in a conflicted ref. 2562 // 2563 // For `merge_ref_targets` to work correctly, `actual_remote_location` must 2564 // be a commit that we locally know about. 2565 // 2566 // This does not lose any generality since for `merge_ref_targets` to 2567 // resolve to `local_target` below, it is conceptually necessary (but not 2568 // sufficient) for the destination_location to be either a descendant of 2569 // actual_remote_location or equal to it. Either way, we would know about that 2570 // commit locally. 2571 if !actual_remote_location.is_none_or(|id| index.has_id(id)) { 2572 return Err(()); 2573 } 2574 let remote_target = RefTarget::resolved(actual_remote_location.cloned()); 2575 let base_target = RefTarget::resolved(expected_remote_location.cloned()); 2576 // The push destination is the local position of the ref 2577 let local_target = RefTarget::resolved(destination_location.cloned()); 2578 if refs::merge_ref_targets(index, &remote_target, &base_target, &local_target) == local_target { 2579 // Fetch would not change the local branch, so the push is OK in spite of 2580 // the discrepancy with the expected location. We return some debug info and 2581 // verify some invariants before OKing the push. 2582 Ok(if actual_remote_location == destination_location { 2583 // This is the situation of what we call "A - B + A = A" 2584 // conflicts, see also test_refs.rs and 2585 // https://github.com/jj-vcs/jj/blob/c9b44f382824301e6c0fdd6f4cbc52bb00c50995/lib/src/merge.rs#L92. 2586 PushAllowReason::UnexpectedNoop 2587 } else { 2588 // Due to our ref merge rules, this case should happen if an only 2589 // if: 2590 // 2591 // 1. This is a fast-forward. 2592 // 2. The expected location is an ancestor of both the actual location and the 2593 // destination (local position). 2594 PushAllowReason::ExceptionalFastforward 2595 }) 2596 } else { 2597 Err(()) 2598 } 2599} 2600 2601#[non_exhaustive] 2602#[derive(Default)] 2603#[expect(clippy::type_complexity)] 2604pub struct RemoteCallbacks<'a> { 2605 pub progress: Option<&'a mut dyn FnMut(&Progress)>, 2606 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>, 2607 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>, 2608 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>, 2609 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>, 2610} 2611 2612#[cfg(feature = "git2")] 2613impl<'a> RemoteCallbacks<'a> { 2614 fn into_git(mut self) -> git2::RemoteCallbacks<'a> { 2615 let mut callbacks = git2::RemoteCallbacks::new(); 2616 if let Some(progress_cb) = self.progress { 2617 callbacks.transfer_progress(move |progress| { 2618 progress_cb(&Progress { 2619 bytes_downloaded: (progress.received_objects() < progress.total_objects()) 2620 .then(|| progress.received_bytes() as u64), 2621 overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32 2622 / (progress.total_objects() + progress.total_deltas()) as f32, 2623 }); 2624 true 2625 }); 2626 } 2627 if let Some(sideband_progress_cb) = self.sideband_progress { 2628 callbacks.sideband_progress(move |data| { 2629 sideband_progress_cb(data); 2630 true 2631 }); 2632 } 2633 // TODO: We should expose the callbacks to the caller instead -- the library 2634 // crate shouldn't read environment variables. 2635 let mut tried_ssh_agent = false; 2636 let mut ssh_key_paths_to_try: Option<Vec<PathBuf>> = None; 2637 callbacks.credentials(move |url, username_from_url, allowed_types| { 2638 let span = tracing::debug_span!("RemoteCallbacks.credentials"); 2639 let _ = span.enter(); 2640 2641 let git_config = git2::Config::open_default(); 2642 let credential_helper = git_config 2643 .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url)); 2644 if let Ok(creds) = credential_helper { 2645 tracing::info!("using credential_helper"); 2646 return Ok(creds); 2647 } else if let Some(username) = username_from_url { 2648 if allowed_types.contains(git2::CredentialType::SSH_KEY) { 2649 // Try to get the SSH key from the agent once. We don't even check if 2650 // $SSH_AUTH_SOCK is set because Windows uses another mechanism. 2651 if !tried_ssh_agent { 2652 tracing::info!(username, "trying ssh_key_from_agent"); 2653 tried_ssh_agent = true; 2654 return git2::Cred::ssh_key_from_agent(username).map_err(|err| { 2655 tracing::error!(err = %err); 2656 err 2657 }); 2658 } 2659 2660 let paths = ssh_key_paths_to_try.get_or_insert_with(|| { 2661 if let Some(ref mut cb) = self.get_ssh_keys { 2662 let mut paths = cb(username); 2663 paths.reverse(); 2664 paths 2665 } else { 2666 vec![] 2667 } 2668 }); 2669 2670 if let Some(path) = paths.pop() { 2671 tracing::info!(username, path = ?path, "trying ssh_key"); 2672 return git2::Cred::ssh_key(username, None, &path, None).map_err(|err| { 2673 tracing::error!(err = %err); 2674 err 2675 }); 2676 } 2677 } 2678 if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { 2679 if let Some(ref mut cb) = self.get_password { 2680 if let Some(pw) = cb(url, username) { 2681 tracing::info!( 2682 username, 2683 "using userpass_plaintext with username from url" 2684 ); 2685 return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| { 2686 tracing::error!(err = %err); 2687 err 2688 }); 2689 } 2690 } 2691 } 2692 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { 2693 if let Some(ref mut cb) = self.get_username_password { 2694 if let Some((username, pw)) = cb(url) { 2695 tracing::info!(username, "using userpass_plaintext"); 2696 return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| { 2697 tracing::error!(err = %err); 2698 err 2699 }); 2700 } 2701 } 2702 } 2703 tracing::info!("using default"); 2704 git2::Cred::default() 2705 }); 2706 callbacks 2707 } 2708} 2709 2710#[derive(Clone, Debug)] 2711pub struct Progress { 2712 /// `Some` iff data transfer is currently in progress 2713 pub bytes_downloaded: Option<u64>, 2714 pub overall: f32, 2715}