1#![expect(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::sync::Arc;
26
27use bstr::BStr;
28use bstr::BString;
29use futures::StreamExt as _;
30use gix::refspec::Instruction;
31use itertools::Itertools as _;
32use pollster::FutureExt as _;
33use thiserror::Error;
34
35use crate::backend::BackendError;
36use crate::backend::BackendResult;
37use crate::backend::CommitId;
38use crate::backend::TreeValue;
39use crate::commit::Commit;
40use crate::file_util::IoResultExt as _;
41use crate::file_util::PathError;
42use crate::git_backend::GitBackend;
43use crate::git_subprocess::GitSubprocessContext;
44use crate::git_subprocess::GitSubprocessError;
45use crate::matchers::EverythingMatcher;
46use crate::merged_tree::MergedTree;
47use crate::merged_tree::TreeDiffEntry;
48use crate::object_id::ObjectId as _;
49use crate::op_store::RefTarget;
50use crate::op_store::RefTargetOptionExt as _;
51use crate::op_store::RemoteRef;
52use crate::op_store::RemoteRefState;
53use crate::ref_name::GitRefName;
54use crate::ref_name::GitRefNameBuf;
55use crate::ref_name::RefName;
56use crate::ref_name::RefNameBuf;
57use crate::ref_name::RemoteName;
58use crate::ref_name::RemoteNameBuf;
59use crate::ref_name::RemoteRefSymbol;
60use crate::ref_name::RemoteRefSymbolBuf;
61use crate::refs::BookmarkPushUpdate;
62use crate::repo::MutableRepo;
63use crate::repo::Repo;
64use crate::repo_path::RepoPath;
65use crate::revset::RevsetExpression;
66use crate::settings::GitSettings;
67use crate::store::Store;
68use crate::str_util::StringPattern;
69use crate::view::View;
70
71pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
73pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
75const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
77const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
80
81#[derive(Debug, Error)]
82pub enum GitRemoteNameError {
83 #[error(
84 "Git remote named '{name}' is reserved for local Git repository",
85 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
86 )]
87 ReservedForLocalGitRepo,
88 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
89 WithSlash(RemoteNameBuf),
90}
91
92fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
93 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
94 Err(GitRemoteNameError::ReservedForLocalGitRepo)
95 } else if name.as_str().contains("/") {
96 Err(GitRemoteNameError::WithSlash(name.to_owned()))
97 } else {
98 Ok(())
99 }
100}
101
102#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub enum GitRefKind {
105 Bookmark,
106 Tag,
107}
108
109#[derive(Clone, Debug, Default, Eq, PartialEq)]
111pub struct GitPushStats {
112 pub pushed: Vec<GitRefNameBuf>,
114 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
116 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
118}
119
120impl GitPushStats {
121 pub fn all_ok(&self) -> bool {
122 self.rejected.is_empty() && self.remote_rejected.is_empty()
123 }
124}
125
126#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
131
132impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
133 fn borrow(&self) -> &RemoteRefSymbol<'b> {
134 &self.0
135 }
136}
137
138#[derive(Debug, Hash, PartialEq, Eq)]
144pub(crate) struct RefSpec {
145 forced: bool,
146 source: Option<String>,
149 destination: String,
150}
151
152impl RefSpec {
153 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
154 Self {
155 forced: true,
156 source: Some(source.into()),
157 destination: destination.into(),
158 }
159 }
160
161 fn delete(destination: impl Into<String>) -> Self {
162 Self {
164 forced: false,
165 source: None,
166 destination: destination.into(),
167 }
168 }
169
170 pub(crate) fn to_git_format(&self) -> String {
171 format!(
172 "{}{}",
173 if self.forced { "+" } else { "" },
174 self.to_git_format_not_forced()
175 )
176 }
177
178 pub(crate) fn to_git_format_not_forced(&self) -> String {
184 if let Some(s) = &self.source {
185 format!("{}:{}", s, self.destination)
186 } else {
187 format!(":{}", self.destination)
188 }
189 }
190}
191
192#[derive(Debug)]
194#[repr(transparent)]
195pub(crate) struct NegativeRefSpec {
196 source: String,
197}
198
199impl NegativeRefSpec {
200 fn new(source: impl Into<String>) -> Self {
201 Self {
202 source: source.into(),
203 }
204 }
205
206 pub(crate) fn to_git_format(&self) -> String {
207 format!("^{}", self.source)
208 }
209}
210
211pub(crate) struct RefToPush<'a> {
214 pub(crate) refspec: &'a RefSpec,
215 pub(crate) expected_location: Option<&'a CommitId>,
216}
217
218impl<'a> RefToPush<'a> {
219 fn new(
220 refspec: &'a RefSpec,
221 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
222 ) -> Self {
223 let expected_location = *expected_locations
224 .get(GitRefName::new(&refspec.destination))
225 .expect(
226 "The refspecs and the expected locations were both constructed from the same \
227 source of truth. This means the lookup should always work.",
228 );
229
230 Self {
231 refspec,
232 expected_location,
233 }
234 }
235
236 pub(crate) fn to_git_lease(&self) -> String {
237 format!(
238 "{}:{}",
239 self.refspec.destination,
240 self.expected_location
241 .map(|x| x.to_string())
242 .as_deref()
243 .unwrap_or("")
244 )
245 }
246}
247
248pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
251 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
252 if name == "HEAD" {
254 return None;
255 }
256 let name = RefName::new(name);
257 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
258 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
259 } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
260 let (remote, name) = remote_and_name.split_once('/')?;
261 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
263 return None;
264 }
265 let name = RefName::new(name);
266 let remote = RemoteName::new(remote);
267 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
268 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
269 let name = RefName::new(name);
270 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
271 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
272 } else {
273 None
274 }
275}
276
277fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
278 let RemoteRefSymbol { name, remote } = symbol;
279 let name = name.as_str();
280 let remote = remote.as_str();
281 if name.is_empty() || remote.is_empty() {
282 return None;
283 }
284 match kind {
285 GitRefKind::Bookmark => {
286 if name == "HEAD" {
287 return None;
288 }
289 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
290 Some(format!("refs/heads/{name}").into())
291 } else {
292 Some(format!("refs/remotes/{remote}/{name}").into())
293 }
294 }
295 GitRefKind::Tag => {
296 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
297 }
298 }
299}
300
301#[derive(Debug, Error)]
302#[error("The repo is not backed by a Git repo")]
303pub struct UnexpectedGitBackendError;
304
305pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
307 store.backend_impl().ok_or(UnexpectedGitBackendError)
308}
309
310pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
312 get_git_backend(store).map(|backend| backend.git_repo())
313}
314
315fn resolve_git_ref_to_commit_id(
320 git_ref: &gix::Reference,
321 known_target: &RefTarget,
322) -> Option<CommitId> {
323 let mut peeling_ref = Cow::Borrowed(git_ref);
324
325 if let Some(id) = known_target.as_normal() {
327 let raw_ref = &git_ref.inner;
328 if let Some(oid) = raw_ref.target.try_id()
329 && oid.as_bytes() == id.as_bytes()
330 {
331 return Some(id.clone());
332 }
333 if let Some(oid) = raw_ref.peeled
334 && oid.as_bytes() == id.as_bytes()
335 {
336 return Some(id.clone());
339 }
340 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
344 let maybe_tag = git_ref
345 .try_id()
346 .and_then(|id| id.object().ok())
347 .and_then(|object| object.try_into_tag().ok());
348 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
349 if oid.as_bytes() == id.as_bytes() {
350 return Some(id.clone());
352 }
353 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());
356 }
357 }
358 }
359
360 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
364 let is_commit = peeled_id
365 .object()
366 .is_ok_and(|object| object.kind.is_commit());
367 is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
368}
369
370#[derive(Error, Debug)]
371pub enum GitImportError {
372 #[error("Failed to read Git HEAD target commit {id}")]
373 MissingHeadTarget {
374 id: CommitId,
375 #[source]
376 err: BackendError,
377 },
378 #[error("Ancestor of Git ref {symbol} is missing")]
379 MissingRefAncestor {
380 symbol: RemoteRefSymbolBuf,
381 #[source]
382 err: BackendError,
383 },
384 #[error(transparent)]
385 Backend(BackendError),
386 #[error(transparent)]
387 Git(Box<dyn std::error::Error + Send + Sync>),
388 #[error(transparent)]
389 UnexpectedBackend(#[from] UnexpectedGitBackendError),
390}
391
392impl GitImportError {
393 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
394 Self::Git(source.into())
395 }
396}
397
398#[derive(Clone, Debug, Eq, PartialEq, Default)]
400pub struct GitImportStats {
401 pub abandoned_commits: Vec<CommitId>,
403 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
406 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
409 pub failed_ref_names: Vec<BString>,
414}
415
416#[derive(Debug)]
417struct RefsToImport {
418 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
421 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
424 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
427 failed_ref_names: Vec<BString>,
429}
430
431pub fn import_refs(
436 mut_repo: &mut MutableRepo,
437 git_settings: &GitSettings,
438) -> Result<GitImportStats, GitImportError> {
439 import_some_refs(mut_repo, git_settings, |_, _| true)
440}
441
442pub fn import_some_refs(
447 mut_repo: &mut MutableRepo,
448 git_settings: &GitSettings,
449 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
450) -> Result<GitImportStats, GitImportError> {
451 let store = mut_repo.store();
452 let git_backend = get_git_backend(store)?;
453 let git_repo = git_backend.git_repo();
454
455 let RefsToImport {
456 changed_git_refs,
457 changed_remote_bookmarks,
458 changed_remote_tags,
459 failed_ref_names,
460 } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;
461
462 let index = mut_repo.index();
469 let missing_head_ids = changed_git_refs
470 .iter()
471 .flat_map(|(_, new_target)| new_target.added_ids())
472 .filter(|&id| !index.has_id(id));
473 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
474
475 let mut head_commits = Vec::new();
477 let get_commit = |id| {
478 if !heads_imported && !index.has_id(id) {
480 git_backend.import_head_commits([id])?;
481 }
482 store.get_commit(id)
483 };
484 for (symbol, (_, new_target)) in
485 itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)
486 {
487 for id in new_target.added_ids() {
488 let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {
489 symbol: symbol.clone(),
490 err,
491 })?;
492 head_commits.push(commit);
493 }
494 }
495 mut_repo
498 .add_heads(&head_commits)
499 .map_err(GitImportError::Backend)?;
500
501 for (full_name, new_target) in changed_git_refs {
503 mut_repo.set_git_ref_target(&full_name, new_target);
504 }
505 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
506 let symbol = symbol.as_ref();
507 let base_target = old_remote_ref.tracked_target();
508 let new_remote_ref = RemoteRef {
509 target: new_target.clone(),
510 state: if old_remote_ref.is_present() {
511 old_remote_ref.state
512 } else {
513 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)
514 },
515 };
516 if new_remote_ref.is_tracked() {
517 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target);
518 }
519 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
522 }
523 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
524 let symbol = symbol.as_ref();
525 let base_target = old_remote_ref.tracked_target();
526 let new_remote_ref = RemoteRef {
527 target: new_target.clone(),
528 state: if old_remote_ref.is_present() {
529 old_remote_ref.state
530 } else {
531 default_remote_ref_state_for(GitRefKind::Tag, symbol, git_settings)
532 },
533 };
534 if new_remote_ref.is_tracked() {
535 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target);
536 }
537 mut_repo.set_remote_tag(symbol, new_remote_ref);
540 }
541
542 let abandoned_commits = if git_settings.abandon_unreachable_commits {
543 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
544 .map_err(GitImportError::Backend)?
545 } else {
546 vec![]
547 };
548 let stats = GitImportStats {
549 abandoned_commits,
550 changed_remote_bookmarks,
551 changed_remote_tags,
552 failed_ref_names,
553 };
554 Ok(stats)
555}
556
557fn abandon_unreachable_commits(
560 mut_repo: &mut MutableRepo,
561 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
562 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
563) -> BackendResult<Vec<CommitId>> {
564 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
565 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
566 .cloned()
567 .collect_vec();
568 if hidable_git_heads.is_empty() {
569 return Ok(vec![]);
570 }
571 let pinned_expression = RevsetExpression::union_all(&[
572 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
574 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
575 .intersection(&RevsetExpression::visible_heads().ancestors()),
577 RevsetExpression::root(),
578 ]);
579 let abandoned_expression = pinned_expression
580 .range(&RevsetExpression::commits(hidable_git_heads))
581 .intersection(&RevsetExpression::visible_heads().ancestors());
583 let abandoned_commit_ids: Vec<_> = abandoned_expression
584 .evaluate(mut_repo)
585 .map_err(|err| err.into_backend_error())?
586 .iter()
587 .try_collect()
588 .map_err(|err| err.into_backend_error())?;
589 for id in &abandoned_commit_ids {
590 let commit = mut_repo.store().get_commit(id)?;
591 mut_repo.record_abandoned_commit(&commit);
592 }
593 Ok(abandoned_commit_ids)
594}
595
596fn diff_refs_to_import(
598 view: &View,
599 git_repo: &gix::Repository,
600 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
601) -> Result<RefsToImport, GitImportError> {
602 let mut known_git_refs = view
603 .git_refs()
604 .iter()
605 .filter_map(|(full_name, target)| {
606 let (kind, symbol) =
608 parse_git_ref(full_name).expect("stored git ref should be parsable");
609 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
610 })
611 .collect();
612 let mut known_remote_bookmarks = view
613 .all_remote_bookmarks()
614 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
615 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
616 .collect();
617 let mut known_remote_tags = view
618 .all_remote_tags()
619 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
620 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
621 .collect();
622
623 let mut changed_git_refs = Vec::new();
624 let mut changed_remote_bookmarks = Vec::new();
625 let mut changed_remote_tags = Vec::new();
626 let mut failed_ref_names = Vec::new();
627 let actual = git_repo.references().map_err(GitImportError::from_git)?;
628 collect_changed_refs_to_import(
629 actual.local_branches().map_err(GitImportError::from_git)?,
630 &mut known_git_refs,
631 &mut known_remote_bookmarks,
632 &mut changed_git_refs,
633 &mut changed_remote_bookmarks,
634 &mut failed_ref_names,
635 &git_ref_filter,
636 )?;
637 collect_changed_refs_to_import(
638 actual.remote_branches().map_err(GitImportError::from_git)?,
639 &mut known_git_refs,
640 &mut known_remote_bookmarks,
641 &mut changed_git_refs,
642 &mut changed_remote_bookmarks,
643 &mut failed_ref_names,
644 &git_ref_filter,
645 )?;
646 collect_changed_refs_to_import(
647 actual.tags().map_err(GitImportError::from_git)?,
648 &mut known_git_refs,
649 &mut known_remote_tags,
650 &mut changed_git_refs,
651 &mut changed_remote_tags,
652 &mut failed_ref_names,
653 &git_ref_filter,
654 )?;
655 for full_name in known_git_refs.into_keys() {
656 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
657 }
658 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
659 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
660 }
661 for (RemoteRefKey(symbol), old) in known_remote_tags {
662 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
663 }
664
665 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
667 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
668 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
669 failed_ref_names.sort_unstable();
670 Ok(RefsToImport {
671 changed_git_refs,
672 changed_remote_bookmarks,
673 changed_remote_tags,
674 failed_ref_names,
675 })
676}
677
678fn collect_changed_refs_to_import(
679 actual_git_refs: gix::reference::iter::Iter<'_>,
680 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
681 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
682 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
683 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
684 failed_ref_names: &mut Vec<BString>,
685 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
686) -> Result<(), GitImportError> {
687 for git_ref in actual_git_refs {
688 let git_ref = git_ref.map_err(GitImportError::from_git)?;
689 let full_name_bytes = git_ref.name().as_bstr();
690 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
691 failed_ref_names.push(full_name_bytes.to_owned());
693 continue;
694 };
695 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
696 failed_ref_names.push(full_name_bytes.to_owned());
697 continue;
698 }
699 let full_name = GitRefName::new(full_name);
700 let Some((kind, symbol)) = parse_git_ref(full_name) else {
701 continue;
703 };
704 if !git_ref_filter(kind, symbol) {
705 continue;
706 }
707 let old_git_target = known_git_refs.get(full_name).copied().flatten();
708 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
709 continue;
711 };
712 let new_target = RefTarget::normal(id);
713 known_git_refs.remove(full_name);
714 if new_target != *old_git_target {
715 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
716 }
717 let old_remote_ref = known_remote_refs
720 .remove(&symbol)
721 .unwrap_or_else(|| RemoteRef::absent_ref());
722 if new_target != old_remote_ref.target {
723 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
724 }
725 }
726 Ok(())
727}
728
729fn default_remote_ref_state_for(
730 kind: GitRefKind,
731 symbol: RemoteRefSymbol<'_>,
732 git_settings: &GitSettings,
733) -> RemoteRefState {
734 match kind {
735 GitRefKind::Bookmark => {
736 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
737 RemoteRefState::Tracked
738 } else {
739 RemoteRefState::New
740 }
741 }
742 GitRefKind::Tag => RemoteRefState::Tracked,
744 }
745}
746
747fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
753 itertools::chain(view.local_bookmarks(), view.local_tags())
754 .flat_map(|(_, target)| target.added_ids())
755 .cloned()
756 .collect()
757}
758
759fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
766 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
767 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
768 .map(|(_, remote_ref)| &remote_ref.target)
769 .flat_map(|target| target.added_ids())
770 .cloned()
771 .collect()
772}
773
774pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
782 let store = mut_repo.store();
783 let git_backend = get_git_backend(store)?;
784 let git_repo = git_backend.git_repo();
785
786 let old_git_head = mut_repo.view().git_head();
787 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
788 Some(CommitId::from_bytes(oid.as_bytes()))
789 } else {
790 None
791 };
792 if old_git_head.as_resolved() == Some(&new_git_head_id) {
793 return Ok(());
794 }
795
796 if let Some(head_id) = &new_git_head_id {
798 let index = mut_repo.index();
799 if !index.has_id(head_id) {
800 git_backend.import_head_commits([head_id]).map_err(|err| {
801 GitImportError::MissingHeadTarget {
802 id: head_id.clone(),
803 err,
804 }
805 })?;
806 }
807 store
810 .get_commit(head_id)
811 .and_then(|commit| mut_repo.add_head(&commit))
812 .map_err(GitImportError::Backend)?;
813 }
814
815 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
816 Ok(())
817}
818
819#[derive(Error, Debug)]
820pub enum GitExportError {
821 #[error(transparent)]
822 Git(Box<dyn std::error::Error + Send + Sync>),
823 #[error(transparent)]
824 UnexpectedBackend(#[from] UnexpectedGitBackendError),
825}
826
827impl GitExportError {
828 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
829 Self::Git(source.into())
830 }
831}
832
833#[derive(Debug, Error)]
835pub enum FailedRefExportReason {
836 #[error("Name is not allowed in Git")]
838 InvalidGitName,
839 #[error("Ref was in a conflicted state from the last import")]
842 ConflictedOldState,
843 #[error("Ref cannot point to the root commit in Git")]
845 OnRootCommit,
846 #[error("Deleted ref had been modified in Git")]
848 DeletedInJjModifiedInGit,
849 #[error("Added ref had been added with a different target in Git")]
851 AddedInJjAddedInGit,
852 #[error("Modified ref had been deleted in Git")]
854 ModifiedInJjDeletedInGit,
855 #[error("Failed to delete")]
857 FailedToDelete(#[source] Box<gix::reference::edit::Error>),
858 #[error("Failed to set")]
860 FailedToSet(#[source] Box<gix::reference::edit::Error>),
861}
862
863#[derive(Debug)]
865pub struct GitExportStats {
866 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
868}
869
870#[derive(Debug)]
871struct RefsToExport {
872 bookmarks_to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
875 bookmarks_to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
880 failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
882}
883
884pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
895 export_some_refs(mut_repo, |_, _| true)
896}
897
898pub fn export_some_refs(
899 mut_repo: &mut MutableRepo,
900 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
901) -> Result<GitExportStats, GitExportError> {
902 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
903 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
904 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
905 let (_, value) = &map[index];
906 Some(value)
907 }
908
909 let git_repo = get_git_repo(mut_repo.store())?;
910
911 let RefsToExport {
912 bookmarks_to_update,
913 bookmarks_to_delete,
914 mut failed_bookmarks,
915 } = diff_refs_to_export(
916 mut_repo.view(),
917 mut_repo.store().root_commit_id(),
918 &git_ref_filter,
919 );
920
921 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
923 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
924 if let Some((GitRefKind::Bookmark, symbol)) = target_name
925 .as_ref()
926 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
927 .and_then(|name| parse_git_ref(name.as_ref()))
928 {
929 let old_target = head_ref.inner.target.clone();
930 let current_oid = match head_ref.into_fully_peeled_id() {
931 Ok(id) => Some(id.detach()),
932 Err(gix::reference::peel::Error::ToId(
933 gix::refs::peel::to_id::Error::FollowToObject(
934 gix::refs::peel::to_object::Error::Follow(
935 gix::refs::file::find::existing::Error::NotFound { .. },
936 ),
937 ),
938 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
940 };
941 let new_oid = if let Some((_old_oid, new_oid)) = get(&bookmarks_to_update, symbol) {
942 Some(new_oid)
943 } else if get(&bookmarks_to_delete, symbol).is_some() {
944 None
945 } else {
946 current_oid.as_ref()
947 };
948 if new_oid != current_oid.as_ref() {
949 update_git_head(
950 &git_repo,
951 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
952 current_oid,
953 )
954 .map_err(GitExportError::from_git)?;
955 }
956 }
957 }
958 for (symbol, old_oid) in bookmarks_to_delete {
959 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
960 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
961 continue;
962 };
963 if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) {
964 failed_bookmarks.push((symbol, reason));
965 } else {
966 let new_target = RefTarget::absent();
967 mut_repo.set_git_ref_target(&git_ref_name, new_target);
968 }
969 }
970 for (symbol, (old_oid, new_oid)) in bookmarks_to_update {
971 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
972 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
973 continue;
974 };
975 if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) {
976 failed_bookmarks.push((symbol, reason));
977 } else {
978 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
979 mut_repo.set_git_ref_target(&git_ref_name, new_target);
980 }
981 }
982
983 failed_bookmarks.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
987
988 copy_exportable_local_bookmarks_to_remote_view(
989 mut_repo,
990 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
991 |name| {
992 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
993 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
994 },
995 );
996 Ok(GitExportStats { failed_bookmarks })
999}
1000
1001fn copy_exportable_local_bookmarks_to_remote_view(
1002 mut_repo: &mut MutableRepo,
1003 remote: &RemoteName,
1004 name_filter: impl Fn(&RefName) -> bool,
1005) {
1006 let new_local_bookmarks = mut_repo
1007 .view()
1008 .local_remote_bookmarks(remote)
1009 .filter_map(|(name, targets)| {
1010 let old_target = &targets.remote_ref.target;
1013 let new_target = targets.local_target;
1014 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1015 })
1016 .filter(|&(name, _)| name_filter(name))
1017 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1018 .collect_vec();
1019 for (name, new_target) in new_local_bookmarks {
1020 let new_remote_ref = RemoteRef {
1021 target: new_target,
1022 state: RemoteRefState::Tracked,
1023 };
1024 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1025 }
1026}
1027
1028fn diff_refs_to_export(
1030 view: &View,
1031 root_commit_id: &CommitId,
1032 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1033) -> RefsToExport {
1034 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1037 itertools::chain(
1038 view.local_bookmarks().map(|(name, target)| {
1039 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1040 (symbol, target)
1041 }),
1042 view.all_remote_bookmarks()
1043 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1044 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1045 )
1046 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1047 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1048 .collect();
1049 let known_git_refs = view
1050 .git_refs()
1051 .iter()
1052 .map(|(full_name, target)| {
1053 let (kind, symbol) =
1054 parse_git_ref(full_name).expect("stored git ref should be parsable");
1055 ((kind, symbol), target)
1056 })
1057 .filter(|&((kind, symbol), _)| {
1058 kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol)
1062 });
1063 for ((_kind, symbol), target) in known_git_refs {
1064 all_bookmark_targets
1065 .entry(symbol)
1066 .and_modify(|(old_target, _)| *old_target = target)
1067 .or_insert((target, RefTarget::absent_ref()));
1068 }
1069
1070 let mut bookmarks_to_update = Vec::new();
1071 let mut bookmarks_to_delete = Vec::new();
1072 let mut failed_bookmarks = Vec::new();
1073 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1074 for (symbol, (old_target, new_target)) in all_bookmark_targets {
1075 if new_target == old_target {
1076 continue;
1077 }
1078 if *new_target == root_commit_target {
1079 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1081 continue;
1082 }
1083 let old_oid = if let Some(id) = old_target.as_normal() {
1084 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1085 } else if old_target.has_conflict() {
1086 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1089 continue;
1090 } else {
1091 assert!(old_target.is_absent());
1092 None
1093 };
1094 if let Some(id) = new_target.as_normal() {
1095 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1096 bookmarks_to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1097 } else if new_target.has_conflict() {
1098 continue;
1100 } else {
1101 assert!(new_target.is_absent());
1102 bookmarks_to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1103 }
1104 }
1105
1106 bookmarks_to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1108 bookmarks_to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1109 failed_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1110 RefsToExport {
1111 bookmarks_to_update,
1112 bookmarks_to_delete,
1113 failed_bookmarks,
1114 }
1115}
1116
1117fn delete_git_ref(
1118 git_repo: &gix::Repository,
1119 git_ref_name: &GitRefName,
1120 old_oid: &gix::oid,
1121) -> Result<(), FailedRefExportReason> {
1122 if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1123 if git_ref.inner.target.try_id() == Some(old_oid) {
1124 git_ref
1126 .delete()
1127 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1128 } else {
1129 return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1131 }
1132 } else {
1133 }
1135 Ok(())
1136}
1137
1138fn update_git_ref(
1139 git_repo: &gix::Repository,
1140 git_ref_name: &GitRefName,
1141 old_oid: Option<gix::ObjectId>,
1142 new_oid: gix::ObjectId,
1143) -> Result<(), FailedRefExportReason> {
1144 match old_oid {
1145 None => {
1146 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1147 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1150 return Err(FailedRefExportReason::AddedInJjAddedInGit);
1151 }
1152 } else {
1153 git_repo
1155 .reference(
1156 git_ref_name.as_str(),
1157 new_oid,
1158 gix::refs::transaction::PreviousValue::MustNotExist,
1159 "export from jj",
1160 )
1161 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1162 }
1163 }
1164 Some(old_oid) => {
1165 if let Err(err) = git_repo.reference(
1167 git_ref_name.as_str(),
1168 new_oid,
1169 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1170 "export from jj",
1171 ) {
1172 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1174 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1176 return Err(FailedRefExportReason::FailedToSet(err.into()));
1177 }
1178 } else {
1179 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1181 }
1182 } else {
1183 }
1186 }
1187 }
1188 Ok(())
1189}
1190
1191fn update_git_head(
1194 git_repo: &gix::Repository,
1195 expected_ref: gix::refs::transaction::PreviousValue,
1196 new_oid: Option<gix::ObjectId>,
1197) -> Result<(), gix::reference::edit::Error> {
1198 let mut ref_edits = Vec::new();
1199 let new_target = if let Some(oid) = new_oid {
1200 gix::refs::Target::Object(oid)
1201 } else {
1202 ref_edits.push(gix::refs::transaction::RefEdit {
1207 change: gix::refs::transaction::Change::Delete {
1208 expected: gix::refs::transaction::PreviousValue::Any,
1209 log: gix::refs::transaction::RefLog::AndReference,
1210 },
1211 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1212 deref: false,
1213 });
1214 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1215 };
1216 ref_edits.push(gix::refs::transaction::RefEdit {
1217 change: gix::refs::transaction::Change::Update {
1218 log: gix::refs::transaction::LogChange {
1219 message: "export from jj".into(),
1220 ..Default::default()
1221 },
1222 expected: expected_ref,
1223 new: new_target,
1224 },
1225 name: "HEAD".try_into().unwrap(),
1226 deref: false,
1227 });
1228 git_repo.edit_references(ref_edits)?;
1229 Ok(())
1230}
1231
1232#[derive(Debug, Error)]
1233pub enum GitResetHeadError {
1234 #[error(transparent)]
1235 Backend(#[from] BackendError),
1236 #[error(transparent)]
1237 Git(Box<dyn std::error::Error + Send + Sync>),
1238 #[error("Failed to update Git HEAD ref")]
1239 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1240 #[error(transparent)]
1241 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1242}
1243
1244impl GitResetHeadError {
1245 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1246 Self::Git(source.into())
1247 }
1248}
1249
1250pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1253 let git_repo = get_git_repo(mut_repo.store())?;
1254
1255 let first_parent_id = &wc_commit.parent_ids()[0];
1256 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1257 RefTarget::normal(first_parent_id.clone())
1258 } else {
1259 RefTarget::absent()
1260 };
1261
1262 let old_head_target = mut_repo.git_head();
1264 if old_head_target != new_head_target {
1265 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1266 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1269 if actual_head.is_detached() {
1270 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1271 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1272 } else {
1273 gix::refs::transaction::PreviousValue::MustExist
1276 }
1277 } else {
1278 gix::refs::transaction::PreviousValue::MustExist
1280 };
1281 let new_oid = new_head_target
1282 .as_normal()
1283 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1284 update_git_head(&git_repo, expected_ref, new_oid)
1285 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1286 mut_repo.set_git_head_target(new_head_target);
1287 }
1288
1289 if git_repo.state().is_some() {
1292 clear_operation_state(&git_repo)?;
1293 }
1294
1295 reset_index(mut_repo, &git_repo, wc_commit)
1296}
1297
1298fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1300 const STATE_FILE_NAMES: &[&str] = &[
1304 "MERGE_HEAD",
1305 "MERGE_MODE",
1306 "MERGE_MSG",
1307 "REVERT_HEAD",
1308 "CHERRY_PICK_HEAD",
1309 "BISECT_LOG",
1310 ];
1311 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1312 let handle_err = |err: PathError| match err.source.kind() {
1313 std::io::ErrorKind::NotFound => Ok(()),
1314 _ => Err(GitResetHeadError::from_git(err)),
1315 };
1316 for file_name in STATE_FILE_NAMES {
1317 let path = git_repo.path().join(file_name);
1318 std::fs::remove_file(&path)
1319 .context(&path)
1320 .or_else(handle_err)?;
1321 }
1322 for dir_name in STATE_DIR_NAMES {
1323 let path = git_repo.path().join(dir_name);
1324 std::fs::remove_dir_all(&path)
1325 .context(&path)
1326 .or_else(handle_err)?;
1327 }
1328 Ok(())
1329}
1330
1331fn reset_index(
1332 repo: &dyn Repo,
1333 git_repo: &gix::Repository,
1334 wc_commit: &Commit,
1335) -> Result<(), GitResetHeadError> {
1336 let parent_tree = wc_commit.parent_tree(repo)?;
1337 let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
1341 if tree.id() == repo.store().empty_tree_id() {
1342 gix::index::File::from_state(
1346 gix::index::State::new(git_repo.object_hash()),
1347 git_repo.index_path(),
1348 )
1349 } else {
1350 git_repo
1353 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes()))
1354 .map_err(GitResetHeadError::from_git)?
1355 }
1356 } else {
1357 build_index_from_merged_tree(git_repo, &parent_tree)?
1358 };
1359
1360 let wc_tree = wc_commit.tree()?;
1361 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1362
1363 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1366 index
1367 .entries_mut_with_paths()
1368 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1369 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1370 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1371 })
1372 .filter_map(|merged| merged.both())
1373 .map(|((entry, _), old_entry)| (entry, old_entry))
1374 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1375 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1376 }
1377
1378 debug_assert!(index.verify_entries().is_ok());
1379
1380 index
1381 .write(gix::index::write::Options::default())
1382 .map_err(GitResetHeadError::from_git)
1383}
1384
1385fn build_index_from_merged_tree(
1386 git_repo: &gix::Repository,
1387 merged_tree: &MergedTree,
1388) -> Result<gix::index::File, GitResetHeadError> {
1389 let mut index = gix::index::File::from_state(
1390 gix::index::State::new(git_repo.object_hash()),
1391 git_repo.index_path(),
1392 );
1393
1394 let mut push_index_entry =
1395 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1396 let Some(entry) = maybe_entry else {
1397 return;
1398 };
1399
1400 let (id, mode) = match entry {
1401 TreeValue::File {
1402 id,
1403 executable,
1404 copy_id: _,
1405 } => {
1406 if *executable {
1407 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1408 } else {
1409 (id.as_bytes(), gix::index::entry::Mode::FILE)
1410 }
1411 }
1412 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1413 TreeValue::Tree(_) => {
1414 return;
1419 }
1420 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1421 };
1422
1423 let path = BStr::new(path.as_internal_file_string());
1424
1425 index.dangerously_push_entry(
1428 gix::index::entry::Stat::default(),
1429 gix::ObjectId::from_bytes_or_panic(id),
1430 gix::index::entry::Flags::from_stage(stage),
1431 mode,
1432 path,
1433 );
1434 };
1435
1436 let mut has_many_sided_conflict = false;
1437
1438 for (path, entry) in merged_tree.entries() {
1439 let entry = entry?;
1440 if let Some(resolved) = entry.as_resolved() {
1441 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1442 continue;
1443 }
1444
1445 let conflict = entry.simplify();
1446 if let [left, base, right] = conflict.as_slice() {
1447 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1449 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1450 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1451 } else {
1452 has_many_sided_conflict = true;
1460 push_index_entry(
1461 &path,
1462 conflict.first(),
1463 gix::index::entry::Stage::Unconflicted,
1464 );
1465 }
1466 }
1467
1468 index.sort_entries();
1471
1472 if has_many_sided_conflict
1475 && index
1476 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1477 .is_err()
1478 {
1479 let file_blob = git_repo
1480 .write_blob(
1481 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1482 )
1483 .map_err(GitResetHeadError::from_git)?;
1484 index.dangerously_push_entry(
1485 gix::index::entry::Stat::default(),
1486 file_blob.detach(),
1487 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1488 gix::index::entry::Mode::FILE,
1489 INDEX_DUMMY_CONFLICT_FILE.into(),
1490 );
1491 index.sort_entries();
1494 }
1495
1496 Ok(index)
1497}
1498
1499pub fn update_intent_to_add(
1506 repo: &dyn Repo,
1507 old_tree: &MergedTree,
1508 new_tree: &MergedTree,
1509) -> Result<(), GitResetHeadError> {
1510 let git_repo = get_git_repo(repo.store())?;
1511 let mut index = git_repo
1512 .index_or_empty()
1513 .map_err(GitResetHeadError::from_git)?;
1514 let mut_index = Arc::make_mut(&mut index);
1515 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1516 debug_assert!(mut_index.verify_entries().is_ok());
1517 mut_index
1518 .write(gix::index::write::Options::default())
1519 .map_err(GitResetHeadError::from_git)?;
1520
1521 Ok(())
1522}
1523
1524async fn update_intent_to_add_impl(
1525 git_repo: &gix::Repository,
1526 index: &mut gix::index::File,
1527 old_tree: &MergedTree,
1528 new_tree: &MergedTree,
1529) -> Result<(), GitResetHeadError> {
1530 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1531 let mut added_paths = vec![];
1532 let mut removed_paths = HashSet::new();
1533 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1534 let values = values?;
1535 if values.before.is_absent() {
1536 let executable = match values.after.as_normal() {
1537 Some(TreeValue::File {
1538 id: _,
1539 executable,
1540 copy_id: _,
1541 }) => *executable,
1542 Some(TreeValue::Symlink(_)) => false,
1543 _ => {
1544 continue;
1545 }
1546 };
1547 if index
1548 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1549 .is_err()
1550 {
1551 added_paths.push((BString::from(path.into_internal_string()), executable));
1552 }
1553 } else if values.after.is_absent() {
1554 removed_paths.insert(BString::from(path.into_internal_string()));
1555 }
1556 }
1557
1558 if added_paths.is_empty() && removed_paths.is_empty() {
1559 return Ok(());
1560 }
1561
1562 if !added_paths.is_empty() {
1563 let empty_blob = git_repo
1565 .write_blob(b"")
1566 .map_err(GitResetHeadError::from_git)?
1567 .detach();
1568 for (path, executable) in added_paths {
1569 index.dangerously_push_entry(
1571 gix::index::entry::Stat::default(),
1572 empty_blob,
1573 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1574 if executable {
1575 gix::index::entry::Mode::FILE_EXECUTABLE
1576 } else {
1577 gix::index::entry::Mode::FILE
1578 },
1579 path.as_ref(),
1580 );
1581 }
1582 }
1583 if !removed_paths.is_empty() {
1584 index.remove_entries(|_size, path, entry| {
1585 entry
1586 .flags
1587 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1588 && removed_paths.contains(path)
1589 });
1590 }
1591
1592 index.sort_entries();
1593
1594 Ok(())
1595}
1596
1597#[derive(Debug, Error)]
1598pub enum GitRemoteManagementError {
1599 #[error("No git remote named '{}'", .0.as_symbol())]
1600 NoSuchRemote(RemoteNameBuf),
1601 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1602 RemoteAlreadyExists(RemoteNameBuf),
1603 #[error(transparent)]
1604 RemoteName(#[from] GitRemoteNameError),
1605 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1606 NonstandardConfiguration(RemoteNameBuf),
1607 #[error("Error saving Git configuration")]
1608 GitConfigSaveError(#[source] std::io::Error),
1609 #[error("Unexpected Git error when managing remotes")]
1610 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1611 #[error(transparent)]
1612 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1613}
1614
1615impl GitRemoteManagementError {
1616 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1617 Self::InternalGitError(source.into())
1618 }
1619}
1620
1621pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1626 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1627}
1628
1629fn default_fetch_refspec(remote: &RemoteName) -> String {
1630 format!(
1631 "+refs/heads/*:refs/remotes/{remote}/*",
1632 remote = remote.as_str()
1633 )
1634}
1635
1636fn add_ref(
1637 name: gix::refs::FullName,
1638 target: gix::refs::Target,
1639 message: BString,
1640) -> gix::refs::transaction::RefEdit {
1641 gix::refs::transaction::RefEdit {
1642 change: gix::refs::transaction::Change::Update {
1643 log: gix::refs::transaction::LogChange {
1644 mode: gix::refs::transaction::RefLog::AndReference,
1645 force_create_reflog: false,
1646 message,
1647 },
1648 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1649 new: target,
1650 },
1651 name,
1652 deref: false,
1653 }
1654}
1655
1656fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1657 gix::refs::transaction::RefEdit {
1658 change: gix::refs::transaction::Change::Delete {
1659 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1660 reference.target().into_owned(),
1661 ),
1662 log: gix::refs::transaction::RefLog::AndReference,
1663 },
1664 name: reference.name().to_owned(),
1665 deref: false,
1666 }
1667}
1668
1669fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1675 let mut config_file = File::create(
1676 config
1677 .meta()
1678 .path
1679 .as_ref()
1680 .expect("Git repository to have a config file"),
1681 )?;
1682 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1683}
1684
1685fn save_remote(
1686 config: &mut gix::config::File<'static>,
1687 remote_name: &RemoteName,
1688 remote: &mut gix::Remote,
1689) -> Result<(), GitRemoteManagementError> {
1690 config
1697 .new_section(
1698 "remote",
1699 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1700 )
1701 .map_err(GitRemoteManagementError::from_git)?;
1702 remote
1703 .save_as_to(remote_name.as_str(), config)
1704 .map_err(GitRemoteManagementError::from_git)?;
1705 Ok(())
1706}
1707
1708fn git_config_branch_section_ids_by_remote(
1709 config: &gix::config::File,
1710 remote_name: &RemoteName,
1711) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1712 config
1713 .sections_by_name("branch")
1714 .into_iter()
1715 .flatten()
1716 .filter_map(|section| {
1717 let remote_values = section.values("remote");
1718 let push_remote_values = section.values("pushRemote");
1719 if !remote_values
1720 .iter()
1721 .chain(push_remote_values.iter())
1722 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1723 {
1724 return None;
1725 }
1726 if remote_values.len() > 1
1727 || push_remote_values.len() > 1
1728 || section.value_names().any(|name| {
1729 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1730 })
1731 {
1732 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1733 remote_name.to_owned(),
1734 )));
1735 }
1736 Some(Ok(section.id()))
1737 })
1738 .collect()
1739}
1740
1741fn rename_remote_in_git_branch_config_sections(
1742 config: &mut gix::config::File,
1743 old_remote_name: &RemoteName,
1744 new_remote_name: &RemoteName,
1745) -> Result<(), GitRemoteManagementError> {
1746 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1747 config
1748 .section_mut_by_id(id)
1749 .expect("found section to exist")
1750 .set(
1751 "remote"
1752 .try_into()
1753 .expect("'remote' to be a valid value name"),
1754 BStr::new(new_remote_name.as_str()),
1755 );
1756 }
1757 Ok(())
1758}
1759
1760fn remove_remote_git_branch_config_sections(
1761 config: &mut gix::config::File,
1762 remote_name: &RemoteName,
1763) -> Result<(), GitRemoteManagementError> {
1764 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1765 config
1766 .remove_section_by_id(id)
1767 .expect("removed section to exist");
1768 }
1769 Ok(())
1770}
1771
1772fn remove_remote_git_config_sections(
1773 config: &mut gix::config::File,
1774 remote_name: &RemoteName,
1775) -> Result<(), GitRemoteManagementError> {
1776 let section_ids_to_remove: Vec<_> = config
1777 .sections_by_name("remote")
1778 .into_iter()
1779 .flatten()
1780 .filter(|section| {
1781 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1782 })
1783 .map(|section| {
1784 if section.value_names().any(|name| {
1785 !name.eq_ignore_ascii_case(b"url")
1786 && !name.eq_ignore_ascii_case(b"fetch")
1787 && !name.eq_ignore_ascii_case(b"tagOpt")
1788 }) {
1789 return Err(GitRemoteManagementError::NonstandardConfiguration(
1790 remote_name.to_owned(),
1791 ));
1792 }
1793 Ok(section.id())
1794 })
1795 .try_collect()?;
1796 for id in section_ids_to_remove {
1797 config
1798 .remove_section_by_id(id)
1799 .expect("removed section to exist");
1800 }
1801 Ok(())
1802}
1803
1804pub fn get_all_remote_names(
1806 store: &Store,
1807) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1808 let git_repo = get_git_repo(store)?;
1809 let names = git_repo
1810 .remote_names()
1811 .into_iter()
1812 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1814 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1816 .map(RemoteNameBuf::from)
1817 .collect();
1818 Ok(names)
1819}
1820
1821pub fn add_remote(
1822 store: &Store,
1823 remote_name: &RemoteName,
1824 url: &str,
1825 fetch_tags: gix::remote::fetch::Tags,
1826) -> Result<(), GitRemoteManagementError> {
1827 let git_repo = get_git_repo(store)?;
1828
1829 validate_remote_name(remote_name)?;
1830
1831 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1832 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1833 remote_name.to_owned(),
1834 ));
1835 }
1836
1837 let mut remote = git_repo
1838 .remote_at(url)
1839 .map_err(GitRemoteManagementError::from_git)?
1840 .with_fetch_tags(fetch_tags)
1841 .with_refspecs(
1842 [default_fetch_refspec(remote_name).as_bytes()],
1843 gix::remote::Direction::Fetch,
1844 )
1845 .expect("default refspec to be valid");
1846
1847 let mut config = git_repo.config_snapshot().clone();
1848 save_remote(&mut config, remote_name, &mut remote)?;
1849 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1850
1851 Ok(())
1852}
1853
1854pub fn remove_remote(
1855 mut_repo: &mut MutableRepo,
1856 remote_name: &RemoteName,
1857) -> Result<(), GitRemoteManagementError> {
1858 let mut git_repo = get_git_repo(mut_repo.store())?;
1859
1860 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1861 return Err(GitRemoteManagementError::NoSuchRemote(
1862 remote_name.to_owned(),
1863 ));
1864 };
1865
1866 let mut config = git_repo.config_snapshot().clone();
1867 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1868 remove_remote_git_config_sections(&mut config, remote_name)?;
1869 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1870
1871 remove_remote_git_refs(&mut git_repo, remote_name)
1872 .map_err(GitRemoteManagementError::from_git)?;
1873
1874 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1875 remove_remote_refs(mut_repo, remote_name);
1876 }
1877
1878 Ok(())
1879}
1880
1881fn remove_remote_git_refs(
1882 git_repo: &mut gix::Repository,
1883 remote: &RemoteName,
1884) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1885 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1886 let edits: Vec<_> = git_repo
1887 .references()?
1888 .prefixed(prefix.as_str())?
1889 .map_ok(remove_ref)
1890 .try_collect()?;
1891 git_repo.edit_references(edits)?;
1892 Ok(())
1893}
1894
1895fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1896 mut_repo.remove_remote(remote);
1897 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1898 let git_refs_to_delete = mut_repo
1899 .view()
1900 .git_refs()
1901 .keys()
1902 .filter(|&r| r.as_str().starts_with(&prefix))
1903 .cloned()
1904 .collect_vec();
1905 for git_ref in git_refs_to_delete {
1906 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1907 }
1908}
1909
1910pub fn rename_remote(
1911 mut_repo: &mut MutableRepo,
1912 old_remote_name: &RemoteName,
1913 new_remote_name: &RemoteName,
1914) -> Result<(), GitRemoteManagementError> {
1915 let mut git_repo = get_git_repo(mut_repo.store())?;
1916
1917 validate_remote_name(new_remote_name)?;
1918
1919 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1920 return Err(GitRemoteManagementError::NoSuchRemote(
1921 old_remote_name.to_owned(),
1922 ));
1923 };
1924 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1925
1926 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1927 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1928 new_remote_name.to_owned(),
1929 ));
1930 }
1931
1932 match (
1933 remote.refspecs(gix::remote::Direction::Fetch),
1934 remote.refspecs(gix::remote::Direction::Push),
1935 ) {
1936 ([refspec], [])
1937 if refspec.to_ref().to_bstring()
1938 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1939 _ => {
1940 return Err(GitRemoteManagementError::NonstandardConfiguration(
1941 old_remote_name.to_owned(),
1942 ));
1943 }
1944 }
1945
1946 remote
1947 .replace_refspecs(
1948 [default_fetch_refspec(new_remote_name).as_bytes()],
1949 gix::remote::Direction::Fetch,
1950 )
1951 .expect("default refspec to be valid");
1952
1953 let mut config = git_repo.config_snapshot().clone();
1954 save_remote(&mut config, new_remote_name, &mut remote)?;
1955 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1956 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1957 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1958
1959 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1960 .map_err(GitRemoteManagementError::from_git)?;
1961
1962 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1963 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1964 }
1965
1966 Ok(())
1967}
1968
1969fn rename_remote_git_refs(
1970 git_repo: &mut gix::Repository,
1971 old_remote_name: &RemoteName,
1972 new_remote_name: &RemoteName,
1973) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1974 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1975 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1976 let ref_log_message = BString::from(format!(
1977 "renamed remote {old_remote_name} to {new_remote_name}",
1978 old_remote_name = old_remote_name.as_symbol(),
1979 new_remote_name = new_remote_name.as_symbol(),
1980 ));
1981
1982 let edits: Vec<_> = git_repo
1983 .references()?
1984 .prefixed(old_prefix.as_str())?
1985 .map_ok(|old_ref| {
1986 let new_name = BString::new(
1987 [
1988 new_prefix.as_bytes(),
1989 &old_ref.name().as_bstr()[old_prefix.len()..],
1990 ]
1991 .concat(),
1992 );
1993 [
1994 add_ref(
1995 new_name.try_into().expect("new ref name to be valid"),
1996 old_ref.target().into_owned(),
1997 ref_log_message.clone(),
1998 ),
1999 remove_ref(old_ref),
2000 ]
2001 })
2002 .flatten_ok()
2003 .try_collect()?;
2004 git_repo.edit_references(edits)?;
2005 Ok(())
2006}
2007
2008fn gix_remote_with_fetch_url<Url, E>(
2014 remote: gix::Remote,
2015 url: Url,
2016) -> Result<gix::Remote, gix::remote::init::Error>
2017where
2018 Url: TryInto<gix::Url, Error = E>,
2019 gix::url::parse::Error: From<E>,
2020{
2021 let mut new_remote = remote.repo().remote_at(url)?;
2022 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2028 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2029 new_remote
2030 .replace_refspecs(
2031 remote
2032 .refspecs(direction)
2033 .iter()
2034 .map(|refspec| refspec.to_ref().to_bstring()),
2035 direction,
2036 )
2037 .expect("existing refspecs to be valid");
2038 }
2039 Ok(new_remote)
2040}
2041
2042pub fn set_remote_url(
2043 store: &Store,
2044 remote_name: &RemoteName,
2045 new_remote_url: &str,
2046) -> Result<(), GitRemoteManagementError> {
2047 let git_repo = get_git_repo(store)?;
2048
2049 validate_remote_name(remote_name)?;
2050
2051 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2052 return Err(GitRemoteManagementError::NoSuchRemote(
2053 remote_name.to_owned(),
2054 ));
2055 };
2056 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2057
2058 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2059 return Err(GitRemoteManagementError::NonstandardConfiguration(
2060 remote_name.to_owned(),
2061 ));
2062 }
2063
2064 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2065 .map_err(GitRemoteManagementError::from_git)?;
2066
2067 let mut config = git_repo.config_snapshot().clone();
2068 save_remote(&mut config, remote_name, &mut remote)?;
2069 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2070
2071 Ok(())
2072}
2073
2074fn rename_remote_refs(
2075 mut_repo: &mut MutableRepo,
2076 old_remote_name: &RemoteName,
2077 new_remote_name: &RemoteName,
2078) {
2079 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2080 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2081 let git_refs = mut_repo
2082 .view()
2083 .git_refs()
2084 .iter()
2085 .filter_map(|(old, target)| {
2086 old.as_str().strip_prefix(&prefix).map(|p| {
2087 let new: GitRefNameBuf =
2088 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2089 (old.clone(), new, target.clone())
2090 })
2091 })
2092 .collect_vec();
2093 for (old, new, target) in git_refs {
2094 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2095 mut_repo.set_git_ref_target(&new, target);
2096 }
2097}
2098
2099const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2100
2101#[derive(Error, Debug)]
2102pub enum GitFetchError {
2103 #[error("No git remote named '{}'", .0.as_symbol())]
2104 NoSuchRemote(RemoteNameBuf),
2105 #[error(
2106 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2107 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2108 )]
2109 InvalidBranchPattern(StringPattern),
2110 #[error(transparent)]
2111 RemoteName(#[from] GitRemoteNameError),
2112 #[error(transparent)]
2113 Subprocess(#[from] GitSubprocessError),
2114}
2115
2116#[derive(Error, Debug)]
2117pub enum GitDefaultRefspecError {
2118 #[error("No git remote named '{}'", .0.as_symbol())]
2119 NoSuchRemote(RemoteNameBuf),
2120 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2121 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2122}
2123
2124struct FetchedBranches {
2125 remote: RemoteNameBuf,
2126 branches: Vec<StringPattern>,
2127}
2128
2129#[derive(Debug)]
2131pub struct ExpandedFetchRefSpecs {
2132 expected_branch_names: Vec<StringPattern>,
2136 refspecs: Vec<RefSpec>,
2137 negative_refspecs: Vec<NegativeRefSpec>,
2138}
2139
2140pub fn expand_fetch_refspecs(
2142 remote: &RemoteName,
2143 branch_names: Vec<StringPattern>,
2144) -> Result<ExpandedFetchRefSpecs, GitFetchError> {
2145 let refspecs = branch_names
2146 .iter()
2147 .map(|pattern| {
2148 pattern
2149 .to_glob()
2150 .filter(
2151 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2154 )
2155 .map(|glob| {
2156 RefSpec::forced(
2157 format!("refs/heads/{glob}"),
2158 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2159 )
2160 })
2161 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2162 })
2163 .try_collect()?;
2164
2165 Ok(ExpandedFetchRefSpecs {
2166 expected_branch_names: branch_names,
2167 refspecs,
2168 negative_refspecs: Vec::new(),
2169 })
2170}
2171
2172#[derive(Debug)]
2176#[must_use = "warnings should be surfaced in the UI"]
2177pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2178
2179#[derive(Debug)]
2182pub struct IgnoredRefspec {
2183 pub refspec: BString,
2185 pub reason: &'static str,
2187}
2188
2189pub fn expand_default_fetch_refspecs(
2191 remote: &RemoteName,
2192 git_repo: &gix::Repository,
2193) -> Result<(IgnoredRefspecs, ExpandedFetchRefSpecs), GitDefaultRefspecError> {
2194 let remote_name = remote.as_str();
2195 let remote = git_repo
2196 .try_find_remote(remote_name)
2197 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote.to_owned()))?
2198 .map_err(|e| {
2199 GitDefaultRefspecError::InvalidRemoteConfiguration(remote.to_owned(), Box::new(e))
2200 })?;
2201
2202 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2203
2204 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2205 let mut expected_branch_names = Vec::with_capacity(remote_refspecs.len());
2206 let mut negative_refspecs = Vec::new();
2207
2208 let refspecs = remote_refspecs
2209 .iter()
2210 .filter_map(|refspec| {
2211 let forced = refspec.allow_non_fast_forward();
2212 let refspec = refspec.to_ref();
2213
2214 let mut ensure_utf8 = |s| match str::from_utf8(s) {
2215 Ok(s) => Some(s),
2216 Err(_) => {
2217 ignored_refspecs.push(IgnoredRefspec {
2218 refspec: refspec.to_bstring(),
2219 reason: "invalid UTF-8",
2220 });
2221 None
2222 }
2223 };
2224
2225 let (src, dst) = match refspec.instruction() {
2226 Instruction::Push(_) => unreachable!(),
2228 Instruction::Fetch(fetch) => match fetch {
2229 gix::refspec::instruction::Fetch::Only { src: _ } => {
2230 ignored_refspecs.push(IgnoredRefspec {
2231 refspec: refspec.to_bstring(),
2232 reason: "fetch-only refspecs are not supported",
2233 });
2234 return None;
2235 }
2236
2237 gix::refspec::instruction::Fetch::AndUpdate {
2238 src,
2239 dst,
2240 allow_non_fast_forward: _, } => (ensure_utf8(src)?, ensure_utf8(dst)?),
2242
2243 gix::refspec::instruction::Fetch::Exclude { src } => {
2244 let src = ensure_utf8(src)?;
2245 negative_refspecs.push(NegativeRefSpec::new(src));
2246 return None;
2247 }
2248 },
2249 };
2250
2251 if !forced {
2252 ignored_refspecs.push(IgnoredRefspec {
2253 refspec: refspec.to_bstring(),
2254 reason: "non-forced refspecs are not supported",
2255 });
2256 return None;
2257 }
2258
2259 let Some(src_branch) = src.strip_prefix("refs/heads/") else {
2260 ignored_refspecs.push(IgnoredRefspec {
2261 refspec: refspec.to_bstring(),
2262 reason: "only refs/heads/ is supported for refspec sources",
2263 });
2264 return None;
2265 };
2266
2267 let dst = {
2268 let Some(dst_without_prefix) = dst.strip_prefix("refs/remotes/") else {
2269 ignored_refspecs.push(IgnoredRefspec {
2270 refspec: refspec.to_bstring(),
2271 reason: "only refs/remotes/ is supported for fetch destinations",
2272 });
2273 return None;
2274 };
2275
2276 let Some(dst_branch) = dst_without_prefix
2277 .strip_prefix(remote_name)
2278 .and_then(|d| d.strip_prefix("/"))
2279 else {
2280 ignored_refspecs.push(IgnoredRefspec {
2281 refspec: refspec.to_bstring(),
2282 reason: "remote renaming not supported",
2283 });
2284 return None;
2285 };
2286
2287 if src_branch == dst_branch {
2288 dst.to_owned()
2289 } else {
2290 ignored_refspecs.push(IgnoredRefspec {
2291 refspec: refspec.to_bstring(),
2292 reason: "renaming is not supported",
2293 });
2294 return None;
2295 }
2296 };
2297
2298 let Ok(branch) = StringPattern::glob(src_branch) else {
2300 ignored_refspecs.push(IgnoredRefspec {
2301 refspec: refspec.to_bstring(),
2302 reason: "invalid pattern",
2303 });
2304 return None;
2305 };
2306 expected_branch_names.push(branch);
2307
2308 Some(RefSpec::forced(src, dst))
2309 })
2310 .collect();
2311
2312 Ok((
2313 IgnoredRefspecs(ignored_refspecs),
2314 ExpandedFetchRefSpecs {
2315 expected_branch_names,
2316 refspecs,
2317 negative_refspecs,
2318 },
2319 ))
2320}
2321
2322pub struct GitFetch<'a> {
2324 mut_repo: &'a mut MutableRepo,
2325 git_repo: Box<gix::Repository>,
2326 git_ctx: GitSubprocessContext<'a>,
2327 git_settings: &'a GitSettings,
2328 fetched: Vec<FetchedBranches>,
2329}
2330
2331impl<'a> GitFetch<'a> {
2332 pub fn new(
2333 mut_repo: &'a mut MutableRepo,
2334 git_settings: &'a GitSettings,
2335 ) -> Result<Self, UnexpectedGitBackendError> {
2336 let git_backend = get_git_backend(mut_repo.store())?;
2337 let git_repo = Box::new(git_backend.git_repo());
2338 let git_ctx =
2339 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2340 Ok(GitFetch {
2341 mut_repo,
2342 git_repo,
2343 git_ctx,
2344 git_settings,
2345 fetched: vec![],
2346 })
2347 }
2348
2349 #[tracing::instrument(skip(self, callbacks))]
2355 pub fn fetch(
2356 &mut self,
2357 remote_name: &RemoteName,
2358 ExpandedFetchRefSpecs {
2359 expected_branch_names,
2360 refspecs: mut remaining_refspecs,
2361 negative_refspecs,
2362 }: ExpandedFetchRefSpecs,
2363 mut callbacks: RemoteCallbacks<'_>,
2364 depth: Option<NonZeroU32>,
2365 fetch_tags_override: Option<FetchTagsOverride>,
2366 ) -> Result<(), GitFetchError> {
2367 validate_remote_name(remote_name)?;
2368
2369 if self
2371 .git_repo
2372 .try_find_remote(remote_name.as_str())
2373 .is_none()
2374 {
2375 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2376 }
2377
2378 if remaining_refspecs.is_empty() {
2379 return Ok(());
2381 }
2382
2383 let mut branches_to_prune = Vec::new();
2384 while let Some(failing_refspec) = self.git_ctx.spawn_fetch(
2392 remote_name,
2393 &remaining_refspecs,
2394 &negative_refspecs,
2395 &mut callbacks,
2396 depth,
2397 fetch_tags_override,
2398 )? {
2399 tracing::debug!(failing_refspec, "failed to fetch ref");
2400 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2401
2402 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2403 branches_to_prune.push(format!(
2404 "{remote_name}/{branch_name}",
2405 remote_name = remote_name.as_str()
2406 ));
2407 }
2408 }
2409
2410 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2413
2414 self.fetched.push(FetchedBranches {
2415 remote: remote_name.to_owned(),
2416 branches: expected_branch_names,
2417 });
2418 Ok(())
2419 }
2420
2421 #[tracing::instrument(skip(self))]
2423 pub fn get_default_branch(
2424 &self,
2425 remote_name: &RemoteName,
2426 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2427 if self
2428 .git_repo
2429 .try_find_remote(remote_name.as_str())
2430 .is_none()
2431 {
2432 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2433 }
2434 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2435 tracing::debug!(?default_branch);
2436 Ok(default_branch)
2437 }
2438
2439 #[tracing::instrument(skip(self))]
2447 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2448 tracing::debug!("import_refs");
2449 let import_stats =
2450 import_some_refs(
2451 self.mut_repo,
2452 self.git_settings,
2453 |kind, symbol| match kind {
2454 GitRefKind::Bookmark => self
2455 .fetched
2456 .iter()
2457 .filter(|fetched| fetched.remote == symbol.remote)
2458 .any(|fetched| {
2459 fetched
2460 .branches
2461 .iter()
2462 .any(|pattern| pattern.is_match(symbol.name.as_str()))
2463 }),
2464 GitRefKind::Tag => true,
2465 },
2466 )?;
2467
2468 self.fetched.clear();
2469
2470 Ok(import_stats)
2471 }
2472}
2473
2474#[derive(Error, Debug)]
2475pub enum GitPushError {
2476 #[error("No git remote named '{}'", .0.as_symbol())]
2477 NoSuchRemote(RemoteNameBuf),
2478 #[error(transparent)]
2479 RemoteName(#[from] GitRemoteNameError),
2480 #[error(transparent)]
2481 Subprocess(#[from] GitSubprocessError),
2482 #[error(transparent)]
2483 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2484}
2485
2486#[derive(Clone, Debug)]
2487pub struct GitBranchPushTargets {
2488 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2489}
2490
2491pub struct GitRefUpdate {
2492 pub qualified_name: GitRefNameBuf,
2493 pub expected_current_target: Option<CommitId>,
2498 pub new_target: Option<CommitId>,
2499}
2500
2501pub fn push_branches(
2503 mut_repo: &mut MutableRepo,
2504 git_settings: &GitSettings,
2505 remote: &RemoteName,
2506 targets: &GitBranchPushTargets,
2507 callbacks: RemoteCallbacks<'_>,
2508) -> Result<GitPushStats, GitPushError> {
2509 validate_remote_name(remote)?;
2510
2511 let ref_updates = targets
2512 .branch_updates
2513 .iter()
2514 .map(|(name, update)| GitRefUpdate {
2515 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2516 expected_current_target: update.old_target.clone(),
2517 new_target: update.new_target.clone(),
2518 })
2519 .collect_vec();
2520
2521 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2522 tracing::debug!(?push_stats);
2523
2524 if push_stats.all_ok() {
2528 for (name, update) in &targets.branch_updates {
2529 let git_ref_name: GitRefNameBuf = format!(
2530 "refs/remotes/{remote}/{name}",
2531 remote = remote.as_str(),
2532 name = name.as_str()
2533 )
2534 .into();
2535 let new_remote_ref = RemoteRef {
2536 target: RefTarget::resolved(update.new_target.clone()),
2537 state: RemoteRefState::Tracked,
2538 };
2539 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2540 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2541 }
2542 }
2543
2544 Ok(push_stats)
2545}
2546
2547pub fn push_updates(
2549 repo: &dyn Repo,
2550 git_settings: &GitSettings,
2551 remote_name: &RemoteName,
2552 updates: &[GitRefUpdate],
2553 mut callbacks: RemoteCallbacks<'_>,
2554) -> Result<GitPushStats, GitPushError> {
2555 let mut qualified_remote_refs_expected_locations = HashMap::new();
2556 let mut refspecs = vec![];
2557 for update in updates {
2558 qualified_remote_refs_expected_locations.insert(
2559 update.qualified_name.as_ref(),
2560 update.expected_current_target.as_ref(),
2561 );
2562 if let Some(new_target) = &update.new_target {
2563 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2567 } else {
2568 refspecs.push(RefSpec::delete(&update.qualified_name));
2572 }
2573 }
2574
2575 let git_backend = get_git_backend(repo.store())?;
2576 let git_repo = git_backend.git_repo();
2577 let git_ctx =
2578 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2579
2580 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2582 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2583 }
2584
2585 let refs_to_push: Vec<RefToPush> = refspecs
2586 .iter()
2587 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2588 .collect();
2589
2590 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2591 push_stats.pushed.sort();
2592 push_stats.rejected.sort();
2593 push_stats.remote_rejected.sort();
2594 Ok(push_stats)
2595}
2596
2597#[non_exhaustive]
2598#[derive(Default)]
2599#[expect(clippy::type_complexity)]
2600pub struct RemoteCallbacks<'a> {
2601 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2602 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2603 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2604 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2605 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2606}
2607
2608#[derive(Clone, Debug)]
2609pub struct Progress {
2610 pub bytes_downloaded: Option<u64>,
2612 pub overall: f32,
2613}
2614
2615#[derive(Copy, Clone, Debug)]
2618pub enum FetchTagsOverride {
2619 AllTags,
2622 NoTags,
2625}