1#![allow(missing_docs)]
16
17use std::borrow::Borrow;
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::default::Default;
22use std::fs::File;
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::str;
26use std::sync::Arc;
27
28use bstr::BStr;
29use bstr::BString;
30use futures::StreamExt as _;
31use gix::refspec::Instruction;
32use itertools::Itertools as _;
33use pollster::FutureExt as _;
34use thiserror::Error;
35
36use crate::backend::BackendError;
37use crate::backend::BackendResult;
38use crate::backend::CommitId;
39use crate::backend::TreeValue;
40use crate::commit::Commit;
41use crate::file_util::IoResultExt as _;
42use crate::file_util::PathError;
43use crate::git_backend::GitBackend;
44use crate::git_subprocess::GitSubprocessContext;
45use crate::git_subprocess::GitSubprocessError;
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
309 .backend_impl()
310 .downcast_ref()
311 .ok_or(UnexpectedGitBackendError)
312}
313
314pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
316 get_git_backend(store).map(|backend| backend.git_repo())
317}
318
319fn resolve_git_ref_to_commit_id(
324 git_ref: &gix::Reference,
325 known_target: &RefTarget,
326) -> Option<CommitId> {
327 let mut peeling_ref = Cow::Borrowed(git_ref);
328
329 if let Some(id) = known_target.as_normal() {
331 let raw_ref = &git_ref.inner;
332 if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
333 return Some(id.clone());
334 }
335 if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {
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_tag(symbol.name, base_target, &new_remote_ref.target);
536 }
537 }
539
540 let abandoned_commits = if git_settings.abandon_unreachable_commits {
541 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
542 .map_err(GitImportError::Backend)?
543 } else {
544 vec![]
545 };
546 let stats = GitImportStats {
547 abandoned_commits,
548 changed_remote_bookmarks,
549 changed_remote_tags,
550 failed_ref_names,
551 };
552 Ok(stats)
553}
554
555fn abandon_unreachable_commits(
558 mut_repo: &mut MutableRepo,
559 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
560 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
561) -> BackendResult<Vec<CommitId>> {
562 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
563 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
564 .cloned()
565 .collect_vec();
566 if hidable_git_heads.is_empty() {
567 return Ok(vec![]);
568 }
569 let pinned_expression = RevsetExpression::union_all(&[
570 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
572 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
573 .intersection(&RevsetExpression::visible_heads().ancestors()),
575 RevsetExpression::root(),
576 ]);
577 let abandoned_expression = pinned_expression
578 .range(&RevsetExpression::commits(hidable_git_heads))
579 .intersection(&RevsetExpression::visible_heads().ancestors());
581 let abandoned_commit_ids: Vec<_> = abandoned_expression
582 .evaluate(mut_repo)
583 .map_err(|err| err.into_backend_error())?
584 .iter()
585 .try_collect()
586 .map_err(|err| err.into_backend_error())?;
587 for id in &abandoned_commit_ids {
588 let commit = mut_repo.store().get_commit(id)?;
589 mut_repo.record_abandoned_commit(&commit);
590 }
591 Ok(abandoned_commit_ids)
592}
593
594fn diff_refs_to_import(
596 view: &View,
597 git_repo: &gix::Repository,
598 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
599) -> Result<RefsToImport, GitImportError> {
600 let mut known_git_refs = view
601 .git_refs()
602 .iter()
603 .filter_map(|(full_name, target)| {
604 let (kind, symbol) =
606 parse_git_ref(full_name).expect("stored git ref should be parsable");
607 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
608 })
609 .collect();
610 let mut known_remote_bookmarks = view
612 .all_remote_bookmarks()
613 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
614 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), (&remote_ref.target, remote_ref.state)))
615 .collect();
616 let mut known_remote_tags = view
619 .tags()
620 .iter()
621 .map(|(name, target)| {
622 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
623 let state = RemoteRefState::Tracked;
624 (symbol, (target, state))
625 })
626 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
627 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
628 .collect();
629
630 let mut changed_git_refs = Vec::new();
631 let mut changed_remote_bookmarks = Vec::new();
632 let mut changed_remote_tags = Vec::new();
633 let mut failed_ref_names = Vec::new();
634 let actual = git_repo.references().map_err(GitImportError::from_git)?;
635 collect_changed_refs_to_import(
636 actual.local_branches().map_err(GitImportError::from_git)?,
637 &mut known_git_refs,
638 &mut known_remote_bookmarks,
639 &mut changed_git_refs,
640 &mut changed_remote_bookmarks,
641 &mut failed_ref_names,
642 &git_ref_filter,
643 )?;
644 collect_changed_refs_to_import(
645 actual.remote_branches().map_err(GitImportError::from_git)?,
646 &mut known_git_refs,
647 &mut known_remote_bookmarks,
648 &mut changed_git_refs,
649 &mut changed_remote_bookmarks,
650 &mut failed_ref_names,
651 &git_ref_filter,
652 )?;
653 collect_changed_refs_to_import(
654 actual.tags().map_err(GitImportError::from_git)?,
655 &mut known_git_refs,
656 &mut known_remote_tags,
657 &mut changed_git_refs,
658 &mut changed_remote_tags,
659 &mut failed_ref_names,
660 &git_ref_filter,
661 )?;
662 for full_name in known_git_refs.into_keys() {
663 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
664 }
665 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_bookmarks {
666 let old_remote_ref = RemoteRef {
667 target: old_target.clone(),
668 state: old_state,
669 };
670 changed_remote_bookmarks.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
671 }
672 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_tags {
673 let old_remote_ref = RemoteRef {
674 target: old_target.clone(),
675 state: old_state,
676 };
677 changed_remote_tags.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
678 }
679
680 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
682 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
683 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
684 failed_ref_names.sort_unstable();
685 Ok(RefsToImport {
686 changed_git_refs,
687 changed_remote_bookmarks,
688 changed_remote_tags,
689 failed_ref_names,
690 })
691}
692
693fn collect_changed_refs_to_import(
694 actual_git_refs: gix::reference::iter::Iter<'_>,
695 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
696 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, (&RefTarget, RemoteRefState)>,
697 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
698 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
699 failed_ref_names: &mut Vec<BString>,
700 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
701) -> Result<(), GitImportError> {
702 for git_ref in actual_git_refs {
703 let git_ref = git_ref.map_err(GitImportError::from_git)?;
704 let full_name_bytes = git_ref.name().as_bstr();
705 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
706 failed_ref_names.push(full_name_bytes.to_owned());
708 continue;
709 };
710 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
711 failed_ref_names.push(full_name_bytes.to_owned());
712 continue;
713 }
714 let full_name = GitRefName::new(full_name);
715 let Some((kind, symbol)) = parse_git_ref(full_name) else {
716 continue;
718 };
719 if !git_ref_filter(kind, symbol) {
720 continue;
721 }
722 let old_git_target = known_git_refs.get(full_name).copied().flatten();
723 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
724 continue;
726 };
727 let new_target = RefTarget::normal(id);
728 known_git_refs.remove(full_name);
729 if new_target != *old_git_target {
730 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
731 }
732 let (old_remote_target, old_remote_state) = known_remote_refs
735 .remove(&symbol)
736 .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));
737 if new_target != *old_remote_target {
738 let old_remote_ref = RemoteRef {
739 target: old_remote_target.clone(),
740 state: old_remote_state,
741 };
742 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref, new_target)));
743 }
744 }
745 Ok(())
746}
747
748fn default_remote_ref_state_for(
749 kind: GitRefKind,
750 symbol: RemoteRefSymbol<'_>,
751 git_settings: &GitSettings,
752) -> RemoteRefState {
753 match kind {
754 GitRefKind::Bookmark => {
755 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
756 RemoteRefState::Tracked
757 } else {
758 RemoteRefState::New
759 }
760 }
761 GitRefKind::Tag => RemoteRefState::Tracked,
762 }
763}
764
765fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
771 itertools::chain(
772 view.local_bookmarks().map(|(_, target)| target),
773 view.tags().values(),
774 )
775 .flat_map(|target| target.added_ids())
776 .cloned()
777 .collect()
778}
779
780fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
787 view.all_remote_bookmarks()
788 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
789 .map(|(_, remote_ref)| &remote_ref.target)
790 .flat_map(|target| target.added_ids())
791 .cloned()
792 .collect()
793}
794
795pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
803 let store = mut_repo.store();
804 let git_backend = get_git_backend(store)?;
805 let git_repo = git_backend.git_repo();
806
807 let old_git_head = mut_repo.view().git_head();
808 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
809 Some(CommitId::from_bytes(oid.as_bytes()))
810 } else {
811 None
812 };
813 if old_git_head.as_resolved() == Some(&new_git_head_id) {
814 return Ok(());
815 }
816
817 if let Some(head_id) = &new_git_head_id {
819 let index = mut_repo.index();
820 if !index.has_id(head_id) {
821 git_backend.import_head_commits([head_id]).map_err(|err| {
822 GitImportError::MissingHeadTarget {
823 id: head_id.clone(),
824 err,
825 }
826 })?;
827 }
828 store
831 .get_commit(head_id)
832 .and_then(|commit| mut_repo.add_head(&commit))
833 .map_err(GitImportError::Backend)?;
834 }
835
836 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
837 Ok(())
838}
839
840#[derive(Error, Debug)]
841pub enum GitExportError {
842 #[error(transparent)]
843 Git(Box<dyn std::error::Error + Send + Sync>),
844 #[error(transparent)]
845 UnexpectedBackend(#[from] UnexpectedGitBackendError),
846}
847
848impl GitExportError {
849 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
850 Self::Git(source.into())
851 }
852}
853
854#[derive(Debug, Error)]
856pub enum FailedRefExportReason {
857 #[error("Name is not allowed in Git")]
859 InvalidGitName,
860 #[error("Ref was in a conflicted state from the last import")]
863 ConflictedOldState,
864 #[error("Ref cannot point to the root commit in Git")]
866 OnRootCommit,
867 #[error("Deleted ref had been modified in Git")]
869 DeletedInJjModifiedInGit,
870 #[error("Added ref had been added with a different target in Git")]
872 AddedInJjAddedInGit,
873 #[error("Modified ref had been deleted in Git")]
875 ModifiedInJjDeletedInGit,
876 #[error("Failed to delete")]
878 FailedToDelete(#[source] Box<gix::reference::edit::Error>),
879 #[error("Failed to set")]
881 FailedToSet(#[source] Box<gix::reference::edit::Error>),
882}
883
884#[derive(Debug)]
886pub struct GitExportStats {
887 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
889}
890
891#[derive(Debug)]
892struct RefsToExport {
893 bookmarks_to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
896 bookmarks_to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
901 failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
903}
904
905pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
916 export_some_refs(mut_repo, |_, _| true)
917}
918
919pub fn export_some_refs(
920 mut_repo: &mut MutableRepo,
921 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
922) -> Result<GitExportStats, GitExportError> {
923 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
924 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
925 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
926 let (_, value) = &map[index];
927 Some(value)
928 }
929
930 let git_repo = get_git_repo(mut_repo.store())?;
931
932 let RefsToExport {
933 bookmarks_to_update,
934 bookmarks_to_delete,
935 mut failed_bookmarks,
936 } = diff_refs_to_export(
937 mut_repo.view(),
938 mut_repo.store().root_commit_id(),
939 &git_ref_filter,
940 );
941
942 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
944 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
945 if let Some((GitRefKind::Bookmark, symbol)) = target_name
946 .as_ref()
947 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
948 .and_then(|name| parse_git_ref(name.as_ref()))
949 {
950 let old_target = head_ref.inner.target.clone();
951 let current_oid = match head_ref.into_fully_peeled_id() {
952 Ok(id) => Some(id.detach()),
953 Err(gix::reference::peel::Error::ToId(
954 gix::refs::peel::to_id::Error::FollowToObject(
955 gix::refs::peel::to_object::Error::Follow(
956 gix::refs::file::find::existing::Error::NotFound { .. },
957 ),
958 ),
959 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
961 };
962 let new_oid = if let Some((_old_oid, new_oid)) = get(&bookmarks_to_update, symbol) {
963 Some(new_oid)
964 } else if get(&bookmarks_to_delete, symbol).is_some() {
965 None
966 } else {
967 current_oid.as_ref()
968 };
969 if new_oid != current_oid.as_ref() {
970 update_git_head(
971 &git_repo,
972 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
973 current_oid,
974 )
975 .map_err(GitExportError::from_git)?;
976 }
977 }
978 }
979 for (symbol, old_oid) in bookmarks_to_delete {
980 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
981 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
982 continue;
983 };
984 if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) {
985 failed_bookmarks.push((symbol, reason));
986 } else {
987 let new_target = RefTarget::absent();
988 mut_repo.set_git_ref_target(&git_ref_name, new_target);
989 }
990 }
991 for (symbol, (old_oid, new_oid)) in bookmarks_to_update {
992 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
993 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
994 continue;
995 };
996 if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) {
997 failed_bookmarks.push((symbol, reason));
998 } else {
999 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
1000 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1001 }
1002 }
1003
1004 failed_bookmarks.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1006
1007 copy_exportable_local_bookmarks_to_remote_view(
1008 mut_repo,
1009 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1010 |name| {
1011 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1012 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1013 },
1014 );
1015
1016 Ok(GitExportStats { failed_bookmarks })
1017}
1018
1019fn copy_exportable_local_bookmarks_to_remote_view(
1020 mut_repo: &mut MutableRepo,
1021 remote: &RemoteName,
1022 name_filter: impl Fn(&RefName) -> bool,
1023) {
1024 let new_local_bookmarks = mut_repo
1025 .view()
1026 .local_remote_bookmarks(remote)
1027 .filter_map(|(name, targets)| {
1028 let old_target = &targets.remote_ref.target;
1031 let new_target = targets.local_target;
1032 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1033 })
1034 .filter(|&(name, _)| name_filter(name))
1035 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1036 .collect_vec();
1037 for (name, new_target) in new_local_bookmarks {
1038 let new_remote_ref = RemoteRef {
1039 target: new_target,
1040 state: RemoteRefState::Tracked,
1041 };
1042 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1043 }
1044}
1045
1046fn diff_refs_to_export(
1048 view: &View,
1049 root_commit_id: &CommitId,
1050 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1051) -> RefsToExport {
1052 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1055 itertools::chain(
1056 view.local_bookmarks().map(|(name, target)| {
1057 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1058 (symbol, target)
1059 }),
1060 view.all_remote_bookmarks()
1061 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1062 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1063 )
1064 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1065 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1066 .collect();
1067 let known_git_refs = view
1068 .git_refs()
1069 .iter()
1070 .map(|(full_name, target)| {
1071 let (kind, symbol) =
1072 parse_git_ref(full_name).expect("stored git ref should be parsable");
1073 ((kind, symbol), target)
1074 })
1075 .filter(|&((kind, symbol), _)| {
1076 kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol)
1080 });
1081 for ((_kind, symbol), target) in known_git_refs {
1082 all_bookmark_targets
1083 .entry(symbol)
1084 .and_modify(|(old_target, _)| *old_target = target)
1085 .or_insert((target, RefTarget::absent_ref()));
1086 }
1087
1088 let mut bookmarks_to_update = Vec::new();
1089 let mut bookmarks_to_delete = Vec::new();
1090 let mut failed_bookmarks = Vec::new();
1091 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1092 for (symbol, (old_target, new_target)) in all_bookmark_targets {
1093 if new_target == old_target {
1094 continue;
1095 }
1096 if *new_target == root_commit_target {
1097 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1099 continue;
1100 }
1101 let old_oid = if let Some(id) = old_target.as_normal() {
1102 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1103 } else if old_target.has_conflict() {
1104 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1107 continue;
1108 } else {
1109 assert!(old_target.is_absent());
1110 None
1111 };
1112 if let Some(id) = new_target.as_normal() {
1113 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1114 bookmarks_to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1115 } else if new_target.has_conflict() {
1116 continue;
1118 } else {
1119 assert!(new_target.is_absent());
1120 bookmarks_to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1121 }
1122 }
1123
1124 bookmarks_to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1126 bookmarks_to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1127 failed_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1128 RefsToExport {
1129 bookmarks_to_update,
1130 bookmarks_to_delete,
1131 failed_bookmarks,
1132 }
1133}
1134
1135fn delete_git_ref(
1136 git_repo: &gix::Repository,
1137 git_ref_name: &GitRefName,
1138 old_oid: &gix::oid,
1139) -> Result<(), FailedRefExportReason> {
1140 if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1141 if git_ref.inner.target.try_id() == Some(old_oid) {
1142 git_ref
1144 .delete()
1145 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1146 } else {
1147 return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1149 }
1150 } else {
1151 }
1153 Ok(())
1154}
1155
1156fn update_git_ref(
1157 git_repo: &gix::Repository,
1158 git_ref_name: &GitRefName,
1159 old_oid: Option<gix::ObjectId>,
1160 new_oid: gix::ObjectId,
1161) -> Result<(), FailedRefExportReason> {
1162 match old_oid {
1163 None => {
1164 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1165 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1168 return Err(FailedRefExportReason::AddedInJjAddedInGit);
1169 }
1170 } else {
1171 git_repo
1173 .reference(
1174 git_ref_name.as_str(),
1175 new_oid,
1176 gix::refs::transaction::PreviousValue::MustNotExist,
1177 "export from jj",
1178 )
1179 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1180 }
1181 }
1182 Some(old_oid) => {
1183 if let Err(err) = git_repo.reference(
1185 git_ref_name.as_str(),
1186 new_oid,
1187 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1188 "export from jj",
1189 ) {
1190 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1192 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1194 return Err(FailedRefExportReason::FailedToSet(err.into()));
1195 }
1196 } else {
1197 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1199 }
1200 } else {
1201 }
1204 }
1205 }
1206 Ok(())
1207}
1208
1209fn update_git_head(
1212 git_repo: &gix::Repository,
1213 expected_ref: gix::refs::transaction::PreviousValue,
1214 new_oid: Option<gix::ObjectId>,
1215) -> Result<(), gix::reference::edit::Error> {
1216 let mut ref_edits = Vec::new();
1217 let new_target = if let Some(oid) = new_oid {
1218 gix::refs::Target::Object(oid)
1219 } else {
1220 ref_edits.push(gix::refs::transaction::RefEdit {
1225 change: gix::refs::transaction::Change::Delete {
1226 expected: gix::refs::transaction::PreviousValue::Any,
1227 log: gix::refs::transaction::RefLog::AndReference,
1228 },
1229 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1230 deref: false,
1231 });
1232 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1233 };
1234 ref_edits.push(gix::refs::transaction::RefEdit {
1235 change: gix::refs::transaction::Change::Update {
1236 log: gix::refs::transaction::LogChange {
1237 message: "export from jj".into(),
1238 ..Default::default()
1239 },
1240 expected: expected_ref,
1241 new: new_target,
1242 },
1243 name: "HEAD".try_into().unwrap(),
1244 deref: false,
1245 });
1246 git_repo.edit_references(ref_edits)?;
1247 Ok(())
1248}
1249
1250#[derive(Debug, Error)]
1251pub enum GitResetHeadError {
1252 #[error(transparent)]
1253 Backend(#[from] BackendError),
1254 #[error(transparent)]
1255 Git(Box<dyn std::error::Error + Send + Sync>),
1256 #[error("Failed to update Git HEAD ref")]
1257 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1258 #[error(transparent)]
1259 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1260}
1261
1262impl GitResetHeadError {
1263 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1264 Self::Git(source.into())
1265 }
1266}
1267
1268pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1271 let git_repo = get_git_repo(mut_repo.store())?;
1272
1273 let first_parent_id = &wc_commit.parent_ids()[0];
1274 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1275 RefTarget::normal(first_parent_id.clone())
1276 } else {
1277 RefTarget::absent()
1278 };
1279
1280 let old_head_target = mut_repo.git_head();
1282 if old_head_target != new_head_target {
1283 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1284 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1287 if actual_head.is_detached() {
1288 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1289 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1290 } else {
1291 gix::refs::transaction::PreviousValue::MustExist
1294 }
1295 } else {
1296 gix::refs::transaction::PreviousValue::MustExist
1298 };
1299 let new_oid = new_head_target
1300 .as_normal()
1301 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1302 update_git_head(&git_repo, expected_ref, new_oid)
1303 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1304 mut_repo.set_git_head_target(new_head_target);
1305 }
1306
1307 if git_repo.state().is_some() {
1312 const STATE_FILE_NAMES: &[&str] = &[
1316 "MERGE_HEAD",
1317 "MERGE_MODE",
1318 "MERGE_MSG",
1319 "REVERT_HEAD",
1320 "CHERRY_PICK_HEAD",
1321 "BISECT_LOG",
1322 ];
1323 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1324 let handle_err = |err: PathError| match err.source.kind() {
1325 std::io::ErrorKind::NotFound => Ok(()),
1326 _ => Err(GitResetHeadError::from_git(err)),
1327 };
1328 for file_name in STATE_FILE_NAMES {
1329 let path = git_repo.path().join(file_name);
1330 std::fs::remove_file(&path)
1331 .context(&path)
1332 .or_else(handle_err)?;
1333 }
1334 for dir_name in STATE_DIR_NAMES {
1335 let path = git_repo.path().join(dir_name);
1336 std::fs::remove_dir_all(&path)
1337 .context(&path)
1338 .or_else(handle_err)?;
1339 }
1340 }
1341
1342 let parent_tree = wc_commit.parent_tree(mut_repo)?;
1343
1344 let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
1348 if tree.id() == mut_repo.store().empty_tree_id() {
1349 gix::index::File::from_state(
1353 gix::index::State::new(git_repo.object_hash()),
1354 git_repo.index_path(),
1355 )
1356 } else {
1357 git_repo
1360 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes()))
1361 .map_err(GitResetHeadError::from_git)?
1362 }
1363 } else {
1364 build_index_from_merged_tree(&git_repo, parent_tree.clone())?
1365 };
1366
1367 let wc_tree = wc_commit.tree()?;
1368 update_intent_to_add_impl(&git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1369
1370 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1373 index
1374 .entries_mut_with_paths()
1375 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1376 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1377 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1378 })
1379 .filter_map(|merged| merged.both())
1380 .map(|((entry, _), old_entry)| (entry, old_entry))
1381 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1382 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1383 }
1384
1385 debug_assert!(index.verify_entries().is_ok());
1386
1387 index
1388 .write(gix::index::write::Options::default())
1389 .map_err(GitResetHeadError::from_git)?;
1390
1391 Ok(())
1392}
1393
1394fn build_index_from_merged_tree(
1395 git_repo: &gix::Repository,
1396 merged_tree: MergedTree,
1397) -> Result<gix::index::File, GitResetHeadError> {
1398 let mut index = gix::index::File::from_state(
1399 gix::index::State::new(git_repo.object_hash()),
1400 git_repo.index_path(),
1401 );
1402
1403 let mut push_index_entry =
1404 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1405 let Some(entry) = maybe_entry else {
1406 return;
1407 };
1408
1409 let (id, mode) = match entry {
1410 TreeValue::File {
1411 id,
1412 executable,
1413 copy_id: _,
1414 } => {
1415 if *executable {
1416 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1417 } else {
1418 (id.as_bytes(), gix::index::entry::Mode::FILE)
1419 }
1420 }
1421 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1422 TreeValue::Tree(_) => {
1423 return;
1428 }
1429 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1430 TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"),
1431 };
1432
1433 let path = BStr::new(path.as_internal_file_string());
1434
1435 index.dangerously_push_entry(
1438 gix::index::entry::Stat::default(),
1439 gix::ObjectId::from_bytes_or_panic(id),
1440 gix::index::entry::Flags::from_stage(stage),
1441 mode,
1442 path,
1443 );
1444 };
1445
1446 let mut has_many_sided_conflict = false;
1447
1448 for (path, entry) in merged_tree.entries() {
1449 let entry = entry?;
1450 if let Some(resolved) = entry.as_resolved() {
1451 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1452 continue;
1453 }
1454
1455 let conflict = entry.simplify();
1456 if let [left, base, right] = conflict.as_slice() {
1457 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1459 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1460 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1461 } else {
1462 has_many_sided_conflict = true;
1470 push_index_entry(
1471 &path,
1472 conflict.first(),
1473 gix::index::entry::Stage::Unconflicted,
1474 );
1475 }
1476 }
1477
1478 index.sort_entries();
1481
1482 if has_many_sided_conflict
1485 && index
1486 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1487 .is_err()
1488 {
1489 let file_blob = git_repo
1490 .write_blob(
1491 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1492 )
1493 .map_err(GitResetHeadError::from_git)?;
1494 index.dangerously_push_entry(
1495 gix::index::entry::Stat::default(),
1496 file_blob.detach(),
1497 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1498 gix::index::entry::Mode::FILE,
1499 INDEX_DUMMY_CONFLICT_FILE.into(),
1500 );
1501 index.sort_entries();
1504 }
1505
1506 Ok(index)
1507}
1508
1509pub fn update_intent_to_add(
1516 repo: &dyn Repo,
1517 old_tree: &MergedTree,
1518 new_tree: &MergedTree,
1519) -> Result<(), GitResetHeadError> {
1520 let git_repo = get_git_repo(repo.store())?;
1521 let mut index = git_repo
1522 .index_or_empty()
1523 .map_err(GitResetHeadError::from_git)?;
1524 let mut_index = Arc::make_mut(&mut index);
1525 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1526 debug_assert!(mut_index.verify_entries().is_ok());
1527 mut_index
1528 .write(gix::index::write::Options::default())
1529 .map_err(GitResetHeadError::from_git)?;
1530
1531 Ok(())
1532}
1533
1534async fn update_intent_to_add_impl(
1535 git_repo: &gix::Repository,
1536 index: &mut gix::index::File,
1537 old_tree: &MergedTree,
1538 new_tree: &MergedTree,
1539) -> Result<(), GitResetHeadError> {
1540 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1541 let mut added_paths = vec![];
1542 let mut removed_paths = HashSet::new();
1543 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1544 let (before, after) = values?;
1545 if before.is_absent() {
1546 let executable = match after.as_normal() {
1547 Some(TreeValue::File {
1548 id: _,
1549 executable,
1550 copy_id: _,
1551 }) => *executable,
1552 Some(TreeValue::Symlink(_)) => false,
1553 _ => {
1554 continue;
1555 }
1556 };
1557 if index
1558 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1559 .is_err()
1560 {
1561 added_paths.push((BString::from(path.into_internal_string()), executable));
1562 }
1563 } else if after.is_absent() {
1564 removed_paths.insert(BString::from(path.into_internal_string()));
1565 }
1566 }
1567
1568 if added_paths.is_empty() && removed_paths.is_empty() {
1569 return Ok(());
1570 }
1571
1572 if !added_paths.is_empty() {
1573 let empty_blob = git_repo
1575 .write_blob(b"")
1576 .map_err(GitResetHeadError::from_git)?
1577 .detach();
1578 for (path, executable) in added_paths {
1579 index.dangerously_push_entry(
1581 gix::index::entry::Stat::default(),
1582 empty_blob,
1583 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1584 if executable {
1585 gix::index::entry::Mode::FILE_EXECUTABLE
1586 } else {
1587 gix::index::entry::Mode::FILE
1588 },
1589 path.as_ref(),
1590 );
1591 }
1592 }
1593 if !removed_paths.is_empty() {
1594 index.remove_entries(|_size, path, entry| {
1595 entry
1596 .flags
1597 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1598 && removed_paths.contains(path)
1599 });
1600 }
1601
1602 index.sort_entries();
1603
1604 Ok(())
1605}
1606
1607#[derive(Debug, Error)]
1608pub enum GitRemoteManagementError {
1609 #[error("No git remote named '{}'", .0.as_symbol())]
1610 NoSuchRemote(RemoteNameBuf),
1611 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1612 RemoteAlreadyExists(RemoteNameBuf),
1613 #[error(transparent)]
1614 RemoteName(#[from] GitRemoteNameError),
1615 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1616 NonstandardConfiguration(RemoteNameBuf),
1617 #[error("Error saving Git configuration")]
1618 GitConfigSaveError(#[source] std::io::Error),
1619 #[error("Unexpected Git error when managing remotes")]
1620 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1621 #[error(transparent)]
1622 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1623}
1624
1625impl GitRemoteManagementError {
1626 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1627 Self::InternalGitError(source.into())
1628 }
1629}
1630
1631pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1636 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1637}
1638
1639fn default_fetch_refspec(remote: &RemoteName) -> String {
1640 format!(
1641 "+refs/heads/*:refs/remotes/{remote}/*",
1642 remote = remote.as_str()
1643 )
1644}
1645
1646fn add_ref(
1647 name: gix::refs::FullName,
1648 target: gix::refs::Target,
1649 message: BString,
1650) -> gix::refs::transaction::RefEdit {
1651 gix::refs::transaction::RefEdit {
1652 change: gix::refs::transaction::Change::Update {
1653 log: gix::refs::transaction::LogChange {
1654 mode: gix::refs::transaction::RefLog::AndReference,
1655 force_create_reflog: false,
1656 message,
1657 },
1658 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1659 new: target,
1660 },
1661 name,
1662 deref: false,
1663 }
1664}
1665
1666fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1667 gix::refs::transaction::RefEdit {
1668 change: gix::refs::transaction::Change::Delete {
1669 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1670 reference.target().into_owned(),
1671 ),
1672 log: gix::refs::transaction::RefLog::AndReference,
1673 },
1674 name: reference.name().to_owned(),
1675 deref: false,
1676 }
1677}
1678
1679fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1685 let mut config_file = File::create(
1686 config
1687 .meta()
1688 .path
1689 .as_ref()
1690 .expect("Git repository to have a config file"),
1691 )?;
1692 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1693}
1694
1695fn save_remote(
1696 config: &mut gix::config::File<'static>,
1697 remote_name: &RemoteName,
1698 remote: &mut gix::Remote,
1699) -> Result<(), GitRemoteManagementError> {
1700 config
1707 .new_section(
1708 "remote",
1709 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1710 )
1711 .map_err(GitRemoteManagementError::from_git)?;
1712 remote
1713 .save_as_to(remote_name.as_str(), config)
1714 .map_err(GitRemoteManagementError::from_git)?;
1715 Ok(())
1716}
1717
1718fn git_config_branch_section_ids_by_remote(
1719 config: &gix::config::File,
1720 remote_name: &RemoteName,
1721) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1722 config
1723 .sections_by_name("branch")
1724 .into_iter()
1725 .flatten()
1726 .filter_map(|section| {
1727 let remote_values = section.values("remote");
1728 let push_remote_values = section.values("pushRemote");
1729 if !remote_values
1730 .iter()
1731 .chain(push_remote_values.iter())
1732 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1733 {
1734 return None;
1735 }
1736 if remote_values.len() > 1
1737 || push_remote_values.len() > 1
1738 || section.value_names().any(|name| {
1739 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1740 })
1741 {
1742 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1743 remote_name.to_owned(),
1744 )));
1745 }
1746 Some(Ok(section.id()))
1747 })
1748 .collect()
1749}
1750
1751fn rename_remote_in_git_branch_config_sections(
1752 config: &mut gix::config::File,
1753 old_remote_name: &RemoteName,
1754 new_remote_name: &RemoteName,
1755) -> Result<(), GitRemoteManagementError> {
1756 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1757 config
1758 .section_mut_by_id(id)
1759 .expect("found section to exist")
1760 .set(
1761 "remote"
1762 .try_into()
1763 .expect("'remote' to be a valid value name"),
1764 BStr::new(new_remote_name.as_str()),
1765 );
1766 }
1767 Ok(())
1768}
1769
1770fn remove_remote_git_branch_config_sections(
1771 config: &mut gix::config::File,
1772 remote_name: &RemoteName,
1773) -> Result<(), GitRemoteManagementError> {
1774 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1775 config
1776 .remove_section_by_id(id)
1777 .expect("removed section to exist");
1778 }
1779 Ok(())
1780}
1781
1782fn remove_remote_git_config_sections(
1783 config: &mut gix::config::File,
1784 remote_name: &RemoteName,
1785) -> Result<(), GitRemoteManagementError> {
1786 let section_ids_to_remove: Vec<_> = config
1787 .sections_by_name("remote")
1788 .into_iter()
1789 .flatten()
1790 .filter(|section| {
1791 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1792 })
1793 .map(|section| {
1794 if section.value_names().any(|name| {
1795 !name.eq_ignore_ascii_case(b"url")
1796 && !name.eq_ignore_ascii_case(b"fetch")
1797 && !name.eq_ignore_ascii_case(b"tagOpt")
1798 }) {
1799 return Err(GitRemoteManagementError::NonstandardConfiguration(
1800 remote_name.to_owned(),
1801 ));
1802 }
1803 Ok(section.id())
1804 })
1805 .try_collect()?;
1806 for id in section_ids_to_remove {
1807 config
1808 .remove_section_by_id(id)
1809 .expect("removed section to exist");
1810 }
1811 Ok(())
1812}
1813
1814pub fn get_all_remote_names(
1816 store: &Store,
1817) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1818 let git_repo = get_git_repo(store)?;
1819 let names = git_repo
1820 .remote_names()
1821 .into_iter()
1822 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1824 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1826 .map(RemoteNameBuf::from)
1827 .collect();
1828 Ok(names)
1829}
1830
1831pub fn add_remote(
1832 store: &Store,
1833 remote_name: &RemoteName,
1834 url: &str,
1835 fetch_tags: gix::remote::fetch::Tags,
1836) -> Result<(), GitRemoteManagementError> {
1837 let git_repo = get_git_repo(store)?;
1838
1839 validate_remote_name(remote_name)?;
1840
1841 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1842 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1843 remote_name.to_owned(),
1844 ));
1845 }
1846
1847 let mut remote = git_repo
1848 .remote_at(url)
1849 .map_err(GitRemoteManagementError::from_git)?
1850 .with_fetch_tags(fetch_tags)
1851 .with_refspecs(
1852 [default_fetch_refspec(remote_name).as_bytes()],
1853 gix::remote::Direction::Fetch,
1854 )
1855 .expect("default refspec to be valid");
1856
1857 let mut config = git_repo.config_snapshot().clone();
1858 save_remote(&mut config, remote_name, &mut remote)?;
1859 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1860
1861 Ok(())
1862}
1863
1864pub fn remove_remote(
1865 mut_repo: &mut MutableRepo,
1866 remote_name: &RemoteName,
1867) -> Result<(), GitRemoteManagementError> {
1868 let mut git_repo = get_git_repo(mut_repo.store())?;
1869
1870 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1871 return Err(GitRemoteManagementError::NoSuchRemote(
1872 remote_name.to_owned(),
1873 ));
1874 };
1875
1876 let mut config = git_repo.config_snapshot().clone();
1877 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1878 remove_remote_git_config_sections(&mut config, remote_name)?;
1879 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1880
1881 remove_remote_git_refs(&mut git_repo, remote_name)
1882 .map_err(GitRemoteManagementError::from_git)?;
1883
1884 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1885 remove_remote_refs(mut_repo, remote_name);
1886 }
1887
1888 Ok(())
1889}
1890
1891fn remove_remote_git_refs(
1892 git_repo: &mut gix::Repository,
1893 remote: &RemoteName,
1894) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1895 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1896 let edits: Vec<_> = git_repo
1897 .references()?
1898 .prefixed(prefix.as_str())?
1899 .map_ok(remove_ref)
1900 .try_collect()?;
1901 git_repo.edit_references(edits)?;
1902 Ok(())
1903}
1904
1905fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1906 mut_repo.remove_remote(remote);
1907 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1908 let git_refs_to_delete = mut_repo
1909 .view()
1910 .git_refs()
1911 .keys()
1912 .filter(|&r| r.as_str().starts_with(&prefix))
1913 .cloned()
1914 .collect_vec();
1915 for git_ref in git_refs_to_delete {
1916 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1917 }
1918}
1919
1920pub fn rename_remote(
1921 mut_repo: &mut MutableRepo,
1922 old_remote_name: &RemoteName,
1923 new_remote_name: &RemoteName,
1924) -> Result<(), GitRemoteManagementError> {
1925 let mut git_repo = get_git_repo(mut_repo.store())?;
1926
1927 validate_remote_name(new_remote_name)?;
1928
1929 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1930 return Err(GitRemoteManagementError::NoSuchRemote(
1931 old_remote_name.to_owned(),
1932 ));
1933 };
1934 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1935
1936 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1937 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1938 new_remote_name.to_owned(),
1939 ));
1940 }
1941
1942 match (
1943 remote.refspecs(gix::remote::Direction::Fetch),
1944 remote.refspecs(gix::remote::Direction::Push),
1945 ) {
1946 ([refspec], [])
1947 if refspec.to_ref().to_bstring()
1948 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1949 _ => {
1950 return Err(GitRemoteManagementError::NonstandardConfiguration(
1951 old_remote_name.to_owned(),
1952 ));
1953 }
1954 }
1955
1956 remote
1957 .replace_refspecs(
1958 [default_fetch_refspec(new_remote_name).as_bytes()],
1959 gix::remote::Direction::Fetch,
1960 )
1961 .expect("default refspec to be valid");
1962
1963 let mut config = git_repo.config_snapshot().clone();
1964 save_remote(&mut config, new_remote_name, &mut remote)?;
1965 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1966 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1967 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1968
1969 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1970 .map_err(GitRemoteManagementError::from_git)?;
1971
1972 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1973 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1974 }
1975
1976 Ok(())
1977}
1978
1979fn rename_remote_git_refs(
1980 git_repo: &mut gix::Repository,
1981 old_remote_name: &RemoteName,
1982 new_remote_name: &RemoteName,
1983) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1984 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1985 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1986 let ref_log_message = BString::from(format!(
1987 "renamed remote {old_remote_name} to {new_remote_name}",
1988 old_remote_name = old_remote_name.as_symbol(),
1989 new_remote_name = new_remote_name.as_symbol(),
1990 ));
1991
1992 let edits: Vec<_> = git_repo
1993 .references()?
1994 .prefixed(old_prefix.as_str())?
1995 .map_ok(|old_ref| {
1996 let new_name = BString::new(
1997 [
1998 new_prefix.as_bytes(),
1999 &old_ref.name().as_bstr()[old_prefix.len()..],
2000 ]
2001 .concat(),
2002 );
2003 [
2004 add_ref(
2005 new_name.try_into().expect("new ref name to be valid"),
2006 old_ref.target().into_owned(),
2007 ref_log_message.clone(),
2008 ),
2009 remove_ref(old_ref),
2010 ]
2011 })
2012 .flatten_ok()
2013 .try_collect()?;
2014 git_repo.edit_references(edits)?;
2015 Ok(())
2016}
2017
2018fn gix_remote_with_fetch_url<Url, E>(
2024 remote: gix::Remote,
2025 url: Url,
2026) -> Result<gix::Remote, gix::remote::init::Error>
2027where
2028 Url: TryInto<gix::Url, Error = E>,
2029 gix::url::parse::Error: From<E>,
2030{
2031 let mut new_remote = remote.repo().remote_at(url)?;
2032 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2038 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2039 new_remote
2040 .replace_refspecs(
2041 remote
2042 .refspecs(direction)
2043 .iter()
2044 .map(|refspec| refspec.to_ref().to_bstring()),
2045 direction,
2046 )
2047 .expect("existing refspecs to be valid");
2048 }
2049 Ok(new_remote)
2050}
2051
2052pub fn set_remote_url(
2053 store: &Store,
2054 remote_name: &RemoteName,
2055 new_remote_url: &str,
2056) -> Result<(), GitRemoteManagementError> {
2057 let git_repo = get_git_repo(store)?;
2058
2059 validate_remote_name(remote_name)?;
2060
2061 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2062 return Err(GitRemoteManagementError::NoSuchRemote(
2063 remote_name.to_owned(),
2064 ));
2065 };
2066 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2067
2068 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2069 return Err(GitRemoteManagementError::NonstandardConfiguration(
2070 remote_name.to_owned(),
2071 ));
2072 }
2073
2074 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2075 .map_err(GitRemoteManagementError::from_git)?;
2076
2077 let mut config = git_repo.config_snapshot().clone();
2078 save_remote(&mut config, remote_name, &mut remote)?;
2079 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2080
2081 Ok(())
2082}
2083
2084fn rename_remote_refs(
2085 mut_repo: &mut MutableRepo,
2086 old_remote_name: &RemoteName,
2087 new_remote_name: &RemoteName,
2088) {
2089 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2090 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2091 let git_refs = mut_repo
2092 .view()
2093 .git_refs()
2094 .iter()
2095 .filter_map(|(old, target)| {
2096 old.as_str().strip_prefix(&prefix).map(|p| {
2097 let new: GitRefNameBuf =
2098 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2099 (old.clone(), new, target.clone())
2100 })
2101 })
2102 .collect_vec();
2103 for (old, new, target) in git_refs {
2104 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2105 mut_repo.set_git_ref_target(&new, target);
2106 }
2107}
2108
2109const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2110
2111#[derive(Error, Debug)]
2112pub enum GitFetchError {
2113 #[error("No git remote named '{}'", .0.as_symbol())]
2114 NoSuchRemote(RemoteNameBuf),
2115 #[error(
2116 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2117 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2118 )]
2119 InvalidBranchPattern(StringPattern),
2120 #[error(transparent)]
2121 RemoteName(#[from] GitRemoteNameError),
2122 #[error(transparent)]
2123 Subprocess(#[from] GitSubprocessError),
2124}
2125
2126#[derive(Error, Debug)]
2127pub enum GitDefaultRefspecError {
2128 #[error("No git remote named '{}'", .0.as_symbol())]
2129 NoSuchRemote(RemoteNameBuf),
2130 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2131 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2132}
2133
2134struct FetchedBranches {
2135 remote: RemoteNameBuf,
2136 branches: Vec<StringPattern>,
2137}
2138
2139#[derive(Debug)]
2141pub struct ExpandedFetchRefSpecs {
2142 expected_branch_names: Vec<StringPattern>,
2146 refspecs: Vec<RefSpec>,
2147 negative_refspecs: Vec<NegativeRefSpec>,
2148}
2149
2150pub fn expand_fetch_refspecs(
2152 remote: &RemoteName,
2153 branch_names: Vec<StringPattern>,
2154) -> Result<ExpandedFetchRefSpecs, GitFetchError> {
2155 let refspecs = branch_names
2156 .iter()
2157 .map(|pattern| {
2158 pattern
2159 .to_glob()
2160 .filter(
2161 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2164 )
2165 .map(|glob| {
2166 RefSpec::forced(
2167 format!("refs/heads/{glob}"),
2168 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2169 )
2170 })
2171 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2172 })
2173 .try_collect()?;
2174
2175 Ok(ExpandedFetchRefSpecs {
2176 expected_branch_names: branch_names,
2177 refspecs,
2178 negative_refspecs: Vec::new(),
2179 })
2180}
2181
2182#[derive(Debug)]
2186#[must_use = "warnings should be surfaced in the UI"]
2187pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2188
2189#[derive(Debug)]
2192pub struct IgnoredRefspec {
2193 pub refspec: BString,
2195 pub reason: &'static str,
2197}
2198
2199pub fn expand_default_fetch_refspecs(
2201 remote: &RemoteName,
2202 git_repo: &gix::Repository,
2203) -> Result<(IgnoredRefspecs, ExpandedFetchRefSpecs), GitDefaultRefspecError> {
2204 let remote_name = remote.as_str();
2205 let remote = git_repo
2206 .try_find_remote(remote_name)
2207 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote.to_owned()))?
2208 .map_err(|e| {
2209 GitDefaultRefspecError::InvalidRemoteConfiguration(remote.to_owned(), Box::new(e))
2210 })?;
2211
2212 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2213
2214 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2215 let mut expected_branch_names = Vec::with_capacity(remote_refspecs.len());
2216 let mut negative_refspecs = Vec::new();
2217
2218 let refspecs = remote_refspecs
2219 .iter()
2220 .filter_map(|refspec| {
2221 let forced = refspec.allow_non_fast_forward();
2222 let refspec = refspec.to_ref();
2223
2224 let mut ensure_utf8 = |s| match str::from_utf8(s) {
2225 Ok(s) => Some(s),
2226 Err(_) => {
2227 ignored_refspecs.push(IgnoredRefspec {
2228 refspec: refspec.to_bstring(),
2229 reason: "invalid UTF-8",
2230 });
2231 None
2232 }
2233 };
2234
2235 let (src, dst) = match refspec.instruction() {
2236 Instruction::Push(_) => unreachable!(),
2238 Instruction::Fetch(fetch) => match fetch {
2239 gix::refspec::instruction::Fetch::Only { src: _ } => {
2240 ignored_refspecs.push(IgnoredRefspec {
2241 refspec: refspec.to_bstring(),
2242 reason: "fetch-only refspecs are not supported",
2243 });
2244 return None;
2245 }
2246
2247 gix::refspec::instruction::Fetch::AndUpdate {
2248 src,
2249 dst,
2250 allow_non_fast_forward: _, } => (ensure_utf8(src)?, ensure_utf8(dst)?),
2252
2253 gix::refspec::instruction::Fetch::Exclude { src } => {
2254 let src = ensure_utf8(src)?;
2255 negative_refspecs.push(NegativeRefSpec::new(src));
2256 return None;
2257 }
2258 },
2259 };
2260
2261 if !forced {
2262 ignored_refspecs.push(IgnoredRefspec {
2263 refspec: refspec.to_bstring(),
2264 reason: "non-forced refspecs are not supported",
2265 });
2266 return None;
2267 }
2268
2269 let Some(src_branch) = src.strip_prefix("refs/heads/") else {
2270 ignored_refspecs.push(IgnoredRefspec {
2271 refspec: refspec.to_bstring(),
2272 reason: "only refs/heads/ is supported for refspec sources",
2273 });
2274 return None;
2275 };
2276
2277 let dst = {
2278 let Some(dst_without_prefix) = dst.strip_prefix("refs/remotes/") else {
2279 ignored_refspecs.push(IgnoredRefspec {
2280 refspec: refspec.to_bstring(),
2281 reason: "only refs/remotes/ is supported for fetch destinations",
2282 });
2283 return None;
2284 };
2285
2286 let Some(dst_branch) = dst_without_prefix
2287 .strip_prefix(remote_name)
2288 .and_then(|d| d.strip_prefix("/"))
2289 else {
2290 ignored_refspecs.push(IgnoredRefspec {
2291 refspec: refspec.to_bstring(),
2292 reason: "remote renaming not supported",
2293 });
2294 return None;
2295 };
2296
2297 if src_branch == dst_branch {
2298 dst.to_owned()
2299 } else {
2300 ignored_refspecs.push(IgnoredRefspec {
2301 refspec: refspec.to_bstring(),
2302 reason: "renaming is not supported",
2303 });
2304 return None;
2305 }
2306 };
2307
2308 let Ok(branch) = StringPattern::glob(src_branch) else {
2310 ignored_refspecs.push(IgnoredRefspec {
2311 refspec: refspec.to_bstring(),
2312 reason: "invalid pattern",
2313 });
2314 return None;
2315 };
2316 expected_branch_names.push(branch);
2317
2318 Some(RefSpec::forced(src, dst))
2319 })
2320 .collect();
2321
2322 Ok((
2323 IgnoredRefspecs(ignored_refspecs),
2324 ExpandedFetchRefSpecs {
2325 expected_branch_names,
2326 refspecs,
2327 negative_refspecs,
2328 },
2329 ))
2330}
2331
2332pub struct GitFetch<'a> {
2334 mut_repo: &'a mut MutableRepo,
2335 git_repo: Box<gix::Repository>,
2336 git_ctx: GitSubprocessContext<'a>,
2337 git_settings: &'a GitSettings,
2338 fetched: Vec<FetchedBranches>,
2339}
2340
2341impl<'a> GitFetch<'a> {
2342 pub fn new(
2343 mut_repo: &'a mut MutableRepo,
2344 git_settings: &'a GitSettings,
2345 ) -> Result<Self, UnexpectedGitBackendError> {
2346 let git_backend = get_git_backend(mut_repo.store())?;
2347 let git_repo = Box::new(git_backend.git_repo());
2348 let git_ctx =
2349 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2350 Ok(GitFetch {
2351 mut_repo,
2352 git_repo,
2353 git_ctx,
2354 git_settings,
2355 fetched: vec![],
2356 })
2357 }
2358
2359 #[tracing::instrument(skip(self, callbacks))]
2365 pub fn fetch(
2366 &mut self,
2367 remote_name: &RemoteName,
2368 ExpandedFetchRefSpecs {
2369 expected_branch_names,
2370 refspecs: mut remaining_refspecs,
2371 negative_refspecs,
2372 }: ExpandedFetchRefSpecs,
2373 mut callbacks: RemoteCallbacks<'_>,
2374 depth: Option<NonZeroU32>,
2375 fetch_tags_override: Option<FetchTagsOverride>,
2376 ) -> Result<(), GitFetchError> {
2377 validate_remote_name(remote_name)?;
2378
2379 if self
2381 .git_repo
2382 .try_find_remote(remote_name.as_str())
2383 .is_none()
2384 {
2385 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2386 }
2387
2388 if remaining_refspecs.is_empty() {
2389 return Ok(());
2391 }
2392
2393 let mut branches_to_prune = Vec::new();
2394 while let Some(failing_refspec) = self.git_ctx.spawn_fetch(
2402 remote_name,
2403 &remaining_refspecs,
2404 &negative_refspecs,
2405 &mut callbacks,
2406 depth,
2407 fetch_tags_override,
2408 )? {
2409 tracing::debug!(failing_refspec, "failed to fetch ref");
2410 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2411
2412 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2413 branches_to_prune.push(format!(
2414 "{remote_name}/{branch_name}",
2415 remote_name = remote_name.as_str()
2416 ));
2417 }
2418 }
2419
2420 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2423
2424 self.fetched.push(FetchedBranches {
2425 remote: remote_name.to_owned(),
2426 branches: expected_branch_names,
2427 });
2428 Ok(())
2429 }
2430
2431 #[tracing::instrument(skip(self))]
2433 pub fn get_default_branch(
2434 &self,
2435 remote_name: &RemoteName,
2436 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2437 if self
2438 .git_repo
2439 .try_find_remote(remote_name.as_str())
2440 .is_none()
2441 {
2442 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2443 }
2444 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2445 tracing::debug!(?default_branch);
2446 Ok(default_branch)
2447 }
2448
2449 #[tracing::instrument(skip(self))]
2457 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2458 tracing::debug!("import_refs");
2459 let import_stats =
2460 import_some_refs(
2461 self.mut_repo,
2462 self.git_settings,
2463 |kind, symbol| match kind {
2464 GitRefKind::Bookmark => self
2465 .fetched
2466 .iter()
2467 .filter(|fetched| fetched.remote == symbol.remote)
2468 .any(|fetched| {
2469 fetched
2470 .branches
2471 .iter()
2472 .any(|pattern| pattern.is_match(symbol.name.as_str()))
2473 }),
2474 GitRefKind::Tag => true,
2475 },
2476 )?;
2477
2478 self.fetched.clear();
2479
2480 Ok(import_stats)
2481 }
2482}
2483
2484#[derive(Error, Debug)]
2485pub enum GitPushError {
2486 #[error("No git remote named '{}'", .0.as_symbol())]
2487 NoSuchRemote(RemoteNameBuf),
2488 #[error(transparent)]
2489 RemoteName(#[from] GitRemoteNameError),
2490 #[error(transparent)]
2491 Subprocess(#[from] GitSubprocessError),
2492 #[error(transparent)]
2493 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2494}
2495
2496#[derive(Clone, Debug)]
2497pub struct GitBranchPushTargets {
2498 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2499}
2500
2501pub struct GitRefUpdate {
2502 pub qualified_name: GitRefNameBuf,
2503 pub expected_current_target: Option<CommitId>,
2508 pub new_target: Option<CommitId>,
2509}
2510
2511pub fn push_branches(
2513 mut_repo: &mut MutableRepo,
2514 git_settings: &GitSettings,
2515 remote: &RemoteName,
2516 targets: &GitBranchPushTargets,
2517 callbacks: RemoteCallbacks<'_>,
2518) -> Result<GitPushStats, GitPushError> {
2519 validate_remote_name(remote)?;
2520
2521 let ref_updates = targets
2522 .branch_updates
2523 .iter()
2524 .map(|(name, update)| GitRefUpdate {
2525 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2526 expected_current_target: update.old_target.clone(),
2527 new_target: update.new_target.clone(),
2528 })
2529 .collect_vec();
2530
2531 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2532 tracing::debug!(?push_stats);
2533
2534 if push_stats.all_ok() {
2538 for (name, update) in &targets.branch_updates {
2539 let git_ref_name: GitRefNameBuf = format!(
2540 "refs/remotes/{remote}/{name}",
2541 remote = remote.as_str(),
2542 name = name.as_str()
2543 )
2544 .into();
2545 let new_remote_ref = RemoteRef {
2546 target: RefTarget::resolved(update.new_target.clone()),
2547 state: RemoteRefState::Tracked,
2548 };
2549 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2550 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2551 }
2552 }
2553
2554 Ok(push_stats)
2555}
2556
2557pub fn push_updates(
2559 repo: &dyn Repo,
2560 git_settings: &GitSettings,
2561 remote_name: &RemoteName,
2562 updates: &[GitRefUpdate],
2563 mut callbacks: RemoteCallbacks<'_>,
2564) -> Result<GitPushStats, GitPushError> {
2565 let mut qualified_remote_refs_expected_locations = HashMap::new();
2566 let mut refspecs = vec![];
2567 for update in updates {
2568 qualified_remote_refs_expected_locations.insert(
2569 update.qualified_name.as_ref(),
2570 update.expected_current_target.as_ref(),
2571 );
2572 if let Some(new_target) = &update.new_target {
2573 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2577 } else {
2578 refspecs.push(RefSpec::delete(&update.qualified_name));
2582 }
2583 }
2584
2585 let git_backend = get_git_backend(repo.store())?;
2586 let git_repo = git_backend.git_repo();
2587 let git_ctx =
2588 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2589
2590 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2592 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2593 }
2594
2595 let refs_to_push: Vec<RefToPush> = refspecs
2596 .iter()
2597 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2598 .collect();
2599
2600 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2601 push_stats.pushed.sort();
2602 push_stats.rejected.sort();
2603 push_stats.remote_rejected.sort();
2604 Ok(push_stats)
2605}
2606
2607#[non_exhaustive]
2608#[derive(Default)]
2609#[expect(clippy::type_complexity)]
2610pub struct RemoteCallbacks<'a> {
2611 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2612 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2613 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2614 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2615 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2616}
2617
2618#[derive(Clone, Debug)]
2619pub struct Progress {
2620 pub bytes_downloaded: Option<u64>,
2622 pub overall: f32,
2623}
2624
2625#[derive(Copy, Clone, Debug)]
2628pub enum FetchTagsOverride {
2629 AllTags,
2632 NoTags,
2635}