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 RefSpec {
155 forced: true,
156 source: Some(source.into()),
157 destination: destination.into(),
158 }
159 }
160
161 fn delete(destination: impl Into<String>) -> Self {
162 RefSpec {
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 RefToPush {
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 GitImportError::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 GitExportError::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 GitResetHeadError::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(&mut index, &parent_tree, &wc_tree, git_repo.object_hash())
1349 .block_on()?;
1350
1351 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1354 index
1355 .entries_mut_with_paths()
1356 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1357 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1358 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1359 })
1360 .filter_map(|merged| merged.both())
1361 .map(|((entry, _), old_entry)| (entry, old_entry))
1362 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1363 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1364 }
1365
1366 debug_assert!(index.verify_entries().is_ok());
1367
1368 index
1369 .write(gix::index::write::Options::default())
1370 .map_err(GitResetHeadError::from_git)?;
1371
1372 Ok(())
1373}
1374
1375fn build_index_from_merged_tree(
1376 git_repo: &gix::Repository,
1377 merged_tree: MergedTree,
1378) -> Result<gix::index::File, GitResetHeadError> {
1379 let mut index = gix::index::File::from_state(
1380 gix::index::State::new(git_repo.object_hash()),
1381 git_repo.index_path(),
1382 );
1383
1384 let mut push_index_entry =
1385 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1386 let Some(entry) = maybe_entry else {
1387 return;
1388 };
1389
1390 let (id, mode) = match entry {
1391 TreeValue::File {
1392 id,
1393 executable,
1394 copy_id: _,
1395 } => {
1396 if *executable {
1397 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1398 } else {
1399 (id.as_bytes(), gix::index::entry::Mode::FILE)
1400 }
1401 }
1402 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1403 TreeValue::Tree(_) => {
1404 return;
1409 }
1410 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1411 TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"),
1412 };
1413
1414 let path = BStr::new(path.as_internal_file_string());
1415
1416 index.dangerously_push_entry(
1419 gix::index::entry::Stat::default(),
1420 gix::ObjectId::from_bytes_or_panic(id),
1421 gix::index::entry::Flags::from_stage(stage),
1422 mode,
1423 path,
1424 );
1425 };
1426
1427 let mut has_many_sided_conflict = false;
1428
1429 for (path, entry) in merged_tree.entries() {
1430 let entry = entry?;
1431 if let Some(resolved) = entry.as_resolved() {
1432 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1433 continue;
1434 }
1435
1436 let conflict = entry.simplify();
1437 if let [left, base, right] = conflict.as_slice() {
1438 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1440 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1441 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1442 } else {
1443 has_many_sided_conflict = true;
1451 push_index_entry(
1452 &path,
1453 conflict.first(),
1454 gix::index::entry::Stage::Unconflicted,
1455 );
1456 }
1457 }
1458
1459 index.sort_entries();
1462
1463 if has_many_sided_conflict
1466 && index
1467 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1468 .is_err()
1469 {
1470 let file_blob = git_repo
1471 .write_blob(
1472 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1473 )
1474 .map_err(GitResetHeadError::from_git)?;
1475 index.dangerously_push_entry(
1476 gix::index::entry::Stat::default(),
1477 file_blob.detach(),
1478 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1479 gix::index::entry::Mode::FILE,
1480 INDEX_DUMMY_CONFLICT_FILE.into(),
1481 );
1482 index.sort_entries();
1485 }
1486
1487 Ok(index)
1488}
1489
1490pub fn update_intent_to_add(
1497 repo: &dyn Repo,
1498 old_tree: &MergedTree,
1499 new_tree: &MergedTree,
1500) -> Result<(), GitResetHeadError> {
1501 let git_repo = get_git_repo(repo.store())?;
1502 let mut index = git_repo
1503 .index_or_empty()
1504 .map_err(GitResetHeadError::from_git)?;
1505 let mut_index = Arc::make_mut(&mut index);
1506 update_intent_to_add_impl(mut_index, old_tree, new_tree, git_repo.object_hash()).block_on()?;
1507 debug_assert!(mut_index.verify_entries().is_ok());
1508 mut_index
1509 .write(gix::index::write::Options::default())
1510 .map_err(GitResetHeadError::from_git)?;
1511
1512 Ok(())
1513}
1514
1515async fn update_intent_to_add_impl(
1516 index: &mut gix::index::File,
1517 old_tree: &MergedTree,
1518 new_tree: &MergedTree,
1519 hash_kind: gix::hash::Kind,
1520) -> BackendResult<()> {
1521 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1522 let mut added_paths = vec![];
1523 let mut removed_paths = HashSet::new();
1524 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1525 let (before, after) = values?;
1526 if before.is_absent() {
1527 let executable = match after.as_normal() {
1528 Some(TreeValue::File {
1529 id: _,
1530 executable,
1531 copy_id: _,
1532 }) => *executable,
1533 Some(TreeValue::Symlink(_)) => false,
1534 _ => {
1535 continue;
1536 }
1537 };
1538 if index
1539 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1540 .is_err()
1541 {
1542 added_paths.push((BString::from(path.into_internal_string()), executable));
1543 }
1544 } else if after.is_absent() {
1545 removed_paths.insert(BString::from(path.into_internal_string()));
1546 }
1547 }
1548
1549 if added_paths.is_empty() && removed_paths.is_empty() {
1550 return Ok(());
1551 }
1552
1553 for (path, executable) in added_paths {
1554 index.dangerously_push_entry(
1556 gix::index::entry::Stat::default(),
1557 gix::ObjectId::empty_blob(hash_kind),
1558 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1559 if executable {
1560 gix::index::entry::Mode::FILE_EXECUTABLE
1561 } else {
1562 gix::index::entry::Mode::FILE
1563 },
1564 path.as_ref(),
1565 );
1566 }
1567 if !removed_paths.is_empty() {
1568 index.remove_entries(|_size, path, entry| {
1569 entry
1570 .flags
1571 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1572 && removed_paths.contains(path)
1573 });
1574 }
1575
1576 index.sort_entries();
1577
1578 Ok(())
1579}
1580
1581#[derive(Debug, Error)]
1582pub enum GitRemoteManagementError {
1583 #[error("No git remote named '{}'", .0.as_symbol())]
1584 NoSuchRemote(RemoteNameBuf),
1585 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1586 RemoteAlreadyExists(RemoteNameBuf),
1587 #[error(transparent)]
1588 RemoteName(#[from] GitRemoteNameError),
1589 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1590 NonstandardConfiguration(RemoteNameBuf),
1591 #[error("Error saving Git configuration")]
1592 GitConfigSaveError(#[source] std::io::Error),
1593 #[error("Unexpected Git error when managing remotes")]
1594 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1595 #[error(transparent)]
1596 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1597}
1598
1599impl GitRemoteManagementError {
1600 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1601 GitRemoteManagementError::InternalGitError(source.into())
1602 }
1603}
1604
1605pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1610 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1611}
1612
1613fn default_fetch_refspec(remote: &RemoteName) -> String {
1614 format!(
1615 "+refs/heads/*:refs/remotes/{remote}/*",
1616 remote = remote.as_str()
1617 )
1618}
1619
1620fn add_ref(
1621 name: gix::refs::FullName,
1622 target: gix::refs::Target,
1623 message: BString,
1624) -> gix::refs::transaction::RefEdit {
1625 gix::refs::transaction::RefEdit {
1626 change: gix::refs::transaction::Change::Update {
1627 log: gix::refs::transaction::LogChange {
1628 mode: gix::refs::transaction::RefLog::AndReference,
1629 force_create_reflog: false,
1630 message,
1631 },
1632 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1633 new: target,
1634 },
1635 name,
1636 deref: false,
1637 }
1638}
1639
1640fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1641 gix::refs::transaction::RefEdit {
1642 change: gix::refs::transaction::Change::Delete {
1643 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1644 reference.target().into_owned(),
1645 ),
1646 log: gix::refs::transaction::RefLog::AndReference,
1647 },
1648 name: reference.name().to_owned(),
1649 deref: false,
1650 }
1651}
1652
1653fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1659 let mut config_file = File::create(
1660 config
1661 .meta()
1662 .path
1663 .as_ref()
1664 .expect("Git repository to have a config file"),
1665 )?;
1666 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1667}
1668
1669fn save_remote(
1670 config: &mut gix::config::File<'static>,
1671 remote_name: &RemoteName,
1672 remote: &mut gix::Remote,
1673) -> Result<(), GitRemoteManagementError> {
1674 config
1681 .new_section(
1682 "remote",
1683 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1684 )
1685 .map_err(GitRemoteManagementError::from_git)?;
1686 remote
1687 .save_as_to(remote_name.as_str(), config)
1688 .map_err(GitRemoteManagementError::from_git)?;
1689 Ok(())
1690}
1691
1692fn git_config_branch_section_ids_by_remote(
1693 config: &gix::config::File,
1694 remote_name: &RemoteName,
1695) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1696 config
1697 .sections_by_name("branch")
1698 .into_iter()
1699 .flatten()
1700 .filter_map(|section| {
1701 let remote_values = section.values("remote");
1702 let push_remote_values = section.values("pushRemote");
1703 if !remote_values
1704 .iter()
1705 .chain(push_remote_values.iter())
1706 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1707 {
1708 return None;
1709 }
1710 if remote_values.len() > 1
1711 || push_remote_values.len() > 1
1712 || section.value_names().any(|name| {
1713 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1714 })
1715 {
1716 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1717 remote_name.to_owned(),
1718 )));
1719 }
1720 Some(Ok(section.id()))
1721 })
1722 .collect()
1723}
1724
1725fn rename_remote_in_git_branch_config_sections(
1726 config: &mut gix::config::File,
1727 old_remote_name: &RemoteName,
1728 new_remote_name: &RemoteName,
1729) -> Result<(), GitRemoteManagementError> {
1730 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1731 config
1732 .section_mut_by_id(id)
1733 .expect("found section to exist")
1734 .set(
1735 "remote"
1736 .try_into()
1737 .expect("'remote' to be a valid value name"),
1738 BStr::new(new_remote_name.as_str()),
1739 );
1740 }
1741 Ok(())
1742}
1743
1744fn remove_remote_git_branch_config_sections(
1745 config: &mut gix::config::File,
1746 remote_name: &RemoteName,
1747) -> Result<(), GitRemoteManagementError> {
1748 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1749 config
1750 .remove_section_by_id(id)
1751 .expect("removed section to exist");
1752 }
1753 Ok(())
1754}
1755
1756fn remove_remote_git_config_sections(
1757 config: &mut gix::config::File,
1758 remote_name: &RemoteName,
1759) -> Result<(), GitRemoteManagementError> {
1760 let section_ids_to_remove: Vec<_> = config
1761 .sections_by_name("remote")
1762 .into_iter()
1763 .flatten()
1764 .filter(|section| {
1765 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1766 })
1767 .map(|section| {
1768 if section.value_names().any(|name| {
1769 !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1770 }) {
1771 return Err(GitRemoteManagementError::NonstandardConfiguration(
1772 remote_name.to_owned(),
1773 ));
1774 }
1775 Ok(section.id())
1776 })
1777 .try_collect()?;
1778 for id in section_ids_to_remove {
1779 config
1780 .remove_section_by_id(id)
1781 .expect("removed section to exist");
1782 }
1783 Ok(())
1784}
1785
1786pub fn get_all_remote_names(
1788 store: &Store,
1789) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1790 let git_repo = get_git_repo(store)?;
1791 let names = git_repo
1792 .remote_names()
1793 .into_iter()
1794 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1796 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1798 .map(RemoteNameBuf::from)
1799 .collect();
1800 Ok(names)
1801}
1802
1803pub fn add_remote(
1804 store: &Store,
1805 remote_name: &RemoteName,
1806 url: &str,
1807) -> Result<(), GitRemoteManagementError> {
1808 let git_repo = get_git_repo(store)?;
1809
1810 validate_remote_name(remote_name)?;
1811
1812 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1813 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1814 remote_name.to_owned(),
1815 ));
1816 }
1817
1818 let mut remote = git_repo
1819 .remote_at(url)
1820 .map_err(GitRemoteManagementError::from_git)?
1821 .with_refspecs(
1822 [default_fetch_refspec(remote_name).as_bytes()],
1823 gix::remote::Direction::Fetch,
1824 )
1825 .expect("default refspec to be valid");
1826
1827 let mut config = git_repo.config_snapshot().clone();
1828 save_remote(&mut config, remote_name, &mut remote)?;
1829 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1830
1831 Ok(())
1832}
1833
1834pub fn remove_remote(
1835 mut_repo: &mut MutableRepo,
1836 remote_name: &RemoteName,
1837) -> Result<(), GitRemoteManagementError> {
1838 let mut git_repo = get_git_repo(mut_repo.store())?;
1839
1840 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1841 return Err(GitRemoteManagementError::NoSuchRemote(
1842 remote_name.to_owned(),
1843 ));
1844 };
1845
1846 let mut config = git_repo.config_snapshot().clone();
1847 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1848 remove_remote_git_config_sections(&mut config, remote_name)?;
1849 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1850
1851 remove_remote_git_refs(&mut git_repo, remote_name)
1852 .map_err(GitRemoteManagementError::from_git)?;
1853
1854 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1855 remove_remote_refs(mut_repo, remote_name);
1856 }
1857
1858 Ok(())
1859}
1860
1861fn remove_remote_git_refs(
1862 git_repo: &mut gix::Repository,
1863 remote: &RemoteName,
1864) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1865 let edits: Vec<_> = git_repo
1866 .references()?
1867 .prefixed(format!("refs/remotes/{remote}/", remote = remote.as_str()))?
1868 .map_ok(remove_ref)
1869 .try_collect()?;
1870 git_repo.edit_references(edits)?;
1871 Ok(())
1872}
1873
1874fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1875 mut_repo.remove_remote(remote);
1876 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1877 let git_refs_to_delete = mut_repo
1878 .view()
1879 .git_refs()
1880 .keys()
1881 .filter(|&r| r.as_str().starts_with(&prefix))
1882 .cloned()
1883 .collect_vec();
1884 for git_ref in git_refs_to_delete {
1885 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1886 }
1887}
1888
1889pub fn rename_remote(
1890 mut_repo: &mut MutableRepo,
1891 old_remote_name: &RemoteName,
1892 new_remote_name: &RemoteName,
1893) -> Result<(), GitRemoteManagementError> {
1894 let mut git_repo = get_git_repo(mut_repo.store())?;
1895
1896 validate_remote_name(new_remote_name)?;
1897
1898 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1899 return Err(GitRemoteManagementError::NoSuchRemote(
1900 old_remote_name.to_owned(),
1901 ));
1902 };
1903 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1904
1905 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1906 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1907 new_remote_name.to_owned(),
1908 ));
1909 }
1910
1911 match (
1912 remote.refspecs(gix::remote::Direction::Fetch),
1913 remote.refspecs(gix::remote::Direction::Push),
1914 ) {
1915 ([refspec], [])
1916 if refspec.to_ref().to_bstring()
1917 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1918 _ => {
1919 return Err(GitRemoteManagementError::NonstandardConfiguration(
1920 old_remote_name.to_owned(),
1921 ))
1922 }
1923 }
1924
1925 remote
1926 .replace_refspecs(
1927 [default_fetch_refspec(new_remote_name).as_bytes()],
1928 gix::remote::Direction::Fetch,
1929 )
1930 .expect("default refspec to be valid");
1931
1932 let mut config = git_repo.config_snapshot().clone();
1933 save_remote(&mut config, new_remote_name, &mut remote)?;
1934 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1935 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1936 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1937
1938 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1939 .map_err(GitRemoteManagementError::from_git)?;
1940
1941 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1942 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1943 }
1944
1945 Ok(())
1946}
1947
1948fn rename_remote_git_refs(
1949 git_repo: &mut gix::Repository,
1950 old_remote_name: &RemoteName,
1951 new_remote_name: &RemoteName,
1952) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1953 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1954 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1955 let ref_log_message = BString::from(format!(
1956 "renamed remote {old_remote_name} to {new_remote_name}",
1957 old_remote_name = old_remote_name.as_symbol(),
1958 new_remote_name = new_remote_name.as_symbol(),
1959 ));
1960
1961 let edits: Vec<_> = git_repo
1962 .references()?
1963 .prefixed(old_prefix.clone())?
1964 .map_ok(|old_ref| {
1965 let new_name = BString::new(
1966 [
1967 new_prefix.as_bytes(),
1968 &old_ref.name().as_bstr()[old_prefix.len()..],
1969 ]
1970 .concat(),
1971 );
1972 [
1973 add_ref(
1974 new_name.try_into().expect("new ref name to be valid"),
1975 old_ref.target().into_owned(),
1976 ref_log_message.clone(),
1977 ),
1978 remove_ref(old_ref),
1979 ]
1980 })
1981 .flatten_ok()
1982 .try_collect()?;
1983 git_repo.edit_references(edits)?;
1984 Ok(())
1985}
1986
1987fn gix_remote_with_fetch_url<Url, E>(
1993 remote: gix::Remote,
1994 url: Url,
1995) -> Result<gix::Remote, gix::remote::init::Error>
1996where
1997 Url: TryInto<gix::Url, Error = E>,
1998 gix::url::parse::Error: From<E>,
1999{
2000 let mut new_remote = remote.repo().remote_at(url)?;
2001 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2007 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2008 new_remote
2009 .replace_refspecs(
2010 remote
2011 .refspecs(direction)
2012 .iter()
2013 .map(|refspec| refspec.to_ref().to_bstring()),
2014 direction,
2015 )
2016 .expect("existing refspecs to be valid");
2017 }
2018 Ok(new_remote)
2019}
2020
2021pub fn set_remote_url(
2022 store: &Store,
2023 remote_name: &RemoteName,
2024 new_remote_url: &str,
2025) -> Result<(), GitRemoteManagementError> {
2026 let git_repo = get_git_repo(store)?;
2027
2028 validate_remote_name(remote_name)?;
2029
2030 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2031 return Err(GitRemoteManagementError::NoSuchRemote(
2032 remote_name.to_owned(),
2033 ));
2034 };
2035 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2036
2037 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2038 return Err(GitRemoteManagementError::NonstandardConfiguration(
2039 remote_name.to_owned(),
2040 ));
2041 }
2042
2043 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2044 .map_err(GitRemoteManagementError::from_git)?;
2045
2046 let mut config = git_repo.config_snapshot().clone();
2047 save_remote(&mut config, remote_name, &mut remote)?;
2048 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2049
2050 Ok(())
2051}
2052
2053fn rename_remote_refs(
2054 mut_repo: &mut MutableRepo,
2055 old_remote_name: &RemoteName,
2056 new_remote_name: &RemoteName,
2057) {
2058 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2059 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2060 let git_refs = mut_repo
2061 .view()
2062 .git_refs()
2063 .iter()
2064 .filter_map(|(old, target)| {
2065 old.as_str().strip_prefix(&prefix).map(|p| {
2066 let new: GitRefNameBuf =
2067 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2068 (old.clone(), new, target.clone())
2069 })
2070 })
2071 .collect_vec();
2072 for (old, new, target) in git_refs {
2073 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2074 mut_repo.set_git_ref_target(&new, target);
2075 }
2076}
2077
2078const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2079
2080#[derive(Error, Debug)]
2081pub enum GitFetchError {
2082 #[error("No git remote named '{}'", .0.as_symbol())]
2083 NoSuchRemote(RemoteNameBuf),
2084 #[error(
2085 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2086 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2087 )]
2088 InvalidBranchPattern(StringPattern),
2089 #[error(transparent)]
2090 RemoteName(#[from] GitRemoteNameError),
2091 #[error(transparent)]
2092 Subprocess(#[from] GitSubprocessError),
2093}
2094
2095struct FetchedBranches {
2096 remote: RemoteNameBuf,
2097 branches: Vec<StringPattern>,
2098}
2099
2100fn expand_fetch_refspecs(
2101 remote: &RemoteName,
2102 branch_names: &[StringPattern],
2103) -> Result<Vec<RefSpec>, GitFetchError> {
2104 branch_names
2105 .iter()
2106 .map(|pattern| {
2107 pattern
2108 .to_glob()
2109 .filter(
2110 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2113 )
2114 .map(|glob| {
2115 RefSpec::forced(
2116 format!("refs/heads/{glob}"),
2117 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2118 )
2119 })
2120 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2121 })
2122 .collect()
2123}
2124
2125pub struct GitFetch<'a> {
2127 mut_repo: &'a mut MutableRepo,
2128 git_repo: Box<gix::Repository>,
2129 git_ctx: GitSubprocessContext<'a>,
2130 git_settings: &'a GitSettings,
2131 fetched: Vec<FetchedBranches>,
2132}
2133
2134impl<'a> GitFetch<'a> {
2135 pub fn new(
2136 mut_repo: &'a mut MutableRepo,
2137 git_settings: &'a GitSettings,
2138 ) -> Result<Self, UnexpectedGitBackendError> {
2139 let git_backend = get_git_backend(mut_repo.store())?;
2140 let git_repo = Box::new(git_backend.git_repo());
2141 let git_ctx =
2142 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2143 Ok(GitFetch {
2144 mut_repo,
2145 git_repo,
2146 git_ctx,
2147 git_settings,
2148 fetched: vec![],
2149 })
2150 }
2151
2152 #[tracing::instrument(skip(self, callbacks))]
2158 pub fn fetch(
2159 &mut self,
2160 remote_name: &RemoteName,
2161 branch_names: &[StringPattern],
2162 mut callbacks: RemoteCallbacks<'_>,
2163 depth: Option<NonZeroU32>,
2164 ) -> Result<(), GitFetchError> {
2165 validate_remote_name(remote_name)?;
2166
2167 if self
2169 .git_repo
2170 .try_find_remote(remote_name.as_str())
2171 .is_none()
2172 {
2173 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2174 }
2175 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2178 if remaining_refspecs.is_empty() {
2179 return Ok(());
2181 }
2182
2183 let mut branches_to_prune = Vec::new();
2184 while let Some(failing_refspec) =
2192 self.git_ctx
2193 .spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2194 {
2195 tracing::debug!(failing_refspec, "failed to fetch ref");
2196 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2197
2198 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2199 branches_to_prune.push(format!(
2200 "{remote_name}/{branch_name}",
2201 remote_name = remote_name.as_str()
2202 ));
2203 }
2204 }
2205
2206 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2209
2210 self.fetched.push(FetchedBranches {
2211 remote: remote_name.to_owned(),
2212 branches: branch_names.to_vec(),
2213 });
2214 Ok(())
2215 }
2216
2217 #[tracing::instrument(skip(self))]
2219 pub fn get_default_branch(
2220 &self,
2221 remote_name: &RemoteName,
2222 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2223 if self
2224 .git_repo
2225 .try_find_remote(remote_name.as_str())
2226 .is_none()
2227 {
2228 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2229 }
2230 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2231 tracing::debug!(?default_branch);
2232 Ok(default_branch)
2233 }
2234
2235 #[tracing::instrument(skip(self))]
2243 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2244 tracing::debug!("import_refs");
2245 let import_stats =
2246 import_some_refs(
2247 self.mut_repo,
2248 self.git_settings,
2249 |kind, symbol| match kind {
2250 GitRefKind::Bookmark => self
2251 .fetched
2252 .iter()
2253 .filter(|fetched| fetched.remote == symbol.remote)
2254 .any(|fetched| {
2255 fetched
2256 .branches
2257 .iter()
2258 .any(|pattern| pattern.matches(symbol.name.as_str()))
2259 }),
2260 GitRefKind::Tag => true,
2261 },
2262 )?;
2263
2264 self.fetched.clear();
2265
2266 Ok(import_stats)
2267 }
2268}
2269
2270#[derive(Error, Debug)]
2271pub enum GitPushError {
2272 #[error("No git remote named '{}'", .0.as_symbol())]
2273 NoSuchRemote(RemoteNameBuf),
2274 #[error(transparent)]
2275 RemoteName(#[from] GitRemoteNameError),
2276 #[error(transparent)]
2277 Subprocess(#[from] GitSubprocessError),
2278 #[error(transparent)]
2279 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2280}
2281
2282#[derive(Clone, Debug)]
2283pub struct GitBranchPushTargets {
2284 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2285}
2286
2287pub struct GitRefUpdate {
2288 pub qualified_name: GitRefNameBuf,
2289 pub expected_current_target: Option<CommitId>,
2294 pub new_target: Option<CommitId>,
2295}
2296
2297pub fn push_branches(
2299 mut_repo: &mut MutableRepo,
2300 git_settings: &GitSettings,
2301 remote: &RemoteName,
2302 targets: &GitBranchPushTargets,
2303 callbacks: RemoteCallbacks<'_>,
2304) -> Result<GitPushStats, GitPushError> {
2305 validate_remote_name(remote)?;
2306
2307 let ref_updates = targets
2308 .branch_updates
2309 .iter()
2310 .map(|(name, update)| GitRefUpdate {
2311 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2312 expected_current_target: update.old_target.clone(),
2313 new_target: update.new_target.clone(),
2314 })
2315 .collect_vec();
2316
2317 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2318 tracing::debug!(?push_stats);
2319
2320 if push_stats.all_ok() {
2324 for (name, update) in &targets.branch_updates {
2325 let git_ref_name: GitRefNameBuf = format!(
2326 "refs/remotes/{remote}/{name}",
2327 remote = remote.as_str(),
2328 name = name.as_str()
2329 )
2330 .into();
2331 let new_remote_ref = RemoteRef {
2332 target: RefTarget::resolved(update.new_target.clone()),
2333 state: RemoteRefState::Tracked,
2334 };
2335 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2336 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2337 }
2338 }
2339
2340 Ok(push_stats)
2341}
2342
2343pub fn push_updates(
2345 repo: &dyn Repo,
2346 git_settings: &GitSettings,
2347 remote_name: &RemoteName,
2348 updates: &[GitRefUpdate],
2349 mut callbacks: RemoteCallbacks<'_>,
2350) -> Result<GitPushStats, GitPushError> {
2351 let mut qualified_remote_refs_expected_locations = HashMap::new();
2352 let mut refspecs = vec![];
2353 for update in updates {
2354 qualified_remote_refs_expected_locations.insert(
2355 update.qualified_name.as_ref(),
2356 update.expected_current_target.as_ref(),
2357 );
2358 if let Some(new_target) = &update.new_target {
2359 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2363 } else {
2364 refspecs.push(RefSpec::delete(&update.qualified_name));
2368 }
2369 }
2370
2371 let git_backend = get_git_backend(repo.store())?;
2372 let git_repo = git_backend.git_repo();
2373 let git_ctx =
2374 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2375
2376 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2378 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2379 }
2380
2381 let refs_to_push: Vec<RefToPush> = refspecs
2382 .iter()
2383 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2384 .collect();
2385
2386 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2387 push_stats.pushed.sort();
2388 push_stats.rejected.sort();
2389 push_stats.remote_rejected.sort();
2390 Ok(push_stats)
2391}
2392
2393#[non_exhaustive]
2394#[derive(Default)]
2395#[expect(clippy::type_complexity)]
2396pub struct RemoteCallbacks<'a> {
2397 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2398 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2399 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2400 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2401 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2402}
2403
2404#[derive(Clone, Debug)]
2405pub struct Progress {
2406 pub bytes_downloaded: Option<u64>,
2408 pub overall: f32,
2409}