1#![allow(missing_docs)]
16
17use std::borrow::Borrow;
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::default::Default;
22use std::fs::File;
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::str;
26use std::sync::Arc;
27
28use bstr::BStr;
29use bstr::BString;
30use futures::StreamExt as _;
31use itertools::Itertools as _;
32use pollster::FutureExt 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;
45use crate::matchers::EverythingMatcher;
46use crate::merged_tree::MergedTree;
47use crate::merged_tree::TreeDiffEntry;
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;
53use crate::ref_name::GitRefName;
54use crate::ref_name::GitRefNameBuf;
55use crate::ref_name::RefName;
56use crate::ref_name::RefNameBuf;
57use crate::ref_name::RemoteName;
58use crate::ref_name::RemoteNameBuf;
59use crate::ref_name::RemoteRefSymbol;
60use crate::ref_name::RemoteRefSymbolBuf;
61use crate::refs::BookmarkPushUpdate;
62use crate::repo::MutableRepo;
63use crate::repo::Repo;
64use crate::repo_path::RepoPath;
65use crate::revset::RevsetExpression;
66use crate::settings::GitSettings;
67use crate::store::Store;
68use crate::str_util::StringPattern;
69use crate::view::View;
70
71pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
73pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
75const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
77const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
80
81#[derive(Debug, Error)]
82pub enum GitRemoteNameError {
83 #[error(
84 "Git remote named '{name}' is reserved for local Git repository",
85 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
86 )]
87 ReservedForLocalGitRepo,
88 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
89 WithSlash(RemoteNameBuf),
90}
91
92fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
93 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
94 Err(GitRemoteNameError::ReservedForLocalGitRepo)
95 } else if name.as_str().contains("/") {
96 Err(GitRemoteNameError::WithSlash(name.to_owned()))
97 } else {
98 Ok(())
99 }
100}
101
102#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub enum GitRefKind {
105 Bookmark,
106 Tag,
107}
108
109#[derive(Clone, Debug, Default, Eq, PartialEq)]
111pub struct GitPushStats {
112 pub pushed: Vec<GitRefNameBuf>,
114 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
116 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
118}
119
120impl GitPushStats {
121 pub fn all_ok(&self) -> bool {
122 self.rejected.is_empty() && self.remote_rejected.is_empty()
123 }
124}
125
126#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
131
132impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
133 fn borrow(&self) -> &RemoteRefSymbol<'b> {
134 &self.0
135 }
136}
137
138#[derive(Debug, Hash, PartialEq, Eq)]
144pub(crate) struct RefSpec {
145 forced: bool,
146 source: Option<String>,
149 destination: String,
150}
151
152impl RefSpec {
153 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
154 Self {
155 forced: true,
156 source: Some(source.into()),
157 destination: destination.into(),
158 }
159 }
160
161 fn delete(destination: impl Into<String>) -> Self {
162 Self {
164 forced: false,
165 source: None,
166 destination: destination.into(),
167 }
168 }
169
170 pub(crate) fn to_git_format(&self) -> String {
171 format!(
172 "{}{}",
173 if self.forced { "+" } else { "" },
174 self.to_git_format_not_forced()
175 )
176 }
177
178 pub(crate) fn to_git_format_not_forced(&self) -> String {
184 if let Some(s) = &self.source {
185 format!("{}:{}", s, self.destination)
186 } else {
187 format!(":{}", self.destination)
188 }
189 }
190}
191
192pub(crate) struct RefToPush<'a> {
195 pub(crate) refspec: &'a RefSpec,
196 pub(crate) expected_location: Option<&'a CommitId>,
197}
198
199impl<'a> RefToPush<'a> {
200 fn new(
201 refspec: &'a RefSpec,
202 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
203 ) -> Self {
204 let expected_location = *expected_locations
205 .get(GitRefName::new(&refspec.destination))
206 .expect(
207 "The refspecs and the expected locations were both constructed from the same \
208 source of truth. This means the lookup should always work.",
209 );
210
211 Self {
212 refspec,
213 expected_location,
214 }
215 }
216
217 pub(crate) fn to_git_lease(&self) -> String {
218 format!(
219 "{}:{}",
220 self.refspec.destination,
221 self.expected_location
222 .map(|x| x.to_string())
223 .as_deref()
224 .unwrap_or("")
225 )
226 }
227}
228
229pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
232 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
233 if name == "HEAD" {
235 return None;
236 }
237 let name = RefName::new(name);
238 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
239 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
240 } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
241 let (remote, name) = remote_and_name.split_once('/')?;
242 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
244 return None;
245 }
246 let name = RefName::new(name);
247 let remote = RemoteName::new(remote);
248 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
249 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
250 let name = RefName::new(name);
251 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
252 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
253 } else {
254 None
255 }
256}
257
258fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
259 let RemoteRefSymbol { name, remote } = symbol;
260 let name = name.as_str();
261 let remote = remote.as_str();
262 if name.is_empty() || remote.is_empty() {
263 return None;
264 }
265 match kind {
266 GitRefKind::Bookmark => {
267 if name == "HEAD" {
268 return None;
269 }
270 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
271 Some(format!("refs/heads/{name}").into())
272 } else {
273 Some(format!("refs/remotes/{remote}/{name}").into())
274 }
275 }
276 GitRefKind::Tag => {
277 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
278 }
279 }
280}
281
282#[derive(Debug, Error)]
283#[error("The repo is not backed by a Git repo")]
284pub struct UnexpectedGitBackendError;
285
286pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
288 store
289 .backend_impl()
290 .downcast_ref()
291 .ok_or(UnexpectedGitBackendError)
292}
293
294pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
296 get_git_backend(store).map(|backend| backend.git_repo())
297}
298
299fn resolve_git_ref_to_commit_id(
304 git_ref: &gix::Reference,
305 known_target: &RefTarget,
306) -> Option<CommitId> {
307 let mut peeling_ref = Cow::Borrowed(git_ref);
308
309 if let Some(id) = known_target.as_normal() {
311 let raw_ref = &git_ref.inner;
312 if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
313 return Some(id.clone());
314 }
315 if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {
316 return Some(id.clone());
319 }
320 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
324 let maybe_tag = git_ref
325 .try_id()
326 .and_then(|id| id.object().ok())
327 .and_then(|object| object.try_into_tag().ok());
328 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
329 if oid.as_bytes() == id.as_bytes() {
330 return Some(id.clone());
332 }
333 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());
336 }
337 }
338 }
339
340 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
344 let is_commit = peeled_id
345 .object()
346 .is_ok_and(|object| object.kind.is_commit());
347 is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
348}
349
350#[derive(Error, Debug)]
351pub enum GitImportError {
352 #[error("Failed to read Git HEAD target commit {id}")]
353 MissingHeadTarget {
354 id: CommitId,
355 #[source]
356 err: BackendError,
357 },
358 #[error("Ancestor of Git ref {symbol} is missing")]
359 MissingRefAncestor {
360 symbol: RemoteRefSymbolBuf,
361 #[source]
362 err: BackendError,
363 },
364 #[error(transparent)]
365 Backend(BackendError),
366 #[error(transparent)]
367 Git(Box<dyn std::error::Error + Send + Sync>),
368 #[error(transparent)]
369 UnexpectedBackend(#[from] UnexpectedGitBackendError),
370}
371
372impl GitImportError {
373 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
374 Self::Git(source.into())
375 }
376}
377
378#[derive(Clone, Debug, Eq, PartialEq, Default)]
380pub struct GitImportStats {
381 pub abandoned_commits: Vec<CommitId>,
383 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
386 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
389 pub failed_ref_names: Vec<BString>,
394}
395
396#[derive(Debug)]
397struct RefsToImport {
398 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
401 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
404 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
407 failed_ref_names: Vec<BString>,
409}
410
411pub fn import_refs(
416 mut_repo: &mut MutableRepo,
417 git_settings: &GitSettings,
418) -> Result<GitImportStats, GitImportError> {
419 import_some_refs(mut_repo, git_settings, |_, _| true)
420}
421
422pub fn import_some_refs(
427 mut_repo: &mut MutableRepo,
428 git_settings: &GitSettings,
429 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
430) -> Result<GitImportStats, GitImportError> {
431 let store = mut_repo.store();
432 let git_backend = get_git_backend(store)?;
433 let git_repo = git_backend.git_repo();
434
435 let RefsToImport {
436 changed_git_refs,
437 changed_remote_bookmarks,
438 changed_remote_tags,
439 failed_ref_names,
440 } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;
441
442 let index = mut_repo.index();
449 let missing_head_ids = changed_git_refs
450 .iter()
451 .flat_map(|(_, new_target)| new_target.added_ids())
452 .filter(|&id| !index.has_id(id));
453 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
454
455 let mut head_commits = Vec::new();
457 let get_commit = |id| {
458 if !heads_imported && !index.has_id(id) {
460 git_backend.import_head_commits([id])?;
461 }
462 store.get_commit(id)
463 };
464 for (symbol, (_, new_target)) in
465 itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)
466 {
467 for id in new_target.added_ids() {
468 let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {
469 symbol: symbol.clone(),
470 err,
471 })?;
472 head_commits.push(commit);
473 }
474 }
475 mut_repo
478 .add_heads(&head_commits)
479 .map_err(GitImportError::Backend)?;
480
481 for (full_name, new_target) in changed_git_refs {
483 mut_repo.set_git_ref_target(&full_name, new_target);
484 }
485 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
486 let symbol = symbol.as_ref();
487 let base_target = old_remote_ref.tracked_target();
488 let new_remote_ref = RemoteRef {
489 target: new_target.clone(),
490 state: if old_remote_ref.is_present() {
491 old_remote_ref.state
492 } else {
493 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)
494 },
495 };
496 if new_remote_ref.is_tracked() {
497 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target);
498 }
499 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
502 }
503 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
504 let symbol = symbol.as_ref();
505 let base_target = old_remote_ref.tracked_target();
506 let new_remote_ref = RemoteRef {
507 target: new_target.clone(),
508 state: if old_remote_ref.is_present() {
509 old_remote_ref.state
510 } else {
511 default_remote_ref_state_for(GitRefKind::Tag, symbol, git_settings)
512 },
513 };
514 if new_remote_ref.is_tracked() {
515 mut_repo.merge_tag(symbol.name, base_target, &new_remote_ref.target);
516 }
517 }
519
520 let abandoned_commits = if git_settings.abandon_unreachable_commits {
521 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
522 .map_err(GitImportError::Backend)?
523 } else {
524 vec![]
525 };
526 let stats = GitImportStats {
527 abandoned_commits,
528 changed_remote_bookmarks,
529 changed_remote_tags,
530 failed_ref_names,
531 };
532 Ok(stats)
533}
534
535fn abandon_unreachable_commits(
538 mut_repo: &mut MutableRepo,
539 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
540 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
541) -> BackendResult<Vec<CommitId>> {
542 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
543 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
544 .cloned()
545 .collect_vec();
546 if hidable_git_heads.is_empty() {
547 return Ok(vec![]);
548 }
549 let pinned_expression = RevsetExpression::union_all(&[
550 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
552 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
553 .intersection(&RevsetExpression::visible_heads().ancestors()),
555 RevsetExpression::root(),
556 ]);
557 let abandoned_expression = pinned_expression
558 .range(&RevsetExpression::commits(hidable_git_heads))
559 .intersection(&RevsetExpression::visible_heads().ancestors());
561 let abandoned_commit_ids: Vec<_> = abandoned_expression
562 .evaluate(mut_repo)
563 .map_err(|err| err.into_backend_error())?
564 .iter()
565 .try_collect()
566 .map_err(|err| err.into_backend_error())?;
567 for id in &abandoned_commit_ids {
568 let commit = mut_repo.store().get_commit(id)?;
569 mut_repo.record_abandoned_commit(&commit);
570 }
571 Ok(abandoned_commit_ids)
572}
573
574fn diff_refs_to_import(
576 view: &View,
577 git_repo: &gix::Repository,
578 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
579) -> Result<RefsToImport, GitImportError> {
580 let mut known_git_refs = view
581 .git_refs()
582 .iter()
583 .filter_map(|(full_name, target)| {
584 let (kind, symbol) =
586 parse_git_ref(full_name).expect("stored git ref should be parsable");
587 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
588 })
589 .collect();
590 let mut known_remote_bookmarks = view
592 .all_remote_bookmarks()
593 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
594 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), (&remote_ref.target, remote_ref.state)))
595 .collect();
596 let mut known_remote_tags = view
599 .tags()
600 .iter()
601 .map(|(name, target)| {
602 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
603 let state = RemoteRefState::Tracked;
604 (symbol, (target, state))
605 })
606 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
607 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
608 .collect();
609
610 let mut changed_git_refs = Vec::new();
611 let mut changed_remote_bookmarks = Vec::new();
612 let mut changed_remote_tags = Vec::new();
613 let mut failed_ref_names = Vec::new();
614 let actual = git_repo.references().map_err(GitImportError::from_git)?;
615 collect_changed_refs_to_import(
616 actual.local_branches().map_err(GitImportError::from_git)?,
617 &mut known_git_refs,
618 &mut known_remote_bookmarks,
619 &mut changed_git_refs,
620 &mut changed_remote_bookmarks,
621 &mut failed_ref_names,
622 &git_ref_filter,
623 )?;
624 collect_changed_refs_to_import(
625 actual.remote_branches().map_err(GitImportError::from_git)?,
626 &mut known_git_refs,
627 &mut known_remote_bookmarks,
628 &mut changed_git_refs,
629 &mut changed_remote_bookmarks,
630 &mut failed_ref_names,
631 &git_ref_filter,
632 )?;
633 collect_changed_refs_to_import(
634 actual.tags().map_err(GitImportError::from_git)?,
635 &mut known_git_refs,
636 &mut known_remote_tags,
637 &mut changed_git_refs,
638 &mut changed_remote_tags,
639 &mut failed_ref_names,
640 &git_ref_filter,
641 )?;
642 for full_name in known_git_refs.into_keys() {
643 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
644 }
645 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_bookmarks {
646 let old_remote_ref = RemoteRef {
647 target: old_target.clone(),
648 state: old_state,
649 };
650 changed_remote_bookmarks.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
651 }
652 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_tags {
653 let old_remote_ref = RemoteRef {
654 target: old_target.clone(),
655 state: old_state,
656 };
657 changed_remote_tags.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
658 }
659
660 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
662 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
663 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
664 failed_ref_names.sort_unstable();
665 Ok(RefsToImport {
666 changed_git_refs,
667 changed_remote_bookmarks,
668 changed_remote_tags,
669 failed_ref_names,
670 })
671}
672
673fn collect_changed_refs_to_import(
674 actual_git_refs: gix::reference::iter::Iter<'_>,
675 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
676 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, (&RefTarget, RemoteRefState)>,
677 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
678 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
679 failed_ref_names: &mut Vec<BString>,
680 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
681) -> Result<(), GitImportError> {
682 for git_ref in actual_git_refs {
683 let git_ref = git_ref.map_err(GitImportError::from_git)?;
684 let full_name_bytes = git_ref.name().as_bstr();
685 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
686 failed_ref_names.push(full_name_bytes.to_owned());
688 continue;
689 };
690 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
691 failed_ref_names.push(full_name_bytes.to_owned());
692 continue;
693 }
694 let full_name = GitRefName::new(full_name);
695 let Some((kind, symbol)) = parse_git_ref(full_name) else {
696 continue;
698 };
699 if !git_ref_filter(kind, symbol) {
700 continue;
701 }
702 let old_git_target = known_git_refs.get(full_name).copied().flatten();
703 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
704 continue;
706 };
707 let new_target = RefTarget::normal(id);
708 known_git_refs.remove(full_name);
709 if new_target != *old_git_target {
710 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
711 }
712 let (old_remote_target, old_remote_state) = known_remote_refs
715 .remove(&symbol)
716 .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));
717 if new_target != *old_remote_target {
718 let old_remote_ref = RemoteRef {
719 target: old_remote_target.clone(),
720 state: old_remote_state,
721 };
722 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref, new_target)));
723 }
724 }
725 Ok(())
726}
727
728fn default_remote_ref_state_for(
729 kind: GitRefKind,
730 symbol: RemoteRefSymbol<'_>,
731 git_settings: &GitSettings,
732) -> RemoteRefState {
733 match kind {
734 GitRefKind::Bookmark => {
735 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
736 RemoteRefState::Tracked
737 } else {
738 RemoteRefState::New
739 }
740 }
741 GitRefKind::Tag => RemoteRefState::Tracked,
742 }
743}
744
745fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
751 itertools::chain(
752 view.local_bookmarks().map(|(_, target)| target),
753 view.tags().values(),
754 )
755 .flat_map(|target| target.added_ids())
756 .cloned()
757 .collect()
758}
759
760fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
767 view.all_remote_bookmarks()
768 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
769 .map(|(_, remote_ref)| &remote_ref.target)
770 .flat_map(|target| target.added_ids())
771 .cloned()
772 .collect()
773}
774
775pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
783 let store = mut_repo.store();
784 let git_backend = get_git_backend(store)?;
785 let git_repo = git_backend.git_repo();
786
787 let old_git_head = mut_repo.view().git_head();
788 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
789 Some(CommitId::from_bytes(oid.as_bytes()))
790 } else {
791 None
792 };
793 if old_git_head.as_resolved() == Some(&new_git_head_id) {
794 return Ok(());
795 }
796
797 if let Some(head_id) = &new_git_head_id {
799 let index = mut_repo.index();
800 if !index.has_id(head_id) {
801 git_backend.import_head_commits([head_id]).map_err(|err| {
802 GitImportError::MissingHeadTarget {
803 id: head_id.clone(),
804 err,
805 }
806 })?;
807 }
808 store
811 .get_commit(head_id)
812 .and_then(|commit| mut_repo.add_head(&commit))
813 .map_err(GitImportError::Backend)?;
814 }
815
816 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
817 Ok(())
818}
819
820#[derive(Error, Debug)]
821pub enum GitExportError {
822 #[error(transparent)]
823 Git(Box<dyn std::error::Error + Send + Sync>),
824 #[error(transparent)]
825 UnexpectedBackend(#[from] UnexpectedGitBackendError),
826}
827
828impl GitExportError {
829 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
830 Self::Git(source.into())
831 }
832}
833
834#[derive(Debug, Error)]
836pub enum FailedRefExportReason {
837 #[error("Name is not allowed in Git")]
839 InvalidGitName,
840 #[error("Ref was in a conflicted state from the last import")]
843 ConflictedOldState,
844 #[error("Ref cannot point to the root commit in Git")]
846 OnRootCommit,
847 #[error("Deleted ref had been modified in Git")]
849 DeletedInJjModifiedInGit,
850 #[error("Added ref had been added with a different target in Git")]
852 AddedInJjAddedInGit,
853 #[error("Modified ref had been deleted in Git")]
855 ModifiedInJjDeletedInGit,
856 #[error("Failed to delete")]
858 FailedToDelete(#[source] Box<gix::reference::edit::Error>),
859 #[error("Failed to set")]
861 FailedToSet(#[source] Box<gix::reference::edit::Error>),
862}
863
864#[derive(Debug)]
866pub struct GitExportStats {
867 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
869}
870
871#[derive(Debug)]
872struct RefsToExport {
873 bookmarks_to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
876 bookmarks_to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
881 failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
883}
884
885pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
896 export_some_refs(mut_repo, |_, _| true)
897}
898
899pub fn export_some_refs(
900 mut_repo: &mut MutableRepo,
901 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
902) -> Result<GitExportStats, GitExportError> {
903 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
904 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
905 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
906 let (_, value) = &map[index];
907 Some(value)
908 }
909
910 let git_repo = get_git_repo(mut_repo.store())?;
911
912 let RefsToExport {
913 bookmarks_to_update,
914 bookmarks_to_delete,
915 mut failed_bookmarks,
916 } = diff_refs_to_export(
917 mut_repo.view(),
918 mut_repo.store().root_commit_id(),
919 &git_ref_filter,
920 );
921
922 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
924 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
925 if let Some((GitRefKind::Bookmark, symbol)) = target_name
926 .as_ref()
927 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
928 .and_then(|name| parse_git_ref(name.as_ref()))
929 {
930 let old_target = head_ref.inner.target.clone();
931 let current_oid = match head_ref.into_fully_peeled_id() {
932 Ok(id) => Some(id.detach()),
933 Err(gix::reference::peel::Error::ToId(
934 gix::refs::peel::to_id::Error::FollowToObject(
935 gix::refs::peel::to_object::Error::Follow(
936 gix::refs::file::find::existing::Error::NotFound { .. },
937 ),
938 ),
939 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
941 };
942 let new_oid = if let Some((_old_oid, new_oid)) = get(&bookmarks_to_update, symbol) {
943 Some(new_oid)
944 } else if get(&bookmarks_to_delete, symbol).is_some() {
945 None
946 } else {
947 current_oid.as_ref()
948 };
949 if new_oid != current_oid.as_ref() {
950 update_git_head(
951 &git_repo,
952 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
953 current_oid,
954 )
955 .map_err(GitExportError::from_git)?;
956 }
957 }
958 }
959 for (symbol, old_oid) in bookmarks_to_delete {
960 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
961 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
962 continue;
963 };
964 if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) {
965 failed_bookmarks.push((symbol, reason));
966 } else {
967 let new_target = RefTarget::absent();
968 mut_repo.set_git_ref_target(&git_ref_name, new_target);
969 }
970 }
971 for (symbol, (old_oid, new_oid)) in bookmarks_to_update {
972 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
973 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
974 continue;
975 };
976 if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) {
977 failed_bookmarks.push((symbol, reason));
978 } else {
979 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
980 mut_repo.set_git_ref_target(&git_ref_name, new_target);
981 }
982 }
983
984 failed_bookmarks.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
986
987 copy_exportable_local_bookmarks_to_remote_view(
988 mut_repo,
989 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
990 |name| {
991 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
992 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
993 },
994 );
995
996 Ok(GitExportStats { failed_bookmarks })
997}
998
999fn copy_exportable_local_bookmarks_to_remote_view(
1000 mut_repo: &mut MutableRepo,
1001 remote: &RemoteName,
1002 name_filter: impl Fn(&RefName) -> bool,
1003) {
1004 let new_local_bookmarks = mut_repo
1005 .view()
1006 .local_remote_bookmarks(remote)
1007 .filter_map(|(name, targets)| {
1008 let old_target = &targets.remote_ref.target;
1011 let new_target = targets.local_target;
1012 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1013 })
1014 .filter(|&(name, _)| name_filter(name))
1015 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1016 .collect_vec();
1017 for (name, new_target) in new_local_bookmarks {
1018 let new_remote_ref = RemoteRef {
1019 target: new_target,
1020 state: RemoteRefState::Tracked,
1021 };
1022 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1023 }
1024}
1025
1026fn diff_refs_to_export(
1028 view: &View,
1029 root_commit_id: &CommitId,
1030 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1031) -> RefsToExport {
1032 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1035 itertools::chain(
1036 view.local_bookmarks().map(|(name, target)| {
1037 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1038 (symbol, target)
1039 }),
1040 view.all_remote_bookmarks()
1041 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1042 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1043 )
1044 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1045 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1046 .collect();
1047 let known_git_refs = view
1048 .git_refs()
1049 .iter()
1050 .map(|(full_name, target)| {
1051 let (kind, symbol) =
1052 parse_git_ref(full_name).expect("stored git ref should be parsable");
1053 ((kind, symbol), target)
1054 })
1055 .filter(|&((kind, symbol), _)| {
1056 kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol)
1060 });
1061 for ((_kind, symbol), target) in known_git_refs {
1062 all_bookmark_targets
1063 .entry(symbol)
1064 .and_modify(|(old_target, _)| *old_target = target)
1065 .or_insert((target, RefTarget::absent_ref()));
1066 }
1067
1068 let mut bookmarks_to_update = Vec::new();
1069 let mut bookmarks_to_delete = Vec::new();
1070 let mut failed_bookmarks = Vec::new();
1071 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1072 for (symbol, (old_target, new_target)) in all_bookmark_targets {
1073 if new_target == old_target {
1074 continue;
1075 }
1076 if *new_target == root_commit_target {
1077 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1079 continue;
1080 }
1081 let old_oid = if let Some(id) = old_target.as_normal() {
1082 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1083 } else if old_target.has_conflict() {
1084 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1087 continue;
1088 } else {
1089 assert!(old_target.is_absent());
1090 None
1091 };
1092 if let Some(id) = new_target.as_normal() {
1093 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1094 bookmarks_to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1095 } else if new_target.has_conflict() {
1096 continue;
1098 } else {
1099 assert!(new_target.is_absent());
1100 bookmarks_to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1101 }
1102 }
1103
1104 bookmarks_to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1106 bookmarks_to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1107 failed_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1108 RefsToExport {
1109 bookmarks_to_update,
1110 bookmarks_to_delete,
1111 failed_bookmarks,
1112 }
1113}
1114
1115fn delete_git_ref(
1116 git_repo: &gix::Repository,
1117 git_ref_name: &GitRefName,
1118 old_oid: &gix::oid,
1119) -> Result<(), FailedRefExportReason> {
1120 if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1121 if git_ref.inner.target.try_id() == Some(old_oid) {
1122 git_ref
1124 .delete()
1125 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1126 } else {
1127 return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1129 }
1130 } else {
1131 }
1133 Ok(())
1134}
1135
1136fn update_git_ref(
1137 git_repo: &gix::Repository,
1138 git_ref_name: &GitRefName,
1139 old_oid: Option<gix::ObjectId>,
1140 new_oid: gix::ObjectId,
1141) -> Result<(), FailedRefExportReason> {
1142 match old_oid {
1143 None => {
1144 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1145 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1148 return Err(FailedRefExportReason::AddedInJjAddedInGit);
1149 }
1150 } else {
1151 git_repo
1153 .reference(
1154 git_ref_name.as_str(),
1155 new_oid,
1156 gix::refs::transaction::PreviousValue::MustNotExist,
1157 "export from jj",
1158 )
1159 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1160 }
1161 }
1162 Some(old_oid) => {
1163 if let Err(err) = git_repo.reference(
1165 git_ref_name.as_str(),
1166 new_oid,
1167 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1168 "export from jj",
1169 ) {
1170 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1172 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1174 return Err(FailedRefExportReason::FailedToSet(err.into()));
1175 }
1176 } else {
1177 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1179 }
1180 } else {
1181 }
1184 }
1185 }
1186 Ok(())
1187}
1188
1189fn update_git_head(
1192 git_repo: &gix::Repository,
1193 expected_ref: gix::refs::transaction::PreviousValue,
1194 new_oid: Option<gix::ObjectId>,
1195) -> Result<(), gix::reference::edit::Error> {
1196 let mut ref_edits = Vec::new();
1197 let new_target = if let Some(oid) = new_oid {
1198 gix::refs::Target::Object(oid)
1199 } else {
1200 ref_edits.push(gix::refs::transaction::RefEdit {
1205 change: gix::refs::transaction::Change::Delete {
1206 expected: gix::refs::transaction::PreviousValue::Any,
1207 log: gix::refs::transaction::RefLog::AndReference,
1208 },
1209 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1210 deref: false,
1211 });
1212 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1213 };
1214 ref_edits.push(gix::refs::transaction::RefEdit {
1215 change: gix::refs::transaction::Change::Update {
1216 log: gix::refs::transaction::LogChange {
1217 message: "export from jj".into(),
1218 ..Default::default()
1219 },
1220 expected: expected_ref,
1221 new: new_target,
1222 },
1223 name: "HEAD".try_into().unwrap(),
1224 deref: false,
1225 });
1226 git_repo.edit_references(ref_edits)?;
1227 Ok(())
1228}
1229
1230#[derive(Debug, Error)]
1231pub enum GitResetHeadError {
1232 #[error(transparent)]
1233 Backend(#[from] BackendError),
1234 #[error(transparent)]
1235 Git(Box<dyn std::error::Error + Send + Sync>),
1236 #[error("Failed to update Git HEAD ref")]
1237 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1238 #[error(transparent)]
1239 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1240}
1241
1242impl GitResetHeadError {
1243 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1244 Self::Git(source.into())
1245 }
1246}
1247
1248pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1251 let git_repo = get_git_repo(mut_repo.store())?;
1252
1253 let first_parent_id = &wc_commit.parent_ids()[0];
1254 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1255 RefTarget::normal(first_parent_id.clone())
1256 } else {
1257 RefTarget::absent()
1258 };
1259
1260 let old_head_target = mut_repo.git_head();
1262 if old_head_target != new_head_target {
1263 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1264 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1267 if actual_head.is_detached() {
1268 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1269 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1270 } else {
1271 gix::refs::transaction::PreviousValue::MustExist
1274 }
1275 } else {
1276 gix::refs::transaction::PreviousValue::MustExist
1278 };
1279 let new_oid = new_head_target
1280 .as_normal()
1281 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1282 update_git_head(&git_repo, expected_ref, new_oid)
1283 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1284 mut_repo.set_git_head_target(new_head_target);
1285 }
1286
1287 if git_repo.state().is_some() {
1292 const STATE_FILE_NAMES: &[&str] = &[
1296 "MERGE_HEAD",
1297 "MERGE_MODE",
1298 "MERGE_MSG",
1299 "REVERT_HEAD",
1300 "CHERRY_PICK_HEAD",
1301 "BISECT_LOG",
1302 ];
1303 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1304 let handle_err = |err: PathError| match err.error.kind() {
1305 std::io::ErrorKind::NotFound => Ok(()),
1306 _ => Err(GitResetHeadError::from_git(err)),
1307 };
1308 for file_name in STATE_FILE_NAMES {
1309 let path = git_repo.path().join(file_name);
1310 std::fs::remove_file(&path)
1311 .context(&path)
1312 .or_else(handle_err)?;
1313 }
1314 for dir_name in STATE_DIR_NAMES {
1315 let path = git_repo.path().join(dir_name);
1316 std::fs::remove_dir_all(&path)
1317 .context(&path)
1318 .or_else(handle_err)?;
1319 }
1320 }
1321
1322 let parent_tree = wc_commit.parent_tree(mut_repo)?;
1323
1324 let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
1328 if tree.id() == mut_repo.store().empty_tree_id() {
1329 gix::index::File::from_state(
1333 gix::index::State::new(git_repo.object_hash()),
1334 git_repo.index_path(),
1335 )
1336 } else {
1337 git_repo
1340 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes()))
1341 .map_err(GitResetHeadError::from_git)?
1342 }
1343 } else {
1344 build_index_from_merged_tree(&git_repo, parent_tree.clone())?
1345 };
1346
1347 let wc_tree = wc_commit.tree()?;
1348 update_intent_to_add_impl(&git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1349
1350 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1353 index
1354 .entries_mut_with_paths()
1355 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1356 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1357 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1358 })
1359 .filter_map(|merged| merged.both())
1360 .map(|((entry, _), old_entry)| (entry, old_entry))
1361 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1362 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1363 }
1364
1365 debug_assert!(index.verify_entries().is_ok());
1366
1367 index
1368 .write(gix::index::write::Options::default())
1369 .map_err(GitResetHeadError::from_git)?;
1370
1371 Ok(())
1372}
1373
1374fn build_index_from_merged_tree(
1375 git_repo: &gix::Repository,
1376 merged_tree: MergedTree,
1377) -> Result<gix::index::File, GitResetHeadError> {
1378 let mut index = gix::index::File::from_state(
1379 gix::index::State::new(git_repo.object_hash()),
1380 git_repo.index_path(),
1381 );
1382
1383 let mut push_index_entry =
1384 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1385 let Some(entry) = maybe_entry else {
1386 return;
1387 };
1388
1389 let (id, mode) = match entry {
1390 TreeValue::File {
1391 id,
1392 executable,
1393 copy_id: _,
1394 } => {
1395 if *executable {
1396 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1397 } else {
1398 (id.as_bytes(), gix::index::entry::Mode::FILE)
1399 }
1400 }
1401 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1402 TreeValue::Tree(_) => {
1403 return;
1408 }
1409 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1410 TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"),
1411 };
1412
1413 let path = BStr::new(path.as_internal_file_string());
1414
1415 index.dangerously_push_entry(
1418 gix::index::entry::Stat::default(),
1419 gix::ObjectId::from_bytes_or_panic(id),
1420 gix::index::entry::Flags::from_stage(stage),
1421 mode,
1422 path,
1423 );
1424 };
1425
1426 let mut has_many_sided_conflict = false;
1427
1428 for (path, entry) in merged_tree.entries() {
1429 let entry = entry?;
1430 if let Some(resolved) = entry.as_resolved() {
1431 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1432 continue;
1433 }
1434
1435 let conflict = entry.simplify();
1436 if let [left, base, right] = conflict.as_slice() {
1437 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1439 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1440 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1441 } else {
1442 has_many_sided_conflict = true;
1450 push_index_entry(
1451 &path,
1452 conflict.first(),
1453 gix::index::entry::Stage::Unconflicted,
1454 );
1455 }
1456 }
1457
1458 index.sort_entries();
1461
1462 if has_many_sided_conflict
1465 && index
1466 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1467 .is_err()
1468 {
1469 let file_blob = git_repo
1470 .write_blob(
1471 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1472 )
1473 .map_err(GitResetHeadError::from_git)?;
1474 index.dangerously_push_entry(
1475 gix::index::entry::Stat::default(),
1476 file_blob.detach(),
1477 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1478 gix::index::entry::Mode::FILE,
1479 INDEX_DUMMY_CONFLICT_FILE.into(),
1480 );
1481 index.sort_entries();
1484 }
1485
1486 Ok(index)
1487}
1488
1489pub fn update_intent_to_add(
1496 repo: &dyn Repo,
1497 old_tree: &MergedTree,
1498 new_tree: &MergedTree,
1499) -> Result<(), GitResetHeadError> {
1500 let git_repo = get_git_repo(repo.store())?;
1501 let mut index = git_repo
1502 .index_or_empty()
1503 .map_err(GitResetHeadError::from_git)?;
1504 let mut_index = Arc::make_mut(&mut index);
1505 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1506 debug_assert!(mut_index.verify_entries().is_ok());
1507 mut_index
1508 .write(gix::index::write::Options::default())
1509 .map_err(GitResetHeadError::from_git)?;
1510
1511 Ok(())
1512}
1513
1514async fn update_intent_to_add_impl(
1515 git_repo: &gix::Repository,
1516 index: &mut gix::index::File,
1517 old_tree: &MergedTree,
1518 new_tree: &MergedTree,
1519) -> Result<(), GitResetHeadError> {
1520 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1521 let mut added_paths = vec![];
1522 let mut removed_paths = HashSet::new();
1523 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1524 let (before, after) = values?;
1525 if before.is_absent() {
1526 let executable = match after.as_normal() {
1527 Some(TreeValue::File {
1528 id: _,
1529 executable,
1530 copy_id: _,
1531 }) => *executable,
1532 Some(TreeValue::Symlink(_)) => false,
1533 _ => {
1534 continue;
1535 }
1536 };
1537 if index
1538 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1539 .is_err()
1540 {
1541 added_paths.push((BString::from(path.into_internal_string()), executable));
1542 }
1543 } else if after.is_absent() {
1544 removed_paths.insert(BString::from(path.into_internal_string()));
1545 }
1546 }
1547
1548 if added_paths.is_empty() && removed_paths.is_empty() {
1549 return Ok(());
1550 }
1551
1552 if !added_paths.is_empty() {
1553 let empty_blob = git_repo
1555 .write_blob(b"")
1556 .map_err(GitResetHeadError::from_git)?
1557 .detach();
1558 for (path, executable) in added_paths {
1559 index.dangerously_push_entry(
1561 gix::index::entry::Stat::default(),
1562 empty_blob,
1563 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1564 if executable {
1565 gix::index::entry::Mode::FILE_EXECUTABLE
1566 } else {
1567 gix::index::entry::Mode::FILE
1568 },
1569 path.as_ref(),
1570 );
1571 }
1572 }
1573 if !removed_paths.is_empty() {
1574 index.remove_entries(|_size, path, entry| {
1575 entry
1576 .flags
1577 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1578 && removed_paths.contains(path)
1579 });
1580 }
1581
1582 index.sort_entries();
1583
1584 Ok(())
1585}
1586
1587#[derive(Debug, Error)]
1588pub enum GitRemoteManagementError {
1589 #[error("No git remote named '{}'", .0.as_symbol())]
1590 NoSuchRemote(RemoteNameBuf),
1591 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1592 RemoteAlreadyExists(RemoteNameBuf),
1593 #[error(transparent)]
1594 RemoteName(#[from] GitRemoteNameError),
1595 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1596 NonstandardConfiguration(RemoteNameBuf),
1597 #[error("Error saving Git configuration")]
1598 GitConfigSaveError(#[source] std::io::Error),
1599 #[error("Unexpected Git error when managing remotes")]
1600 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1601 #[error(transparent)]
1602 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1603}
1604
1605impl GitRemoteManagementError {
1606 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1607 Self::InternalGitError(source.into())
1608 }
1609}
1610
1611pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1616 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1617}
1618
1619fn default_fetch_refspec(remote: &RemoteName) -> String {
1620 format!(
1621 "+refs/heads/*:refs/remotes/{remote}/*",
1622 remote = remote.as_str()
1623 )
1624}
1625
1626fn add_ref(
1627 name: gix::refs::FullName,
1628 target: gix::refs::Target,
1629 message: BString,
1630) -> gix::refs::transaction::RefEdit {
1631 gix::refs::transaction::RefEdit {
1632 change: gix::refs::transaction::Change::Update {
1633 log: gix::refs::transaction::LogChange {
1634 mode: gix::refs::transaction::RefLog::AndReference,
1635 force_create_reflog: false,
1636 message,
1637 },
1638 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1639 new: target,
1640 },
1641 name,
1642 deref: false,
1643 }
1644}
1645
1646fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1647 gix::refs::transaction::RefEdit {
1648 change: gix::refs::transaction::Change::Delete {
1649 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1650 reference.target().into_owned(),
1651 ),
1652 log: gix::refs::transaction::RefLog::AndReference,
1653 },
1654 name: reference.name().to_owned(),
1655 deref: false,
1656 }
1657}
1658
1659fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1665 let mut config_file = File::create(
1666 config
1667 .meta()
1668 .path
1669 .as_ref()
1670 .expect("Git repository to have a config file"),
1671 )?;
1672 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1673}
1674
1675fn save_remote(
1676 config: &mut gix::config::File<'static>,
1677 remote_name: &RemoteName,
1678 remote: &mut gix::Remote,
1679) -> Result<(), GitRemoteManagementError> {
1680 config
1687 .new_section(
1688 "remote",
1689 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1690 )
1691 .map_err(GitRemoteManagementError::from_git)?;
1692 remote
1693 .save_as_to(remote_name.as_str(), config)
1694 .map_err(GitRemoteManagementError::from_git)?;
1695 Ok(())
1696}
1697
1698fn git_config_branch_section_ids_by_remote(
1699 config: &gix::config::File,
1700 remote_name: &RemoteName,
1701) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1702 config
1703 .sections_by_name("branch")
1704 .into_iter()
1705 .flatten()
1706 .filter_map(|section| {
1707 let remote_values = section.values("remote");
1708 let push_remote_values = section.values("pushRemote");
1709 if !remote_values
1710 .iter()
1711 .chain(push_remote_values.iter())
1712 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1713 {
1714 return None;
1715 }
1716 if remote_values.len() > 1
1717 || push_remote_values.len() > 1
1718 || section.value_names().any(|name| {
1719 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1720 })
1721 {
1722 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1723 remote_name.to_owned(),
1724 )));
1725 }
1726 Some(Ok(section.id()))
1727 })
1728 .collect()
1729}
1730
1731fn rename_remote_in_git_branch_config_sections(
1732 config: &mut gix::config::File,
1733 old_remote_name: &RemoteName,
1734 new_remote_name: &RemoteName,
1735) -> Result<(), GitRemoteManagementError> {
1736 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1737 config
1738 .section_mut_by_id(id)
1739 .expect("found section to exist")
1740 .set(
1741 "remote"
1742 .try_into()
1743 .expect("'remote' to be a valid value name"),
1744 BStr::new(new_remote_name.as_str()),
1745 );
1746 }
1747 Ok(())
1748}
1749
1750fn remove_remote_git_branch_config_sections(
1751 config: &mut gix::config::File,
1752 remote_name: &RemoteName,
1753) -> Result<(), GitRemoteManagementError> {
1754 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1755 config
1756 .remove_section_by_id(id)
1757 .expect("removed section to exist");
1758 }
1759 Ok(())
1760}
1761
1762fn remove_remote_git_config_sections(
1763 config: &mut gix::config::File,
1764 remote_name: &RemoteName,
1765) -> Result<(), GitRemoteManagementError> {
1766 let section_ids_to_remove: Vec<_> = config
1767 .sections_by_name("remote")
1768 .into_iter()
1769 .flatten()
1770 .filter(|section| {
1771 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1772 })
1773 .map(|section| {
1774 if section.value_names().any(|name| {
1775 !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1776 }) {
1777 return Err(GitRemoteManagementError::NonstandardConfiguration(
1778 remote_name.to_owned(),
1779 ));
1780 }
1781 Ok(section.id())
1782 })
1783 .try_collect()?;
1784 for id in section_ids_to_remove {
1785 config
1786 .remove_section_by_id(id)
1787 .expect("removed section to exist");
1788 }
1789 Ok(())
1790}
1791
1792pub fn get_all_remote_names(
1794 store: &Store,
1795) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1796 let git_repo = get_git_repo(store)?;
1797 let names = git_repo
1798 .remote_names()
1799 .into_iter()
1800 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1802 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1804 .map(RemoteNameBuf::from)
1805 .collect();
1806 Ok(names)
1807}
1808
1809pub fn add_remote(
1810 store: &Store,
1811 remote_name: &RemoteName,
1812 url: &str,
1813) -> Result<(), GitRemoteManagementError> {
1814 let git_repo = get_git_repo(store)?;
1815
1816 validate_remote_name(remote_name)?;
1817
1818 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1819 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1820 remote_name.to_owned(),
1821 ));
1822 }
1823
1824 let mut remote = git_repo
1825 .remote_at(url)
1826 .map_err(GitRemoteManagementError::from_git)?
1827 .with_refspecs(
1828 [default_fetch_refspec(remote_name).as_bytes()],
1829 gix::remote::Direction::Fetch,
1830 )
1831 .expect("default refspec to be valid");
1832
1833 let mut config = git_repo.config_snapshot().clone();
1834 save_remote(&mut config, remote_name, &mut remote)?;
1835 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1836
1837 Ok(())
1838}
1839
1840pub fn remove_remote(
1841 mut_repo: &mut MutableRepo,
1842 remote_name: &RemoteName,
1843) -> Result<(), GitRemoteManagementError> {
1844 let mut git_repo = get_git_repo(mut_repo.store())?;
1845
1846 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1847 return Err(GitRemoteManagementError::NoSuchRemote(
1848 remote_name.to_owned(),
1849 ));
1850 };
1851
1852 let mut config = git_repo.config_snapshot().clone();
1853 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1854 remove_remote_git_config_sections(&mut config, remote_name)?;
1855 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1856
1857 remove_remote_git_refs(&mut git_repo, remote_name)
1858 .map_err(GitRemoteManagementError::from_git)?;
1859
1860 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1861 remove_remote_refs(mut_repo, remote_name);
1862 }
1863
1864 Ok(())
1865}
1866
1867fn remove_remote_git_refs(
1868 git_repo: &mut gix::Repository,
1869 remote: &RemoteName,
1870) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1871 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1872 let edits: Vec<_> = git_repo
1873 .references()?
1874 .prefixed(prefix.as_str())?
1875 .map_ok(remove_ref)
1876 .try_collect()?;
1877 git_repo.edit_references(edits)?;
1878 Ok(())
1879}
1880
1881fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1882 mut_repo.remove_remote(remote);
1883 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1884 let git_refs_to_delete = mut_repo
1885 .view()
1886 .git_refs()
1887 .keys()
1888 .filter(|&r| r.as_str().starts_with(&prefix))
1889 .cloned()
1890 .collect_vec();
1891 for git_ref in git_refs_to_delete {
1892 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1893 }
1894}
1895
1896pub fn rename_remote(
1897 mut_repo: &mut MutableRepo,
1898 old_remote_name: &RemoteName,
1899 new_remote_name: &RemoteName,
1900) -> Result<(), GitRemoteManagementError> {
1901 let mut git_repo = get_git_repo(mut_repo.store())?;
1902
1903 validate_remote_name(new_remote_name)?;
1904
1905 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1906 return Err(GitRemoteManagementError::NoSuchRemote(
1907 old_remote_name.to_owned(),
1908 ));
1909 };
1910 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1911
1912 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1913 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1914 new_remote_name.to_owned(),
1915 ));
1916 }
1917
1918 match (
1919 remote.refspecs(gix::remote::Direction::Fetch),
1920 remote.refspecs(gix::remote::Direction::Push),
1921 ) {
1922 ([refspec], [])
1923 if refspec.to_ref().to_bstring()
1924 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1925 _ => {
1926 return Err(GitRemoteManagementError::NonstandardConfiguration(
1927 old_remote_name.to_owned(),
1928 ));
1929 }
1930 }
1931
1932 remote
1933 .replace_refspecs(
1934 [default_fetch_refspec(new_remote_name).as_bytes()],
1935 gix::remote::Direction::Fetch,
1936 )
1937 .expect("default refspec to be valid");
1938
1939 let mut config = git_repo.config_snapshot().clone();
1940 save_remote(&mut config, new_remote_name, &mut remote)?;
1941 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1942 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1943 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1944
1945 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1946 .map_err(GitRemoteManagementError::from_git)?;
1947
1948 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1949 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1950 }
1951
1952 Ok(())
1953}
1954
1955fn rename_remote_git_refs(
1956 git_repo: &mut gix::Repository,
1957 old_remote_name: &RemoteName,
1958 new_remote_name: &RemoteName,
1959) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1960 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1961 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1962 let ref_log_message = BString::from(format!(
1963 "renamed remote {old_remote_name} to {new_remote_name}",
1964 old_remote_name = old_remote_name.as_symbol(),
1965 new_remote_name = new_remote_name.as_symbol(),
1966 ));
1967
1968 let edits: Vec<_> = git_repo
1969 .references()?
1970 .prefixed(old_prefix.as_str())?
1971 .map_ok(|old_ref| {
1972 let new_name = BString::new(
1973 [
1974 new_prefix.as_bytes(),
1975 &old_ref.name().as_bstr()[old_prefix.len()..],
1976 ]
1977 .concat(),
1978 );
1979 [
1980 add_ref(
1981 new_name.try_into().expect("new ref name to be valid"),
1982 old_ref.target().into_owned(),
1983 ref_log_message.clone(),
1984 ),
1985 remove_ref(old_ref),
1986 ]
1987 })
1988 .flatten_ok()
1989 .try_collect()?;
1990 git_repo.edit_references(edits)?;
1991 Ok(())
1992}
1993
1994fn gix_remote_with_fetch_url<Url, E>(
2000 remote: gix::Remote,
2001 url: Url,
2002) -> Result<gix::Remote, gix::remote::init::Error>
2003where
2004 Url: TryInto<gix::Url, Error = E>,
2005 gix::url::parse::Error: From<E>,
2006{
2007 let mut new_remote = remote.repo().remote_at(url)?;
2008 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2014 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2015 new_remote
2016 .replace_refspecs(
2017 remote
2018 .refspecs(direction)
2019 .iter()
2020 .map(|refspec| refspec.to_ref().to_bstring()),
2021 direction,
2022 )
2023 .expect("existing refspecs to be valid");
2024 }
2025 Ok(new_remote)
2026}
2027
2028pub fn set_remote_url(
2029 store: &Store,
2030 remote_name: &RemoteName,
2031 new_remote_url: &str,
2032) -> Result<(), GitRemoteManagementError> {
2033 let git_repo = get_git_repo(store)?;
2034
2035 validate_remote_name(remote_name)?;
2036
2037 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2038 return Err(GitRemoteManagementError::NoSuchRemote(
2039 remote_name.to_owned(),
2040 ));
2041 };
2042 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2043
2044 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2045 return Err(GitRemoteManagementError::NonstandardConfiguration(
2046 remote_name.to_owned(),
2047 ));
2048 }
2049
2050 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2051 .map_err(GitRemoteManagementError::from_git)?;
2052
2053 let mut config = git_repo.config_snapshot().clone();
2054 save_remote(&mut config, remote_name, &mut remote)?;
2055 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2056
2057 Ok(())
2058}
2059
2060fn rename_remote_refs(
2061 mut_repo: &mut MutableRepo,
2062 old_remote_name: &RemoteName,
2063 new_remote_name: &RemoteName,
2064) {
2065 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2066 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2067 let git_refs = mut_repo
2068 .view()
2069 .git_refs()
2070 .iter()
2071 .filter_map(|(old, target)| {
2072 old.as_str().strip_prefix(&prefix).map(|p| {
2073 let new: GitRefNameBuf =
2074 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2075 (old.clone(), new, target.clone())
2076 })
2077 })
2078 .collect_vec();
2079 for (old, new, target) in git_refs {
2080 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2081 mut_repo.set_git_ref_target(&new, target);
2082 }
2083}
2084
2085const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2086
2087#[derive(Error, Debug)]
2088pub enum GitFetchError {
2089 #[error("No git remote named '{}'", .0.as_symbol())]
2090 NoSuchRemote(RemoteNameBuf),
2091 #[error(
2092 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2093 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2094 )]
2095 InvalidBranchPattern(StringPattern),
2096 #[error(transparent)]
2097 RemoteName(#[from] GitRemoteNameError),
2098 #[error(transparent)]
2099 Subprocess(#[from] GitSubprocessError),
2100}
2101
2102struct FetchedBranches {
2103 remote: RemoteNameBuf,
2104 branches: Vec<StringPattern>,
2105}
2106
2107fn expand_fetch_refspecs(
2108 remote: &RemoteName,
2109 branch_names: &[StringPattern],
2110) -> Result<Vec<RefSpec>, GitFetchError> {
2111 branch_names
2112 .iter()
2113 .map(|pattern| {
2114 pattern
2115 .to_glob()
2116 .filter(
2117 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2120 )
2121 .map(|glob| {
2122 RefSpec::forced(
2123 format!("refs/heads/{glob}"),
2124 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2125 )
2126 })
2127 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2128 })
2129 .collect()
2130}
2131
2132pub struct GitFetch<'a> {
2134 mut_repo: &'a mut MutableRepo,
2135 git_repo: Box<gix::Repository>,
2136 git_ctx: GitSubprocessContext<'a>,
2137 git_settings: &'a GitSettings,
2138 fetched: Vec<FetchedBranches>,
2139}
2140
2141impl<'a> GitFetch<'a> {
2142 pub fn new(
2143 mut_repo: &'a mut MutableRepo,
2144 git_settings: &'a GitSettings,
2145 ) -> Result<Self, UnexpectedGitBackendError> {
2146 let git_backend = get_git_backend(mut_repo.store())?;
2147 let git_repo = Box::new(git_backend.git_repo());
2148 let git_ctx =
2149 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2150 Ok(GitFetch {
2151 mut_repo,
2152 git_repo,
2153 git_ctx,
2154 git_settings,
2155 fetched: vec![],
2156 })
2157 }
2158
2159 #[tracing::instrument(skip(self, callbacks))]
2165 pub fn fetch(
2166 &mut self,
2167 remote_name: &RemoteName,
2168 branch_names: &[StringPattern],
2169 mut callbacks: RemoteCallbacks<'_>,
2170 depth: Option<NonZeroU32>,
2171 ) -> Result<(), GitFetchError> {
2172 validate_remote_name(remote_name)?;
2173
2174 if self
2176 .git_repo
2177 .try_find_remote(remote_name.as_str())
2178 .is_none()
2179 {
2180 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2181 }
2182 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2185 if remaining_refspecs.is_empty() {
2186 return Ok(());
2188 }
2189
2190 let mut branches_to_prune = Vec::new();
2191 while let Some(failing_refspec) =
2199 self.git_ctx
2200 .spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2201 {
2202 tracing::debug!(failing_refspec, "failed to fetch ref");
2203 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2204
2205 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2206 branches_to_prune.push(format!(
2207 "{remote_name}/{branch_name}",
2208 remote_name = remote_name.as_str()
2209 ));
2210 }
2211 }
2212
2213 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2216
2217 self.fetched.push(FetchedBranches {
2218 remote: remote_name.to_owned(),
2219 branches: branch_names.to_vec(),
2220 });
2221 Ok(())
2222 }
2223
2224 #[tracing::instrument(skip(self))]
2226 pub fn get_default_branch(
2227 &self,
2228 remote_name: &RemoteName,
2229 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2230 if self
2231 .git_repo
2232 .try_find_remote(remote_name.as_str())
2233 .is_none()
2234 {
2235 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2236 }
2237 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2238 tracing::debug!(?default_branch);
2239 Ok(default_branch)
2240 }
2241
2242 #[tracing::instrument(skip(self))]
2250 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2251 tracing::debug!("import_refs");
2252 let import_stats =
2253 import_some_refs(
2254 self.mut_repo,
2255 self.git_settings,
2256 |kind, symbol| match kind {
2257 GitRefKind::Bookmark => self
2258 .fetched
2259 .iter()
2260 .filter(|fetched| fetched.remote == symbol.remote)
2261 .any(|fetched| {
2262 fetched
2263 .branches
2264 .iter()
2265 .any(|pattern| pattern.is_match(symbol.name.as_str()))
2266 }),
2267 GitRefKind::Tag => true,
2268 },
2269 )?;
2270
2271 self.fetched.clear();
2272
2273 Ok(import_stats)
2274 }
2275}
2276
2277#[derive(Error, Debug)]
2278pub enum GitPushError {
2279 #[error("No git remote named '{}'", .0.as_symbol())]
2280 NoSuchRemote(RemoteNameBuf),
2281 #[error(transparent)]
2282 RemoteName(#[from] GitRemoteNameError),
2283 #[error(transparent)]
2284 Subprocess(#[from] GitSubprocessError),
2285 #[error(transparent)]
2286 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2287}
2288
2289#[derive(Clone, Debug)]
2290pub struct GitBranchPushTargets {
2291 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2292}
2293
2294pub struct GitRefUpdate {
2295 pub qualified_name: GitRefNameBuf,
2296 pub expected_current_target: Option<CommitId>,
2301 pub new_target: Option<CommitId>,
2302}
2303
2304pub fn push_branches(
2306 mut_repo: &mut MutableRepo,
2307 git_settings: &GitSettings,
2308 remote: &RemoteName,
2309 targets: &GitBranchPushTargets,
2310 callbacks: RemoteCallbacks<'_>,
2311) -> Result<GitPushStats, GitPushError> {
2312 validate_remote_name(remote)?;
2313
2314 let ref_updates = targets
2315 .branch_updates
2316 .iter()
2317 .map(|(name, update)| GitRefUpdate {
2318 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2319 expected_current_target: update.old_target.clone(),
2320 new_target: update.new_target.clone(),
2321 })
2322 .collect_vec();
2323
2324 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2325 tracing::debug!(?push_stats);
2326
2327 if push_stats.all_ok() {
2331 for (name, update) in &targets.branch_updates {
2332 let git_ref_name: GitRefNameBuf = format!(
2333 "refs/remotes/{remote}/{name}",
2334 remote = remote.as_str(),
2335 name = name.as_str()
2336 )
2337 .into();
2338 let new_remote_ref = RemoteRef {
2339 target: RefTarget::resolved(update.new_target.clone()),
2340 state: RemoteRefState::Tracked,
2341 };
2342 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2343 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2344 }
2345 }
2346
2347 Ok(push_stats)
2348}
2349
2350pub fn push_updates(
2352 repo: &dyn Repo,
2353 git_settings: &GitSettings,
2354 remote_name: &RemoteName,
2355 updates: &[GitRefUpdate],
2356 mut callbacks: RemoteCallbacks<'_>,
2357) -> Result<GitPushStats, GitPushError> {
2358 let mut qualified_remote_refs_expected_locations = HashMap::new();
2359 let mut refspecs = vec![];
2360 for update in updates {
2361 qualified_remote_refs_expected_locations.insert(
2362 update.qualified_name.as_ref(),
2363 update.expected_current_target.as_ref(),
2364 );
2365 if let Some(new_target) = &update.new_target {
2366 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2370 } else {
2371 refspecs.push(RefSpec::delete(&update.qualified_name));
2375 }
2376 }
2377
2378 let git_backend = get_git_backend(repo.store())?;
2379 let git_repo = git_backend.git_repo();
2380 let git_ctx =
2381 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2382
2383 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2385 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2386 }
2387
2388 let refs_to_push: Vec<RefToPush> = refspecs
2389 .iter()
2390 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2391 .collect();
2392
2393 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2394 push_stats.pushed.sort();
2395 push_stats.rejected.sort();
2396 push_stats.remote_rejected.sort();
2397 Ok(push_stats)
2398}
2399
2400#[non_exhaustive]
2401#[derive(Default)]
2402#[expect(clippy::type_complexity)]
2403pub struct RemoteCallbacks<'a> {
2404 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2405 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2406 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2407 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2408 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2409}
2410
2411#[derive(Clone, Debug)]
2412pub struct Progress {
2413 pub bytes_downloaded: Option<u64>,
2415 pub overall: f32,
2416}