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