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