just playing with tangled
at tmp-tutorial 1030 lines 42 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::collections::{BTreeMap, HashMap, HashSet}; 18use std::default::Default; 19use std::io::Read; 20use std::path::PathBuf; 21 22use git2::Oid; 23use itertools::Itertools; 24use tempfile::NamedTempFile; 25use thiserror::Error; 26 27use crate::backend::{CommitId, ObjectId}; 28use crate::git_backend::NO_GC_REF_NAMESPACE; 29use crate::op_store::{BranchTarget, RefTarget, RefTargetOptionExt}; 30use crate::repo::{MutableRepo, Repo}; 31use crate::revset; 32use crate::settings::GitSettings; 33use crate::view::{RefName, View}; 34 35#[derive(Error, Debug, PartialEq)] 36pub enum GitImportError { 37 #[error("Unexpected git error when importing refs: {0}")] 38 InternalGitError(#[from] git2::Error), 39} 40 41fn parse_git_ref(ref_name: &str) -> Option<RefName> { 42 if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") { 43 // Git CLI says 'HEAD' is not a valid branch name 44 (branch_name != "HEAD").then(|| RefName::LocalBranch(branch_name.to_string())) 45 } else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") { 46 remote_and_branch 47 .split_once('/') 48 // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch 49 .filter(|&(_, branch)| branch != "HEAD") 50 .map(|(remote, branch)| RefName::RemoteBranch { 51 remote: remote.to_string(), 52 branch: branch.to_string(), 53 }) 54 } else { 55 ref_name 56 .strip_prefix("refs/tags/") 57 .map(|tag_name| RefName::Tag(tag_name.to_string())) 58 } 59} 60 61fn to_git_ref_name(parsed_ref: &RefName) -> Option<String> { 62 match parsed_ref { 63 RefName::LocalBranch(branch) => (branch != "HEAD").then(|| format!("refs/heads/{branch}")), 64 RefName::RemoteBranch { branch, remote } => { 65 (branch != "HEAD").then(|| format!("refs/remotes/{remote}/{branch}")) 66 } 67 RefName::Tag(tag) => Some(format!("refs/tags/{tag}")), 68 RefName::GitRef(name) => Some(name.to_owned()), 69 } 70} 71 72fn to_remote_branch<'a>(parsed_ref: &'a RefName, remote_name: &str) -> Option<&'a str> { 73 match parsed_ref { 74 RefName::RemoteBranch { branch, remote } => (remote == remote_name).then_some(branch), 75 RefName::LocalBranch(..) | RefName::Tag(..) | RefName::GitRef(..) => None, 76 } 77} 78 79/// Checks if `git_ref` points to a Git commit object, and returns its id. 80/// 81/// If the ref points to the previously `known_target` (i.e. unchanged), this 82/// should be faster than `git_ref.peel_to_commit()`. 83fn resolve_git_ref_to_commit_id( 84 git_ref: &git2::Reference<'_>, 85 known_target: &RefTarget, 86) -> Option<CommitId> { 87 // Try fast path if we have a candidate id which is known to be a commit object. 88 if let Some(id) = known_target.as_normal() { 89 if matches!(git_ref.target(), Some(oid) if oid.as_bytes() == id.as_bytes()) { 90 return Some(id.clone()); 91 } 92 if matches!(git_ref.target_peel(), Some(oid) if oid.as_bytes() == id.as_bytes()) { 93 // Perhaps an annotated tag stored in packed-refs file, and pointing to the 94 // already known target commit. 95 return Some(id.clone()); 96 } 97 // A tag (according to ref name.) Try to peel one more level. This is slightly 98 // faster than recurse into peel_to_commit(). If we recorded a tag oid, we 99 // could skip this at all. 100 if let Some(Ok(tag)) = git_ref.is_tag().then(|| git_ref.peel_to_tag()) { 101 if tag.target_id().as_bytes() == id.as_bytes() { 102 // An annotated tag pointing to the already known target commit. 103 return Some(id.clone()); 104 } else { 105 // Unknown id. Recurse from the current state as git_object_peel() of 106 // libgit2 would do. A tag may point to non-commit object. 107 let git_commit = tag.into_object().peel_to_commit().ok()?; 108 return Some(CommitId::from_bytes(git_commit.id().as_bytes())); 109 } 110 } 111 } 112 113 let git_commit = git_ref.peel_to_commit().ok()?; 114 Some(CommitId::from_bytes(git_commit.id().as_bytes())) 115} 116 117/// Builds a map of branches which also includes pseudo `@git` remote. 118/// 119/// If there's an existing remote named `git`, a list of conflicting branch 120/// names will be returned. 121pub fn build_unified_branches_map(view: &View) -> (BTreeMap<String, BranchTarget>, Vec<String>) { 122 let mut all_branches = view.branches().clone(); 123 let mut bad_branch_names = Vec::new(); 124 for (branch_name, git_tracking_target) in local_branch_git_tracking_refs(view) { 125 let branch_target = all_branches.entry(branch_name.to_owned()).or_default(); 126 if branch_target.remote_targets.contains_key("git") { 127 // TODO(#1690): There should be a mechanism to prevent importing a 128 // remote named "git" in `jj git import`. 129 bad_branch_names.push(branch_name.to_owned()); 130 } else { 131 // TODO: `BTreeMap::try_insert` could be used here once that's stabilized. 132 branch_target 133 .remote_targets 134 .insert("git".to_owned(), git_tracking_target.clone()); 135 } 136 } 137 (all_branches, bad_branch_names) 138} 139 140fn local_branch_git_tracking_refs(view: &View) -> impl Iterator<Item = (&str, &RefTarget)> { 141 view.git_refs().iter().filter_map(|(ref_name, target)| { 142 ref_name 143 .strip_prefix("refs/heads/") 144 .map(|branch_name| (branch_name, target)) 145 }) 146} 147 148pub fn get_local_git_tracking_branch<'a>(view: &'a View, branch: &str) -> &'a RefTarget { 149 view.get_git_ref(&format!("refs/heads/{branch}")) 150} 151 152fn prevent_gc(git_repo: &git2::Repository, id: &CommitId) -> Result<(), git2::Error> { 153 // If multiple processes do git::import_refs() in parallel, this can fail to 154 // acquire a lock file even with force=true. 155 git_repo.reference( 156 &format!("{}{}", NO_GC_REF_NAMESPACE, id.hex()), 157 Oid::from_bytes(id.as_bytes()).unwrap(), 158 true, 159 "used by jj", 160 )?; 161 Ok(()) 162} 163 164/// Reflect changes made in the underlying Git repo in the Jujutsu repo. 165/// 166/// This function detects conflicts (if both Git and JJ modified a branch) and 167/// records them in JJ's view. 168pub fn import_refs( 169 mut_repo: &mut MutableRepo, 170 git_repo: &git2::Repository, 171 git_settings: &GitSettings, 172) -> Result<(), GitImportError> { 173 import_some_refs(mut_repo, git_repo, git_settings, |_| true) 174} 175 176/// Reflect changes made in the underlying Git repo in the Jujutsu repo. 177/// 178/// Only branches whose git full reference name pass the filter will be 179/// considered for addition, update, or deletion. 180pub fn import_some_refs( 181 mut_repo: &mut MutableRepo, 182 git_repo: &git2::Repository, 183 git_settings: &GitSettings, 184 git_ref_filter: impl Fn(&RefName) -> bool, 185) -> Result<(), GitImportError> { 186 let store = mut_repo.store().clone(); 187 let mut jj_view_git_refs = mut_repo.view().git_refs().clone(); 188 let mut pinned_git_heads = HashMap::new(); 189 190 // TODO: Should this be a separate function? We may not always want to import 191 // the Git HEAD (and add it to our set of heads). 192 if let Ok(head_git_commit) = git_repo 193 .head() 194 .and_then(|head_ref| head_ref.peel_to_commit()) 195 { 196 // Add the current HEAD to `pinned_git_heads` to pin the branch. It's not added 197 // to `hidable_git_heads` because HEAD move doesn't automatically mean the old 198 // HEAD branch has been rewritten. 199 let head_ref_name = RefName::GitRef("HEAD".to_owned()); 200 let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes()); 201 pinned_git_heads.insert(head_ref_name, vec![head_commit_id.clone()]); 202 if !matches!(mut_repo.git_head().as_normal(), Some(id) if id == &head_commit_id) { 203 let head_commit = store.get_commit(&head_commit_id).unwrap(); 204 prevent_gc(git_repo, &head_commit_id)?; 205 mut_repo.add_head(&head_commit); 206 mut_repo.set_git_head_target(RefTarget::normal(head_commit_id)); 207 } 208 } else { 209 mut_repo.set_git_head_target(RefTarget::absent()); 210 } 211 212 let mut changed_git_refs = BTreeMap::new(); 213 let git_repo_refs = git_repo.references()?; 214 for git_repo_ref in git_repo_refs { 215 let git_repo_ref = git_repo_ref?; 216 let Some(full_name) = git_repo_ref.name() else { 217 // Skip non-utf8 refs. 218 continue; 219 }; 220 let Some(ref_name) = parse_git_ref(full_name) else { 221 // Skip other refs (such as notes) and symbolic refs. 222 continue; 223 }; 224 let Some(id) = 225 resolve_git_ref_to_commit_id(&git_repo_ref, jj_view_git_refs.get(full_name).flatten()) 226 else { 227 // Skip invalid refs. 228 continue; 229 }; 230 pinned_git_heads.insert(ref_name.clone(), vec![id.clone()]); 231 if !git_ref_filter(&ref_name) { 232 continue; 233 } 234 // TODO: Make it configurable which remotes are publishing and update public 235 // heads here. 236 let old_target = jj_view_git_refs.remove(full_name).flatten(); 237 let new_target = RefTarget::normal(id.clone()); 238 if new_target != old_target { 239 prevent_gc(git_repo, &id)?; 240 mut_repo.set_git_ref_target(full_name, RefTarget::normal(id.clone())); 241 let commit = store.get_commit(&id).unwrap(); 242 mut_repo.add_head(&commit); 243 changed_git_refs.insert(ref_name, (old_target, new_target)); 244 } 245 } 246 for (full_name, target) in jj_view_git_refs { 247 // TODO: or clean up invalid ref in case it was stored due to historical bug? 248 let ref_name = parse_git_ref(&full_name).expect("stored git ref should be parsable"); 249 if git_ref_filter(&ref_name) { 250 mut_repo.set_git_ref_target(&full_name, RefTarget::absent()); 251 changed_git_refs.insert(ref_name, (target, RefTarget::absent())); 252 } else { 253 pinned_git_heads.insert(ref_name, target.added_ids().cloned().collect()); 254 } 255 } 256 for (ref_name, (old_git_target, new_git_target)) in &changed_git_refs { 257 // Apply the change that happened in git since last time we imported refs 258 mut_repo.merge_single_ref(ref_name, old_git_target, new_git_target); 259 // If a git remote-tracking branch changed, apply the change to the local branch 260 // as well 261 if !git_settings.auto_local_branch { 262 continue; 263 } 264 if let RefName::RemoteBranch { branch, remote: _ } = ref_name { 265 let local_ref_name = RefName::LocalBranch(branch.clone()); 266 mut_repo.merge_single_ref(&local_ref_name, old_git_target, new_git_target); 267 let target = mut_repo.get_local_branch(branch); 268 if target.is_absent() { 269 pinned_git_heads.remove(&local_ref_name); 270 } else { 271 // Note that we are mostly *replacing*, not inserting 272 pinned_git_heads.insert(local_ref_name, target.added_ids().cloned().collect()); 273 } 274 } 275 } 276 277 // Find commits that are no longer referenced in the git repo and abandon them 278 // in jj as well. 279 let hidable_git_heads = changed_git_refs 280 .values() 281 .flat_map(|(old_git_target, _)| old_git_target.added_ids()) 282 .cloned() 283 .collect_vec(); 284 if hidable_git_heads.is_empty() { 285 return Ok(()); 286 } 287 // We must remove non-existing commits from pinned_git_heads, as they could have 288 // come from branches which were never fetched. 289 let mut pinned_git_heads_set = HashSet::new(); 290 for heads_for_ref in pinned_git_heads.into_values() { 291 pinned_git_heads_set.extend(heads_for_ref); 292 } 293 pinned_git_heads_set.retain(|id| mut_repo.index().has_id(id)); 294 // We could use mut_repo.record_rewrites() here but we know we only need to care 295 // about abandoned commits for now. We may want to change this if we ever 296 // add a way of preserving change IDs across rewrites by `git` (e.g. by 297 // putting them in the commit message). 298 let abandoned_commits = revset::walk_revs( 299 mut_repo, 300 &hidable_git_heads, 301 &pinned_git_heads_set.into_iter().collect_vec(), 302 ) 303 .unwrap() 304 .iter() 305 .collect_vec(); 306 let root_commit_id = mut_repo.store().root_commit_id().clone(); 307 for abandoned_commit in abandoned_commits { 308 if abandoned_commit != root_commit_id { 309 mut_repo.record_abandoned_commit(abandoned_commit); 310 } 311 } 312 313 Ok(()) 314} 315 316#[derive(Error, Debug, PartialEq)] 317pub enum GitExportError { 318 #[error("Cannot export conflicted branch '{0}'")] 319 ConflictedBranch(String), 320 #[error("Failed to read export state: {0}")] 321 ReadStateError(String), 322 #[error("Failed to write export state: {0}")] 323 WriteStateError(String), 324 #[error("Git error: {0}")] 325 InternalGitError(#[from] git2::Error), 326} 327 328/// Export changes to branches made in the Jujutsu repo compared to our last 329/// seen view of the Git repo in `mut_repo.view().git_refs()`. Returns a list of 330/// refs that failed to export. 331/// 332/// We ignore changed branches that are conflicted (were also changed in the Git 333/// repo compared to our last remembered view of the Git repo). These will be 334/// marked conflicted by the next `jj git import`. 335/// 336/// We do not export tags and other refs at the moment, since these aren't 337/// supposed to be modified by JJ. For them, the Git state is considered 338/// authoritative. 339// TODO: Also indicate why we failed to export these branches 340pub fn export_refs( 341 mut_repo: &mut MutableRepo, 342 git_repo: &git2::Repository, 343) -> Result<Vec<RefName>, GitExportError> { 344 export_some_refs(mut_repo, git_repo, |_| true) 345} 346 347pub fn export_some_refs( 348 mut_repo: &mut MutableRepo, 349 git_repo: &git2::Repository, 350 git_ref_filter: impl Fn(&RefName) -> bool, 351) -> Result<Vec<RefName>, GitExportError> { 352 // First find the changes we want need to make without modifying mut_repo 353 let mut branches_to_update = BTreeMap::new(); 354 let mut branches_to_delete = BTreeMap::new(); 355 let mut failed_branches = vec![]; 356 let view = mut_repo.view(); 357 let jj_repo_iter_all_branches = view.branches().iter().flat_map(|(branch, target)| { 358 itertools::chain( 359 target 360 .local_target 361 .is_present() 362 .then(|| RefName::LocalBranch(branch.to_owned())), 363 target 364 .remote_targets 365 .keys() 366 .map(|remote| RefName::RemoteBranch { 367 branch: branch.to_string(), 368 remote: remote.to_string(), 369 }), 370 ) 371 }); 372 let jj_known_refs_passing_filter: HashSet<_> = view 373 .git_refs() 374 .keys() 375 .filter_map(|name| parse_git_ref(name)) 376 .chain(jj_repo_iter_all_branches) 377 .filter(git_ref_filter) 378 .collect(); 379 for jj_known_ref in jj_known_refs_passing_filter { 380 let new_branch = match &jj_known_ref { 381 RefName::LocalBranch(branch) => view.get_local_branch(branch), 382 RefName::RemoteBranch { remote, branch } => { 383 // Currently, the only situation where this case occurs *and* new_branch != 384 // old_branch is after a `jj branch forget`. So, in practice, for 385 // remote-tracking branches either `new_branch == old_branch` or 386 // `new_branch == None`. 387 view.get_remote_branch(branch, remote) 388 } 389 _ => continue, 390 }; 391 let old_branch = if let Some(name) = to_git_ref_name(&jj_known_ref) { 392 view.get_git_ref(&name) 393 } else { 394 // Invalid branch name in Git sense 395 failed_branches.push(jj_known_ref); 396 continue; 397 }; 398 if new_branch == old_branch { 399 continue; 400 } 401 let old_oid = if let Some(id) = old_branch.as_normal() { 402 Some(Oid::from_bytes(id.as_bytes()).unwrap()) 403 } else if old_branch.has_conflict() { 404 // The old git ref should only be a conflict if there were concurrent import 405 // operations while the value changed. Don't overwrite these values. 406 failed_branches.push(jj_known_ref); 407 continue; 408 } else { 409 assert!(old_branch.is_absent()); 410 None 411 }; 412 if let Some(id) = new_branch.as_normal() { 413 let new_oid = Oid::from_bytes(id.as_bytes()); 414 branches_to_update.insert(jj_known_ref, (old_oid, new_oid.unwrap())); 415 } else if new_branch.has_conflict() { 416 // Skip conflicts and leave the old value in git_refs 417 continue; 418 } else { 419 assert!(new_branch.is_absent()); 420 branches_to_delete.insert(jj_known_ref, old_oid.unwrap()); 421 } 422 } 423 // TODO: Also check other worktrees' HEAD. 424 if let Ok(head_ref) = git_repo.find_reference("HEAD") { 425 if let (Some(head_git_ref), Ok(current_git_commit)) = 426 (head_ref.symbolic_target(), head_ref.peel_to_commit()) 427 { 428 if let Some(parsed_ref) = parse_git_ref(head_git_ref) { 429 let detach_head = 430 if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) { 431 *new_oid != current_git_commit.id() 432 } else { 433 branches_to_delete.contains_key(&parsed_ref) 434 }; 435 if detach_head { 436 git_repo.set_head_detached(current_git_commit.id())?; 437 } 438 } 439 } 440 } 441 for (parsed_ref_name, old_oid) in branches_to_delete { 442 let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap(); 443 let success = if let Ok(mut git_repo_ref) = git_repo.find_reference(&git_ref_name) { 444 if git_repo_ref.target() == Some(old_oid) { 445 // The branch has not been updated by git, so go ahead and delete it 446 git_repo_ref.delete().is_ok() 447 } else { 448 // The branch was updated by git 449 false 450 } 451 } else { 452 // The branch is already deleted 453 true 454 }; 455 if success { 456 mut_repo.set_git_ref_target(&git_ref_name, RefTarget::absent()); 457 } else { 458 failed_branches.push(parsed_ref_name); 459 } 460 } 461 for (parsed_ref_name, (old_oid, new_oid)) in branches_to_update { 462 let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap(); 463 let success = match old_oid { 464 None => { 465 if let Ok(git_repo_ref) = git_repo.find_reference(&git_ref_name) { 466 // The branch was added in jj and in git. We're good if and only if git 467 // pointed it to our desired target. 468 git_repo_ref.target() == Some(new_oid) 469 } else { 470 // The branch was added in jj but still doesn't exist in git, so add it 471 git_repo 472 .reference(&git_ref_name, new_oid, true, "export from jj") 473 .is_ok() 474 } 475 } 476 Some(old_oid) => { 477 // The branch was modified in jj. We can use libgit2's API for updating under a 478 // lock. 479 if git_repo 480 .reference_matching(&git_ref_name, new_oid, true, old_oid, "export from jj") 481 .is_ok() 482 { 483 // Successfully updated from old_oid to new_oid (unchanged in git) 484 true 485 } else { 486 // The reference was probably updated in git 487 if let Ok(git_repo_ref) = git_repo.find_reference(&git_ref_name) { 488 // We still consider this a success if it was updated to our desired target 489 git_repo_ref.target() == Some(new_oid) 490 } else { 491 // The reference was deleted in git and moved in jj 492 false 493 } 494 } 495 } 496 }; 497 if success { 498 mut_repo.set_git_ref_target( 499 &git_ref_name, 500 RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes())), 501 ); 502 } else { 503 failed_branches.push(parsed_ref_name); 504 } 505 } 506 Ok(failed_branches) 507} 508 509pub fn remove_remote( 510 mut_repo: &mut MutableRepo, 511 git_repo: &git2::Repository, 512 remote_name: &str, 513) -> Result<(), git2::Error> { 514 git_repo.remote_delete(remote_name)?; 515 let mut branches_to_delete = vec![]; 516 for (branch, target) in mut_repo.view().branches() { 517 if target.remote_targets.contains_key(remote_name) { 518 branches_to_delete.push(branch.clone()); 519 } 520 } 521 let prefix = format!("refs/remotes/{remote_name}/"); 522 let git_refs_to_delete = mut_repo 523 .view() 524 .git_refs() 525 .keys() 526 .filter_map(|r| r.starts_with(&prefix).then(|| r.clone())) 527 .collect_vec(); 528 for branch in branches_to_delete { 529 mut_repo.set_remote_branch_target(&branch, remote_name, RefTarget::absent()); 530 } 531 for git_ref in git_refs_to_delete { 532 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent()); 533 } 534 Ok(()) 535} 536 537pub fn rename_remote( 538 mut_repo: &mut MutableRepo, 539 git_repo: &git2::Repository, 540 old_remote_name: &str, 541 new_remote_name: &str, 542) -> Result<(), git2::Error> { 543 git_repo.remote_rename(old_remote_name, new_remote_name)?; 544 mut_repo.rename_remote(old_remote_name, new_remote_name); 545 let prefix = format!("refs/remotes/{old_remote_name}/"); 546 let git_refs = mut_repo 547 .view() 548 .git_refs() 549 .iter() 550 .filter_map(|(r, target)| { 551 r.strip_prefix(&prefix).map(|p| { 552 ( 553 r.clone(), 554 format!("refs/remotes/{new_remote_name}/{p}"), 555 target.clone(), 556 ) 557 }) 558 }) 559 .collect_vec(); 560 for (old, new, target) in git_refs { 561 mut_repo.set_git_ref_target(&old, RefTarget::absent()); 562 mut_repo.set_git_ref_target(&new, target); 563 } 564 Ok(()) 565} 566 567#[derive(Error, Debug, PartialEq)] 568pub enum GitFetchError { 569 #[error("No git remote named '{0}'")] 570 NoSuchRemote(String), 571 #[error("Invalid glob provided. Globs may not contain the characters `:` or `^`.")] 572 InvalidGlob, 573 // TODO: I'm sure there are other errors possible, such as transport-level errors. 574 #[error("Unexpected git error when fetching: {0}")] 575 InternalGitError(#[from] git2::Error), 576} 577 578#[tracing::instrument(skip(mut_repo, git_repo, callbacks))] 579pub fn fetch( 580 mut_repo: &mut MutableRepo, 581 git_repo: &git2::Repository, 582 remote_name: &str, 583 branch_name_globs: Option<&[&str]>, 584 callbacks: RemoteCallbacks<'_>, 585 git_settings: &GitSettings, 586) -> Result<Option<String>, GitFetchError> { 587 let branch_name_filter = { 588 let regex = if let Some(globs) = branch_name_globs { 589 let result = regex::RegexSet::new( 590 globs 591 .iter() 592 .map(|glob| format!("^{}$", glob.replace('*', ".*"))), 593 ) 594 .map_err(|_| GitFetchError::InvalidGlob)?; 595 tracing::debug!(?globs, ?result, "globs as regex"); 596 Some(result) 597 } else { 598 None 599 }; 600 move |branch: &str| regex.as_ref().map(|r| r.is_match(branch)).unwrap_or(true) 601 }; 602 603 // In non-colocated repositories, it's possible that `jj branch forget` was run 604 // at some point and no `jj git export` happened since. 605 // 606 // This would mean that remote-tracking branches, forgotten in the jj repo, 607 // still exist in the git repo. If the branches didn't move on the remote, and 608 // we fetched them, jj would think that they are unmodified and wouldn't 609 // resurrect them. 610 // 611 // Export will delete the remote-tracking branches in the git repo, so it's 612 // possible to fetch them again. 613 // 614 // For more details, see the `test_branch_forget_fetched_branch` test, and PRs 615 // #1714 and #1771 616 // 617 // Apart from `jj branch forget`, jj doesn't provide commands to manipulate 618 // remote-tracking branches, and local git branches don't affect fetch 619 // behaviors. So, it's unnecessary to export anything else. 620 // 621 // TODO: Create a command the user can use to reset jj's 622 // branch state to the git repo's state. In this case, `jj branch forget` 623 // doesn't work as it tries to delete the latter. One possible name is `jj 624 // git import --reset BRANCH`. 625 // TODO: Once the command described above exists, it should be mentioned in `jj 626 // help branch forget`. 627 let nonempty_branches: HashSet<_> = mut_repo 628 .view() 629 .branches() 630 .iter() 631 .filter_map(|(branch, target)| target.local_target.is_present().then(|| branch.to_owned())) 632 .collect(); 633 // TODO: Inform the user if the export failed? In most cases, export is not 634 // essential for fetch to work. 635 let _ = export_some_refs(mut_repo, git_repo, |ref_name| { 636 to_remote_branch(ref_name, remote_name) 637 .map(|branch| branch_name_filter(branch) && !nonempty_branches.contains(branch)) 638 .unwrap_or(false) 639 }); 640 641 // Perform a `git fetch` on the local git repo, updating the remote-tracking 642 // branches in the git repo. 643 let mut remote = 644 git_repo 645 .find_remote(remote_name) 646 .map_err(|err| match (err.class(), err.code()) { 647 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => { 648 GitFetchError::NoSuchRemote(remote_name.to_string()) 649 } 650 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => { 651 GitFetchError::NoSuchRemote(remote_name.to_string()) 652 } 653 _ => GitFetchError::InternalGitError(err), 654 })?; 655 let mut fetch_options = git2::FetchOptions::new(); 656 let mut proxy_options = git2::ProxyOptions::new(); 657 proxy_options.auto(); 658 fetch_options.proxy_options(proxy_options); 659 let callbacks = callbacks.into_git(); 660 fetch_options.remote_callbacks(callbacks); 661 let refspecs = { 662 // If no globs have been given, import all branches 663 let globs = branch_name_globs.unwrap_or(&["*"]); 664 if globs.iter().any(|g| g.contains(|c| ":^".contains(c))) { 665 return Err(GitFetchError::InvalidGlob); 666 } 667 // At this point, we are only updating Git's remote tracking branches, not the 668 // local branches. 669 globs 670 .iter() 671 .map(|glob| format!("+refs/heads/{glob}:refs/remotes/{remote_name}/{glob}")) 672 .collect_vec() 673 }; 674 tracing::debug!("remote.download"); 675 remote.download(&refspecs, Some(&mut fetch_options))?; 676 tracing::debug!("remote.prune"); 677 remote.prune(None)?; 678 tracing::debug!("remote.update_tips"); 679 remote.update_tips(None, false, git2::AutotagOption::Unspecified, None)?; 680 // TODO: We could make it optional to get the default branch since we only care 681 // about it on clone. 682 let mut default_branch = None; 683 if let Ok(default_ref_buf) = remote.default_branch() { 684 if let Some(default_ref) = default_ref_buf.as_str() { 685 // LocalBranch here is the local branch on the remote, so it's really the remote 686 // branch 687 if let Some(RefName::LocalBranch(branch_name)) = parse_git_ref(default_ref) { 688 tracing::debug!(default_branch = branch_name); 689 default_branch = Some(branch_name); 690 } 691 } 692 } 693 tracing::debug!("remote.disconnect"); 694 remote.disconnect()?; 695 696 // `import_some_refs` will import the remote-tracking branches into the jj repo 697 // and update jj's local branches. 698 tracing::debug!("import_refs"); 699 import_some_refs(mut_repo, git_repo, git_settings, |ref_name| { 700 to_remote_branch(ref_name, remote_name) 701 .map(&branch_name_filter) 702 .unwrap_or(false) 703 }) 704 .map_err(|err| match err { 705 GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source), 706 })?; 707 Ok(default_branch) 708} 709 710#[derive(Error, Debug, PartialEq)] 711pub enum GitPushError { 712 #[error("No git remote named '{0}'")] 713 NoSuchRemote(String), 714 #[error("Push is not fast-forwardable")] 715 NotFastForward, 716 #[error("Remote rejected the update of some refs (do you have permission to push to {0:?}?)")] 717 RefUpdateRejected(Vec<String>), 718 // TODO: I'm sure there are other errors possible, such as transport-level errors, 719 // and errors caused by the remote rejecting the push. 720 #[error("Unexpected git error when pushing: {0}")] 721 InternalGitError(#[from] git2::Error), 722} 723 724pub struct GitRefUpdate { 725 pub qualified_name: String, 726 // TODO: We want this to be a `current_target: Option<CommitId>` for the expected current 727 // commit on the remote. It's a blunt "force" option instead until git2-rs supports the 728 // "push negotiation" callback (https://github.com/rust-lang/git2-rs/issues/733). 729 pub force: bool, 730 pub new_target: Option<CommitId>, 731} 732 733pub fn push_updates( 734 git_repo: &git2::Repository, 735 remote_name: &str, 736 updates: &[GitRefUpdate], 737 callbacks: RemoteCallbacks<'_>, 738) -> Result<(), GitPushError> { 739 let mut temp_refs = vec![]; 740 let mut qualified_remote_refs = vec![]; 741 let mut refspecs = vec![]; 742 for update in updates { 743 qualified_remote_refs.push(update.qualified_name.as_str()); 744 if let Some(new_target) = &update.new_target { 745 // Create a temporary ref to work around https://github.com/libgit2/libgit2/issues/3178 746 let temp_ref_name = format!("refs/jj/git-push/{}", new_target.hex()); 747 temp_refs.push(git_repo.reference( 748 &temp_ref_name, 749 git2::Oid::from_bytes(new_target.as_bytes()).unwrap(), 750 true, 751 "temporary reference for git push", 752 )?); 753 refspecs.push(format!( 754 "{}{}:{}", 755 (if update.force { "+" } else { "" }), 756 temp_ref_name, 757 update.qualified_name 758 )); 759 } else { 760 refspecs.push(format!(":{}", update.qualified_name)); 761 } 762 } 763 let result = push_refs( 764 git_repo, 765 remote_name, 766 &qualified_remote_refs, 767 &refspecs, 768 callbacks, 769 ); 770 for mut temp_ref in temp_refs { 771 // TODO: Figure out how to do the equivalent of absl::Cleanup for 772 // temp_ref.delete(). 773 if let Err(err) = temp_ref.delete() { 774 // Propagate error only if we don't already have an error to return and it's not 775 // NotFound (there may be duplicates if the list if multiple branches moved to 776 // the same commit). 777 if result.is_ok() && err.code() != git2::ErrorCode::NotFound { 778 return Err(GitPushError::InternalGitError(err)); 779 } 780 } 781 } 782 result 783} 784 785fn push_refs( 786 git_repo: &git2::Repository, 787 remote_name: &str, 788 qualified_remote_refs: &[&str], 789 refspecs: &[String], 790 callbacks: RemoteCallbacks<'_>, 791) -> Result<(), GitPushError> { 792 let mut remote = 793 git_repo 794 .find_remote(remote_name) 795 .map_err(|err| match (err.class(), err.code()) { 796 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => { 797 GitPushError::NoSuchRemote(remote_name.to_string()) 798 } 799 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => { 800 GitPushError::NoSuchRemote(remote_name.to_string()) 801 } 802 _ => GitPushError::InternalGitError(err), 803 })?; 804 let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs.iter().copied().collect(); 805 let mut push_options = git2::PushOptions::new(); 806 let mut proxy_options = git2::ProxyOptions::new(); 807 proxy_options.auto(); 808 push_options.proxy_options(proxy_options); 809 let mut callbacks = callbacks.into_git(); 810 callbacks.push_update_reference(|refname, status| { 811 // The status is Some if the ref update was rejected 812 if status.is_none() { 813 remaining_remote_refs.remove(refname); 814 } 815 Ok(()) 816 }); 817 push_options.remote_callbacks(callbacks); 818 remote 819 .push(refspecs, Some(&mut push_options)) 820 .map_err(|err| match (err.class(), err.code()) { 821 (git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => { 822 GitPushError::NotFastForward 823 } 824 _ => GitPushError::InternalGitError(err), 825 })?; 826 drop(push_options); 827 if remaining_remote_refs.is_empty() { 828 Ok(()) 829 } else { 830 Err(GitPushError::RefUpdateRejected( 831 remaining_remote_refs 832 .iter() 833 .sorted() 834 .map(|name| name.to_string()) 835 .collect(), 836 )) 837 } 838} 839 840#[non_exhaustive] 841#[derive(Default)] 842#[allow(clippy::type_complexity)] 843pub struct RemoteCallbacks<'a> { 844 pub progress: Option<&'a mut dyn FnMut(&Progress)>, 845 pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<PathBuf>>, 846 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>, 847 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>, 848} 849 850impl<'a> RemoteCallbacks<'a> { 851 fn into_git(mut self) -> git2::RemoteCallbacks<'a> { 852 let mut callbacks = git2::RemoteCallbacks::new(); 853 if let Some(progress_cb) = self.progress { 854 callbacks.transfer_progress(move |progress| { 855 progress_cb(&Progress { 856 bytes_downloaded: (progress.received_objects() < progress.total_objects()) 857 .then(|| progress.received_bytes() as u64), 858 overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32 859 / (progress.total_objects() + progress.total_deltas()) as f32, 860 }); 861 true 862 }); 863 } 864 // TODO: We should expose the callbacks to the caller instead -- the library 865 // crate shouldn't read environment variables. 866 callbacks.credentials(move |url, username_from_url, allowed_types| { 867 let span = tracing::debug_span!("RemoteCallbacks.credentials"); 868 let _ = span.enter(); 869 870 let git_config = git2::Config::open_default(); 871 let credential_helper = git_config 872 .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url)); 873 if let Ok(creds) = credential_helper { 874 tracing::info!("using credential_helper"); 875 return Ok(creds); 876 } else if let Some(username) = username_from_url { 877 if allowed_types.contains(git2::CredentialType::SSH_KEY) { 878 // Try to get the SSH key from the agent by default, and report an error 879 // only if it _seems_ like that's what the user wanted. 880 // 881 // Note that the env variables read below are **not** the only way to 882 // communicate with the agent, which is why we request a key from it no 883 // matter what. 884 match git2::Cred::ssh_key_from_agent(username) { 885 Ok(key) => { 886 tracing::info!(username, "using ssh_key_from_agent"); 887 return Ok(key); 888 } 889 Err(err) => { 890 if std::env::var("SSH_AUTH_SOCK").is_ok() 891 || std::env::var("SSH_AGENT_PID").is_ok() 892 { 893 tracing::error!(err = %err); 894 return Err(err); 895 } 896 // There is no agent-related env variable so we 897 // consider that the user doesn't care about using 898 // the agent and proceed. 899 } 900 } 901 902 if let Some(ref mut cb) = self.get_ssh_key { 903 if let Some(path) = cb(username) { 904 tracing::info!(username, path = ?path, "using ssh_key"); 905 return git2::Cred::ssh_key(username, None, &path, None).map_err( 906 |err| { 907 tracing::error!(err = %err); 908 err 909 }, 910 ); 911 } 912 } 913 } 914 if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { 915 if let Some(ref mut cb) = self.get_password { 916 if let Some(pw) = cb(url, username) { 917 tracing::info!( 918 username, 919 "using userpass_plaintext with username from url" 920 ); 921 return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| { 922 tracing::error!(err = %err); 923 err 924 }); 925 } 926 } 927 } 928 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { 929 if let Some(ref mut cb) = self.get_username_password { 930 if let Some((username, pw)) = cb(url) { 931 tracing::info!(username, "using userpass_plaintext"); 932 return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| { 933 tracing::error!(err = %err); 934 err 935 }); 936 } 937 } 938 } 939 tracing::info!("using default"); 940 git2::Cred::default() 941 }); 942 callbacks 943 } 944} 945 946pub struct Progress { 947 /// `Some` iff data transfer is currently in progress 948 pub bytes_downloaded: Option<u64>, 949 pub overall: f32, 950} 951 952#[derive(Default)] 953struct PartialSubmoduleConfig { 954 path: Option<String>, 955 url: Option<String>, 956} 957 958/// Represents configuration from a submodule, e.g. in .gitmodules 959/// This doesn't include all possible fields, only the ones we care about 960#[derive(Debug, PartialEq, Eq)] 961pub struct SubmoduleConfig { 962 pub name: String, 963 pub path: String, 964 pub url: String, 965} 966 967#[derive(Error, Debug)] 968pub enum GitConfigParseError { 969 #[error("Unexpected io error when parsing config: {0}")] 970 IoError(#[from] std::io::Error), 971 #[error("Unexpected git error when parsing config: {0}")] 972 InternalGitError(#[from] git2::Error), 973} 974 975pub fn parse_gitmodules( 976 config: &mut dyn Read, 977) -> Result<BTreeMap<String, SubmoduleConfig>, GitConfigParseError> { 978 // git2 can only read from a path, so set one up 979 let mut temp_file = NamedTempFile::new()?; 980 std::io::copy(config, &mut temp_file)?; 981 let path = temp_file.into_temp_path(); 982 let git_config = git2::Config::open(&path)?; 983 // Partial config value for each submodule name 984 let mut partial_configs: BTreeMap<String, PartialSubmoduleConfig> = BTreeMap::new(); 985 986 let entries = git_config.entries(Some(r"submodule\..+\."))?; 987 entries.for_each(|entry| { 988 let (config_name, config_value) = match (entry.name(), entry.value()) { 989 // Reject non-utf8 entries 990 (Some(name), Some(value)) => (name, value), 991 _ => return, 992 }; 993 994 // config_name is of the form submodule.<name>.<variable> 995 let (submod_name, submod_var) = config_name 996 .strip_prefix("submodule.") 997 .unwrap() 998 .split_once('.') 999 .unwrap(); 1000 1001 let map_entry = partial_configs.entry(submod_name.to_string()).or_default(); 1002 1003 match (submod_var.to_ascii_lowercase().as_str(), &map_entry) { 1004 // TODO Git warns when a duplicate config entry is found, we should 1005 // consider doing the same. 1006 ("path", PartialSubmoduleConfig { path: None, .. }) => { 1007 map_entry.path = Some(config_value.to_string()) 1008 } 1009 ("url", PartialSubmoduleConfig { url: None, .. }) => { 1010 map_entry.url = Some(config_value.to_string()) 1011 } 1012 _ => (), 1013 }; 1014 })?; 1015 1016 let ret = partial_configs 1017 .into_iter() 1018 .filter_map(|(name, val)| { 1019 Some(( 1020 name.clone(), 1021 SubmoduleConfig { 1022 name, 1023 path: val.path?, 1024 url: val.url?, 1025 }, 1026 )) 1027 }) 1028 .collect(); 1029 Ok(ret) 1030}