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 futures::TryStreamExt as _;
33use gix::refspec::Instruction;
34use itertools::Itertools as _;
35use thiserror::Error;
36
37use crate::backend::BackendError;
38use crate::backend::ChangeId;
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::merge::Diff;
55use crate::merged_tree::MergedTree;
56use crate::merged_tree::TreeDiffEntry;
57use crate::object_id::ObjectId as _;
58use crate::op_store::RefTarget;
59use crate::op_store::RefTargetOptionExt as _;
60use crate::op_store::RemoteRef;
61use crate::op_store::RemoteRefState;
62use crate::ref_name::GitRefName;
63use crate::ref_name::GitRefNameBuf;
64use crate::ref_name::RefName;
65use crate::ref_name::RefNameBuf;
66use crate::ref_name::RemoteName;
67use crate::ref_name::RemoteNameBuf;
68use crate::ref_name::RemoteRefSymbol;
69use crate::ref_name::RemoteRefSymbolBuf;
70use crate::repo::MutableRepo;
71use crate::repo::Repo;
72use crate::repo_path::RepoPath;
73use crate::revset::RevsetEvaluationError;
74use crate::revset::RevsetExpression;
75use crate::revset::RevsetStreamExt as _;
76use crate::settings::UserSettings;
77use crate::store::Store;
78use crate::str_util::StringExpression;
79use crate::str_util::StringMatcher;
80use crate::str_util::StringPattern;
81use crate::view::View;
82
83pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
85pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
87const REMOTE_BOOKMARK_REF_NAMESPACE: &str = "refs/remotes/";
89const REMOTE_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";
91const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
93const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
96
97#[derive(Clone, Debug)]
98pub struct GitSettings {
99 pub abandon_unreachable_commits: bool,
100 pub executable_path: PathBuf,
101 pub record_synthetic_predecessors: bool,
102 pub write_change_id_header: bool,
103}
104
105impl GitSettings {
106 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
107 Ok(Self {
108 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
109 executable_path: settings.get("git.executable-path")?,
110 record_synthetic_predecessors: settings
111 .get_bool("git.record-synthetic-predecessors")?,
112 write_change_id_header: settings.get("git.write-change-id-header")?,
113 })
114 }
115
116 pub fn to_subprocess_options(&self) -> GitSubprocessOptions {
117 GitSubprocessOptions {
118 executable_path: self.executable_path.clone(),
119 environment: HashMap::new(),
120 }
121 }
122}
123
124#[derive(Clone, Debug)]
126pub struct GitSubprocessOptions {
127 pub executable_path: PathBuf,
128 pub environment: HashMap<OsString, OsString>,
133}
134
135impl GitSubprocessOptions {
136 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
137 Ok(Self {
138 executable_path: settings.get("git.executable-path")?,
139 environment: HashMap::new(),
140 })
141 }
142}
143
144#[derive(Debug, Error)]
145pub enum GitRemoteNameError {
146 #[error(
147 "Git remote named '{name}' is reserved for local Git repository",
148 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
149 )]
150 ReservedForLocalGitRepo,
151 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
152 WithSlash(RemoteNameBuf),
153 #[error("Invalid Git remote name")]
154 InvalidName(#[from] gix::remote::name::Error),
155}
156
157fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
158 gix::remote::name::validated(name.as_str())?;
159 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
160 Err(GitRemoteNameError::ReservedForLocalGitRepo)
161 } else if name.as_str().contains('/') {
162 Err(GitRemoteNameError::WithSlash(name.to_owned()))
163 } else {
164 Ok(())
165 }
166}
167
168fn oid_from_commit_id(id: &CommitId) -> &gix::oid {
170 gix::oid::from_bytes_unchecked(id.as_bytes())
171}
172
173fn owned_oid_from_commit_id(id: &CommitId) -> gix::ObjectId {
175 gix::ObjectId::from_bytes_or_panic(id.as_bytes())
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum GitRefKind {
181 Bookmark,
182 Tag,
183}
184
185#[derive(Debug, Default)]
187pub struct GitPushStats {
188 pub pushed: Vec<GitRefNameBuf>,
190 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
192 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
194 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
196}
197
198impl GitPushStats {
199 pub fn all_ok(&self) -> bool {
200 self.rejected.is_empty()
201 && self.remote_rejected.is_empty()
202 && self.unexported_bookmarks.is_empty()
203 }
204
205 pub fn some_exported(&self) -> bool {
208 self.pushed.len() > self.unexported_bookmarks.len()
209 }
210}
211
212#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
216struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
217
218impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
219 fn borrow(&self) -> &RemoteRefSymbol<'b> {
220 &self.0
221 }
222}
223
224#[derive(Debug, Hash, PartialEq, Eq)]
230pub(crate) struct RefSpec {
231 forced: bool,
232 source: Option<String>,
235 destination: String,
236}
237
238impl RefSpec {
239 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
240 Self {
241 forced: true,
242 source: Some(source.into()),
243 destination: destination.into(),
244 }
245 }
246
247 fn delete(destination: impl Into<String>) -> Self {
248 Self {
250 forced: false,
251 source: None,
252 destination: destination.into(),
253 }
254 }
255
256 pub(crate) fn to_git_format(&self) -> String {
257 format!(
258 "{}{}",
259 if self.forced { "+" } else { "" },
260 self.to_git_format_not_forced()
261 )
262 }
263
264 pub(crate) fn to_git_format_not_forced(&self) -> String {
270 if let Some(s) = &self.source {
271 format!("{}:{}", s, self.destination)
272 } else {
273 format!(":{}", self.destination)
274 }
275 }
276}
277
278#[derive(Debug)]
280#[repr(transparent)]
281pub(crate) struct NegativeRefSpec {
282 source: String,
283}
284
285impl NegativeRefSpec {
286 fn new(source: impl Into<String>) -> Self {
287 Self {
288 source: source.into(),
289 }
290 }
291
292 pub(crate) fn to_git_format(&self) -> String {
293 format!("^{}", self.source)
294 }
295}
296
297pub(crate) struct RefToPush<'a> {
300 pub(crate) refspec: &'a RefSpec,
301 pub(crate) expected_location: Option<&'a gix::oid>,
302}
303
304impl<'a> RefToPush<'a> {
305 fn new(
306 refspec: &'a RefSpec,
307 expected_locations: &'a HashMap<&GitRefName, Option<&gix::oid>>,
308 ) -> Self {
309 let expected_location = *expected_locations
310 .get(GitRefName::new(&refspec.destination))
311 .expect(
312 "The refspecs and the expected locations were both constructed from the same \
313 source of truth. This means the lookup should always work.",
314 );
315
316 Self {
317 refspec,
318 expected_location,
319 }
320 }
321
322 pub(crate) fn to_git_lease(&self) -> String {
323 format!(
324 "{}:{}",
325 self.refspec.destination,
326 self.expected_location
327 .map(|x| x.to_string())
328 .as_deref()
329 .unwrap_or("")
330 )
331 }
332}
333
334pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
337 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
338 if name == "HEAD" {
340 return None;
341 }
342 let name = RefName::new(name);
343 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
344 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
345 } else if let Some(remote_and_name) = full_name
346 .as_str()
347 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
348 {
349 let (remote, name) = remote_and_name.split_once('/')?;
350 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
352 return None;
353 }
354 let name = RefName::new(name);
355 let remote = RemoteName::new(remote);
356 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
357 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
358 let name = RefName::new(name);
359 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
360 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
361 } else {
362 None
363 }
364}
365
366fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
367 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
368 let (remote, name) = remote_and_name.split_once('/')?;
369 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
370 return None;
371 }
372 let name = RefName::new(name);
373 let remote = RemoteName::new(remote);
374 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
375}
376
377fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
378 let RemoteRefSymbol { name, remote } = symbol;
379 let name = name.as_str();
380 let remote = remote.as_str();
381 if name.is_empty() || remote.is_empty() {
382 return None;
383 }
384 match kind {
385 GitRefKind::Bookmark => {
386 if name == "HEAD" {
387 return None;
388 }
389 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
390 Some(format!("refs/heads/{name}").into())
391 } else {
392 Some(format!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())
393 }
394 }
395 GitRefKind::Tag => {
396 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
398 }
399 }
400}
401
402fn to_git_or_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> GitRefNameBuf {
403 let RemoteRefSymbol { name, remote } = symbol;
404 let name = name.as_str();
405 let remote = remote.as_str();
406 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
407 format!("refs/tags/{name}").into()
408 } else {
409 format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}").into()
410 }
411}
412
413#[derive(Debug, Error)]
414#[error("The repo is not backed by a Git repo")]
415pub struct UnexpectedGitBackendError;
416
417pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
419 store.backend_impl().ok_or(UnexpectedGitBackendError)
420}
421
422pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
424 get_git_backend(store).map(|backend| backend.git_repo())
425}
426
427fn resolve_git_ref_to_commit_id(
432 git_ref: &gix::Reference,
433 known_commit_oid: Option<&gix::oid>,
434) -> Option<gix::ObjectId> {
435 let mut peeling_ref = Cow::Borrowed(git_ref);
436
437 if let Some(known_oid) = known_commit_oid {
439 let raw_ref = &git_ref.inner;
440 if let Some(oid) = raw_ref.target.try_id()
441 && oid == known_oid
442 {
443 return Some(oid.to_owned());
444 }
445 if let Some(oid) = raw_ref.peeled
446 && oid == known_oid
447 {
448 return Some(oid);
451 }
452 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
456 let maybe_tag = git_ref
457 .try_id()
458 .and_then(|id| id.object().ok())
459 .and_then(|object| object.try_into_tag().ok());
460 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
461 let oid = oid.detach();
462 if oid == known_oid {
463 return Some(oid);
465 }
466 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
469 }
470 }
471 }
472
473 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
477 let is_commit = peeled_id
478 .object()
479 .is_ok_and(|object| object.kind.is_commit());
480 is_commit.then_some(peeled_id.detach())
481}
482
483#[derive(Error, Debug)]
484pub enum GitImportError {
485 #[error("Failed to read Git HEAD target commit {id}")]
486 MissingHeadTarget {
487 id: CommitId,
488 #[source]
489 err: BackendError,
490 },
491 #[error("Ancestor of Git ref {symbol} is missing")]
492 MissingRefAncestor {
493 symbol: RemoteRefSymbolBuf,
494 #[source]
495 err: BackendError,
496 },
497 #[error(transparent)]
498 Backend(#[from] BackendError),
499 #[error(transparent)]
500 Index(#[from] IndexError),
501 #[error(transparent)]
502 RevsetEvaluation(#[from] RevsetEvaluationError),
503 #[error(transparent)]
504 Git(Box<dyn std::error::Error + Send + Sync>),
505 #[error(transparent)]
506 UnexpectedBackend(#[from] UnexpectedGitBackendError),
507}
508
509impl GitImportError {
510 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
511 Self::Git(source.into())
512 }
513}
514
515#[derive(Debug)]
517pub struct GitImportOptions {
518 pub abandon_unreachable_commits: bool,
520 pub record_synthetic_predecessors: bool,
522 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
524}
525
526#[derive(Clone, Debug, Eq, PartialEq, Default)]
528pub struct GitImportStats {
529 pub abandoned_commits: Vec<Commit>,
531 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
534 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
537 pub failed_ref_names: Vec<BString>,
542}
543
544#[derive(Debug)]
545struct RefsToImport {
546 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
549 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
552 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
555 failed_ref_names: Vec<BString>,
557}
558
559pub async fn import_refs(
564 mut_repo: &mut MutableRepo,
565 options: &GitImportOptions,
566) -> Result<GitImportStats, GitImportError> {
567 import_some_refs(mut_repo, options, |_, _| true).await
568}
569
570pub async fn import_some_refs(
575 mut_repo: &mut MutableRepo,
576 options: &GitImportOptions,
577 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
578) -> Result<GitImportStats, GitImportError> {
579 let git_repo = get_git_repo(mut_repo.store())?;
580
581 for remote_name in iter_remote_names(&git_repo) {
585 mut_repo.ensure_remote(&remote_name);
586 }
587
588 let all_remote_tags = false;
590 let refs_to_import =
591 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
592 import_refs_inner(mut_repo, refs_to_import, options).await
593}
594
595async fn import_refs_inner(
596 mut_repo: &mut MutableRepo,
597 refs_to_import: RefsToImport,
598 options: &GitImportOptions,
599) -> Result<GitImportStats, GitImportError> {
600 let store = mut_repo.store();
601 let git_backend = get_git_backend(store).expect("backend type should have been tested");
602
603 let RefsToImport {
604 changed_git_refs,
605 changed_remote_bookmarks,
606 changed_remote_tags,
607 failed_ref_names,
608 } = refs_to_import;
609
610 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
611 let (old_referenced_heads, new_referenced_heads) = {
613 let mut old_heads = Vec::new();
614 let mut new_heads = Vec::new();
615 for (_, (old_remote_ref, new_target)) in iter_changed_refs() {
616 old_heads.extend(old_remote_ref.target.added_ids().cloned());
617 new_heads.extend(new_target.added_ids().cloned());
618 }
619 (old_heads, new_heads)
620 };
621 let old_visible_heads = mut_repo.view().heads().iter().cloned().collect_vec();
622
623 let index = mut_repo.index();
629 let missing_head_ids: Vec<&CommitId> = new_referenced_heads
630 .iter()
631 .filter_map(|id| match index.has_id(id) {
632 Ok(false) => Some(Ok(id)),
633 Ok(true) => None,
634 Err(e) => Some(Err(e)),
635 })
636 .try_collect()?;
637 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
638
639 let mut head_commits = Vec::new();
641 let get_commit = async |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
642 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
643 symbol: symbol.clone(),
644 err,
645 };
646 if !heads_imported && !index.has_id(id)? {
648 git_backend
649 .import_head_commits([id])
650 .map_err(missing_ref_err)?;
651 }
652 store.get_commit_async(id).await.map_err(missing_ref_err)
653 };
654 for (symbol, (_, new_target)) in iter_changed_refs() {
657 for id in new_target.added_ids() {
658 let commit = get_commit(id, symbol).await?;
659 head_commits.push(commit);
660 }
661 }
662 let imported_commits = mut_repo.index_commits(&head_commits).await?;
665 mut_repo.add_heads(&head_commits).await?;
666
667 for (full_name, new_target) in changed_git_refs {
669 mut_repo.set_git_ref_target(&full_name, new_target);
670 }
671 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
672 let symbol = symbol.as_ref();
673 let base_target = old_remote_ref.tracked_target();
674 let new_remote_ref = RemoteRef {
675 target: new_target.clone(),
676 state: if old_remote_ref != RemoteRef::absent_ref() {
677 old_remote_ref.state
678 } else {
679 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
680 },
681 };
682 if new_remote_ref.is_tracked() {
683 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
684 }
685 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
688 }
689 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
690 let symbol = symbol.as_ref();
691 let base_target = old_remote_ref.tracked_target();
692 let new_remote_ref = RemoteRef {
693 target: new_target.clone(),
694 state: if old_remote_ref != RemoteRef::absent_ref() {
695 old_remote_ref.state
696 } else {
697 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
698 },
699 };
700 if new_remote_ref.is_tracked() {
701 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
702 }
703 mut_repo.set_remote_tag(symbol, new_remote_ref);
706 }
707
708 let abandoned_commits = if options.abandon_unreachable_commits {
709 abandon_unreachable_commits(mut_repo, old_referenced_heads).await?
710 } else {
711 vec![]
712 };
713 if options.record_synthetic_predecessors {
714 record_synthetic_predecessors(
715 mut_repo,
716 old_visible_heads,
717 new_referenced_heads,
718 &abandoned_commits,
719 &imported_commits,
720 )
721 .await?;
722 }
723 let stats = GitImportStats {
724 abandoned_commits,
725 changed_remote_bookmarks,
726 changed_remote_tags,
727 failed_ref_names,
728 };
729 Ok(stats)
730}
731
732async fn abandon_unreachable_commits(
735 mut_repo: &mut MutableRepo,
736 hidable_git_heads: Vec<CommitId>,
737) -> Result<Vec<Commit>, GitImportError> {
738 if hidable_git_heads.is_empty() {
739 return Ok(vec![]);
740 }
741 let pinned_expression = RevsetExpression::union_all(&[
742 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
744 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
745 .intersection(&RevsetExpression::visible_heads().ancestors()),
747 RevsetExpression::root(),
748 ]);
749 let abandoned_expression = pinned_expression
750 .range(&RevsetExpression::commits(hidable_git_heads))
751 .intersection(&RevsetExpression::visible_heads().ancestors());
753 let abandoned_commits: Vec<_> = abandoned_expression
754 .evaluate(mut_repo)?
755 .stream()
756 .commits(mut_repo.store())
757 .try_collect()
758 .await?;
759 for commit in &abandoned_commits {
760 mut_repo.record_abandoned_commit(commit);
761 }
762 Ok(abandoned_commits)
763}
764
765async fn record_synthetic_predecessors(
771 mut_repo: &mut MutableRepo,
772 old_visible_heads: Vec<CommitId>,
773 new_referenced_heads: Vec<CommitId>,
774 abandoned_commits: &[Commit],
775 imported_commits: &[Commit],
776) -> Result<(), GitImportError> {
777 if new_referenced_heads.is_empty() {
778 return Ok(());
779 }
780
781 let new_referenced_change_to_commit_ids = {
782 let mut change_to_commit_ids: HashMap<ChangeId, Vec<CommitId>> = HashMap::new();
783 let mut stream = RevsetExpression::commits(old_visible_heads)
784 .range(&RevsetExpression::commits(new_referenced_heads))
785 .evaluate(mut_repo)?
786 .commit_change_ids();
787 while let Some((commit_id, change_id)) = stream.try_next().await? {
788 let commit_ids = change_to_commit_ids.entry(change_id).or_default();
789 commit_ids.push(commit_id);
790 }
791 change_to_commit_ids
792 };
793 let abandoned_change_to_commit_ids = {
797 let mut change_to_commit_ids: HashMap<&ChangeId, Vec<&CommitId>> = HashMap::new();
798 for commit in abandoned_commits {
799 let commit_ids = change_to_commit_ids.entry(commit.change_id()).or_default();
800 commit_ids.push(commit.id());
801 }
802 change_to_commit_ids
803 };
804 let imported_commit_ids: HashSet<_> = imported_commits.iter().map(Commit::id).collect();
805 debug_assert!(
806 new_referenced_change_to_commit_ids
807 .values()
808 .flatten()
809 .all(|new_id| abandoned_commits.iter().all(|old| old.id() != new_id)),
810 "new referenced commits should never be reachable from old refs"
811 );
812
813 for (change_id, new_commit_ids) in &new_referenced_change_to_commit_ids {
814 let predecessor_id: Option<CommitId>;
815 let rewrite_source_ids: &[&CommitId];
816 if let Some(old_commit_ids) = abandoned_change_to_commit_ids.get(change_id) {
817 predecessor_id = Some(old_commit_ids[0].clone());
820 rewrite_source_ids = old_commit_ids;
821 } else {
822 predecessor_id = None;
824 rewrite_source_ids = &[];
825 }
826 for new_commit_id in new_commit_ids
831 .iter()
832 .filter(|&id| imported_commit_ids.contains(id))
833 {
834 mut_repo.set_predecessors(new_commit_id.clone(), predecessor_id.as_slice().to_vec());
835 }
836 if let [new_commit_id] = &**new_commit_ids {
837 for &old_commit_id in rewrite_source_ids {
838 mut_repo.set_rewritten_commit(old_commit_id.clone(), new_commit_id.clone());
839 }
840 } else {
841 for &old_commit_id in rewrite_source_ids {
842 mut_repo
843 .set_divergent_rewrite(old_commit_id.clone(), new_commit_ids.iter().cloned());
844 }
845 }
846 }
847
848 Ok(())
849}
850
851fn diff_refs_to_import(
853 view: &View,
854 git_repo: &gix::Repository,
855 all_remote_tags: bool,
856 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
857) -> Result<RefsToImport, GitImportError> {
858 let mut known_git_refs = view
859 .git_refs()
860 .iter()
861 .filter_map(|(full_name, target)| {
862 let (kind, symbol) =
864 parse_git_ref(full_name).expect("stored git ref should be parsable");
865 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
866 })
867 .collect();
868 let mut known_remote_bookmarks = view
869 .all_remote_bookmarks()
870 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
871 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
872 .collect();
873 let mut known_remote_tags = if all_remote_tags {
874 view.all_remote_tags()
875 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
876 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
877 .collect()
878 } else {
879 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
880 view.remote_tags(remote)
881 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
882 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
883 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
884 .collect()
885 };
886
887 let mut changed_git_refs = Vec::new();
892 let mut changed_remote_bookmarks = Vec::new();
893 let mut changed_remote_tags = Vec::new();
894 let mut failed_ref_names = Vec::new();
895 let actual = git_repo.references().map_err(GitImportError::from_git)?;
896 collect_changed_refs_to_import(
897 actual.local_branches().map_err(GitImportError::from_git)?,
898 &mut known_git_refs,
899 &mut known_remote_bookmarks,
900 &mut changed_git_refs,
901 &mut changed_remote_bookmarks,
902 &mut failed_ref_names,
903 &git_ref_filter,
904 )?;
905 collect_changed_refs_to_import(
906 actual.remote_branches().map_err(GitImportError::from_git)?,
907 &mut known_git_refs,
908 &mut known_remote_bookmarks,
909 &mut changed_git_refs,
910 &mut changed_remote_bookmarks,
911 &mut failed_ref_names,
912 &git_ref_filter,
913 )?;
914 collect_changed_refs_to_import(
915 actual.tags().map_err(GitImportError::from_git)?,
916 &mut known_git_refs,
917 &mut known_remote_tags,
918 &mut changed_git_refs,
919 &mut changed_remote_tags,
920 &mut failed_ref_names,
921 &git_ref_filter,
922 )?;
923 if all_remote_tags {
924 collect_changed_remote_tags_to_import(
925 actual
926 .prefixed(REMOTE_TAG_REF_NAMESPACE)
927 .map_err(GitImportError::from_git)?,
928 &mut known_remote_tags,
929 &mut changed_remote_tags,
930 &mut failed_ref_names,
931 &git_ref_filter,
932 )?;
933 }
934 for full_name in known_git_refs.into_keys() {
935 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
936 }
937 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
938 if old.is_present() {
939 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
940 }
941 }
942 for (RemoteRefKey(symbol), old) in known_remote_tags {
943 if old.is_present() {
944 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
945 }
946 }
947
948 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
950 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
951 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
952 failed_ref_names.sort_unstable();
953 Ok(RefsToImport {
954 changed_git_refs,
955 changed_remote_bookmarks,
956 changed_remote_tags,
957 failed_ref_names,
958 })
959}
960
961fn collect_changed_refs_to_import(
962 actual_git_refs: gix::reference::iter::Iter,
963 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
964 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
965 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
966 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
967 failed_ref_names: &mut Vec<BString>,
968 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
969) -> Result<(), GitImportError> {
970 for git_ref in actual_git_refs {
971 let git_ref = git_ref.map_err(GitImportError::from_git)?;
972 let full_name_bytes = git_ref.name().as_bstr();
973 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
974 failed_ref_names.push(full_name_bytes.to_owned());
976 continue;
977 };
978 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
979 failed_ref_names.push(full_name_bytes.to_owned());
980 continue;
981 }
982 let full_name = GitRefName::new(full_name);
983 let Some((kind, symbol)) = parse_git_ref(full_name) else {
984 continue;
986 };
987 if !git_ref_filter(kind, symbol) {
988 continue;
989 }
990 let old_git_target = known_git_refs.get(full_name).copied().flatten();
991 let old_git_oid = old_git_target.as_normal().map(oid_from_commit_id);
992 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
993 continue;
995 };
996 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
997 known_git_refs.remove(full_name);
998 if new_target != *old_git_target {
999 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
1000 }
1001 let old_remote_ref = known_remote_refs
1004 .remove(&symbol)
1005 .unwrap_or_else(|| RemoteRef::absent_ref());
1006 if new_target != old_remote_ref.target {
1007 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
1008 }
1009 }
1010 Ok(())
1011}
1012
1013fn collect_changed_remote_tags_to_import(
1016 actual_git_refs: gix::reference::iter::Iter,
1017 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
1018 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
1019 failed_ref_names: &mut Vec<BString>,
1020 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1021) -> Result<(), GitImportError> {
1022 for git_ref in actual_git_refs {
1023 let git_ref = git_ref.map_err(GitImportError::from_git)?;
1024 let full_name_bytes = git_ref.name().as_bstr();
1025 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
1026 failed_ref_names.push(full_name_bytes.to_owned());
1028 continue;
1029 };
1030 let full_name = GitRefName::new(full_name);
1031 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
1032 continue;
1034 };
1035 if !git_ref_filter(kind, symbol) {
1036 continue;
1037 }
1038 let old_remote_ref = known_remote_refs
1039 .get(&symbol)
1040 .copied()
1041 .unwrap_or_else(|| RemoteRef::absent_ref());
1042 let old_git_oid = old_remote_ref.target.as_normal().map(oid_from_commit_id);
1043 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
1044 continue;
1046 };
1047 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
1048 known_remote_refs.remove(&symbol);
1049 if new_target != old_remote_ref.target {
1050 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
1051 }
1052 }
1053 Ok(())
1054}
1055
1056fn default_remote_ref_state_for(
1057 kind: GitRefKind,
1058 symbol: RemoteRefSymbol<'_>,
1059 options: &GitImportOptions,
1060) -> RemoteRefState {
1061 match kind {
1062 GitRefKind::Bookmark => {
1063 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1064 || options
1065 .remote_auto_track_bookmarks
1066 .get(symbol.remote)
1067 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
1068 {
1069 RemoteRefState::Tracked
1070 } else {
1071 RemoteRefState::New
1072 }
1073 }
1074 GitRefKind::Tag => RemoteRefState::Tracked,
1076 }
1077}
1078
1079fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
1085 itertools::chain(view.local_bookmarks(), view.local_tags())
1086 .flat_map(|(_, target)| target.added_ids())
1087 .cloned()
1088 .collect()
1089}
1090
1091fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
1098 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
1099 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
1100 .map(|(_, remote_ref)| &remote_ref.target)
1101 .flat_map(|target| target.added_ids())
1102 .cloned()
1103 .collect()
1104}
1105
1106pub async fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
1114 let store = mut_repo.store();
1115 let git_backend = get_git_backend(store)?;
1116 let git_repo = git_backend.git_repo();
1117
1118 let old_git_head = mut_repo.view().git_head();
1119 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1120 Some(CommitId::from_bytes(oid.as_bytes()))
1121 } else {
1122 None
1123 };
1124 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1125 return Ok(());
1126 }
1127
1128 if let Some(head_id) = &new_git_head_id {
1130 let index = mut_repo.index();
1131 if !index.has_id(head_id)? {
1132 git_backend.import_head_commits([head_id]).map_err(|err| {
1133 GitImportError::MissingHeadTarget {
1134 id: head_id.clone(),
1135 err,
1136 }
1137 })?;
1138 }
1139 let commit = store.get_commit_async(head_id).await?;
1142 mut_repo.add_head(&commit).await?;
1143 }
1144
1145 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1146 Ok(())
1147}
1148
1149#[derive(Error, Debug)]
1150pub enum GitExportError {
1151 #[error(transparent)]
1152 Git(Box<dyn std::error::Error + Send + Sync>),
1153 #[error(transparent)]
1154 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1155}
1156
1157impl GitExportError {
1158 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1159 Self::Git(source.into())
1160 }
1161}
1162
1163#[derive(Debug, Error)]
1165pub enum FailedRefExportReason {
1166 #[error("Name is not allowed in Git")]
1168 InvalidGitName,
1169 #[error("Ref was in a conflicted state from the last import")]
1172 ConflictedOldState,
1173 #[error("Ref cannot point to the root commit in Git")]
1175 OnRootCommit,
1176 #[error("Deleted ref had been modified in Git")]
1178 DeletedInJjModifiedInGit,
1179 #[error("Added ref had been added with a different target in Git")]
1181 AddedInJjAddedInGit,
1182 #[error("Modified ref had been deleted in Git")]
1184 ModifiedInJjDeletedInGit,
1185 #[error("Failed to delete")]
1187 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1188 #[error("Failed to set")]
1190 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1191}
1192
1193#[derive(Debug)]
1195pub struct GitExportStats {
1196 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1198 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1202}
1203
1204#[derive(Debug)]
1205struct AllRefsToExport {
1206 bookmarks: RefsToExport,
1207 tags: RefsToExport,
1208}
1209
1210#[derive(Debug)]
1211struct RefsToExport {
1212 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1214 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1219 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1221}
1222
1223pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1232 export_some_refs(mut_repo, |_, _| true)
1233}
1234
1235pub fn export_some_refs(
1236 mut_repo: &mut MutableRepo,
1237 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1238) -> Result<GitExportStats, GitExportError> {
1239 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1240 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1241 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1242 let (_, value) = &map[index];
1243 Some(value)
1244 }
1245
1246 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1247 mut_repo.view(),
1248 mut_repo.store().root_commit_id(),
1249 &git_ref_filter,
1250 );
1251
1252 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1253 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1254 return Ok(());
1255 };
1256 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1257 if let Some((kind, symbol)) = target_name
1258 .as_ref()
1259 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1260 .and_then(|name| parse_git_ref(name.as_ref()))
1261 {
1262 let old_target = head_ref.inner.target.clone();
1263 let current_oid = match head_ref.into_fully_peeled_id() {
1264 Ok(id) => Some(id.detach()),
1265 Err(gix::reference::peel::Error::ToId(
1266 gix::refs::peel::to_id::Error::FollowToObject(
1267 gix::refs::peel::to_object::Error::Follow(
1268 gix::refs::file::find::existing::Error::NotFound { .. },
1269 ),
1270 ),
1271 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1273 };
1274 let refs = match kind {
1275 GitRefKind::Bookmark => &bookmarks,
1276 GitRefKind::Tag => &tags,
1277 };
1278 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1279 Some(new_oid)
1280 } else if get(&refs.to_delete, symbol).is_some() {
1281 None
1282 } else {
1283 current_oid.as_ref()
1284 };
1285 if new_oid != current_oid.as_ref() {
1286 update_git_head(
1287 git_repo,
1288 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1289 current_oid,
1290 )
1291 .map_err(GitExportError::from_git)?;
1292 }
1293 }
1294 Ok(())
1295 };
1296
1297 let git_repo = get_git_repo(mut_repo.store())?;
1298
1299 check_and_detach_head(&git_repo)?;
1300 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1301 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1302 check_and_detach_head(&worktree_repo)?;
1303 }
1304 }
1305
1306 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1307 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1308
1309 copy_exportable_local_bookmarks_to_remote_view(
1310 mut_repo,
1311 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1312 |name| {
1313 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1314 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1315 },
1316 );
1317 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1318 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1319 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1320 });
1321
1322 Ok(GitExportStats {
1323 failed_bookmarks,
1324 failed_tags,
1325 })
1326}
1327
1328fn export_refs_to_git(
1329 mut_repo: &mut MutableRepo,
1330 git_repo: &gix::Repository,
1331 kind: GitRefKind,
1332 refs: RefsToExport,
1333) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1334 let mut failed = refs.failed;
1335 for (symbol, old_oid) in refs.to_delete {
1336 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1337 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1338 continue;
1339 };
1340 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1341 failed.push((symbol, reason));
1342 } else {
1343 let new_target = RefTarget::absent();
1344 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1345 }
1346 }
1347 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1348 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1349 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1350 continue;
1351 };
1352 let new_ref_oid = match kind {
1353 GitRefKind::Bookmark => None,
1354 GitRefKind::Tag => {
1356 let remote_matcher = StringMatcher::all();
1357 find_git_tag_oid_to_copy(
1358 mut_repo.view(),
1359 git_repo,
1360 &symbol.name,
1361 &remote_matcher,
1362 &new_commit_oid,
1363 )
1364 }
1365 };
1366 if let Err(reason) = update_git_ref(
1367 git_repo,
1368 &git_ref_name,
1369 old_commit_oid,
1370 new_commit_oid,
1371 new_ref_oid,
1372 ) {
1373 failed.push((symbol, reason));
1374 } else {
1375 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1376 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1377 }
1378 }
1379
1380 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1382 failed
1383}
1384
1385fn copy_exportable_local_bookmarks_to_remote_view(
1386 mut_repo: &mut MutableRepo,
1387 remote: &RemoteName,
1388 name_filter: impl Fn(&RefName) -> bool,
1389) {
1390 let new_local_bookmarks = mut_repo
1391 .view()
1392 .local_remote_bookmarks(remote)
1393 .filter_map(|(name, targets)| {
1394 let old_target = &targets.remote_ref.target;
1397 let new_target = targets.local_target;
1398 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1399 })
1400 .filter(|&(name, _)| name_filter(name))
1401 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1402 .collect_vec();
1403 for (name, new_target) in new_local_bookmarks {
1404 let new_remote_ref = RemoteRef {
1405 target: new_target,
1406 state: RemoteRefState::Tracked,
1407 };
1408 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1409 }
1410}
1411
1412fn copy_exportable_local_tags_to_remote_view(
1413 mut_repo: &mut MutableRepo,
1414 remote: &RemoteName,
1415 name_filter: impl Fn(&RefName) -> bool,
1416) {
1417 let new_local_tags = mut_repo
1418 .view()
1419 .local_remote_tags(remote)
1420 .filter_map(|(name, targets)| {
1421 let old_target = &targets.remote_ref.target;
1423 let new_target = targets.local_target;
1424 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1425 })
1426 .filter(|&(name, _)| name_filter(name))
1427 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1428 .collect_vec();
1429 for (name, new_target) in new_local_tags {
1430 let new_remote_ref = RemoteRef {
1431 target: new_target,
1432 state: RemoteRefState::Tracked,
1433 };
1434 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1435 }
1436}
1437
1438fn diff_refs_to_export(
1440 view: &View,
1441 root_commit_id: &CommitId,
1442 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1443) -> AllRefsToExport {
1444 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1447 itertools::chain(
1448 view.local_bookmarks().map(|(name, target)| {
1449 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1450 (symbol, target)
1451 }),
1452 view.all_remote_bookmarks()
1453 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1454 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1455 )
1456 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1457 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1458 .collect();
1459 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1461 .local_tags()
1462 .map(|(name, target)| {
1463 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1464 (symbol, target)
1465 })
1466 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1467 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1468 .collect();
1469 let known_git_refs = view
1470 .git_refs()
1471 .iter()
1472 .map(|(full_name, target)| {
1473 let (kind, symbol) =
1474 parse_git_ref(full_name).expect("stored git ref should be parsable");
1475 ((kind, symbol), target)
1476 })
1477 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1481 for ((kind, symbol), target) in known_git_refs {
1482 let ref_targets = match kind {
1483 GitRefKind::Bookmark => &mut all_bookmark_targets,
1484 GitRefKind::Tag => &mut all_tag_targets,
1485 };
1486 ref_targets
1487 .entry(symbol)
1488 .and_modify(|(old_target, _)| *old_target = target)
1489 .or_insert((target, RefTarget::absent_ref()));
1490 }
1491
1492 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1493 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1494 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1495 AllRefsToExport { bookmarks, tags }
1496}
1497
1498fn collect_changed_refs_to_export(
1499 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1500 root_commit_target: &RefTarget,
1501) -> RefsToExport {
1502 let mut to_update = Vec::new();
1503 let mut to_delete = Vec::new();
1504 let mut failed = Vec::new();
1505 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1506 if new_target == old_target {
1507 continue;
1508 }
1509 if new_target == root_commit_target {
1510 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1512 continue;
1513 }
1514 let old_oid = if let Some(id) = old_target.as_normal() {
1515 Some(owned_oid_from_commit_id(id))
1516 } else if old_target.has_conflict() {
1517 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1520 continue;
1521 } else {
1522 assert!(old_target.is_absent());
1523 None
1524 };
1525 if let Some(id) = new_target.as_normal() {
1526 let new_oid = owned_oid_from_commit_id(id);
1527 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1528 } else if new_target.has_conflict() {
1529 continue;
1531 } else {
1532 assert!(new_target.is_absent());
1533 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1534 }
1535 }
1536
1537 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1539 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1540 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1541 RefsToExport {
1542 to_update,
1543 to_delete,
1544 failed,
1545 }
1546}
1547
1548fn find_git_tag_oid_to_copy(
1551 view: &View,
1552 git_repo: &gix::Repository,
1553 name: &RefName,
1554 remote_matcher: &StringMatcher,
1555 commit_oid: &gix::oid,
1556) -> Option<gix::ObjectId> {
1557 view.remote_tags_matching(&StringMatcher::exact(name), remote_matcher)
1559 .filter(|(_, remote_ref)| {
1560 let maybe_id = remote_ref.tracked_target().as_normal();
1561 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1562 })
1563 .filter_map(|(symbol, _)| {
1565 let git_ref_name = to_git_or_remote_tag_ref_name(symbol);
1566 git_repo.find_reference(git_ref_name.as_str()).ok()
1567 })
1568 .filter(|git_ref| {
1571 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1572 })
1573 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1574}
1575
1576fn delete_git_ref(
1577 git_repo: &gix::Repository,
1578 git_ref_name: &GitRefName,
1579 old_oid: &gix::oid,
1580) -> Result<(), FailedRefExportReason> {
1581 let Some(git_ref) = git_repo
1582 .try_find_reference(git_ref_name.as_str())
1583 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1584 else {
1585 return Ok(());
1587 };
1588 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1589 git_ref
1591 .delete()
1592 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1593 } else {
1594 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1596 }
1597}
1598
1599fn create_git_ref(
1601 git_repo: &gix::Repository,
1602 git_ref_name: &GitRefName,
1603 new_commit_oid: gix::ObjectId,
1604 new_ref_oid: Option<gix::ObjectId>,
1605) -> Result<(), FailedRefExportReason> {
1606 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1607 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1608 let Err(set_err) =
1609 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1610 else {
1611 return Ok(());
1613 };
1614 let Some(git_ref) = git_repo
1615 .try_find_reference(git_ref_name.as_str())
1616 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1617 else {
1618 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1619 };
1620 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1623 Ok(())
1624 } else {
1625 Err(FailedRefExportReason::AddedInJjAddedInGit)
1626 }
1627}
1628
1629fn move_git_ref(
1631 git_repo: &gix::Repository,
1632 git_ref_name: &GitRefName,
1633 old_commit_oid: gix::ObjectId,
1634 new_commit_oid: gix::ObjectId,
1635 new_ref_oid: Option<gix::ObjectId>,
1636) -> Result<(), FailedRefExportReason> {
1637 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1638 let constraint =
1639 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1640 let Err(set_err) =
1641 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1642 else {
1643 return Ok(());
1645 };
1646 let Some(git_ref) = git_repo
1648 .try_find_reference(git_ref_name.as_str())
1649 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1650 else {
1651 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1653 };
1654 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1656 if git_commit_oid == Some(new_commit_oid) {
1657 Ok(())
1658 } else if git_commit_oid == Some(old_commit_oid) {
1659 let constraint =
1661 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1662 git_repo
1663 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1664 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1665 Ok(())
1666 } else {
1667 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1668 }
1669}
1670
1671fn update_git_ref(
1672 git_repo: &gix::Repository,
1673 git_ref_name: &GitRefName,
1674 old_commit_oid: Option<gix::ObjectId>,
1675 new_commit_oid: gix::ObjectId,
1676 new_ref_oid: Option<gix::ObjectId>,
1677) -> Result<(), FailedRefExportReason> {
1678 match old_commit_oid {
1679 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1680 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1681 }
1682}
1683
1684fn update_git_head(
1687 git_repo: &gix::Repository,
1688 expected_ref: gix::refs::transaction::PreviousValue,
1689 new_oid: Option<gix::ObjectId>,
1690) -> Result<(), gix::reference::edit::Error> {
1691 let mut ref_edits = Vec::new();
1692 let new_target = if let Some(oid) = new_oid {
1693 gix::refs::Target::Object(oid)
1694 } else {
1695 ref_edits.push(gix::refs::transaction::RefEdit {
1700 change: gix::refs::transaction::Change::Delete {
1701 expected: gix::refs::transaction::PreviousValue::Any,
1702 log: gix::refs::transaction::RefLog::AndReference,
1703 },
1704 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1705 deref: false,
1706 });
1707 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1708 };
1709 ref_edits.push(gix::refs::transaction::RefEdit {
1710 change: gix::refs::transaction::Change::Update {
1711 log: gix::refs::transaction::LogChange {
1712 message: "export from jj".into(),
1713 ..Default::default()
1714 },
1715 expected: expected_ref,
1716 new: new_target,
1717 },
1718 name: "HEAD".try_into().unwrap(),
1719 deref: false,
1720 });
1721 git_repo.edit_references(ref_edits)?;
1722 Ok(())
1723}
1724
1725#[derive(Debug, Error)]
1726pub enum GitResetHeadError {
1727 #[error(transparent)]
1728 Backend(#[from] BackendError),
1729 #[error(transparent)]
1730 Git(Box<dyn std::error::Error + Send + Sync>),
1731 #[error("Failed to update Git HEAD ref")]
1732 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1733 #[error(transparent)]
1734 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1735}
1736
1737impl GitResetHeadError {
1738 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1739 Self::Git(source.into())
1740 }
1741}
1742
1743pub async fn reset_head(
1746 mut_repo: &mut MutableRepo,
1747 wc_commit: &Commit,
1748) -> Result<(), GitResetHeadError> {
1749 let git_repo = get_git_repo(mut_repo.store())?;
1750
1751 let first_parent_id = &wc_commit.parent_ids()[0];
1752 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1753 RefTarget::normal(first_parent_id.clone())
1754 } else {
1755 RefTarget::absent()
1756 };
1757
1758 let old_head_target = mut_repo.git_head();
1760 if old_head_target != new_head_target {
1761 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1762 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1765 if actual_head.is_detached() {
1766 let id = owned_oid_from_commit_id(id);
1767 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1768 } else {
1769 gix::refs::transaction::PreviousValue::MustExist
1772 }
1773 } else {
1774 gix::refs::transaction::PreviousValue::MustExist
1776 };
1777 let new_oid = new_head_target.as_normal().map(owned_oid_from_commit_id);
1778 update_git_head(&git_repo, expected_ref, new_oid)
1779 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1780 mut_repo.set_git_head_target(new_head_target);
1781 }
1782
1783 if git_repo.state().is_some() {
1786 clear_operation_state(&git_repo)?;
1787 }
1788
1789 reset_index(mut_repo, &git_repo, wc_commit).await
1790}
1791
1792fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1794 const STATE_FILE_NAMES: &[&str] = &[
1798 "MERGE_HEAD",
1799 "MERGE_MODE",
1800 "MERGE_MSG",
1801 "REVERT_HEAD",
1802 "CHERRY_PICK_HEAD",
1803 "BISECT_LOG",
1804 ];
1805 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1806 let handle_err = |err: PathError| match err.source.kind() {
1807 std::io::ErrorKind::NotFound => Ok(()),
1808 _ => Err(GitResetHeadError::from_git(err)),
1809 };
1810 for file_name in STATE_FILE_NAMES {
1811 let path = git_repo.path().join(file_name);
1812 std::fs::remove_file(&path)
1813 .context(&path)
1814 .or_else(handle_err)?;
1815 }
1816 for dir_name in STATE_DIR_NAMES {
1817 let path = git_repo.path().join(dir_name);
1818 std::fs::remove_dir_all(&path)
1819 .context(&path)
1820 .or_else(handle_err)?;
1821 }
1822 Ok(())
1823}
1824
1825async fn reset_index(
1826 repo: &dyn Repo,
1827 git_repo: &gix::Repository,
1828 wc_commit: &Commit,
1829) -> Result<(), GitResetHeadError> {
1830 let parent_tree = wc_commit.parent_tree(repo).await?;
1831 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1835 if tree_id == repo.store().empty_tree_id() {
1836 gix::index::File::from_state(
1840 gix::index::State::new(git_repo.object_hash()),
1841 git_repo.index_path(),
1842 )
1843 } else {
1844 git_repo
1847 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1848 .map_err(GitResetHeadError::from_git)?
1849 }
1850 } else {
1851 build_index_from_merged_tree(git_repo, &parent_tree)?
1852 };
1853
1854 let wc_tree = wc_commit.tree();
1855 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).await?;
1856
1857 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1860 index
1861 .entries_mut_with_paths()
1862 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1863 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1864 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1865 })
1866 .filter_map(|merged| merged.both())
1867 .map(|((entry, _), old_entry)| (entry, old_entry))
1868 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1869 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1870 }
1871
1872 debug_assert!(index.verify_entries().is_ok());
1873
1874 index
1875 .write(gix::index::write::Options::default())
1876 .map_err(GitResetHeadError::from_git)
1877}
1878
1879fn build_index_from_merged_tree(
1880 git_repo: &gix::Repository,
1881 merged_tree: &MergedTree,
1882) -> Result<gix::index::File, GitResetHeadError> {
1883 let mut index = gix::index::File::from_state(
1884 gix::index::State::new(git_repo.object_hash()),
1885 git_repo.index_path(),
1886 );
1887
1888 let mut push_index_entry =
1889 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1890 let Some(entry) = maybe_entry else {
1891 return;
1892 };
1893
1894 let (id, mode) = match entry {
1895 TreeValue::File {
1896 id,
1897 executable,
1898 copy_id: _,
1899 } => {
1900 if *executable {
1901 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1902 } else {
1903 (id.as_bytes(), gix::index::entry::Mode::FILE)
1904 }
1905 }
1906 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1907 TreeValue::Tree(_) => {
1908 return;
1913 }
1914 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1915 };
1916
1917 let path = BStr::new(path.as_internal_file_string());
1918
1919 index.dangerously_push_entry(
1922 gix::index::entry::Stat::default(),
1923 gix::ObjectId::from_bytes_or_panic(id),
1924 gix::index::entry::Flags::from_stage(stage),
1925 mode,
1926 path,
1927 );
1928 };
1929
1930 let mut has_many_sided_conflict = false;
1931
1932 for (path, entry) in merged_tree.entries() {
1933 let entry = entry?;
1934 if let Some(resolved) = entry.as_resolved() {
1935 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1936 continue;
1937 }
1938
1939 let conflict = entry.simplify();
1940 if let [left, base, right] = conflict.as_slice() {
1941 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1943 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1944 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1945 } else {
1946 has_many_sided_conflict = true;
1954 push_index_entry(
1955 &path,
1956 conflict.first(),
1957 gix::index::entry::Stage::Unconflicted,
1958 );
1959 }
1960 }
1961
1962 index.sort_entries();
1965
1966 if has_many_sided_conflict
1969 && index
1970 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1971 .is_err()
1972 {
1973 let file_blob = git_repo
1974 .write_blob(
1975 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1976 )
1977 .map_err(GitResetHeadError::from_git)?;
1978 index.dangerously_push_entry(
1979 gix::index::entry::Stat::default(),
1980 file_blob.detach(),
1981 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1982 gix::index::entry::Mode::FILE,
1983 INDEX_DUMMY_CONFLICT_FILE.into(),
1984 );
1985 index.sort_entries();
1988 }
1989
1990 Ok(index)
1991}
1992
1993pub async fn update_intent_to_add(
2000 repo: &dyn Repo,
2001 old_tree: &MergedTree,
2002 new_tree: &MergedTree,
2003) -> Result<(), GitResetHeadError> {
2004 let git_repo = get_git_repo(repo.store())?;
2005 let mut index = git_repo
2006 .index_or_empty()
2007 .map_err(GitResetHeadError::from_git)?;
2008 let mut_index = Arc::make_mut(&mut index);
2009 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).await?;
2010 debug_assert!(mut_index.verify_entries().is_ok());
2011 mut_index
2012 .write(gix::index::write::Options::default())
2013 .map_err(GitResetHeadError::from_git)?;
2014
2015 Ok(())
2016}
2017
2018async fn update_intent_to_add_impl(
2019 git_repo: &gix::Repository,
2020 index: &mut gix::index::File,
2021 old_tree: &MergedTree,
2022 new_tree: &MergedTree,
2023) -> Result<(), GitResetHeadError> {
2024 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
2025 let mut added_paths = vec![];
2026 let mut removed_paths = HashSet::new();
2027 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2028 let values = values?;
2029 if values.before.is_absent() {
2030 let executable = match values.after.as_normal() {
2031 Some(TreeValue::File {
2032 id: _,
2033 executable,
2034 copy_id: _,
2035 }) => *executable,
2036 Some(TreeValue::Symlink(_)) => false,
2037 _ => {
2038 continue;
2039 }
2040 };
2041 if index
2042 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
2043 .is_err()
2044 {
2045 added_paths.push((BString::from(path.into_internal_string()), executable));
2046 }
2047 } else if values.after.is_absent() {
2048 removed_paths.insert(BString::from(path.into_internal_string()));
2049 }
2050 }
2051
2052 if added_paths.is_empty() && removed_paths.is_empty() {
2053 return Ok(());
2054 }
2055
2056 if !added_paths.is_empty() {
2057 let empty_blob = git_repo
2059 .write_blob(b"")
2060 .map_err(GitResetHeadError::from_git)?
2061 .detach();
2062 for (path, executable) in added_paths {
2063 index.dangerously_push_entry(
2065 gix::index::entry::Stat::default(),
2066 empty_blob,
2067 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
2068 if executable {
2069 gix::index::entry::Mode::FILE_EXECUTABLE
2070 } else {
2071 gix::index::entry::Mode::FILE
2072 },
2073 path.as_ref(),
2074 );
2075 }
2076 }
2077 if !removed_paths.is_empty() {
2078 index.remove_entries(|_size, path, entry| {
2079 entry
2080 .flags
2081 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
2082 && removed_paths.contains(path)
2083 });
2084 }
2085
2086 index.sort_entries();
2087
2088 Ok(())
2089}
2090
2091#[derive(Debug, Error)]
2092pub enum GitRemoteManagementError {
2093 #[error("No git remote named '{}'", .0.as_symbol())]
2094 NoSuchRemote(RemoteNameBuf),
2095 #[error("Git remote named '{}' already exists", .0.as_symbol())]
2096 RemoteAlreadyExists(RemoteNameBuf),
2097 #[error(transparent)]
2098 RemoteName(#[from] GitRemoteNameError),
2099 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
2100 NonstandardConfiguration(RemoteNameBuf),
2101 #[error("Error saving Git configuration")]
2102 GitConfigSaveError(#[source] std::io::Error),
2103 #[error("Unexpected Git error when managing remotes")]
2104 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
2105 #[error(transparent)]
2106 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2107 #[error(transparent)]
2108 RefExpansionError(#[from] GitRefExpansionError),
2109}
2110
2111impl GitRemoteManagementError {
2112 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
2113 Self::InternalGitError(source.into())
2114 }
2115}
2116
2117fn default_fetch_refspec(remote: &RemoteName) -> String {
2118 format!(
2119 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
2120 remote = remote.as_str()
2121 )
2122}
2123
2124fn add_ref(
2125 name: gix::refs::FullName,
2126 target: gix::refs::Target,
2127 message: BString,
2128) -> gix::refs::transaction::RefEdit {
2129 gix::refs::transaction::RefEdit {
2130 change: gix::refs::transaction::Change::Update {
2131 log: gix::refs::transaction::LogChange {
2132 mode: gix::refs::transaction::RefLog::AndReference,
2133 force_create_reflog: false,
2134 message,
2135 },
2136 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2137 new: target,
2138 },
2139 name,
2140 deref: false,
2141 }
2142}
2143
2144fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2145 gix::refs::transaction::RefEdit {
2146 change: gix::refs::transaction::Change::Delete {
2147 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2148 reference.target().into_owned(),
2149 ),
2150 log: gix::refs::transaction::RefLog::AndReference,
2151 },
2152 name: reference.name().to_owned(),
2153 deref: false,
2154 }
2155}
2156
2157pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2163 let mut config_file = File::create(
2164 config
2165 .meta()
2166 .path
2167 .as_ref()
2168 .expect("Git repository to have a config file"),
2169 )?;
2170 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2171}
2172
2173fn save_remote(
2174 config: &mut gix::config::File<'static>,
2175 remote_name: &RemoteName,
2176 remote: &mut gix::Remote,
2177) -> Result<(), GitRemoteManagementError> {
2178 config
2185 .new_section(
2186 "remote",
2187 Some(Cow::Owned(BString::from(remote_name.as_str()))),
2188 )
2189 .map_err(GitRemoteManagementError::from_git)?;
2190 remote
2191 .save_as_to(remote_name.as_str(), config)
2192 .map_err(GitRemoteManagementError::from_git)?;
2193 Ok(())
2194}
2195
2196fn git_config_branch_section_ids_by_remote(
2197 config: &gix::config::File,
2198 remote_name: &RemoteName,
2199) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2200 config
2201 .sections_by_name("branch")
2202 .into_iter()
2203 .flatten()
2204 .filter_map(|section| {
2205 let remote_values = section.values("remote");
2206 let push_remote_values = section.values("pushRemote");
2207 if !remote_values
2208 .iter()
2209 .chain(push_remote_values.iter())
2210 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2211 {
2212 return None;
2213 }
2214 let is_supported_key = |name: &gix::config::parse::section::ValueName| -> bool {
2216 name.eq_ignore_ascii_case(b"remote")
2217 || name.eq_ignore_ascii_case(b"merge")
2218 || name.eq_ignore_ascii_case(b"rebase")
2219 };
2220 if remote_values.len() > 1
2221 || push_remote_values.len() > 1
2222 || !section.value_names().all(is_supported_key)
2223 {
2224 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2225 remote_name.to_owned(),
2226 )));
2227 }
2228 Some(Ok(section.id()))
2229 })
2230 .collect()
2231}
2232
2233fn rename_remote_in_git_branch_config_sections(
2234 config: &mut gix::config::File,
2235 old_remote_name: &RemoteName,
2236 new_remote_name: &RemoteName,
2237) -> Result<(), GitRemoteManagementError> {
2238 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2239 config
2240 .section_mut_by_id(id)
2241 .expect("found section to exist")
2242 .set(
2243 "remote"
2244 .try_into()
2245 .expect("'remote' to be a valid value name"),
2246 BStr::new(new_remote_name.as_str()),
2247 );
2248 }
2249 Ok(())
2250}
2251
2252fn remove_remote_git_branch_config_sections(
2253 config: &mut gix::config::File,
2254 remote_name: &RemoteName,
2255) -> Result<(), GitRemoteManagementError> {
2256 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2257 config
2258 .remove_section_by_id(id)
2259 .expect("removed section to exist");
2260 }
2261 Ok(())
2262}
2263
2264fn remove_remote_git_config_sections(
2265 config: &mut gix::config::File,
2266 remote_name: &RemoteName,
2267) -> Result<(), GitRemoteManagementError> {
2268 let section_ids_to_remove: Vec<_> = config
2269 .sections_by_name("remote")
2270 .into_iter()
2271 .flatten()
2272 .filter(|section| {
2273 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2274 })
2275 .map(|section| {
2276 if section.value_names().any(|name| {
2277 !name.eq_ignore_ascii_case(b"url")
2278 && !name.eq_ignore_ascii_case(b"fetch")
2279 && !name.eq_ignore_ascii_case(b"tagOpt")
2280 }) {
2281 return Err(GitRemoteManagementError::NonstandardConfiguration(
2282 remote_name.to_owned(),
2283 ));
2284 }
2285 Ok(section.id())
2286 })
2287 .try_collect()?;
2288 for id in section_ids_to_remove {
2289 config
2290 .remove_section_by_id(id)
2291 .expect("removed section to exist");
2292 }
2293 Ok(())
2294}
2295
2296pub fn get_all_remote_names(
2298 store: &Store,
2299) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2300 let git_repo = get_git_repo(store)?;
2301 Ok(iter_remote_names(&git_repo).collect())
2302}
2303
2304fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2305 git_repo
2306 .remote_names()
2307 .into_iter()
2308 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2310 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2312 .map(RemoteNameBuf::from)
2313}
2314
2315pub fn add_remote(
2316 mut_repo: &mut MutableRepo,
2317 remote_name: &RemoteName,
2318 url: &str,
2319 push_url: Option<&str>,
2320 fetch_tags: gix::remote::fetch::Tags,
2321) -> Result<(), GitRemoteManagementError> {
2322 let git_repo = get_git_repo(mut_repo.store())?;
2323
2324 validate_remote_name(remote_name)?;
2325
2326 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2327 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2328 remote_name.to_owned(),
2329 ));
2330 }
2331
2332 let mut remote = git_repo
2333 .remote_at(url)
2334 .map_err(GitRemoteManagementError::from_git)?
2335 .with_fetch_tags(fetch_tags)
2336 .with_refspecs(
2337 [default_fetch_refspec(remote_name).as_bytes()],
2338 gix::remote::Direction::Fetch,
2339 )
2340 .expect("default refspec to be valid");
2341
2342 if let Some(push_url) = push_url {
2343 remote = remote
2344 .with_push_url(push_url)
2345 .map_err(GitRemoteManagementError::from_git)?;
2346 }
2347
2348 let mut config = git_repo.config_snapshot().clone();
2349 save_remote(&mut config, remote_name, &mut remote)?;
2350 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2351
2352 mut_repo.ensure_remote(remote_name);
2353
2354 Ok(())
2355}
2356
2357pub fn remove_remote(
2358 mut_repo: &mut MutableRepo,
2359 remote_name: &RemoteName,
2360) -> Result<(), GitRemoteManagementError> {
2361 let mut git_repo = get_git_repo(mut_repo.store())?;
2362
2363 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2364 return Err(GitRemoteManagementError::NoSuchRemote(
2365 remote_name.to_owned(),
2366 ));
2367 }
2368
2369 let mut config = git_repo.config_snapshot().clone();
2370 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2371 remove_remote_git_config_sections(&mut config, remote_name)?;
2372 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2373
2374 remove_remote_git_refs(&mut git_repo, remote_name)
2375 .map_err(GitRemoteManagementError::from_git)?;
2376
2377 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2378 remove_remote_refs(mut_repo, remote_name);
2379 }
2380
2381 Ok(())
2382}
2383
2384fn remove_remote_git_refs(
2385 git_repo: &mut gix::Repository,
2386 remote: &RemoteName,
2387) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2388 let bookmark_prefix = format!(
2389 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2390 remote = remote.as_str()
2391 );
2392 let tag_prefix = format!(
2393 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2394 remote = remote.as_str()
2395 );
2396 let edits: Vec<_> = itertools::chain(
2397 git_repo
2398 .references()?
2399 .prefixed(bookmark_prefix.as_str())?
2400 .map_ok(remove_ref),
2401 git_repo
2402 .references()?
2403 .prefixed(tag_prefix.as_str())?
2404 .map_ok(remove_ref),
2405 )
2406 .try_collect()?;
2407 git_repo.edit_references(edits)?;
2408 Ok(())
2409}
2410
2411fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2412 mut_repo.remove_remote(remote);
2413 let prefix = format!(
2414 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2415 remote = remote.as_str()
2416 );
2417 let git_refs_to_delete = mut_repo
2418 .view()
2419 .git_refs()
2420 .keys()
2421 .filter(|&r| r.as_str().starts_with(&prefix))
2422 .cloned()
2423 .collect_vec();
2424 for git_ref in git_refs_to_delete {
2425 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2426 }
2427}
2428
2429pub fn rename_remote(
2430 mut_repo: &mut MutableRepo,
2431 old_remote_name: &RemoteName,
2432 new_remote_name: &RemoteName,
2433) -> Result<(), GitRemoteManagementError> {
2434 let mut git_repo = get_git_repo(mut_repo.store())?;
2435
2436 validate_remote_name(new_remote_name)?;
2437
2438 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2439 return Err(GitRemoteManagementError::NoSuchRemote(
2440 old_remote_name.to_owned(),
2441 ));
2442 };
2443 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2444
2445 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2446 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2447 new_remote_name.to_owned(),
2448 ));
2449 }
2450
2451 match (
2452 remote.refspecs(gix::remote::Direction::Fetch),
2453 remote.refspecs(gix::remote::Direction::Push),
2454 ) {
2455 ([refspec], [])
2456 if refspec.to_ref().to_bstring()
2457 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2458 _ => {
2459 return Err(GitRemoteManagementError::NonstandardConfiguration(
2460 old_remote_name.to_owned(),
2461 ));
2462 }
2463 }
2464
2465 remote
2466 .replace_refspecs(
2467 [default_fetch_refspec(new_remote_name).as_bytes()],
2468 gix::remote::Direction::Fetch,
2469 )
2470 .expect("default refspec to be valid");
2471
2472 let mut config = git_repo.config_snapshot().clone();
2473 save_remote(&mut config, new_remote_name, &mut remote)?;
2474 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2475 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2476 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2477
2478 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2479 .map_err(GitRemoteManagementError::from_git)?;
2480
2481 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2482 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2483 }
2484
2485 Ok(())
2486}
2487
2488fn rename_remote_git_refs(
2489 git_repo: &mut gix::Repository,
2490 old_remote_name: &RemoteName,
2491 new_remote_name: &RemoteName,
2492) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2493 let to_prefixes = |namespace: &str| {
2494 (
2495 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2496 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2497 )
2498 };
2499 let to_rename_edits = {
2500 let ref_log_message = BString::from(format!(
2501 "renamed remote {old_remote_name} to {new_remote_name}",
2502 old_remote_name = old_remote_name.as_symbol(),
2503 new_remote_name = new_remote_name.as_symbol(),
2504 ));
2505 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2506 let new_name = BString::new(
2507 [
2508 new_prefix.as_bytes(),
2509 &old_ref.name().as_bstr()[old_prefix.len()..],
2510 ]
2511 .concat(),
2512 );
2513 [
2514 add_ref(
2515 new_name.try_into().expect("new ref name to be valid"),
2516 old_ref.target().into_owned(),
2517 ref_log_message.clone(),
2518 ),
2519 remove_ref(old_ref),
2520 ]
2521 }
2522 };
2523
2524 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2525 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2526 let edits: Vec<_> = itertools::chain(
2527 git_repo
2528 .references()?
2529 .prefixed(old_bookmark_prefix.as_str())?
2530 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2531 git_repo
2532 .references()?
2533 .prefixed(old_tag_prefix.as_str())?
2534 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2535 )
2536 .flatten_ok()
2537 .try_collect()?;
2538 git_repo.edit_references(edits)?;
2539 Ok(())
2540}
2541
2542pub fn set_remote_urls(
2546 store: &Store,
2547 remote_name: &RemoteName,
2548 new_url: Option<&str>,
2549 new_push_url: Option<&str>,
2550) -> Result<(), GitRemoteManagementError> {
2551 if new_url.is_none() && new_push_url.is_none() {
2553 return Ok(());
2554 }
2555
2556 let git_repo = get_git_repo(store)?;
2557
2558 validate_remote_name(remote_name)?;
2559
2560 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2561 return Err(GitRemoteManagementError::NoSuchRemote(
2562 remote_name.to_owned(),
2563 ));
2564 };
2565 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2566
2567 if let Some(url) = new_url {
2568 remote = remote
2569 .with_url(url)
2570 .map_err(GitRemoteManagementError::from_git)?;
2571 }
2572
2573 if let Some(url) = new_push_url {
2574 remote = remote
2575 .with_push_url(url)
2576 .map_err(GitRemoteManagementError::from_git)?;
2577 }
2578
2579 let mut config = git_repo.config_snapshot().clone();
2580 save_remote(&mut config, remote_name, &mut remote)?;
2581 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2582
2583 Ok(())
2584}
2585
2586fn rename_remote_refs(
2587 mut_repo: &mut MutableRepo,
2588 old_remote_name: &RemoteName,
2589 new_remote_name: &RemoteName,
2590) {
2591 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2592 let prefix = format!(
2593 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2594 remote = old_remote_name.as_str()
2595 );
2596 let git_refs = mut_repo
2597 .view()
2598 .git_refs()
2599 .iter()
2600 .filter_map(|(old, target)| {
2601 old.as_str().strip_prefix(&prefix).map(|p| {
2602 let new: GitRefNameBuf = format!(
2603 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2604 remote = new_remote_name.as_str()
2605 )
2606 .into();
2607 (old.clone(), new, target.clone())
2608 })
2609 })
2610 .collect_vec();
2611 for (old, new, target) in git_refs {
2612 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2613 mut_repo.set_git_ref_target(&new, target);
2614 }
2615}
2616
2617const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2618
2619#[derive(Error, Debug)]
2620pub enum GitFetchError {
2621 #[error("No git remote named '{}'", .0.as_symbol())]
2622 NoSuchRemote(RemoteNameBuf),
2623 #[error(transparent)]
2624 RemoteName(#[from] GitRemoteNameError),
2625 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2626 RejectedUpdates(Vec<GitRefNameBuf>),
2627 #[error(transparent)]
2628 Subprocess(#[from] GitSubprocessError),
2629}
2630
2631#[derive(Error, Debug)]
2632pub enum GitDefaultRefspecError {
2633 #[error("No git remote named '{}'", .0.as_symbol())]
2634 NoSuchRemote(RemoteNameBuf),
2635 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2636 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2637}
2638
2639struct FetchedRefs {
2640 remote: RemoteNameBuf,
2641 bookmark_matcher: StringMatcher,
2642 tag_matcher: StringMatcher,
2643}
2644
2645#[derive(Clone, Debug)]
2647pub struct GitFetchRefExpression {
2648 pub bookmark: StringExpression,
2650 pub tag: StringExpression,
2656}
2657
2658#[derive(Debug)]
2660pub struct ExpandedFetchRefSpecs {
2661 expr: GitFetchRefExpression,
2663 refspecs: Vec<RefSpec>,
2664 negative_refspecs: Vec<NegativeRefSpec>,
2665}
2666
2667#[derive(Error, Debug)]
2668pub enum GitRefExpansionError {
2669 #[error(transparent)]
2670 Expression(#[from] GitRefExpressionError),
2671 #[error(
2672 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2673 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2674 )]
2675 InvalidBranchPattern(StringPattern),
2676}
2677
2678pub fn expand_fetch_refspecs(
2680 remote: &RemoteName,
2681 expr: GitFetchRefExpression,
2682) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2683 let (positive_bookmarks, negative_bookmarks) =
2684 split_into_positive_negative_patterns(&expr.bookmark)?;
2685 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2686
2687 let refspecs = itertools::chain(
2688 positive_bookmarks
2689 .iter()
2690 .map(|&pattern| pattern_to_refspec_glob(pattern))
2691 .map_ok(|glob| {
2692 RefSpec::forced(
2693 format!("refs/heads/{glob}"),
2694 format!(
2695 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2696 remote = remote.as_str()
2697 ),
2698 )
2699 }),
2700 positive_tags
2701 .iter()
2702 .map(|&pattern| pattern_to_refspec_glob(pattern))
2703 .map_ok(|glob| {
2704 RefSpec::forced(
2705 format!("refs/tags/{glob}"),
2706 format!(
2707 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2708 remote = remote.as_str()
2709 ),
2710 )
2711 }),
2712 )
2713 .try_collect()?;
2714
2715 let negative_refspecs = itertools::chain(
2716 negative_bookmarks
2717 .iter()
2718 .map(|&pattern| pattern_to_refspec_glob(pattern))
2719 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2720 negative_tags
2721 .iter()
2722 .map(|&pattern| pattern_to_refspec_glob(pattern))
2723 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2724 )
2725 .try_collect()?;
2726
2727 Ok(ExpandedFetchRefSpecs {
2728 expr,
2729 refspecs,
2730 negative_refspecs,
2731 })
2732}
2733
2734fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2735 pattern
2736 .to_glob()
2737 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2740 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2741}
2742
2743#[derive(Debug, Error)]
2744pub enum GitRefExpressionError {
2745 #[error("Cannot use `~` in sub expression")]
2746 NestedNotIn,
2747 #[error("Cannot use `&` in sub expression")]
2748 NestedIntersection,
2749 #[error("Cannot use `&` for positive expressions")]
2750 PositiveIntersection,
2751}
2752
2753fn split_into_positive_negative_patterns(
2756 expr: &StringExpression,
2757) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2758 static ALL: StringPattern = StringPattern::all();
2759
2760 fn visit_positive<'a>(
2774 expr: &'a StringExpression,
2775 positives: &mut Vec<&'a StringPattern>,
2776 negatives: &mut Vec<&'a StringPattern>,
2777 ) -> Result<(), GitRefExpressionError> {
2778 match expr {
2779 StringExpression::Pattern(pattern) => {
2780 positives.push(pattern);
2781 Ok(())
2782 }
2783 StringExpression::NotIn(complement) => {
2784 positives.push(&ALL);
2785 visit_negative(complement, negatives)
2786 }
2787 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2788 StringExpression::Intersection(expr1, expr2) => {
2789 match (expr1.as_ref(), expr2.as_ref()) {
2790 (other, StringExpression::NotIn(complement))
2791 | (StringExpression::NotIn(complement), other) => {
2792 visit_positive(other, positives, negatives)?;
2793 visit_negative(complement, negatives)
2794 }
2795 _ => Err(GitRefExpressionError::PositiveIntersection),
2796 }
2797 }
2798 }
2799 }
2800
2801 fn visit_negative<'a>(
2802 expr: &'a StringExpression,
2803 negatives: &mut Vec<&'a StringPattern>,
2804 ) -> Result<(), GitRefExpressionError> {
2805 match expr {
2806 StringExpression::Pattern(pattern) => {
2807 negatives.push(pattern);
2808 Ok(())
2809 }
2810 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2811 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2812 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2813 }
2814 }
2815
2816 fn visit_union<'a>(
2817 expr1: &'a StringExpression,
2818 expr2: &'a StringExpression,
2819 patterns: &mut Vec<&'a StringPattern>,
2820 ) -> Result<(), GitRefExpressionError> {
2821 visit_union_sub(expr1, patterns)?;
2822 visit_union_sub(expr2, patterns)
2823 }
2824
2825 fn visit_union_sub<'a>(
2826 expr: &'a StringExpression,
2827 patterns: &mut Vec<&'a StringPattern>,
2828 ) -> Result<(), GitRefExpressionError> {
2829 match expr {
2830 StringExpression::Pattern(pattern) => {
2831 patterns.push(pattern);
2832 Ok(())
2833 }
2834 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2835 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2836 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2837 }
2838 }
2839
2840 let mut positives = Vec::new();
2841 let mut negatives = Vec::new();
2842 visit_positive(expr, &mut positives, &mut negatives)?;
2843 if positives.iter().all(|pattern| pattern.is_all())
2846 && !negatives.is_empty()
2847 && negatives.iter().all(|pattern| pattern.is_all())
2848 {
2849 Ok((vec![], vec![]))
2850 } else {
2851 Ok((positives, negatives))
2852 }
2853}
2854
2855#[derive(Debug)]
2859#[must_use = "warnings should be surfaced in the UI"]
2860pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2861
2862#[derive(Debug)]
2865pub struct IgnoredRefspec {
2866 pub refspec: BString,
2868 pub reason: &'static str,
2870}
2871
2872#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2873enum FetchRefSpecKind {
2874 Positive,
2875 Negative,
2876}
2877
2878pub fn load_default_fetch_bookmarks(
2880 remote_name: &RemoteName,
2881 git_repo: &gix::Repository,
2882) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2883 let remote = git_repo
2884 .try_find_remote(remote_name.as_str())
2885 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2886 .map_err(|e| {
2887 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2888 })?;
2889
2890 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2891 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2892 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2893 let mut negative_bookmarks = Vec::new();
2894 for refspec in remote_refspecs {
2895 let refspec = refspec.to_ref();
2896 match parse_fetch_refspec(remote_name, refspec) {
2897 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2898 positive_bookmarks.push(StringExpression::pattern(bookmark));
2899 }
2900 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2901 negative_bookmarks.push(StringExpression::pattern(bookmark));
2902 }
2903 Err(reason) => {
2904 let refspec = refspec.to_bstring();
2905 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2906 }
2907 }
2908 }
2909
2910 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2911 if !negative_bookmarks.is_empty() {
2913 bookmark_expr =
2914 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2915 }
2916
2917 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2918}
2919
2920fn parse_fetch_refspec(
2921 remote_name: &RemoteName,
2922 refspec: gix::refspec::RefSpecRef<'_>,
2923) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2924 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2925
2926 let (src, positive_dst) = match refspec.instruction() {
2927 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2928 Instruction::Fetch(fetch) => match fetch {
2929 gix::refspec::instruction::Fetch::Only { src: _ } => {
2930 return Err("fetch-only refspecs are not supported");
2931 }
2932 gix::refspec::instruction::Fetch::AndUpdate {
2933 src,
2934 dst,
2935 allow_non_fast_forward,
2936 } => {
2937 if !allow_non_fast_forward {
2938 return Err("non-forced refspecs are not supported");
2939 }
2940 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2941 }
2942 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2943 },
2944 };
2945
2946 let src_branch = src
2947 .strip_prefix("refs/heads/")
2948 .ok_or("only refs/heads/ is supported for refspec sources")?;
2949 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2950
2951 if let Some(dst) = positive_dst {
2952 let dst_without_prefix = dst
2953 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
2954 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2955 let dst_branch = dst_without_prefix
2956 .strip_prefix(remote_name.as_str())
2957 .and_then(|d| d.strip_prefix("/"))
2958 .ok_or("remote renaming not supported")?;
2959 if src_branch != dst_branch {
2960 return Err("renaming is not supported");
2961 }
2962 Ok((FetchRefSpecKind::Positive, branch))
2963 } else {
2964 Ok((FetchRefSpecKind::Negative, branch))
2965 }
2966}
2967
2968pub struct GitFetch<'a> {
2970 mut_repo: &'a mut MutableRepo,
2971 git_repo: Box<gix::Repository>,
2972 git_ctx: GitSubprocessContext,
2973 import_options: &'a GitImportOptions,
2974 fetched: Vec<FetchedRefs>,
2975}
2976
2977impl<'a> GitFetch<'a> {
2978 pub fn new(
2979 mut_repo: &'a mut MutableRepo,
2980 subprocess_options: GitSubprocessOptions,
2981 import_options: &'a GitImportOptions,
2982 ) -> Result<Self, UnexpectedGitBackendError> {
2983 let git_backend = get_git_backend(mut_repo.store())?;
2984 let git_repo = Box::new(git_backend.git_repo());
2985 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
2986 Ok(GitFetch {
2987 mut_repo,
2988 git_repo,
2989 git_ctx,
2990 import_options,
2991 fetched: vec![],
2992 })
2993 }
2994
2995 #[tracing::instrument(skip(self, callback))]
3001 pub fn fetch(
3002 &mut self,
3003 remote_name: &RemoteName,
3004 ExpandedFetchRefSpecs {
3005 expr,
3006 refspecs: mut remaining_refspecs,
3007 negative_refspecs,
3008 }: ExpandedFetchRefSpecs,
3009 callback: &mut dyn GitSubprocessCallback,
3010 depth: Option<NonZeroU32>,
3011 fetch_tags_override: Option<FetchTagsOverride>,
3012 ) -> Result<(), GitFetchError> {
3013 validate_remote_name(remote_name)?;
3014
3015 if self
3017 .git_repo
3018 .try_find_remote(remote_name.as_str())
3019 .is_none()
3020 {
3021 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3022 }
3023
3024 if remaining_refspecs.is_empty() {
3025 return Ok(());
3027 }
3028
3029 let mut branches_to_prune = Vec::new();
3030 let updates = loop {
3038 let status = self.git_ctx.spawn_fetch(
3039 remote_name,
3040 &remaining_refspecs,
3041 &negative_refspecs,
3042 callback,
3043 depth,
3044 fetch_tags_override,
3045 )?;
3046 let failing_refspec = match status {
3047 GitFetchStatus::Updates(updates) => break updates,
3048 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
3049 };
3050 tracing::debug!(failing_refspec, "failed to fetch ref");
3051 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
3052
3053 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
3054 branches_to_prune.push(format!(
3055 "{remote_name}/{branch_name}",
3056 remote_name = remote_name.as_str()
3057 ));
3058 }
3059 };
3060
3061 if !updates.rejected.is_empty() {
3064 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
3065 return Err(GitFetchError::RejectedUpdates(names));
3066 }
3067
3068 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
3071
3072 self.fetched.push(FetchedRefs {
3073 remote: remote_name.to_owned(),
3074 bookmark_matcher: expr.bookmark.to_matcher(),
3075 tag_matcher: expr.tag.to_matcher(),
3076 });
3077 Ok(())
3078 }
3079
3080 #[tracing::instrument(skip(self))]
3082 pub fn get_default_branch(
3083 &self,
3084 remote_name: &RemoteName,
3085 ) -> Result<Option<RefNameBuf>, GitFetchError> {
3086 if self
3087 .git_repo
3088 .try_find_remote(remote_name.as_str())
3089 .is_none()
3090 {
3091 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3092 }
3093 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
3094 tracing::debug!(?default_branch);
3095 Ok(default_branch)
3096 }
3097
3098 #[tracing::instrument(skip(self))]
3105 pub async fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
3106 tracing::debug!("import_refs");
3107 let all_remote_tags = true;
3108 let refs_to_import = diff_refs_to_import(
3109 self.mut_repo.view(),
3110 &self.git_repo,
3111 all_remote_tags,
3112 |kind, symbol| match kind {
3113 GitRefKind::Bookmark => self
3114 .fetched
3115 .iter()
3116 .filter(|fetched| fetched.remote == symbol.remote)
3117 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3118 GitRefKind::Tag => {
3119 symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
3123 || self
3124 .fetched
3125 .iter()
3126 .filter(|fetched| fetched.remote == symbol.remote)
3127 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str()))
3128 }
3129 },
3130 )?;
3131 let import_stats =
3132 import_refs_inner(self.mut_repo, refs_to_import, self.import_options).await?;
3133
3134 self.fetched.clear();
3135
3136 Ok(import_stats)
3137 }
3138}
3139
3140#[derive(Error, Debug)]
3141pub enum GitPushError {
3142 #[error("No git remote named '{}'", .0.as_symbol())]
3143 NoSuchRemote(RemoteNameBuf),
3144 #[error(transparent)]
3145 RemoteName(#[from] GitRemoteNameError),
3146 #[error(transparent)]
3147 Subprocess(#[from] GitSubprocessError),
3148 #[error(transparent)]
3149 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3150}
3151
3152#[derive(Clone, Debug, Default)]
3153pub struct GitPushRefTargets {
3154 pub bookmarks: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3156 pub tags: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3158}
3159
3160pub struct GitRefUpdate {
3161 pub qualified_name: GitRefNameBuf,
3162 pub targets: Diff<Option<gix::ObjectId>>,
3167}
3168
3169#[derive(Clone, Debug, Default)]
3171pub struct GitPushOptions {
3172 pub remote_push_options: Vec<String>,
3174}
3175
3176pub fn push_refs(
3178 mut_repo: &mut MutableRepo,
3179 subprocess_options: GitSubprocessOptions,
3180 remote: &RemoteName,
3181 targets: &GitPushRefTargets,
3182 callback: &mut dyn GitSubprocessCallback,
3183 options: &GitPushOptions,
3184) -> Result<GitPushStats, GitPushError> {
3185 validate_remote_name(remote)?;
3186
3187 let git_repo = get_git_repo(mut_repo.store())?;
3188 let to_tag_target = |name: &RefName, remote: &RemoteName, id: &CommitId| {
3189 let remote_matcher = StringMatcher::exact(remote);
3190 let oid = owned_oid_from_commit_id(id);
3191 find_git_tag_oid_to_copy(mut_repo.view(), &git_repo, name, &remote_matcher, &oid)
3192 .unwrap_or(oid)
3193 };
3194 let ref_updates = itertools::chain(
3195 targets.bookmarks.iter().map(|(name, update)| GitRefUpdate {
3196 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3197 targets: update
3198 .as_ref()
3199 .map(|id| id.as_ref().map(owned_oid_from_commit_id)),
3200 }),
3201 targets.tags.iter().map(|(name, update)| GitRefUpdate {
3202 qualified_name: format!("refs/tags/{name}", name = name.as_str()).into(),
3203 targets: Diff {
3204 before: update
3205 .before
3206 .as_ref()
3207 .map(|id| to_tag_target(name, remote, id)),
3208 after: update
3209 .after
3210 .as_ref()
3211 .map(|id| to_tag_target(name, REMOTE_NAME_FOR_LOCAL_GIT_REPO, id)),
3212 },
3213 }),
3214 )
3215 .collect_vec();
3216
3217 let push_stats = push_updates(
3218 mut_repo,
3219 subprocess_options,
3220 remote,
3221 &ref_updates,
3222 callback,
3223 options,
3224 )?;
3225 tracing::debug!(?push_stats);
3226
3227 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3228 let pushed_bookmark_updates = || {
3229 iter::zip(&targets.bookmarks, &ref_updates[..targets.bookmarks.len()])
3230 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3231 .map(|((name, update), _)| (&**name, update))
3232 };
3233 let pushed_tag_updates = || {
3234 iter::zip(&targets.tags, &ref_updates[targets.bookmarks.len()..])
3235 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3236 .map(|((name, update), ref_update)| (&**name, update, ref_update))
3237 };
3238
3239 let unexported_bookmarks = {
3242 let refs = build_pushed_bookmarks_to_export(remote, pushed_bookmark_updates());
3243 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3244 };
3245 for (name, _, ref_update) in pushed_tag_updates() {
3249 let symbol = name.to_remote_symbol(remote);
3250 let edit = to_remote_tag_ref_update(symbol, ref_update.targets.after);
3251 if let Err(err) = git_repo.edit_reference(edit) {
3252 tracing::warn!(?symbol, ?err, "failed to update remote tag ref");
3253 }
3254 }
3255
3256 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3257 let is_exported_bookmark = |name: &RefName| {
3258 unexported_bookmarks
3259 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3260 .is_err()
3261 };
3262 for (name, update) in pushed_bookmark_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3263 let new_remote_ref = RemoteRef {
3264 target: RefTarget::resolved(update.after.clone()),
3265 state: RemoteRefState::Tracked,
3266 };
3267 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3268 }
3269 for (name, update, _) in pushed_tag_updates() {
3270 let new_remote_ref = RemoteRef {
3271 target: RefTarget::resolved(update.after.clone()),
3272 state: RemoteRefState::Tracked,
3273 };
3274 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
3275 }
3276
3277 assert!(push_stats.unexported_bookmarks.is_empty());
3281 let push_stats = GitPushStats {
3282 pushed: push_stats.pushed,
3283 rejected: push_stats.rejected,
3284 remote_rejected: push_stats.remote_rejected,
3285 unexported_bookmarks,
3286 };
3287 Ok(push_stats)
3288}
3289
3290pub fn push_updates(
3292 repo: &dyn Repo,
3293 subprocess_options: GitSubprocessOptions,
3294 remote_name: &RemoteName,
3295 updates: &[GitRefUpdate],
3296 callback: &mut dyn GitSubprocessCallback,
3297 options: &GitPushOptions,
3298) -> Result<GitPushStats, GitPushError> {
3299 let mut qualified_remote_refs_expected_locations = HashMap::new();
3300 let mut refspecs = vec![];
3301 for update in updates {
3302 qualified_remote_refs_expected_locations.insert(
3303 update.qualified_name.as_ref(),
3304 update.targets.before.as_deref(),
3305 );
3306 if let Some(new_target) = &update.targets.after {
3307 refspecs.push(RefSpec::forced(
3311 new_target.to_string(),
3312 &update.qualified_name,
3313 ));
3314 } else {
3315 refspecs.push(RefSpec::delete(&update.qualified_name));
3319 }
3320 }
3321
3322 let git_backend = get_git_backend(repo.store())?;
3323 let git_repo = git_backend.git_repo();
3324 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3325
3326 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3328 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3329 }
3330
3331 let refs_to_push: Vec<RefToPush> = refspecs
3332 .iter()
3333 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3334 .collect();
3335
3336 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3337 push_stats.pushed.sort();
3338 push_stats.rejected.sort();
3339 push_stats.remote_rejected.sort();
3340 Ok(push_stats)
3341}
3342
3343fn build_pushed_bookmarks_to_export<'a>(
3345 remote: &RemoteName,
3346 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a Diff<Option<CommitId>>)>,
3347) -> RefsToExport {
3348 let mut to_update = Vec::new();
3349 let mut to_delete = Vec::new();
3350 for (name, update) in pushed_updates {
3351 let symbol = name.to_remote_symbol(remote);
3352 match (update.before.as_ref(), update.after.as_ref()) {
3353 (old, Some(new)) => {
3354 let old_oid = old.map(owned_oid_from_commit_id);
3355 let new_oid = owned_oid_from_commit_id(new);
3356 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3357 }
3358 (Some(old), None) => {
3359 let old_oid = owned_oid_from_commit_id(old);
3360 to_delete.push((symbol.to_owned(), old_oid));
3361 }
3362 (None, None) => panic!("old/new targets should differ"),
3363 }
3364 }
3365
3366 RefsToExport {
3367 to_update,
3368 to_delete,
3369 failed: vec![],
3370 }
3371}
3372
3373fn to_remote_tag_ref_update(
3375 symbol: RemoteRefSymbol<'_>,
3376 new_oid: Option<gix::ObjectId>,
3377) -> gix::refs::transaction::RefEdit {
3378 let expected = gix::refs::transaction::PreviousValue::Any;
3381 let change = match new_oid {
3382 Some(oid) => gix::refs::transaction::Change::Update {
3383 log: gix::refs::transaction::LogChange::default(),
3384 expected,
3385 new: oid.into(),
3386 },
3387 None => gix::refs::transaction::Change::Delete {
3388 expected,
3389 log: gix::refs::transaction::RefLog::AndReference,
3390 },
3391 };
3392 let name = format!(
3393 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}",
3394 remote = symbol.remote.as_str(),
3395 name = symbol.name.as_str()
3396 );
3397 gix::refs::transaction::RefEdit {
3398 change,
3399 name: name.try_into().expect("pushed ref name should be valid"),
3400 deref: false,
3401 }
3402}
3403
3404#[derive(Copy, Clone, Debug)]
3407pub enum FetchTagsOverride {
3408 AllTags,
3411 NoTags,
3414}
3415
3416#[cfg(test)]
3417mod tests {
3418 use assert_matches::assert_matches;
3419
3420 use super::*;
3421 use crate::revset;
3422 use crate::revset::RevsetDiagnostics;
3423
3424 #[test]
3425 fn test_split_positive_negative_patterns() {
3426 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3427 try_split(text).unwrap()
3428 }
3429
3430 fn try_split(
3431 text: &str,
3432 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3433 let mut diagnostics = RevsetDiagnostics::new();
3434 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3435 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3436 Ok((
3437 positives.into_iter().cloned().collect(),
3438 negatives.into_iter().cloned().collect(),
3439 ))
3440 }
3441
3442 insta::assert_compact_debug_snapshot!(
3443 split("a"),
3444 @r#"([Exact("a")], [])"#);
3445 insta::assert_compact_debug_snapshot!(
3446 split("~a"),
3447 @r#"([Substring("")], [Exact("a")])"#);
3448 insta::assert_compact_debug_snapshot!(
3449 split("~a~b"),
3450 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3451 insta::assert_compact_debug_snapshot!(
3452 split("~(a|b)"),
3453 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3454 insta::assert_compact_debug_snapshot!(
3455 split("a|b"),
3456 @r#"([Exact("a"), Exact("b")], [])"#);
3457 insta::assert_compact_debug_snapshot!(
3458 split("(a|b)&~c"),
3459 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3460 insta::assert_compact_debug_snapshot!(
3461 split("~a&b"),
3462 @r#"([Exact("b")], [Exact("a")])"#);
3463 insta::assert_compact_debug_snapshot!(
3464 split("a&~b&~c"),
3465 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3466 insta::assert_compact_debug_snapshot!(
3467 split("~a&b&~c"),
3468 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3469 insta::assert_compact_debug_snapshot!(
3470 split("a&~(b|c)"),
3471 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3472 insta::assert_compact_debug_snapshot!(
3473 split("((a|b)|c)&~(d|(e|f))"),
3474 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3475 assert_matches!(
3476 try_split("a&b"),
3477 Err(GitRefExpressionError::PositiveIntersection)
3478 );
3479 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3480 assert_matches!(
3481 try_split("a&~(b&~c)"),
3482 Err(GitRefExpressionError::NestedIntersection)
3483 );
3484 assert_matches!(
3485 try_split("(a|b)&c"),
3486 Err(GitRefExpressionError::PositiveIntersection)
3487 );
3488 assert_matches!(
3489 try_split("(a&~b)&(~c&~d)"),
3490 Err(GitRefExpressionError::PositiveIntersection)
3491 );
3492 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3493 assert_matches!(
3494 try_split("a&~b|c&~d"),
3495 Err(GitRefExpressionError::NestedIntersection)
3496 );
3497
3498 insta::assert_compact_debug_snapshot!(
3501 split("*"),
3502 @r#"([Glob(GlobPattern("*"))], [])"#);
3503 insta::assert_compact_debug_snapshot!(
3504 split("~*"),
3505 @"([], [])");
3506 insta::assert_compact_debug_snapshot!(
3507 split("a~*"),
3508 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3509 insta::assert_compact_debug_snapshot!(
3510 split("~(a|*)"),
3511 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3512 }
3513}