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 prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1866 let edits: Vec<_> = git_repo
1867 .references()?
1868 .prefixed(prefix.as_str())?
1869 .map_ok(remove_ref)
1870 .try_collect()?;
1871 git_repo.edit_references(edits)?;
1872 Ok(())
1873}
1874
1875fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1876 mut_repo.remove_remote(remote);
1877 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1878 let git_refs_to_delete = mut_repo
1879 .view()
1880 .git_refs()
1881 .keys()
1882 .filter(|&r| r.as_str().starts_with(&prefix))
1883 .cloned()
1884 .collect_vec();
1885 for git_ref in git_refs_to_delete {
1886 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1887 }
1888}
1889
1890pub fn rename_remote(
1891 mut_repo: &mut MutableRepo,
1892 old_remote_name: &RemoteName,
1893 new_remote_name: &RemoteName,
1894) -> Result<(), GitRemoteManagementError> {
1895 let mut git_repo = get_git_repo(mut_repo.store())?;
1896
1897 validate_remote_name(new_remote_name)?;
1898
1899 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1900 return Err(GitRemoteManagementError::NoSuchRemote(
1901 old_remote_name.to_owned(),
1902 ));
1903 };
1904 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1905
1906 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1907 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1908 new_remote_name.to_owned(),
1909 ));
1910 }
1911
1912 match (
1913 remote.refspecs(gix::remote::Direction::Fetch),
1914 remote.refspecs(gix::remote::Direction::Push),
1915 ) {
1916 ([refspec], [])
1917 if refspec.to_ref().to_bstring()
1918 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1919 _ => {
1920 return Err(GitRemoteManagementError::NonstandardConfiguration(
1921 old_remote_name.to_owned(),
1922 ))
1923 }
1924 }
1925
1926 remote
1927 .replace_refspecs(
1928 [default_fetch_refspec(new_remote_name).as_bytes()],
1929 gix::remote::Direction::Fetch,
1930 )
1931 .expect("default refspec to be valid");
1932
1933 let mut config = git_repo.config_snapshot().clone();
1934 save_remote(&mut config, new_remote_name, &mut remote)?;
1935 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1936 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1937 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1938
1939 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1940 .map_err(GitRemoteManagementError::from_git)?;
1941
1942 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1943 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1944 }
1945
1946 Ok(())
1947}
1948
1949fn rename_remote_git_refs(
1950 git_repo: &mut gix::Repository,
1951 old_remote_name: &RemoteName,
1952 new_remote_name: &RemoteName,
1953) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1954 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1955 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1956 let ref_log_message = BString::from(format!(
1957 "renamed remote {old_remote_name} to {new_remote_name}",
1958 old_remote_name = old_remote_name.as_symbol(),
1959 new_remote_name = new_remote_name.as_symbol(),
1960 ));
1961
1962 let edits: Vec<_> = git_repo
1963 .references()?
1964 .prefixed(old_prefix.as_str())?
1965 .map_ok(|old_ref| {
1966 let new_name = BString::new(
1967 [
1968 new_prefix.as_bytes(),
1969 &old_ref.name().as_bstr()[old_prefix.len()..],
1970 ]
1971 .concat(),
1972 );
1973 [
1974 add_ref(
1975 new_name.try_into().expect("new ref name to be valid"),
1976 old_ref.target().into_owned(),
1977 ref_log_message.clone(),
1978 ),
1979 remove_ref(old_ref),
1980 ]
1981 })
1982 .flatten_ok()
1983 .try_collect()?;
1984 git_repo.edit_references(edits)?;
1985 Ok(())
1986}
1987
1988fn gix_remote_with_fetch_url<Url, E>(
1994 remote: gix::Remote,
1995 url: Url,
1996) -> Result<gix::Remote, gix::remote::init::Error>
1997where
1998 Url: TryInto<gix::Url, Error = E>,
1999 gix::url::parse::Error: From<E>,
2000{
2001 let mut new_remote = remote.repo().remote_at(url)?;
2002 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2008 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2009 new_remote
2010 .replace_refspecs(
2011 remote
2012 .refspecs(direction)
2013 .iter()
2014 .map(|refspec| refspec.to_ref().to_bstring()),
2015 direction,
2016 )
2017 .expect("existing refspecs to be valid");
2018 }
2019 Ok(new_remote)
2020}
2021
2022pub fn set_remote_url(
2023 store: &Store,
2024 remote_name: &RemoteName,
2025 new_remote_url: &str,
2026) -> Result<(), GitRemoteManagementError> {
2027 let git_repo = get_git_repo(store)?;
2028
2029 validate_remote_name(remote_name)?;
2030
2031 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2032 return Err(GitRemoteManagementError::NoSuchRemote(
2033 remote_name.to_owned(),
2034 ));
2035 };
2036 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2037
2038 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2039 return Err(GitRemoteManagementError::NonstandardConfiguration(
2040 remote_name.to_owned(),
2041 ));
2042 }
2043
2044 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2045 .map_err(GitRemoteManagementError::from_git)?;
2046
2047 let mut config = git_repo.config_snapshot().clone();
2048 save_remote(&mut config, remote_name, &mut remote)?;
2049 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2050
2051 Ok(())
2052}
2053
2054fn rename_remote_refs(
2055 mut_repo: &mut MutableRepo,
2056 old_remote_name: &RemoteName,
2057 new_remote_name: &RemoteName,
2058) {
2059 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2060 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2061 let git_refs = mut_repo
2062 .view()
2063 .git_refs()
2064 .iter()
2065 .filter_map(|(old, target)| {
2066 old.as_str().strip_prefix(&prefix).map(|p| {
2067 let new: GitRefNameBuf =
2068 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2069 (old.clone(), new, target.clone())
2070 })
2071 })
2072 .collect_vec();
2073 for (old, new, target) in git_refs {
2074 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2075 mut_repo.set_git_ref_target(&new, target);
2076 }
2077}
2078
2079const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2080
2081#[derive(Error, Debug)]
2082pub enum GitFetchError {
2083 #[error("No git remote named '{}'", .0.as_symbol())]
2084 NoSuchRemote(RemoteNameBuf),
2085 #[error(
2086 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2087 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2088 )]
2089 InvalidBranchPattern(StringPattern),
2090 #[error(transparent)]
2091 RemoteName(#[from] GitRemoteNameError),
2092 #[error(transparent)]
2093 Subprocess(#[from] GitSubprocessError),
2094}
2095
2096struct FetchedBranches {
2097 remote: RemoteNameBuf,
2098 branches: Vec<StringPattern>,
2099}
2100
2101fn expand_fetch_refspecs(
2102 remote: &RemoteName,
2103 branch_names: &[StringPattern],
2104) -> Result<Vec<RefSpec>, GitFetchError> {
2105 branch_names
2106 .iter()
2107 .map(|pattern| {
2108 pattern
2109 .to_glob()
2110 .filter(
2111 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2114 )
2115 .map(|glob| {
2116 RefSpec::forced(
2117 format!("refs/heads/{glob}"),
2118 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2119 )
2120 })
2121 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2122 })
2123 .collect()
2124}
2125
2126pub struct GitFetch<'a> {
2128 mut_repo: &'a mut MutableRepo,
2129 git_repo: Box<gix::Repository>,
2130 git_ctx: GitSubprocessContext<'a>,
2131 git_settings: &'a GitSettings,
2132 fetched: Vec<FetchedBranches>,
2133}
2134
2135impl<'a> GitFetch<'a> {
2136 pub fn new(
2137 mut_repo: &'a mut MutableRepo,
2138 git_settings: &'a GitSettings,
2139 ) -> Result<Self, UnexpectedGitBackendError> {
2140 let git_backend = get_git_backend(mut_repo.store())?;
2141 let git_repo = Box::new(git_backend.git_repo());
2142 let git_ctx =
2143 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2144 Ok(GitFetch {
2145 mut_repo,
2146 git_repo,
2147 git_ctx,
2148 git_settings,
2149 fetched: vec![],
2150 })
2151 }
2152
2153 #[tracing::instrument(skip(self, callbacks))]
2159 pub fn fetch(
2160 &mut self,
2161 remote_name: &RemoteName,
2162 branch_names: &[StringPattern],
2163 mut callbacks: RemoteCallbacks<'_>,
2164 depth: Option<NonZeroU32>,
2165 ) -> Result<(), GitFetchError> {
2166 validate_remote_name(remote_name)?;
2167
2168 if self
2170 .git_repo
2171 .try_find_remote(remote_name.as_str())
2172 .is_none()
2173 {
2174 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2175 }
2176 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2179 if remaining_refspecs.is_empty() {
2180 return Ok(());
2182 }
2183
2184 let mut branches_to_prune = Vec::new();
2185 while let Some(failing_refspec) =
2193 self.git_ctx
2194 .spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2195 {
2196 tracing::debug!(failing_refspec, "failed to fetch ref");
2197 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2198
2199 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2200 branches_to_prune.push(format!(
2201 "{remote_name}/{branch_name}",
2202 remote_name = remote_name.as_str()
2203 ));
2204 }
2205 }
2206
2207 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2210
2211 self.fetched.push(FetchedBranches {
2212 remote: remote_name.to_owned(),
2213 branches: branch_names.to_vec(),
2214 });
2215 Ok(())
2216 }
2217
2218 #[tracing::instrument(skip(self))]
2220 pub fn get_default_branch(
2221 &self,
2222 remote_name: &RemoteName,
2223 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2224 if self
2225 .git_repo
2226 .try_find_remote(remote_name.as_str())
2227 .is_none()
2228 {
2229 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2230 }
2231 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2232 tracing::debug!(?default_branch);
2233 Ok(default_branch)
2234 }
2235
2236 #[tracing::instrument(skip(self))]
2244 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2245 tracing::debug!("import_refs");
2246 let import_stats =
2247 import_some_refs(
2248 self.mut_repo,
2249 self.git_settings,
2250 |kind, symbol| match kind {
2251 GitRefKind::Bookmark => self
2252 .fetched
2253 .iter()
2254 .filter(|fetched| fetched.remote == symbol.remote)
2255 .any(|fetched| {
2256 fetched
2257 .branches
2258 .iter()
2259 .any(|pattern| pattern.matches(symbol.name.as_str()))
2260 }),
2261 GitRefKind::Tag => true,
2262 },
2263 )?;
2264
2265 self.fetched.clear();
2266
2267 Ok(import_stats)
2268 }
2269}
2270
2271#[derive(Error, Debug)]
2272pub enum GitPushError {
2273 #[error("No git remote named '{}'", .0.as_symbol())]
2274 NoSuchRemote(RemoteNameBuf),
2275 #[error(transparent)]
2276 RemoteName(#[from] GitRemoteNameError),
2277 #[error(transparent)]
2278 Subprocess(#[from] GitSubprocessError),
2279 #[error(transparent)]
2280 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2281}
2282
2283#[derive(Clone, Debug)]
2284pub struct GitBranchPushTargets {
2285 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2286}
2287
2288pub struct GitRefUpdate {
2289 pub qualified_name: GitRefNameBuf,
2290 pub expected_current_target: Option<CommitId>,
2295 pub new_target: Option<CommitId>,
2296}
2297
2298pub fn push_branches(
2300 mut_repo: &mut MutableRepo,
2301 git_settings: &GitSettings,
2302 remote: &RemoteName,
2303 targets: &GitBranchPushTargets,
2304 callbacks: RemoteCallbacks<'_>,
2305) -> Result<GitPushStats, GitPushError> {
2306 validate_remote_name(remote)?;
2307
2308 let ref_updates = targets
2309 .branch_updates
2310 .iter()
2311 .map(|(name, update)| GitRefUpdate {
2312 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2313 expected_current_target: update.old_target.clone(),
2314 new_target: update.new_target.clone(),
2315 })
2316 .collect_vec();
2317
2318 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2319 tracing::debug!(?push_stats);
2320
2321 if push_stats.all_ok() {
2325 for (name, update) in &targets.branch_updates {
2326 let git_ref_name: GitRefNameBuf = format!(
2327 "refs/remotes/{remote}/{name}",
2328 remote = remote.as_str(),
2329 name = name.as_str()
2330 )
2331 .into();
2332 let new_remote_ref = RemoteRef {
2333 target: RefTarget::resolved(update.new_target.clone()),
2334 state: RemoteRefState::Tracked,
2335 };
2336 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2337 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2338 }
2339 }
2340
2341 Ok(push_stats)
2342}
2343
2344pub fn push_updates(
2346 repo: &dyn Repo,
2347 git_settings: &GitSettings,
2348 remote_name: &RemoteName,
2349 updates: &[GitRefUpdate],
2350 mut callbacks: RemoteCallbacks<'_>,
2351) -> Result<GitPushStats, GitPushError> {
2352 let mut qualified_remote_refs_expected_locations = HashMap::new();
2353 let mut refspecs = vec![];
2354 for update in updates {
2355 qualified_remote_refs_expected_locations.insert(
2356 update.qualified_name.as_ref(),
2357 update.expected_current_target.as_ref(),
2358 );
2359 if let Some(new_target) = &update.new_target {
2360 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2364 } else {
2365 refspecs.push(RefSpec::delete(&update.qualified_name));
2369 }
2370 }
2371
2372 let git_backend = get_git_backend(repo.store())?;
2373 let git_repo = git_backend.git_repo();
2374 let git_ctx =
2375 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2376
2377 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2379 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2380 }
2381
2382 let refs_to_push: Vec<RefToPush> = refspecs
2383 .iter()
2384 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2385 .collect();
2386
2387 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2388 push_stats.pushed.sort();
2389 push_stats.rejected.sort();
2390 push_stats.remote_rejected.sort();
2391 Ok(push_stats)
2392}
2393
2394#[non_exhaustive]
2395#[derive(Default)]
2396#[expect(clippy::type_complexity)]
2397pub struct RemoteCallbacks<'a> {
2398 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2399 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2400 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2401 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2402 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2403}
2404
2405#[derive(Clone, Debug)]
2406pub struct Progress {
2407 pub bytes_downloaded: Option<u64>,
2409 pub overall: f32,
2410}