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::ffi::OsString;
23use std::fs::File;
24use std::iter;
25use std::num::NonZeroU32;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use bstr::BStr;
30use bstr::BString;
31use futures::StreamExt as _;
32use gix::refspec::Instruction;
33use itertools::Itertools as _;
34use pollster::FutureExt as _;
35use thiserror::Error;
36
37use crate::backend::BackendError;
38use crate::backend::BackendResult;
39use crate::backend::CommitId;
40use crate::backend::TreeValue;
41use crate::commit::Commit;
42use crate::config::ConfigGetError;
43use crate::file_util::IoResultExt as _;
44use crate::file_util::PathError;
45use crate::git_backend::GitBackend;
46use crate::git_subprocess::GitFetchStatus;
47pub use crate::git_subprocess::GitProgress;
48pub use crate::git_subprocess::GitSidebandLineTerminator;
49pub use crate::git_subprocess::GitSubprocessCallback;
50use crate::git_subprocess::GitSubprocessContext;
51use crate::git_subprocess::GitSubprocessError;
52use crate::index::IndexError;
53use crate::matchers::EverythingMatcher;
54use crate::merged_tree::MergedTree;
55use crate::merged_tree::TreeDiffEntry;
56use crate::object_id::ObjectId as _;
57use crate::op_store::RefTarget;
58use crate::op_store::RefTargetOptionExt as _;
59use crate::op_store::RemoteRef;
60use crate::op_store::RemoteRefState;
61use crate::ref_name::GitRefName;
62use crate::ref_name::GitRefNameBuf;
63use crate::ref_name::RefName;
64use crate::ref_name::RefNameBuf;
65use crate::ref_name::RemoteName;
66use crate::ref_name::RemoteNameBuf;
67use crate::ref_name::RemoteRefSymbol;
68use crate::ref_name::RemoteRefSymbolBuf;
69use crate::refs::BookmarkPushUpdate;
70use crate::repo::MutableRepo;
71use crate::repo::Repo;
72use crate::repo_path::RepoPath;
73use crate::revset::RevsetExpression;
74use crate::settings::UserSettings;
75use crate::store::Store;
76use crate::str_util::StringExpression;
77use crate::str_util::StringMatcher;
78use crate::str_util::StringPattern;
79use crate::view::View;
80
81pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
83pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
85const REMOTE_BOOKMARK_REF_NAMESPACE: &str = "refs/remotes/";
87const REMOTE_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";
89const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
91const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
94
95#[derive(Clone, Debug)]
96pub struct GitSettings {
97 pub auto_local_bookmark: bool,
99 pub abandon_unreachable_commits: bool,
100 pub executable_path: PathBuf,
101 pub write_change_id_header: bool,
102}
103
104impl GitSettings {
105 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
106 Ok(Self {
107 auto_local_bookmark: settings.get_bool("git.auto-local-bookmark")?,
108 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
109 executable_path: settings.get("git.executable-path")?,
110 write_change_id_header: settings.get("git.write-change-id-header")?,
111 })
112 }
113
114 pub fn to_subprocess_options(&self) -> GitSubprocessOptions {
115 GitSubprocessOptions {
116 executable_path: self.executable_path.clone(),
117 environment: HashMap::new(),
118 }
119 }
120}
121
122#[derive(Clone, Debug)]
124pub struct GitSubprocessOptions {
125 pub executable_path: PathBuf,
126 pub environment: HashMap<OsString, OsString>,
131}
132
133impl GitSubprocessOptions {
134 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
135 Ok(Self {
136 executable_path: settings.get("git.executable-path")?,
137 environment: HashMap::new(),
138 })
139 }
140}
141
142#[derive(Debug, Error)]
143pub enum GitRemoteNameError {
144 #[error(
145 "Git remote named '{name}' is reserved for local Git repository",
146 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
147 )]
148 ReservedForLocalGitRepo,
149 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
150 WithSlash(RemoteNameBuf),
151}
152
153fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
154 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
155 Err(GitRemoteNameError::ReservedForLocalGitRepo)
156 } else if name.as_str().contains('/') {
157 Err(GitRemoteNameError::WithSlash(name.to_owned()))
158 } else {
159 Ok(())
160 }
161}
162
163#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
165pub enum GitRefKind {
166 Bookmark,
167 Tag,
168}
169
170#[derive(Debug, Default)]
172pub struct GitPushStats {
173 pub pushed: Vec<GitRefNameBuf>,
175 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
177 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
179 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
181}
182
183impl GitPushStats {
184 pub fn all_ok(&self) -> bool {
185 self.rejected.is_empty()
186 && self.remote_rejected.is_empty()
187 && self.unexported_bookmarks.is_empty()
188 }
189
190 pub fn some_exported(&self) -> bool {
193 self.pushed.len() > self.unexported_bookmarks.len()
194 }
195}
196
197#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
202
203impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
204 fn borrow(&self) -> &RemoteRefSymbol<'b> {
205 &self.0
206 }
207}
208
209#[derive(Debug, Hash, PartialEq, Eq)]
215pub(crate) struct RefSpec {
216 forced: bool,
217 source: Option<String>,
220 destination: String,
221}
222
223impl RefSpec {
224 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
225 Self {
226 forced: true,
227 source: Some(source.into()),
228 destination: destination.into(),
229 }
230 }
231
232 fn delete(destination: impl Into<String>) -> Self {
233 Self {
235 forced: false,
236 source: None,
237 destination: destination.into(),
238 }
239 }
240
241 pub(crate) fn to_git_format(&self) -> String {
242 format!(
243 "{}{}",
244 if self.forced { "+" } else { "" },
245 self.to_git_format_not_forced()
246 )
247 }
248
249 pub(crate) fn to_git_format_not_forced(&self) -> String {
255 if let Some(s) = &self.source {
256 format!("{}:{}", s, self.destination)
257 } else {
258 format!(":{}", self.destination)
259 }
260 }
261}
262
263#[derive(Debug)]
265#[repr(transparent)]
266pub(crate) struct NegativeRefSpec {
267 source: String,
268}
269
270impl NegativeRefSpec {
271 fn new(source: impl Into<String>) -> Self {
272 Self {
273 source: source.into(),
274 }
275 }
276
277 pub(crate) fn to_git_format(&self) -> String {
278 format!("^{}", self.source)
279 }
280}
281
282pub(crate) struct RefToPush<'a> {
285 pub(crate) refspec: &'a RefSpec,
286 pub(crate) expected_location: Option<&'a CommitId>,
287}
288
289impl<'a> RefToPush<'a> {
290 fn new(
291 refspec: &'a RefSpec,
292 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
293 ) -> Self {
294 let expected_location = *expected_locations
295 .get(GitRefName::new(&refspec.destination))
296 .expect(
297 "The refspecs and the expected locations were both constructed from the same \
298 source of truth. This means the lookup should always work.",
299 );
300
301 Self {
302 refspec,
303 expected_location,
304 }
305 }
306
307 pub(crate) fn to_git_lease(&self) -> String {
308 format!(
309 "{}:{}",
310 self.refspec.destination,
311 self.expected_location
312 .map(|x| x.to_string())
313 .as_deref()
314 .unwrap_or("")
315 )
316 }
317}
318
319pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
322 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
323 if name == "HEAD" {
325 return None;
326 }
327 let name = RefName::new(name);
328 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
329 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
330 } else if let Some(remote_and_name) = full_name
331 .as_str()
332 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
333 {
334 let (remote, name) = remote_and_name.split_once('/')?;
335 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
337 return None;
338 }
339 let name = RefName::new(name);
340 let remote = RemoteName::new(remote);
341 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
342 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
343 let name = RefName::new(name);
344 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
345 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
346 } else {
347 None
348 }
349}
350
351fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
352 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
353 let (remote, name) = remote_and_name.split_once('/')?;
354 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
355 return None;
356 }
357 let name = RefName::new(name);
358 let remote = RemoteName::new(remote);
359 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
360}
361
362fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
363 let RemoteRefSymbol { name, remote } = symbol;
364 let name = name.as_str();
365 let remote = remote.as_str();
366 if name.is_empty() || remote.is_empty() {
367 return None;
368 }
369 match kind {
370 GitRefKind::Bookmark => {
371 if name == "HEAD" {
372 return None;
373 }
374 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
375 Some(format!("refs/heads/{name}").into())
376 } else {
377 Some(format!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())
378 }
379 }
380 GitRefKind::Tag => {
381 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
383 }
384 }
385}
386
387fn to_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
388 let RemoteRefSymbol { name, remote } = symbol;
389 let name = name.as_str();
390 let remote = remote.as_str();
391 (remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
392 .then(|| format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}").into())
393}
394
395#[derive(Debug, Error)]
396#[error("The repo is not backed by a Git repo")]
397pub struct UnexpectedGitBackendError;
398
399pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
401 store.backend_impl().ok_or(UnexpectedGitBackendError)
402}
403
404pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
406 get_git_backend(store).map(|backend| backend.git_repo())
407}
408
409fn resolve_git_ref_to_commit_id(
414 git_ref: &gix::Reference,
415 known_commit_oid: Option<&gix::oid>,
416) -> Option<gix::ObjectId> {
417 let mut peeling_ref = Cow::Borrowed(git_ref);
418
419 if let Some(known_oid) = known_commit_oid {
421 let raw_ref = &git_ref.inner;
422 if let Some(oid) = raw_ref.target.try_id()
423 && oid == known_oid
424 {
425 return Some(oid.to_owned());
426 }
427 if let Some(oid) = raw_ref.peeled
428 && oid == known_oid
429 {
430 return Some(oid);
433 }
434 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
438 let maybe_tag = git_ref
439 .try_id()
440 .and_then(|id| id.object().ok())
441 .and_then(|object| object.try_into_tag().ok());
442 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
443 let oid = oid.detach();
444 if oid == known_oid {
445 return Some(oid);
447 }
448 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
451 }
452 }
453 }
454
455 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
459 let is_commit = peeled_id
460 .object()
461 .is_ok_and(|object| object.kind.is_commit());
462 is_commit.then_some(peeled_id.detach())
463}
464
465#[derive(Error, Debug)]
466pub enum GitImportError {
467 #[error("Failed to read Git HEAD target commit {id}")]
468 MissingHeadTarget {
469 id: CommitId,
470 #[source]
471 err: BackendError,
472 },
473 #[error("Ancestor of Git ref {symbol} is missing")]
474 MissingRefAncestor {
475 symbol: RemoteRefSymbolBuf,
476 #[source]
477 err: BackendError,
478 },
479 #[error(transparent)]
480 Backend(#[from] BackendError),
481 #[error(transparent)]
482 Index(#[from] IndexError),
483 #[error(transparent)]
484 Git(Box<dyn std::error::Error + Send + Sync>),
485 #[error(transparent)]
486 UnexpectedBackend(#[from] UnexpectedGitBackendError),
487}
488
489impl GitImportError {
490 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
491 Self::Git(source.into())
492 }
493}
494
495#[derive(Debug)]
497pub struct GitImportOptions {
498 pub auto_local_bookmark: bool,
500 pub abandon_unreachable_commits: bool,
502 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
504}
505
506#[derive(Clone, Debug, Eq, PartialEq, Default)]
508pub struct GitImportStats {
509 pub abandoned_commits: Vec<CommitId>,
511 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
514 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
517 pub failed_ref_names: Vec<BString>,
522}
523
524#[derive(Debug)]
525struct RefsToImport {
526 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
529 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
532 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
535 failed_ref_names: Vec<BString>,
537}
538
539pub fn import_refs(
544 mut_repo: &mut MutableRepo,
545 options: &GitImportOptions,
546) -> Result<GitImportStats, GitImportError> {
547 import_some_refs(mut_repo, options, |_, _| true)
548}
549
550pub fn import_some_refs(
555 mut_repo: &mut MutableRepo,
556 options: &GitImportOptions,
557 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
558) -> Result<GitImportStats, GitImportError> {
559 let git_repo = get_git_repo(mut_repo.store())?;
560
561 for remote_name in iter_remote_names(&git_repo) {
565 mut_repo.ensure_remote(&remote_name);
566 }
567
568 let all_remote_tags = false;
570 let refs_to_import =
571 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
572 import_refs_inner(mut_repo, refs_to_import, options)
573}
574
575fn import_refs_inner(
576 mut_repo: &mut MutableRepo,
577 refs_to_import: RefsToImport,
578 options: &GitImportOptions,
579) -> Result<GitImportStats, GitImportError> {
580 let store = mut_repo.store();
581 let git_backend = get_git_backend(store).expect("backend type should have been tested");
582
583 let RefsToImport {
584 changed_git_refs,
585 changed_remote_bookmarks,
586 changed_remote_tags,
587 failed_ref_names,
588 } = refs_to_import;
589
590 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
596 let index = mut_repo.index();
597 let missing_head_ids: Vec<&CommitId> = iter_changed_refs()
598 .flat_map(|(_, (_, new_target))| new_target.added_ids())
599 .filter_map(|id| match index.has_id(id) {
600 Ok(false) => Some(Ok(id)),
601 Ok(true) => None,
602 Err(e) => Some(Err(e)),
603 })
604 .try_collect()?;
605 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
606
607 let mut head_commits = Vec::new();
609 let get_commit = |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
610 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
611 symbol: symbol.clone(),
612 err,
613 };
614 if !heads_imported && !index.has_id(id).map_err(GitImportError::Index)? {
616 git_backend
617 .import_head_commits([id])
618 .map_err(missing_ref_err)?;
619 }
620 store.get_commit(id).map_err(missing_ref_err)
621 };
622 for (symbol, (_, new_target)) in iter_changed_refs() {
623 for id in new_target.added_ids() {
624 let commit = get_commit(id, symbol)?;
625 head_commits.push(commit);
626 }
627 }
628 mut_repo
631 .add_heads(&head_commits)
632 .map_err(GitImportError::Backend)?;
633
634 for (full_name, new_target) in changed_git_refs {
636 mut_repo.set_git_ref_target(&full_name, new_target);
637 }
638 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
639 let symbol = symbol.as_ref();
640 let base_target = old_remote_ref.tracked_target();
641 let new_remote_ref = RemoteRef {
642 target: new_target.clone(),
643 state: if old_remote_ref != RemoteRef::absent_ref() {
644 old_remote_ref.state
645 } else {
646 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
647 },
648 };
649 if new_remote_ref.is_tracked() {
650 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
651 }
652 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
655 }
656 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
657 let symbol = symbol.as_ref();
658 let base_target = old_remote_ref.tracked_target();
659 let new_remote_ref = RemoteRef {
660 target: new_target.clone(),
661 state: if old_remote_ref != RemoteRef::absent_ref() {
662 old_remote_ref.state
663 } else {
664 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
665 },
666 };
667 if new_remote_ref.is_tracked() {
668 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
669 }
670 mut_repo.set_remote_tag(symbol, new_remote_ref);
673 }
674
675 let abandoned_commits = if options.abandon_unreachable_commits {
676 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
677 .map_err(GitImportError::Backend)?
678 } else {
679 vec![]
680 };
681 let stats = GitImportStats {
682 abandoned_commits,
683 changed_remote_bookmarks,
684 changed_remote_tags,
685 failed_ref_names,
686 };
687 Ok(stats)
688}
689
690fn abandon_unreachable_commits(
693 mut_repo: &mut MutableRepo,
694 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
695 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
696) -> BackendResult<Vec<CommitId>> {
697 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
698 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
699 .cloned()
700 .collect_vec();
701 if hidable_git_heads.is_empty() {
702 return Ok(vec![]);
703 }
704 let pinned_expression = RevsetExpression::union_all(&[
705 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
707 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
708 .intersection(&RevsetExpression::visible_heads().ancestors()),
710 RevsetExpression::root(),
711 ]);
712 let abandoned_expression = pinned_expression
713 .range(&RevsetExpression::commits(hidable_git_heads))
714 .intersection(&RevsetExpression::visible_heads().ancestors());
716 let abandoned_commit_ids: Vec<_> = abandoned_expression
717 .evaluate(mut_repo)
718 .map_err(|err| err.into_backend_error())?
719 .iter()
720 .try_collect()
721 .map_err(|err| err.into_backend_error())?;
722 for id in &abandoned_commit_ids {
723 let commit = mut_repo.store().get_commit(id)?;
724 mut_repo.record_abandoned_commit(&commit);
725 }
726 Ok(abandoned_commit_ids)
727}
728
729fn diff_refs_to_import(
731 view: &View,
732 git_repo: &gix::Repository,
733 all_remote_tags: bool,
734 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
735) -> Result<RefsToImport, GitImportError> {
736 let mut known_git_refs = view
737 .git_refs()
738 .iter()
739 .filter_map(|(full_name, target)| {
740 let (kind, symbol) =
742 parse_git_ref(full_name).expect("stored git ref should be parsable");
743 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
744 })
745 .collect();
746 let mut known_remote_bookmarks = view
747 .all_remote_bookmarks()
748 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
749 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
750 .collect();
751 let mut known_remote_tags = if all_remote_tags {
752 view.all_remote_tags()
753 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
754 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
755 .collect()
756 } else {
757 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
758 view.remote_tags(remote)
759 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
760 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
761 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
762 .collect()
763 };
764
765 let mut changed_git_refs = Vec::new();
770 let mut changed_remote_bookmarks = Vec::new();
771 let mut changed_remote_tags = Vec::new();
772 let mut failed_ref_names = Vec::new();
773 let actual = git_repo.references().map_err(GitImportError::from_git)?;
774 collect_changed_refs_to_import(
775 actual.local_branches().map_err(GitImportError::from_git)?,
776 &mut known_git_refs,
777 &mut known_remote_bookmarks,
778 &mut changed_git_refs,
779 &mut changed_remote_bookmarks,
780 &mut failed_ref_names,
781 &git_ref_filter,
782 )?;
783 collect_changed_refs_to_import(
784 actual.remote_branches().map_err(GitImportError::from_git)?,
785 &mut known_git_refs,
786 &mut known_remote_bookmarks,
787 &mut changed_git_refs,
788 &mut changed_remote_bookmarks,
789 &mut failed_ref_names,
790 &git_ref_filter,
791 )?;
792 collect_changed_refs_to_import(
793 actual.tags().map_err(GitImportError::from_git)?,
794 &mut known_git_refs,
795 &mut known_remote_tags,
796 &mut changed_git_refs,
797 &mut changed_remote_tags,
798 &mut failed_ref_names,
799 &git_ref_filter,
800 )?;
801 if all_remote_tags {
802 collect_changed_remote_tags_to_import(
803 actual
804 .prefixed(REMOTE_TAG_REF_NAMESPACE)
805 .map_err(GitImportError::from_git)?,
806 &mut known_remote_tags,
807 &mut changed_remote_tags,
808 &mut failed_ref_names,
809 &git_ref_filter,
810 )?;
811 }
812 for full_name in known_git_refs.into_keys() {
813 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
814 }
815 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
816 if old.is_present() {
817 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
818 }
819 }
820 for (RemoteRefKey(symbol), old) in known_remote_tags {
821 if old.is_present() {
822 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
823 }
824 }
825
826 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
828 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
829 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
830 failed_ref_names.sort_unstable();
831 Ok(RefsToImport {
832 changed_git_refs,
833 changed_remote_bookmarks,
834 changed_remote_tags,
835 failed_ref_names,
836 })
837}
838
839fn collect_changed_refs_to_import(
840 actual_git_refs: gix::reference::iter::Iter,
841 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
842 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
843 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
844 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
845 failed_ref_names: &mut Vec<BString>,
846 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
847) -> Result<(), GitImportError> {
848 for git_ref in actual_git_refs {
849 let git_ref = git_ref.map_err(GitImportError::from_git)?;
850 let full_name_bytes = git_ref.name().as_bstr();
851 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
852 failed_ref_names.push(full_name_bytes.to_owned());
854 continue;
855 };
856 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
857 failed_ref_names.push(full_name_bytes.to_owned());
858 continue;
859 }
860 let full_name = GitRefName::new(full_name);
861 let Some((kind, symbol)) = parse_git_ref(full_name) else {
862 continue;
864 };
865 if !git_ref_filter(kind, symbol) {
866 continue;
867 }
868 let old_git_target = known_git_refs.get(full_name).copied().flatten();
869 let old_git_oid = old_git_target
870 .as_normal()
871 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
872 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
873 continue;
875 };
876 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
877 known_git_refs.remove(full_name);
878 if new_target != *old_git_target {
879 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
880 }
881 let old_remote_ref = known_remote_refs
884 .remove(&symbol)
885 .unwrap_or_else(|| RemoteRef::absent_ref());
886 if new_target != old_remote_ref.target {
887 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
888 }
889 }
890 Ok(())
891}
892
893fn collect_changed_remote_tags_to_import(
896 actual_git_refs: gix::reference::iter::Iter,
897 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
898 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
899 failed_ref_names: &mut Vec<BString>,
900 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
901) -> Result<(), GitImportError> {
902 for git_ref in actual_git_refs {
903 let git_ref = git_ref.map_err(GitImportError::from_git)?;
904 let full_name_bytes = git_ref.name().as_bstr();
905 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
906 failed_ref_names.push(full_name_bytes.to_owned());
908 continue;
909 };
910 let full_name = GitRefName::new(full_name);
911 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
912 continue;
914 };
915 if !git_ref_filter(kind, symbol) {
916 continue;
917 }
918 let old_remote_ref = known_remote_refs
919 .get(&symbol)
920 .copied()
921 .unwrap_or_else(|| RemoteRef::absent_ref());
922 let old_git_oid = old_remote_ref
923 .target
924 .as_normal()
925 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
926 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
927 continue;
929 };
930 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
931 known_remote_refs.remove(&symbol);
932 if new_target != old_remote_ref.target {
933 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
934 }
935 }
936 Ok(())
937}
938
939fn default_remote_ref_state_for(
940 kind: GitRefKind,
941 symbol: RemoteRefSymbol<'_>,
942 options: &GitImportOptions,
943) -> RemoteRefState {
944 match kind {
945 GitRefKind::Bookmark => {
946 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
947 || options.auto_local_bookmark
948 || options
949 .remote_auto_track_bookmarks
950 .get(symbol.remote)
951 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
952 {
953 RemoteRefState::Tracked
954 } else {
955 RemoteRefState::New
956 }
957 }
958 GitRefKind::Tag => RemoteRefState::Tracked,
960 }
961}
962
963fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
969 itertools::chain(view.local_bookmarks(), view.local_tags())
970 .flat_map(|(_, target)| target.added_ids())
971 .cloned()
972 .collect()
973}
974
975fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
982 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
983 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
984 .map(|(_, remote_ref)| &remote_ref.target)
985 .flat_map(|target| target.added_ids())
986 .cloned()
987 .collect()
988}
989
990pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
998 let store = mut_repo.store();
999 let git_backend = get_git_backend(store)?;
1000 let git_repo = git_backend.git_repo();
1001
1002 let old_git_head = mut_repo.view().git_head();
1003 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1004 Some(CommitId::from_bytes(oid.as_bytes()))
1005 } else {
1006 None
1007 };
1008 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1009 return Ok(());
1010 }
1011
1012 if let Some(head_id) = &new_git_head_id {
1014 let index = mut_repo.index();
1015 if !index.has_id(head_id)? {
1016 git_backend.import_head_commits([head_id]).map_err(|err| {
1017 GitImportError::MissingHeadTarget {
1018 id: head_id.clone(),
1019 err,
1020 }
1021 })?;
1022 }
1023 store
1026 .get_commit(head_id)
1027 .and_then(|commit| mut_repo.add_head(&commit))
1028 .map_err(GitImportError::Backend)?;
1029 }
1030
1031 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1032 Ok(())
1033}
1034
1035#[derive(Error, Debug)]
1036pub enum GitExportError {
1037 #[error(transparent)]
1038 Git(Box<dyn std::error::Error + Send + Sync>),
1039 #[error(transparent)]
1040 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1041}
1042
1043impl GitExportError {
1044 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1045 Self::Git(source.into())
1046 }
1047}
1048
1049#[derive(Debug, Error)]
1051pub enum FailedRefExportReason {
1052 #[error("Name is not allowed in Git")]
1054 InvalidGitName,
1055 #[error("Ref was in a conflicted state from the last import")]
1058 ConflictedOldState,
1059 #[error("Ref cannot point to the root commit in Git")]
1061 OnRootCommit,
1062 #[error("Deleted ref had been modified in Git")]
1064 DeletedInJjModifiedInGit,
1065 #[error("Added ref had been added with a different target in Git")]
1067 AddedInJjAddedInGit,
1068 #[error("Modified ref had been deleted in Git")]
1070 ModifiedInJjDeletedInGit,
1071 #[error("Failed to delete")]
1073 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1074 #[error("Failed to set")]
1076 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1077}
1078
1079#[derive(Debug)]
1081pub struct GitExportStats {
1082 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1084 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1088}
1089
1090#[derive(Debug)]
1091struct AllRefsToExport {
1092 bookmarks: RefsToExport,
1093 tags: RefsToExport,
1094}
1095
1096#[derive(Debug)]
1097struct RefsToExport {
1098 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1100 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1105 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1107}
1108
1109pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1118 export_some_refs(mut_repo, |_, _| true)
1119}
1120
1121pub fn export_some_refs(
1122 mut_repo: &mut MutableRepo,
1123 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1124) -> Result<GitExportStats, GitExportError> {
1125 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1126 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1127 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1128 let (_, value) = &map[index];
1129 Some(value)
1130 }
1131
1132 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1133 mut_repo.view(),
1134 mut_repo.store().root_commit_id(),
1135 &git_ref_filter,
1136 );
1137
1138 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1139 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1140 return Ok(());
1141 };
1142 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1143 if let Some((kind, symbol)) = target_name
1144 .as_ref()
1145 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1146 .and_then(|name| parse_git_ref(name.as_ref()))
1147 {
1148 let old_target = head_ref.inner.target.clone();
1149 let current_oid = match head_ref.into_fully_peeled_id() {
1150 Ok(id) => Some(id.detach()),
1151 Err(gix::reference::peel::Error::ToId(
1152 gix::refs::peel::to_id::Error::FollowToObject(
1153 gix::refs::peel::to_object::Error::Follow(
1154 gix::refs::file::find::existing::Error::NotFound { .. },
1155 ),
1156 ),
1157 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1159 };
1160 let refs = match kind {
1161 GitRefKind::Bookmark => &bookmarks,
1162 GitRefKind::Tag => &tags,
1163 };
1164 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1165 Some(new_oid)
1166 } else if get(&refs.to_delete, symbol).is_some() {
1167 None
1168 } else {
1169 current_oid.as_ref()
1170 };
1171 if new_oid != current_oid.as_ref() {
1172 update_git_head(
1173 git_repo,
1174 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1175 current_oid,
1176 )
1177 .map_err(GitExportError::from_git)?;
1178 }
1179 }
1180 Ok(())
1181 };
1182
1183 let git_repo = get_git_repo(mut_repo.store())?;
1184
1185 check_and_detach_head(&git_repo)?;
1186 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1187 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1188 check_and_detach_head(&worktree_repo)?;
1189 }
1190 }
1191
1192 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1193 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1194
1195 copy_exportable_local_bookmarks_to_remote_view(
1196 mut_repo,
1197 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1198 |name| {
1199 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1200 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1201 },
1202 );
1203 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1204 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1205 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1206 });
1207
1208 Ok(GitExportStats {
1209 failed_bookmarks,
1210 failed_tags,
1211 })
1212}
1213
1214fn export_refs_to_git(
1215 mut_repo: &mut MutableRepo,
1216 git_repo: &gix::Repository,
1217 kind: GitRefKind,
1218 refs: RefsToExport,
1219) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1220 let mut failed = refs.failed;
1221 for (symbol, old_oid) in refs.to_delete {
1222 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1223 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1224 continue;
1225 };
1226 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1227 failed.push((symbol, reason));
1228 } else {
1229 let new_target = RefTarget::absent();
1230 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1231 }
1232 }
1233 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1234 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1235 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1236 continue;
1237 };
1238 let new_ref_oid = match kind {
1239 GitRefKind::Bookmark => None,
1240 GitRefKind::Tag => {
1242 find_git_tag_oid_to_copy(mut_repo.view(), git_repo, &symbol.name, &new_commit_oid)
1243 }
1244 };
1245 if let Err(reason) = update_git_ref(
1246 git_repo,
1247 &git_ref_name,
1248 old_commit_oid,
1249 new_commit_oid,
1250 new_ref_oid,
1251 ) {
1252 failed.push((symbol, reason));
1253 } else {
1254 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1255 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1256 }
1257 }
1258
1259 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1261 failed
1262}
1263
1264fn copy_exportable_local_bookmarks_to_remote_view(
1265 mut_repo: &mut MutableRepo,
1266 remote: &RemoteName,
1267 name_filter: impl Fn(&RefName) -> bool,
1268) {
1269 let new_local_bookmarks = mut_repo
1270 .view()
1271 .local_remote_bookmarks(remote)
1272 .filter_map(|(name, targets)| {
1273 let old_target = &targets.remote_ref.target;
1276 let new_target = targets.local_target;
1277 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1278 })
1279 .filter(|&(name, _)| name_filter(name))
1280 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1281 .collect_vec();
1282 for (name, new_target) in new_local_bookmarks {
1283 let new_remote_ref = RemoteRef {
1284 target: new_target,
1285 state: RemoteRefState::Tracked,
1286 };
1287 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1288 }
1289}
1290
1291fn copy_exportable_local_tags_to_remote_view(
1292 mut_repo: &mut MutableRepo,
1293 remote: &RemoteName,
1294 name_filter: impl Fn(&RefName) -> bool,
1295) {
1296 let new_local_tags = mut_repo
1297 .view()
1298 .local_remote_tags(remote)
1299 .filter_map(|(name, targets)| {
1300 let old_target = &targets.remote_ref.target;
1302 let new_target = targets.local_target;
1303 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1304 })
1305 .filter(|&(name, _)| name_filter(name))
1306 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1307 .collect_vec();
1308 for (name, new_target) in new_local_tags {
1309 let new_remote_ref = RemoteRef {
1310 target: new_target,
1311 state: RemoteRefState::Tracked,
1312 };
1313 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1314 }
1315}
1316
1317fn diff_refs_to_export(
1319 view: &View,
1320 root_commit_id: &CommitId,
1321 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1322) -> AllRefsToExport {
1323 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1326 itertools::chain(
1327 view.local_bookmarks().map(|(name, target)| {
1328 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1329 (symbol, target)
1330 }),
1331 view.all_remote_bookmarks()
1332 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1333 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1334 )
1335 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1336 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1337 .collect();
1338 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1340 .local_tags()
1341 .map(|(name, target)| {
1342 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1343 (symbol, target)
1344 })
1345 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1346 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1347 .collect();
1348 let known_git_refs = view
1349 .git_refs()
1350 .iter()
1351 .map(|(full_name, target)| {
1352 let (kind, symbol) =
1353 parse_git_ref(full_name).expect("stored git ref should be parsable");
1354 ((kind, symbol), target)
1355 })
1356 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1360 for ((kind, symbol), target) in known_git_refs {
1361 let ref_targets = match kind {
1362 GitRefKind::Bookmark => &mut all_bookmark_targets,
1363 GitRefKind::Tag => &mut all_tag_targets,
1364 };
1365 ref_targets
1366 .entry(symbol)
1367 .and_modify(|(old_target, _)| *old_target = target)
1368 .or_insert((target, RefTarget::absent_ref()));
1369 }
1370
1371 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1372 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1373 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1374 AllRefsToExport { bookmarks, tags }
1375}
1376
1377fn collect_changed_refs_to_export(
1378 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1379 root_commit_target: &RefTarget,
1380) -> RefsToExport {
1381 let mut to_update = Vec::new();
1382 let mut to_delete = Vec::new();
1383 let mut failed = Vec::new();
1384 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1385 if new_target == old_target {
1386 continue;
1387 }
1388 if new_target == root_commit_target {
1389 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1391 continue;
1392 }
1393 let old_oid = if let Some(id) = old_target.as_normal() {
1394 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1395 } else if old_target.has_conflict() {
1396 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1399 continue;
1400 } else {
1401 assert!(old_target.is_absent());
1402 None
1403 };
1404 if let Some(id) = new_target.as_normal() {
1405 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1406 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1407 } else if new_target.has_conflict() {
1408 continue;
1410 } else {
1411 assert!(new_target.is_absent());
1412 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1413 }
1414 }
1415
1416 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1418 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1419 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1420 RefsToExport {
1421 to_update,
1422 to_delete,
1423 failed,
1424 }
1425}
1426
1427fn find_git_tag_oid_to_copy(
1430 view: &View,
1431 git_repo: &gix::Repository,
1432 name: &RefName,
1433 commit_oid: &gix::oid,
1434) -> Option<gix::ObjectId> {
1435 view.remote_tags_matching(&StringMatcher::exact(name), &StringMatcher::all())
1437 .filter(|(_, remote_ref)| {
1438 let maybe_id = remote_ref.tracked_target().as_normal();
1439 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1440 })
1441 .filter_map(|(symbol, _)| {
1443 let git_ref_name = to_remote_tag_ref_name(symbol)?;
1444 git_repo.find_reference(git_ref_name.as_str()).ok()
1445 })
1446 .filter(|git_ref| {
1449 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1450 })
1451 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1452}
1453
1454fn delete_git_ref(
1455 git_repo: &gix::Repository,
1456 git_ref_name: &GitRefName,
1457 old_oid: &gix::oid,
1458) -> Result<(), FailedRefExportReason> {
1459 let Some(git_ref) = git_repo
1460 .try_find_reference(git_ref_name.as_str())
1461 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1462 else {
1463 return Ok(());
1465 };
1466 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1467 git_ref
1469 .delete()
1470 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1471 } else {
1472 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1474 }
1475}
1476
1477fn create_git_ref(
1479 git_repo: &gix::Repository,
1480 git_ref_name: &GitRefName,
1481 new_commit_oid: gix::ObjectId,
1482 new_ref_oid: Option<gix::ObjectId>,
1483) -> Result<(), FailedRefExportReason> {
1484 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1485 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1486 let Err(set_err) =
1487 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1488 else {
1489 return Ok(());
1491 };
1492 let Some(git_ref) = git_repo
1493 .try_find_reference(git_ref_name.as_str())
1494 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1495 else {
1496 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1497 };
1498 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1501 Ok(())
1502 } else {
1503 Err(FailedRefExportReason::AddedInJjAddedInGit)
1504 }
1505}
1506
1507fn move_git_ref(
1509 git_repo: &gix::Repository,
1510 git_ref_name: &GitRefName,
1511 old_commit_oid: gix::ObjectId,
1512 new_commit_oid: gix::ObjectId,
1513 new_ref_oid: Option<gix::ObjectId>,
1514) -> Result<(), FailedRefExportReason> {
1515 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1516 let constraint =
1517 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1518 let Err(set_err) =
1519 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1520 else {
1521 return Ok(());
1523 };
1524 let Some(git_ref) = git_repo
1526 .try_find_reference(git_ref_name.as_str())
1527 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1528 else {
1529 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1531 };
1532 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1534 if git_commit_oid == Some(new_commit_oid) {
1535 Ok(())
1536 } else if git_commit_oid == Some(old_commit_oid) {
1537 let constraint =
1539 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1540 git_repo
1541 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1542 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1543 Ok(())
1544 } else {
1545 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1546 }
1547}
1548
1549fn update_git_ref(
1550 git_repo: &gix::Repository,
1551 git_ref_name: &GitRefName,
1552 old_commit_oid: Option<gix::ObjectId>,
1553 new_commit_oid: gix::ObjectId,
1554 new_ref_oid: Option<gix::ObjectId>,
1555) -> Result<(), FailedRefExportReason> {
1556 match old_commit_oid {
1557 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1558 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1559 }
1560}
1561
1562fn update_git_head(
1565 git_repo: &gix::Repository,
1566 expected_ref: gix::refs::transaction::PreviousValue,
1567 new_oid: Option<gix::ObjectId>,
1568) -> Result<(), gix::reference::edit::Error> {
1569 let mut ref_edits = Vec::new();
1570 let new_target = if let Some(oid) = new_oid {
1571 gix::refs::Target::Object(oid)
1572 } else {
1573 ref_edits.push(gix::refs::transaction::RefEdit {
1578 change: gix::refs::transaction::Change::Delete {
1579 expected: gix::refs::transaction::PreviousValue::Any,
1580 log: gix::refs::transaction::RefLog::AndReference,
1581 },
1582 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1583 deref: false,
1584 });
1585 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1586 };
1587 ref_edits.push(gix::refs::transaction::RefEdit {
1588 change: gix::refs::transaction::Change::Update {
1589 log: gix::refs::transaction::LogChange {
1590 message: "export from jj".into(),
1591 ..Default::default()
1592 },
1593 expected: expected_ref,
1594 new: new_target,
1595 },
1596 name: "HEAD".try_into().unwrap(),
1597 deref: false,
1598 });
1599 git_repo.edit_references(ref_edits)?;
1600 Ok(())
1601}
1602
1603#[derive(Debug, Error)]
1604pub enum GitResetHeadError {
1605 #[error(transparent)]
1606 Backend(#[from] BackendError),
1607 #[error(transparent)]
1608 Git(Box<dyn std::error::Error + Send + Sync>),
1609 #[error("Failed to update Git HEAD ref")]
1610 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1611 #[error(transparent)]
1612 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1613}
1614
1615impl GitResetHeadError {
1616 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1617 Self::Git(source.into())
1618 }
1619}
1620
1621pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1624 let git_repo = get_git_repo(mut_repo.store())?;
1625
1626 let first_parent_id = &wc_commit.parent_ids()[0];
1627 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1628 RefTarget::normal(first_parent_id.clone())
1629 } else {
1630 RefTarget::absent()
1631 };
1632
1633 let old_head_target = mut_repo.git_head();
1635 if old_head_target != new_head_target {
1636 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1637 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1640 if actual_head.is_detached() {
1641 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1642 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1643 } else {
1644 gix::refs::transaction::PreviousValue::MustExist
1647 }
1648 } else {
1649 gix::refs::transaction::PreviousValue::MustExist
1651 };
1652 let new_oid = new_head_target
1653 .as_normal()
1654 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1655 update_git_head(&git_repo, expected_ref, new_oid)
1656 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1657 mut_repo.set_git_head_target(new_head_target);
1658 }
1659
1660 if git_repo.state().is_some() {
1663 clear_operation_state(&git_repo)?;
1664 }
1665
1666 reset_index(mut_repo, &git_repo, wc_commit)
1667}
1668
1669fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1671 const STATE_FILE_NAMES: &[&str] = &[
1675 "MERGE_HEAD",
1676 "MERGE_MODE",
1677 "MERGE_MSG",
1678 "REVERT_HEAD",
1679 "CHERRY_PICK_HEAD",
1680 "BISECT_LOG",
1681 ];
1682 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1683 let handle_err = |err: PathError| match err.source.kind() {
1684 std::io::ErrorKind::NotFound => Ok(()),
1685 _ => Err(GitResetHeadError::from_git(err)),
1686 };
1687 for file_name in STATE_FILE_NAMES {
1688 let path = git_repo.path().join(file_name);
1689 std::fs::remove_file(&path)
1690 .context(&path)
1691 .or_else(handle_err)?;
1692 }
1693 for dir_name in STATE_DIR_NAMES {
1694 let path = git_repo.path().join(dir_name);
1695 std::fs::remove_dir_all(&path)
1696 .context(&path)
1697 .or_else(handle_err)?;
1698 }
1699 Ok(())
1700}
1701
1702fn reset_index(
1703 repo: &dyn Repo,
1704 git_repo: &gix::Repository,
1705 wc_commit: &Commit,
1706) -> Result<(), GitResetHeadError> {
1707 let parent_tree = wc_commit.parent_tree(repo).block_on()?;
1708 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1712 if tree_id == repo.store().empty_tree_id() {
1713 gix::index::File::from_state(
1717 gix::index::State::new(git_repo.object_hash()),
1718 git_repo.index_path(),
1719 )
1720 } else {
1721 git_repo
1724 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1725 .map_err(GitResetHeadError::from_git)?
1726 }
1727 } else {
1728 build_index_from_merged_tree(git_repo, &parent_tree)?
1729 };
1730
1731 let wc_tree = wc_commit.tree();
1732 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1733
1734 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1737 index
1738 .entries_mut_with_paths()
1739 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1740 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1741 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1742 })
1743 .filter_map(|merged| merged.both())
1744 .map(|((entry, _), old_entry)| (entry, old_entry))
1745 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1746 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1747 }
1748
1749 debug_assert!(index.verify_entries().is_ok());
1750
1751 index
1752 .write(gix::index::write::Options::default())
1753 .map_err(GitResetHeadError::from_git)
1754}
1755
1756fn build_index_from_merged_tree(
1757 git_repo: &gix::Repository,
1758 merged_tree: &MergedTree,
1759) -> Result<gix::index::File, GitResetHeadError> {
1760 let mut index = gix::index::File::from_state(
1761 gix::index::State::new(git_repo.object_hash()),
1762 git_repo.index_path(),
1763 );
1764
1765 let mut push_index_entry =
1766 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1767 let Some(entry) = maybe_entry else {
1768 return;
1769 };
1770
1771 let (id, mode) = match entry {
1772 TreeValue::File {
1773 id,
1774 executable,
1775 copy_id: _,
1776 } => {
1777 if *executable {
1778 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1779 } else {
1780 (id.as_bytes(), gix::index::entry::Mode::FILE)
1781 }
1782 }
1783 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1784 TreeValue::Tree(_) => {
1785 return;
1790 }
1791 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1792 };
1793
1794 let path = BStr::new(path.as_internal_file_string());
1795
1796 index.dangerously_push_entry(
1799 gix::index::entry::Stat::default(),
1800 gix::ObjectId::from_bytes_or_panic(id),
1801 gix::index::entry::Flags::from_stage(stage),
1802 mode,
1803 path,
1804 );
1805 };
1806
1807 let mut has_many_sided_conflict = false;
1808
1809 for (path, entry) in merged_tree.entries() {
1810 let entry = entry?;
1811 if let Some(resolved) = entry.as_resolved() {
1812 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1813 continue;
1814 }
1815
1816 let conflict = entry.simplify();
1817 if let [left, base, right] = conflict.as_slice() {
1818 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1820 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1821 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1822 } else {
1823 has_many_sided_conflict = true;
1831 push_index_entry(
1832 &path,
1833 conflict.first(),
1834 gix::index::entry::Stage::Unconflicted,
1835 );
1836 }
1837 }
1838
1839 index.sort_entries();
1842
1843 if has_many_sided_conflict
1846 && index
1847 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1848 .is_err()
1849 {
1850 let file_blob = git_repo
1851 .write_blob(
1852 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1853 )
1854 .map_err(GitResetHeadError::from_git)?;
1855 index.dangerously_push_entry(
1856 gix::index::entry::Stat::default(),
1857 file_blob.detach(),
1858 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1859 gix::index::entry::Mode::FILE,
1860 INDEX_DUMMY_CONFLICT_FILE.into(),
1861 );
1862 index.sort_entries();
1865 }
1866
1867 Ok(index)
1868}
1869
1870pub fn update_intent_to_add(
1877 repo: &dyn Repo,
1878 old_tree: &MergedTree,
1879 new_tree: &MergedTree,
1880) -> Result<(), GitResetHeadError> {
1881 let git_repo = get_git_repo(repo.store())?;
1882 let mut index = git_repo
1883 .index_or_empty()
1884 .map_err(GitResetHeadError::from_git)?;
1885 let mut_index = Arc::make_mut(&mut index);
1886 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1887 debug_assert!(mut_index.verify_entries().is_ok());
1888 mut_index
1889 .write(gix::index::write::Options::default())
1890 .map_err(GitResetHeadError::from_git)?;
1891
1892 Ok(())
1893}
1894
1895async fn update_intent_to_add_impl(
1896 git_repo: &gix::Repository,
1897 index: &mut gix::index::File,
1898 old_tree: &MergedTree,
1899 new_tree: &MergedTree,
1900) -> Result<(), GitResetHeadError> {
1901 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1902 let mut added_paths = vec![];
1903 let mut removed_paths = HashSet::new();
1904 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1905 let values = values?;
1906 if values.before.is_absent() {
1907 let executable = match values.after.as_normal() {
1908 Some(TreeValue::File {
1909 id: _,
1910 executable,
1911 copy_id: _,
1912 }) => *executable,
1913 Some(TreeValue::Symlink(_)) => false,
1914 _ => {
1915 continue;
1916 }
1917 };
1918 if index
1919 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1920 .is_err()
1921 {
1922 added_paths.push((BString::from(path.into_internal_string()), executable));
1923 }
1924 } else if values.after.is_absent() {
1925 removed_paths.insert(BString::from(path.into_internal_string()));
1926 }
1927 }
1928
1929 if added_paths.is_empty() && removed_paths.is_empty() {
1930 return Ok(());
1931 }
1932
1933 if !added_paths.is_empty() {
1934 let empty_blob = git_repo
1936 .write_blob(b"")
1937 .map_err(GitResetHeadError::from_git)?
1938 .detach();
1939 for (path, executable) in added_paths {
1940 index.dangerously_push_entry(
1942 gix::index::entry::Stat::default(),
1943 empty_blob,
1944 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1945 if executable {
1946 gix::index::entry::Mode::FILE_EXECUTABLE
1947 } else {
1948 gix::index::entry::Mode::FILE
1949 },
1950 path.as_ref(),
1951 );
1952 }
1953 }
1954 if !removed_paths.is_empty() {
1955 index.remove_entries(|_size, path, entry| {
1956 entry
1957 .flags
1958 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1959 && removed_paths.contains(path)
1960 });
1961 }
1962
1963 index.sort_entries();
1964
1965 Ok(())
1966}
1967
1968#[derive(Debug, Error)]
1969pub enum GitRemoteManagementError {
1970 #[error("No git remote named '{}'", .0.as_symbol())]
1971 NoSuchRemote(RemoteNameBuf),
1972 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1973 RemoteAlreadyExists(RemoteNameBuf),
1974 #[error(transparent)]
1975 RemoteName(#[from] GitRemoteNameError),
1976 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1977 NonstandardConfiguration(RemoteNameBuf),
1978 #[error("Error saving Git configuration")]
1979 GitConfigSaveError(#[source] std::io::Error),
1980 #[error("Unexpected Git error when managing remotes")]
1981 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1982 #[error(transparent)]
1983 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1984 #[error(transparent)]
1985 RefExpansionError(#[from] GitRefExpansionError),
1986}
1987
1988impl GitRemoteManagementError {
1989 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1990 Self::InternalGitError(source.into())
1991 }
1992}
1993
1994fn default_fetch_refspec(remote: &RemoteName) -> String {
1995 format!(
1996 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
1997 remote = remote.as_str()
1998 )
1999}
2000
2001fn add_ref(
2002 name: gix::refs::FullName,
2003 target: gix::refs::Target,
2004 message: BString,
2005) -> gix::refs::transaction::RefEdit {
2006 gix::refs::transaction::RefEdit {
2007 change: gix::refs::transaction::Change::Update {
2008 log: gix::refs::transaction::LogChange {
2009 mode: gix::refs::transaction::RefLog::AndReference,
2010 force_create_reflog: false,
2011 message,
2012 },
2013 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2014 new: target,
2015 },
2016 name,
2017 deref: false,
2018 }
2019}
2020
2021fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2022 gix::refs::transaction::RefEdit {
2023 change: gix::refs::transaction::Change::Delete {
2024 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2025 reference.target().into_owned(),
2026 ),
2027 log: gix::refs::transaction::RefLog::AndReference,
2028 },
2029 name: reference.name().to_owned(),
2030 deref: false,
2031 }
2032}
2033
2034pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2040 let mut config_file = File::create(
2041 config
2042 .meta()
2043 .path
2044 .as_ref()
2045 .expect("Git repository to have a config file"),
2046 )?;
2047 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2048}
2049
2050fn save_remote(
2051 config: &mut gix::config::File<'static>,
2052 remote_name: &RemoteName,
2053 remote: &mut gix::Remote,
2054) -> Result<(), GitRemoteManagementError> {
2055 config
2062 .new_section(
2063 "remote",
2064 Some(Cow::Owned(BString::from(remote_name.as_str()))),
2065 )
2066 .map_err(GitRemoteManagementError::from_git)?;
2067 remote
2068 .save_as_to(remote_name.as_str(), config)
2069 .map_err(GitRemoteManagementError::from_git)?;
2070 Ok(())
2071}
2072
2073fn git_config_branch_section_ids_by_remote(
2074 config: &gix::config::File,
2075 remote_name: &RemoteName,
2076) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2077 config
2078 .sections_by_name("branch")
2079 .into_iter()
2080 .flatten()
2081 .filter_map(|section| {
2082 let remote_values = section.values("remote");
2083 let push_remote_values = section.values("pushRemote");
2084 if !remote_values
2085 .iter()
2086 .chain(push_remote_values.iter())
2087 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2088 {
2089 return None;
2090 }
2091 if remote_values.len() > 1
2092 || push_remote_values.len() > 1
2093 || section.value_names().any(|name| {
2094 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
2095 })
2096 {
2097 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2098 remote_name.to_owned(),
2099 )));
2100 }
2101 Some(Ok(section.id()))
2102 })
2103 .collect()
2104}
2105
2106fn rename_remote_in_git_branch_config_sections(
2107 config: &mut gix::config::File,
2108 old_remote_name: &RemoteName,
2109 new_remote_name: &RemoteName,
2110) -> Result<(), GitRemoteManagementError> {
2111 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2112 config
2113 .section_mut_by_id(id)
2114 .expect("found section to exist")
2115 .set(
2116 "remote"
2117 .try_into()
2118 .expect("'remote' to be a valid value name"),
2119 BStr::new(new_remote_name.as_str()),
2120 );
2121 }
2122 Ok(())
2123}
2124
2125fn remove_remote_git_branch_config_sections(
2126 config: &mut gix::config::File,
2127 remote_name: &RemoteName,
2128) -> Result<(), GitRemoteManagementError> {
2129 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2130 config
2131 .remove_section_by_id(id)
2132 .expect("removed section to exist");
2133 }
2134 Ok(())
2135}
2136
2137fn remove_remote_git_config_sections(
2138 config: &mut gix::config::File,
2139 remote_name: &RemoteName,
2140) -> Result<(), GitRemoteManagementError> {
2141 let section_ids_to_remove: Vec<_> = config
2142 .sections_by_name("remote")
2143 .into_iter()
2144 .flatten()
2145 .filter(|section| {
2146 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2147 })
2148 .map(|section| {
2149 if section.value_names().any(|name| {
2150 !name.eq_ignore_ascii_case(b"url")
2151 && !name.eq_ignore_ascii_case(b"fetch")
2152 && !name.eq_ignore_ascii_case(b"tagOpt")
2153 }) {
2154 return Err(GitRemoteManagementError::NonstandardConfiguration(
2155 remote_name.to_owned(),
2156 ));
2157 }
2158 Ok(section.id())
2159 })
2160 .try_collect()?;
2161 for id in section_ids_to_remove {
2162 config
2163 .remove_section_by_id(id)
2164 .expect("removed section to exist");
2165 }
2166 Ok(())
2167}
2168
2169pub fn get_all_remote_names(
2171 store: &Store,
2172) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2173 let git_repo = get_git_repo(store)?;
2174 Ok(iter_remote_names(&git_repo).collect())
2175}
2176
2177fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2178 git_repo
2179 .remote_names()
2180 .into_iter()
2181 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2183 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2185 .map(RemoteNameBuf::from)
2186}
2187
2188pub fn add_remote(
2189 mut_repo: &mut MutableRepo,
2190 remote_name: &RemoteName,
2191 url: &str,
2192 push_url: Option<&str>,
2193 fetch_tags: gix::remote::fetch::Tags,
2194 bookmark_expr: &StringExpression,
2195) -> Result<(), GitRemoteManagementError> {
2196 let git_repo = get_git_repo(mut_repo.store())?;
2197
2198 validate_remote_name(remote_name)?;
2199
2200 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2201 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2202 remote_name.to_owned(),
2203 ));
2204 }
2205
2206 let ref_expr = GitFetchRefExpression {
2207 bookmark: bookmark_expr.clone(),
2208 tag: StringExpression::none(),
2211 };
2212 let ExpandedFetchRefSpecs {
2213 expr: _,
2214 refspecs,
2215 negative_refspecs,
2216 } = expand_fetch_refspecs(remote_name, ref_expr)?;
2217 let fetch_refspecs = itertools::chain(
2218 refspecs.iter().map(|spec| spec.to_git_format()),
2219 negative_refspecs.iter().map(|spec| spec.to_git_format()),
2220 )
2221 .map(BString::from);
2222
2223 let mut remote = git_repo
2224 .remote_at(url)
2225 .map_err(GitRemoteManagementError::from_git)?
2226 .with_fetch_tags(fetch_tags)
2227 .with_refspecs(fetch_refspecs, gix::remote::Direction::Fetch)
2228 .expect("previously-parsed refspecs to be valid");
2229
2230 if let Some(push_url) = push_url {
2231 remote = remote
2232 .with_push_url(push_url)
2233 .map_err(GitRemoteManagementError::from_git)?;
2234 }
2235
2236 let mut config = git_repo.config_snapshot().clone();
2237 save_remote(&mut config, remote_name, &mut remote)?;
2238 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2239
2240 mut_repo.ensure_remote(remote_name);
2241
2242 Ok(())
2243}
2244
2245pub fn remove_remote(
2246 mut_repo: &mut MutableRepo,
2247 remote_name: &RemoteName,
2248) -> Result<(), GitRemoteManagementError> {
2249 let mut git_repo = get_git_repo(mut_repo.store())?;
2250
2251 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2252 return Err(GitRemoteManagementError::NoSuchRemote(
2253 remote_name.to_owned(),
2254 ));
2255 }
2256
2257 let mut config = git_repo.config_snapshot().clone();
2258 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2259 remove_remote_git_config_sections(&mut config, remote_name)?;
2260 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2261
2262 remove_remote_git_refs(&mut git_repo, remote_name)
2263 .map_err(GitRemoteManagementError::from_git)?;
2264
2265 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2266 remove_remote_refs(mut_repo, remote_name);
2267 }
2268
2269 Ok(())
2270}
2271
2272fn remove_remote_git_refs(
2273 git_repo: &mut gix::Repository,
2274 remote: &RemoteName,
2275) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2276 let bookmark_prefix = format!(
2277 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2278 remote = remote.as_str()
2279 );
2280 let tag_prefix = format!(
2281 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2282 remote = remote.as_str()
2283 );
2284 let edits: Vec<_> = itertools::chain(
2285 git_repo
2286 .references()?
2287 .prefixed(bookmark_prefix.as_str())?
2288 .map_ok(remove_ref),
2289 git_repo
2290 .references()?
2291 .prefixed(tag_prefix.as_str())?
2292 .map_ok(remove_ref),
2293 )
2294 .try_collect()?;
2295 git_repo.edit_references(edits)?;
2296 Ok(())
2297}
2298
2299fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2300 mut_repo.remove_remote(remote);
2301 let prefix = format!(
2302 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2303 remote = remote.as_str()
2304 );
2305 let git_refs_to_delete = mut_repo
2306 .view()
2307 .git_refs()
2308 .keys()
2309 .filter(|&r| r.as_str().starts_with(&prefix))
2310 .cloned()
2311 .collect_vec();
2312 for git_ref in git_refs_to_delete {
2313 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2314 }
2315}
2316
2317pub fn rename_remote(
2318 mut_repo: &mut MutableRepo,
2319 old_remote_name: &RemoteName,
2320 new_remote_name: &RemoteName,
2321) -> Result<(), GitRemoteManagementError> {
2322 let mut git_repo = get_git_repo(mut_repo.store())?;
2323
2324 validate_remote_name(new_remote_name)?;
2325
2326 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2327 return Err(GitRemoteManagementError::NoSuchRemote(
2328 old_remote_name.to_owned(),
2329 ));
2330 };
2331 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2332
2333 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2334 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2335 new_remote_name.to_owned(),
2336 ));
2337 }
2338
2339 match (
2340 remote.refspecs(gix::remote::Direction::Fetch),
2341 remote.refspecs(gix::remote::Direction::Push),
2342 ) {
2343 ([refspec], [])
2344 if refspec.to_ref().to_bstring()
2345 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2346 _ => {
2347 return Err(GitRemoteManagementError::NonstandardConfiguration(
2348 old_remote_name.to_owned(),
2349 ));
2350 }
2351 }
2352
2353 remote
2354 .replace_refspecs(
2355 [default_fetch_refspec(new_remote_name).as_bytes()],
2356 gix::remote::Direction::Fetch,
2357 )
2358 .expect("default refspec to be valid");
2359
2360 let mut config = git_repo.config_snapshot().clone();
2361 save_remote(&mut config, new_remote_name, &mut remote)?;
2362 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2363 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2364 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2365
2366 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2367 .map_err(GitRemoteManagementError::from_git)?;
2368
2369 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2370 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2371 }
2372
2373 Ok(())
2374}
2375
2376fn rename_remote_git_refs(
2377 git_repo: &mut gix::Repository,
2378 old_remote_name: &RemoteName,
2379 new_remote_name: &RemoteName,
2380) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2381 let to_prefixes = |namespace: &str| {
2382 (
2383 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2384 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2385 )
2386 };
2387 let to_rename_edits = {
2388 let ref_log_message = BString::from(format!(
2389 "renamed remote {old_remote_name} to {new_remote_name}",
2390 old_remote_name = old_remote_name.as_symbol(),
2391 new_remote_name = new_remote_name.as_symbol(),
2392 ));
2393 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2394 let new_name = BString::new(
2395 [
2396 new_prefix.as_bytes(),
2397 &old_ref.name().as_bstr()[old_prefix.len()..],
2398 ]
2399 .concat(),
2400 );
2401 [
2402 add_ref(
2403 new_name.try_into().expect("new ref name to be valid"),
2404 old_ref.target().into_owned(),
2405 ref_log_message.clone(),
2406 ),
2407 remove_ref(old_ref),
2408 ]
2409 }
2410 };
2411
2412 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2413 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2414 let edits: Vec<_> = itertools::chain(
2415 git_repo
2416 .references()?
2417 .prefixed(old_bookmark_prefix.as_str())?
2418 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2419 git_repo
2420 .references()?
2421 .prefixed(old_tag_prefix.as_str())?
2422 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2423 )
2424 .flatten_ok()
2425 .try_collect()?;
2426 git_repo.edit_references(edits)?;
2427 Ok(())
2428}
2429
2430pub fn set_remote_urls(
2434 store: &Store,
2435 remote_name: &RemoteName,
2436 new_url: Option<&str>,
2437 new_push_url: Option<&str>,
2438) -> Result<(), GitRemoteManagementError> {
2439 if new_url.is_none() && new_push_url.is_none() {
2441 return Ok(());
2442 }
2443
2444 let git_repo = get_git_repo(store)?;
2445
2446 validate_remote_name(remote_name)?;
2447
2448 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2449 return Err(GitRemoteManagementError::NoSuchRemote(
2450 remote_name.to_owned(),
2451 ));
2452 };
2453 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2454
2455 if let Some(url) = new_url {
2456 remote = remote
2457 .with_url(url)
2458 .map_err(GitRemoteManagementError::from_git)?;
2459 }
2460
2461 if let Some(url) = new_push_url {
2462 remote = remote
2463 .with_push_url(url)
2464 .map_err(GitRemoteManagementError::from_git)?;
2465 }
2466
2467 let mut config = git_repo.config_snapshot().clone();
2468 save_remote(&mut config, remote_name, &mut remote)?;
2469 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2470
2471 Ok(())
2472}
2473
2474fn rename_remote_refs(
2475 mut_repo: &mut MutableRepo,
2476 old_remote_name: &RemoteName,
2477 new_remote_name: &RemoteName,
2478) {
2479 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2480 let prefix = format!(
2481 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2482 remote = old_remote_name.as_str()
2483 );
2484 let git_refs = mut_repo
2485 .view()
2486 .git_refs()
2487 .iter()
2488 .filter_map(|(old, target)| {
2489 old.as_str().strip_prefix(&prefix).map(|p| {
2490 let new: GitRefNameBuf = format!(
2491 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2492 remote = new_remote_name.as_str()
2493 )
2494 .into();
2495 (old.clone(), new, target.clone())
2496 })
2497 })
2498 .collect_vec();
2499 for (old, new, target) in git_refs {
2500 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2501 mut_repo.set_git_ref_target(&new, target);
2502 }
2503}
2504
2505const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2506
2507#[derive(Error, Debug)]
2508pub enum GitFetchError {
2509 #[error("No git remote named '{}'", .0.as_symbol())]
2510 NoSuchRemote(RemoteNameBuf),
2511 #[error(transparent)]
2512 RemoteName(#[from] GitRemoteNameError),
2513 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2514 RejectedUpdates(Vec<GitRefNameBuf>),
2515 #[error(transparent)]
2516 Subprocess(#[from] GitSubprocessError),
2517}
2518
2519#[derive(Error, Debug)]
2520pub enum GitDefaultRefspecError {
2521 #[error("No git remote named '{}'", .0.as_symbol())]
2522 NoSuchRemote(RemoteNameBuf),
2523 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2524 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2525}
2526
2527struct FetchedRefs {
2528 remote: RemoteNameBuf,
2529 bookmark_matcher: StringMatcher,
2530 tag_matcher: StringMatcher,
2531}
2532
2533#[derive(Clone, Debug)]
2535pub struct GitFetchRefExpression {
2536 pub bookmark: StringExpression,
2538 pub tag: StringExpression,
2544}
2545
2546#[derive(Debug)]
2548pub struct ExpandedFetchRefSpecs {
2549 expr: GitFetchRefExpression,
2551 refspecs: Vec<RefSpec>,
2552 negative_refspecs: Vec<NegativeRefSpec>,
2553}
2554
2555#[derive(Error, Debug)]
2556pub enum GitRefExpansionError {
2557 #[error(transparent)]
2558 Expression(#[from] GitRefExpressionError),
2559 #[error(
2560 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2561 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2562 )]
2563 InvalidBranchPattern(StringPattern),
2564}
2565
2566pub fn expand_fetch_refspecs(
2568 remote: &RemoteName,
2569 expr: GitFetchRefExpression,
2570) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2571 let (positive_bookmarks, negative_bookmarks) =
2572 split_into_positive_negative_patterns(&expr.bookmark)?;
2573 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2574
2575 let refspecs = itertools::chain(
2576 positive_bookmarks
2577 .iter()
2578 .map(|&pattern| pattern_to_refspec_glob(pattern))
2579 .map_ok(|glob| {
2580 RefSpec::forced(
2581 format!("refs/heads/{glob}"),
2582 format!(
2583 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2584 remote = remote.as_str()
2585 ),
2586 )
2587 }),
2588 positive_tags
2589 .iter()
2590 .map(|&pattern| pattern_to_refspec_glob(pattern))
2591 .map_ok(|glob| {
2592 RefSpec::forced(
2593 format!("refs/tags/{glob}"),
2594 format!(
2595 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2596 remote = remote.as_str()
2597 ),
2598 )
2599 }),
2600 )
2601 .try_collect()?;
2602
2603 let negative_refspecs = itertools::chain(
2604 negative_bookmarks
2605 .iter()
2606 .map(|&pattern| pattern_to_refspec_glob(pattern))
2607 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2608 negative_tags
2609 .iter()
2610 .map(|&pattern| pattern_to_refspec_glob(pattern))
2611 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2612 )
2613 .try_collect()?;
2614
2615 Ok(ExpandedFetchRefSpecs {
2616 expr,
2617 refspecs,
2618 negative_refspecs,
2619 })
2620}
2621
2622fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2623 pattern
2624 .to_glob()
2625 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2628 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2629}
2630
2631#[derive(Debug, Error)]
2632pub enum GitRefExpressionError {
2633 #[error("Cannot use `~` in sub expression")]
2634 NestedNotIn,
2635 #[error("Cannot use `&` in sub expression")]
2636 NestedIntersection,
2637 #[error("Cannot use `&` for positive expressions")]
2638 PositiveIntersection,
2639}
2640
2641fn split_into_positive_negative_patterns(
2644 expr: &StringExpression,
2645) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2646 static ALL: StringPattern = StringPattern::all();
2647
2648 fn visit_positive<'a>(
2662 expr: &'a StringExpression,
2663 positives: &mut Vec<&'a StringPattern>,
2664 negatives: &mut Vec<&'a StringPattern>,
2665 ) -> Result<(), GitRefExpressionError> {
2666 match expr {
2667 StringExpression::Pattern(pattern) => {
2668 positives.push(pattern);
2669 Ok(())
2670 }
2671 StringExpression::NotIn(complement) => {
2672 positives.push(&ALL);
2673 visit_negative(complement, negatives)
2674 }
2675 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2676 StringExpression::Intersection(expr1, expr2) => {
2677 match (expr1.as_ref(), expr2.as_ref()) {
2678 (other, StringExpression::NotIn(complement))
2679 | (StringExpression::NotIn(complement), other) => {
2680 visit_positive(other, positives, negatives)?;
2681 visit_negative(complement, negatives)
2682 }
2683 _ => Err(GitRefExpressionError::PositiveIntersection),
2684 }
2685 }
2686 }
2687 }
2688
2689 fn visit_negative<'a>(
2690 expr: &'a StringExpression,
2691 negatives: &mut Vec<&'a StringPattern>,
2692 ) -> Result<(), GitRefExpressionError> {
2693 match expr {
2694 StringExpression::Pattern(pattern) => {
2695 negatives.push(pattern);
2696 Ok(())
2697 }
2698 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2699 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2700 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2701 }
2702 }
2703
2704 fn visit_union<'a>(
2705 expr1: &'a StringExpression,
2706 expr2: &'a StringExpression,
2707 patterns: &mut Vec<&'a StringPattern>,
2708 ) -> Result<(), GitRefExpressionError> {
2709 visit_union_sub(expr1, patterns)?;
2710 visit_union_sub(expr2, patterns)
2711 }
2712
2713 fn visit_union_sub<'a>(
2714 expr: &'a StringExpression,
2715 patterns: &mut Vec<&'a StringPattern>,
2716 ) -> Result<(), GitRefExpressionError> {
2717 match expr {
2718 StringExpression::Pattern(pattern) => {
2719 patterns.push(pattern);
2720 Ok(())
2721 }
2722 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2723 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2724 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2725 }
2726 }
2727
2728 let mut positives = Vec::new();
2729 let mut negatives = Vec::new();
2730 visit_positive(expr, &mut positives, &mut negatives)?;
2731 if positives.iter().all(|pattern| pattern.is_all())
2734 && !negatives.is_empty()
2735 && negatives.iter().all(|pattern| pattern.is_all())
2736 {
2737 Ok((vec![], vec![]))
2738 } else {
2739 Ok((positives, negatives))
2740 }
2741}
2742
2743#[derive(Debug)]
2747#[must_use = "warnings should be surfaced in the UI"]
2748pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2749
2750#[derive(Debug)]
2753pub struct IgnoredRefspec {
2754 pub refspec: BString,
2756 pub reason: &'static str,
2758}
2759
2760#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2761enum FetchRefSpecKind {
2762 Positive,
2763 Negative,
2764}
2765
2766pub fn load_default_fetch_bookmarks(
2768 remote_name: &RemoteName,
2769 git_repo: &gix::Repository,
2770) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2771 let remote = git_repo
2772 .try_find_remote(remote_name.as_str())
2773 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2774 .map_err(|e| {
2775 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2776 })?;
2777
2778 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2779 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2780 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2781 let mut negative_bookmarks = Vec::new();
2782 for refspec in remote_refspecs {
2783 let refspec = refspec.to_ref();
2784 match parse_fetch_refspec(remote_name, refspec) {
2785 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2786 positive_bookmarks.push(StringExpression::pattern(bookmark));
2787 }
2788 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2789 negative_bookmarks.push(StringExpression::pattern(bookmark));
2790 }
2791 Err(reason) => {
2792 let refspec = refspec.to_bstring();
2793 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2794 }
2795 }
2796 }
2797
2798 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2799 if !negative_bookmarks.is_empty() {
2801 bookmark_expr =
2802 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2803 }
2804
2805 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2806}
2807
2808fn parse_fetch_refspec(
2809 remote_name: &RemoteName,
2810 refspec: gix::refspec::RefSpecRef<'_>,
2811) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2812 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2813
2814 let (src, positive_dst) = match refspec.instruction() {
2815 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2816 Instruction::Fetch(fetch) => match fetch {
2817 gix::refspec::instruction::Fetch::Only { src: _ } => {
2818 return Err("fetch-only refspecs are not supported");
2819 }
2820 gix::refspec::instruction::Fetch::AndUpdate {
2821 src,
2822 dst,
2823 allow_non_fast_forward,
2824 } => {
2825 if !allow_non_fast_forward {
2826 return Err("non-forced refspecs are not supported");
2827 }
2828 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2829 }
2830 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2831 },
2832 };
2833
2834 let src_branch = src
2835 .strip_prefix("refs/heads/")
2836 .ok_or("only refs/heads/ is supported for refspec sources")?;
2837 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2838
2839 if let Some(dst) = positive_dst {
2840 let dst_without_prefix = dst
2841 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
2842 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2843 let dst_branch = dst_without_prefix
2844 .strip_prefix(remote_name.as_str())
2845 .and_then(|d| d.strip_prefix("/"))
2846 .ok_or("remote renaming not supported")?;
2847 if src_branch != dst_branch {
2848 return Err("renaming is not supported");
2849 }
2850 Ok((FetchRefSpecKind::Positive, branch))
2851 } else {
2852 Ok((FetchRefSpecKind::Negative, branch))
2853 }
2854}
2855
2856pub struct GitFetch<'a> {
2858 mut_repo: &'a mut MutableRepo,
2859 git_repo: Box<gix::Repository>,
2860 git_ctx: GitSubprocessContext,
2861 import_options: &'a GitImportOptions,
2862 fetched: Vec<FetchedRefs>,
2863}
2864
2865impl<'a> GitFetch<'a> {
2866 pub fn new(
2867 mut_repo: &'a mut MutableRepo,
2868 subprocess_options: GitSubprocessOptions,
2869 import_options: &'a GitImportOptions,
2870 ) -> Result<Self, UnexpectedGitBackendError> {
2871 let git_backend = get_git_backend(mut_repo.store())?;
2872 let git_repo = Box::new(git_backend.git_repo());
2873 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
2874 Ok(GitFetch {
2875 mut_repo,
2876 git_repo,
2877 git_ctx,
2878 import_options,
2879 fetched: vec![],
2880 })
2881 }
2882
2883 #[tracing::instrument(skip(self, callback))]
2889 pub fn fetch(
2890 &mut self,
2891 remote_name: &RemoteName,
2892 ExpandedFetchRefSpecs {
2893 expr,
2894 refspecs: mut remaining_refspecs,
2895 negative_refspecs,
2896 }: ExpandedFetchRefSpecs,
2897 callback: &mut dyn GitSubprocessCallback,
2898 depth: Option<NonZeroU32>,
2899 fetch_tags_override: Option<FetchTagsOverride>,
2900 ) -> Result<(), GitFetchError> {
2901 validate_remote_name(remote_name)?;
2902
2903 if self
2905 .git_repo
2906 .try_find_remote(remote_name.as_str())
2907 .is_none()
2908 {
2909 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2910 }
2911
2912 if remaining_refspecs.is_empty() {
2913 return Ok(());
2915 }
2916
2917 let mut branches_to_prune = Vec::new();
2918 let updates = loop {
2926 let status = self.git_ctx.spawn_fetch(
2927 remote_name,
2928 &remaining_refspecs,
2929 &negative_refspecs,
2930 callback,
2931 depth,
2932 fetch_tags_override,
2933 )?;
2934 let failing_refspec = match status {
2935 GitFetchStatus::Updates(updates) => break updates,
2936 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
2937 };
2938 tracing::debug!(failing_refspec, "failed to fetch ref");
2939 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2940
2941 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2942 branches_to_prune.push(format!(
2943 "{remote_name}/{branch_name}",
2944 remote_name = remote_name.as_str()
2945 ));
2946 }
2947 };
2948
2949 if !updates.rejected.is_empty() {
2952 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
2953 return Err(GitFetchError::RejectedUpdates(names));
2954 }
2955
2956 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2959
2960 self.fetched.push(FetchedRefs {
2961 remote: remote_name.to_owned(),
2962 bookmark_matcher: expr.bookmark.to_matcher(),
2963 tag_matcher: expr.tag.to_matcher(),
2964 });
2965 Ok(())
2966 }
2967
2968 #[tracing::instrument(skip(self))]
2970 pub fn get_default_branch(
2971 &self,
2972 remote_name: &RemoteName,
2973 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2974 if self
2975 .git_repo
2976 .try_find_remote(remote_name.as_str())
2977 .is_none()
2978 {
2979 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2980 }
2981 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2982 tracing::debug!(?default_branch);
2983 Ok(default_branch)
2984 }
2985
2986 #[tracing::instrument(skip(self))]
2993 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2994 tracing::debug!("import_refs");
2995 let all_remote_tags = true;
2996 let refs_to_import = diff_refs_to_import(
2997 self.mut_repo.view(),
2998 &self.git_repo,
2999 all_remote_tags,
3000 |kind, symbol| match kind {
3001 GitRefKind::Bookmark => self
3002 .fetched
3003 .iter()
3004 .filter(|fetched| fetched.remote == symbol.remote)
3005 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3006 GitRefKind::Tag => {
3007 symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
3011 || self
3012 .fetched
3013 .iter()
3014 .filter(|fetched| fetched.remote == symbol.remote)
3015 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str()))
3016 }
3017 },
3018 )?;
3019 let import_stats = import_refs_inner(self.mut_repo, refs_to_import, self.import_options)?;
3020
3021 self.fetched.clear();
3022
3023 Ok(import_stats)
3024 }
3025}
3026
3027#[derive(Error, Debug)]
3028pub enum GitPushError {
3029 #[error("No git remote named '{}'", .0.as_symbol())]
3030 NoSuchRemote(RemoteNameBuf),
3031 #[error(transparent)]
3032 RemoteName(#[from] GitRemoteNameError),
3033 #[error(transparent)]
3034 Subprocess(#[from] GitSubprocessError),
3035 #[error(transparent)]
3036 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3037}
3038
3039#[derive(Clone, Debug)]
3040pub struct GitBranchPushTargets {
3041 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
3042}
3043
3044pub struct GitRefUpdate {
3045 pub qualified_name: GitRefNameBuf,
3046 pub expected_current_target: Option<CommitId>,
3051 pub new_target: Option<CommitId>,
3052}
3053
3054#[derive(Clone, Debug, Default)]
3056pub struct GitPushOptions {
3057 pub extra_args: Vec<String>,
3059 pub remote_push_options: Vec<String>,
3061}
3062
3063pub fn push_branches(
3065 mut_repo: &mut MutableRepo,
3066 subprocess_options: GitSubprocessOptions,
3067 remote: &RemoteName,
3068 targets: &GitBranchPushTargets,
3069 callback: &mut dyn GitSubprocessCallback,
3070 options: &GitPushOptions,
3071) -> Result<GitPushStats, GitPushError> {
3072 validate_remote_name(remote)?;
3073
3074 let ref_updates = targets
3075 .branch_updates
3076 .iter()
3077 .map(|(name, update)| GitRefUpdate {
3078 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3079 expected_current_target: update.old_target.clone(),
3080 new_target: update.new_target.clone(),
3081 })
3082 .collect_vec();
3083
3084 let push_stats = push_updates(
3085 mut_repo,
3086 subprocess_options,
3087 remote,
3088 &ref_updates,
3089 callback,
3090 options,
3091 )?;
3092 tracing::debug!(?push_stats);
3093
3094 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3095 let pushed_branch_updates = || {
3096 iter::zip(&targets.branch_updates, &ref_updates)
3097 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3098 .map(|((name, update), _)| (name.as_ref(), update))
3099 };
3100
3101 let unexported_bookmarks = {
3104 let git_repo =
3105 get_git_repo(mut_repo.store()).expect("backend type should have been tested");
3106 let refs = build_pushed_bookmarks_to_export(remote, pushed_branch_updates());
3107 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3108 };
3109
3110 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3111 let is_exported_bookmark = |name: &RefName| {
3112 unexported_bookmarks
3113 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3114 .is_err()
3115 };
3116 for (name, update) in pushed_branch_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3117 let new_remote_ref = RemoteRef {
3118 target: RefTarget::resolved(update.new_target.clone()),
3119 state: RemoteRefState::Tracked,
3120 };
3121 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3122 }
3123
3124 assert!(push_stats.unexported_bookmarks.is_empty());
3128 let push_stats = GitPushStats {
3129 pushed: push_stats.pushed,
3130 rejected: push_stats.rejected,
3131 remote_rejected: push_stats.remote_rejected,
3132 unexported_bookmarks,
3133 };
3134 Ok(push_stats)
3135}
3136
3137pub fn push_updates(
3139 repo: &dyn Repo,
3140 subprocess_options: GitSubprocessOptions,
3141 remote_name: &RemoteName,
3142 updates: &[GitRefUpdate],
3143 callback: &mut dyn GitSubprocessCallback,
3144 options: &GitPushOptions,
3145) -> Result<GitPushStats, GitPushError> {
3146 let mut qualified_remote_refs_expected_locations = HashMap::new();
3147 let mut refspecs = vec![];
3148 for update in updates {
3149 qualified_remote_refs_expected_locations.insert(
3150 update.qualified_name.as_ref(),
3151 update.expected_current_target.as_ref(),
3152 );
3153 if let Some(new_target) = &update.new_target {
3154 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
3158 } else {
3159 refspecs.push(RefSpec::delete(&update.qualified_name));
3163 }
3164 }
3165
3166 let git_backend = get_git_backend(repo.store())?;
3167 let git_repo = git_backend.git_repo();
3168 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3169
3170 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3172 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3173 }
3174
3175 let refs_to_push: Vec<RefToPush> = refspecs
3176 .iter()
3177 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3178 .collect();
3179
3180 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3181 push_stats.pushed.sort();
3182 push_stats.rejected.sort();
3183 push_stats.remote_rejected.sort();
3184 Ok(push_stats)
3185}
3186
3187fn build_pushed_bookmarks_to_export<'a>(
3189 remote: &RemoteName,
3190 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a BookmarkPushUpdate)>,
3191) -> RefsToExport {
3192 let mut to_update = Vec::new();
3193 let mut to_delete = Vec::new();
3194 for (name, update) in pushed_updates {
3195 let symbol = name.to_remote_symbol(remote);
3196 match (update.old_target.as_ref(), update.new_target.as_ref()) {
3197 (old, Some(new)) => {
3198 let old_oid = old.map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
3199 let new_oid = gix::ObjectId::from_bytes_or_panic(new.as_bytes());
3200 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3201 }
3202 (Some(old), None) => {
3203 let old_oid = gix::ObjectId::from_bytes_or_panic(old.as_bytes());
3204 to_delete.push((symbol.to_owned(), old_oid));
3205 }
3206 (None, None) => panic!("old/new targets should differ"),
3207 }
3208 }
3209
3210 RefsToExport {
3211 to_update,
3212 to_delete,
3213 failed: vec![],
3214 }
3215}
3216
3217#[derive(Copy, Clone, Debug)]
3220pub enum FetchTagsOverride {
3221 AllTags,
3224 NoTags,
3227}
3228
3229#[cfg(test)]
3230mod tests {
3231 use assert_matches::assert_matches;
3232
3233 use super::*;
3234 use crate::revset;
3235 use crate::revset::RevsetDiagnostics;
3236
3237 #[test]
3238 fn test_split_positive_negative_patterns() {
3239 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3240 try_split(text).unwrap()
3241 }
3242
3243 fn try_split(
3244 text: &str,
3245 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3246 let mut diagnostics = RevsetDiagnostics::new();
3247 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3248 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3249 Ok((
3250 positives.into_iter().cloned().collect(),
3251 negatives.into_iter().cloned().collect(),
3252 ))
3253 }
3254
3255 insta::assert_compact_debug_snapshot!(
3256 split("a"),
3257 @r#"([Exact("a")], [])"#);
3258 insta::assert_compact_debug_snapshot!(
3259 split("~a"),
3260 @r#"([Substring("")], [Exact("a")])"#);
3261 insta::assert_compact_debug_snapshot!(
3262 split("~a~b"),
3263 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3264 insta::assert_compact_debug_snapshot!(
3265 split("~(a|b)"),
3266 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3267 insta::assert_compact_debug_snapshot!(
3268 split("a|b"),
3269 @r#"([Exact("a"), Exact("b")], [])"#);
3270 insta::assert_compact_debug_snapshot!(
3271 split("(a|b)&~c"),
3272 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3273 insta::assert_compact_debug_snapshot!(
3274 split("~a&b"),
3275 @r#"([Exact("b")], [Exact("a")])"#);
3276 insta::assert_compact_debug_snapshot!(
3277 split("a&~b&~c"),
3278 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3279 insta::assert_compact_debug_snapshot!(
3280 split("~a&b&~c"),
3281 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3282 insta::assert_compact_debug_snapshot!(
3283 split("a&~(b|c)"),
3284 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3285 insta::assert_compact_debug_snapshot!(
3286 split("((a|b)|c)&~(d|(e|f))"),
3287 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3288 assert_matches!(
3289 try_split("a&b"),
3290 Err(GitRefExpressionError::PositiveIntersection)
3291 );
3292 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3293 assert_matches!(
3294 try_split("a&~(b&~c)"),
3295 Err(GitRefExpressionError::NestedIntersection)
3296 );
3297 assert_matches!(
3298 try_split("(a|b)&c"),
3299 Err(GitRefExpressionError::PositiveIntersection)
3300 );
3301 assert_matches!(
3302 try_split("(a&~b)&(~c&~d)"),
3303 Err(GitRefExpressionError::PositiveIntersection)
3304 );
3305 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3306 assert_matches!(
3307 try_split("a&~b|c&~d"),
3308 Err(GitRefExpressionError::NestedIntersection)
3309 );
3310
3311 insta::assert_compact_debug_snapshot!(
3314 split("*"),
3315 @r#"([Glob(GlobPattern("*"))], [])"#);
3316 insta::assert_compact_debug_snapshot!(
3317 split("~*"),
3318 @"([], [])");
3319 insta::assert_compact_debug_snapshot!(
3320 split("a~*"),
3321 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3322 insta::assert_compact_debug_snapshot!(
3323 split("~(a|*)"),
3324 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3325 }
3326}