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)]
528pub struct GitImportRefUpdate {
529 pub symbol: RemoteRefSymbolBuf,
530 pub old_remote_ref: RemoteRef,
531 pub new_target: RefTarget,
532}
533
534impl GitImportRefUpdate {
535 pub fn new(
536 symbol: RemoteRefSymbolBuf,
537 old_remote_ref: RemoteRef,
538 new_target: RefTarget,
539 ) -> Self {
540 Self {
541 symbol,
542 old_remote_ref,
543 new_target,
544 }
545 }
546}
547
548#[derive(Clone, Debug, Eq, PartialEq, Default)]
550pub struct GitImportStats {
551 pub abandoned_commits: Vec<Commit>,
553 pub rewritten_commit_ids: HashSet<CommitId>,
555 pub changed_remote_bookmarks: Vec<GitImportRefUpdate>,
558 pub changed_remote_tags: Vec<GitImportRefUpdate>,
561 pub failed_ref_names: Vec<BString>,
566}
567
568#[derive(Debug)]
569struct RefsToImport {
570 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
573 changed_remote_bookmarks: Vec<GitImportRefUpdate>,
576 changed_remote_tags: Vec<GitImportRefUpdate>,
579 failed_ref_names: Vec<BString>,
581}
582
583pub async fn import_refs(
588 mut_repo: &mut MutableRepo,
589 options: &GitImportOptions,
590) -> Result<GitImportStats, GitImportError> {
591 import_some_refs(mut_repo, options, |_, _| true).await
592}
593
594pub async fn import_some_refs(
599 mut_repo: &mut MutableRepo,
600 options: &GitImportOptions,
601 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
602) -> Result<GitImportStats, GitImportError> {
603 let git_repo = get_git_repo(mut_repo.store())?;
604
605 for remote_name in iter_remote_names(&git_repo) {
609 mut_repo.ensure_remote(&remote_name);
610 }
611
612 let all_remote_tags = false;
614 let refs_to_import =
615 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
616 import_refs_inner(mut_repo, refs_to_import, options).await
617}
618
619async fn import_refs_inner(
620 mut_repo: &mut MutableRepo,
621 refs_to_import: RefsToImport,
622 options: &GitImportOptions,
623) -> Result<GitImportStats, GitImportError> {
624 let store = mut_repo.store();
625 let git_backend = get_git_backend(store).expect("backend type should have been tested");
626
627 let RefsToImport {
628 changed_git_refs,
629 changed_remote_bookmarks,
630 changed_remote_tags,
631 failed_ref_names,
632 } = refs_to_import;
633
634 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
635 let (old_referenced_heads, new_referenced_heads) = {
637 let mut old_heads = Vec::new();
638 let mut new_heads = Vec::new();
639 for update in iter_changed_refs() {
640 old_heads.extend(update.old_remote_ref.target.added_ids().cloned());
641 new_heads.extend(update.new_target.added_ids().cloned());
642 }
643 (old_heads, new_heads)
644 };
645 let old_visible_heads = mut_repo.view().heads().iter().cloned().collect_vec();
646
647 let index = mut_repo.index();
653 let missing_head_ids: Vec<&CommitId> = new_referenced_heads
654 .iter()
655 .filter_map(|id| match index.has_id(id) {
656 Ok(false) => Some(Ok(id)),
657 Ok(true) => None,
658 Err(e) => Some(Err(e)),
659 })
660 .try_collect()?;
661 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
662
663 let mut head_commits = Vec::new();
665 let get_commit = async |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
666 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
667 symbol: symbol.clone(),
668 err,
669 };
670 if !heads_imported && !index.has_id(id)? {
672 git_backend
673 .import_head_commits([id])
674 .map_err(missing_ref_err)?;
675 }
676 store.get_commit_async(id).await.map_err(missing_ref_err)
677 };
678 for update in iter_changed_refs() {
681 for id in update.new_target.added_ids() {
682 let commit = get_commit(id, &update.symbol).await?;
683 head_commits.push(commit);
684 }
685 }
686 let imported_commits = mut_repo.index_commits(&head_commits).await?;
689 mut_repo.add_heads(&head_commits).await?;
690
691 for (full_name, new_target) in changed_git_refs {
693 mut_repo.set_git_ref_target(&full_name, new_target);
694 }
695 for update in &changed_remote_bookmarks {
696 let symbol = update.symbol.as_ref();
697 let base_target = update.old_remote_ref.tracked_target();
698 let new_remote_ref = RemoteRef {
699 target: update.new_target.clone(),
700 state: if &update.old_remote_ref != RemoteRef::absent_ref() {
701 update.old_remote_ref.state
702 } else {
703 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
704 },
705 };
706 if new_remote_ref.is_tracked() {
707 mut_repo
708 .merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)
709 .await?;
710 }
711 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
714 }
715 for update in &changed_remote_tags {
716 let symbol = update.symbol.as_ref();
717 let base_target = update.old_remote_ref.tracked_target();
718 let new_remote_ref = RemoteRef {
719 target: update.new_target.clone(),
720 state: if &update.old_remote_ref != RemoteRef::absent_ref() {
721 update.old_remote_ref.state
722 } else {
723 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
724 },
725 };
726 if new_remote_ref.is_tracked() {
727 mut_repo
728 .merge_local_tag(symbol.name, base_target, &new_remote_ref.target)
729 .await?;
730 }
731 mut_repo.set_remote_tag(symbol, new_remote_ref);
734 }
735
736 let any_old_referenced = !old_referenced_heads.is_empty();
737 let any_new_referenced = !new_referenced_heads.is_empty();
738 let old_visible_heads = RevsetExpression::commits(old_visible_heads);
739 let old_referenced_heads = RevsetExpression::commits(old_referenced_heads);
740 let new_referenced_heads = RevsetExpression::commits(new_referenced_heads);
741 let mut abandoned_commits = if options.abandon_unreachable_commits && any_old_referenced {
742 abandon_unreachable_commits(mut_repo, &old_referenced_heads).await?
743 } else {
744 vec![]
745 };
746 let rewritten_commit_ids = if options.record_synthetic_predecessors && any_new_referenced {
747 record_synthetic_predecessors(
748 mut_repo,
749 &old_visible_heads,
750 Diff::new(&old_referenced_heads, &new_referenced_heads),
751 &imported_commits,
752 options.abandon_unreachable_commits,
755 )
756 .await?
757 } else {
758 HashSet::new()
759 };
760 abandoned_commits.retain(|commit| !rewritten_commit_ids.contains(commit.id()));
761 let stats = GitImportStats {
762 abandoned_commits,
763 rewritten_commit_ids,
764 changed_remote_bookmarks,
765 changed_remote_tags,
766 failed_ref_names,
767 };
768 Ok(stats)
769}
770
771async fn abandon_unreachable_commits(
774 mut_repo: &mut MutableRepo,
775 hidable_git_heads: &Arc<ResolvedRevsetExpression>,
776) -> Result<Vec<Commit>, GitImportError> {
777 let pinned_expression = RevsetExpression::union_all(&[
778 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
780 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
781 .intersection(&RevsetExpression::visible_heads().ancestors()),
783 RevsetExpression::root(),
784 ]);
785 let abandoned_expression = pinned_expression
786 .range(hidable_git_heads)
787 .intersection(&RevsetExpression::visible_heads().ancestors());
789 let abandoned_commits: Vec<_> = abandoned_expression
790 .evaluate(mut_repo)?
791 .stream()
792 .commits(mut_repo.store())
793 .try_collect()
794 .await?;
795 for commit in &abandoned_commits {
796 mut_repo.record_abandoned_commit(commit);
797 }
798 Ok(abandoned_commits)
799}
800
801async fn record_synthetic_predecessors(
809 mut_repo: &mut MutableRepo,
810 old_visible_heads: &Arc<ResolvedRevsetExpression>,
811 Diff {
812 before: old_referenced_heads,
813 after: new_referenced_heads,
814 }: Diff<&Arc<ResolvedRevsetExpression>>,
815 imported_commits: &[Commit],
816 rewrite_commits: bool,
817) -> Result<HashSet<CommitId>, GitImportError> {
818 let build_change_to_commit_ids_map = async |expr: Arc<ResolvedRevsetExpression>| {
819 let mut change_to_commit_ids: HashMap<ChangeId, Vec<CommitId>> = HashMap::new();
820 let mut stream = expr.evaluate(mut_repo)?.commit_change_ids();
821 while let Some((commit_id, change_id)) = stream.try_next().await? {
822 let commit_ids = change_to_commit_ids.entry(change_id).or_default();
823 commit_ids.push(commit_id);
824 }
825 Ok::<_, GitImportError>(change_to_commit_ids)
826 };
827 let old_referenced_change_to_commit_ids =
828 build_change_to_commit_ids_map(new_referenced_heads.range(old_referenced_heads)).await?;
829 let new_referenced_change_to_commit_ids =
830 build_change_to_commit_ids_map(old_visible_heads.range(new_referenced_heads)).await?;
831 let imported_commit_ids: HashSet<_> = imported_commits.iter().map(Commit::id).collect();
832 let rewritable_commit_ids: HashSet<_> = if rewrite_commits {
833 new_referenced_heads
836 .range(old_referenced_heads)
837 .intersection(&old_visible_heads.ancestors())
838 .evaluate(mut_repo)?
839 .stream()
840 .try_collect()
841 .await?
842 } else {
843 HashSet::new()
844 };
845
846 let mut rewritten_commit_ids = HashSet::new();
847 for (change_id, new_commit_ids) in &new_referenced_change_to_commit_ids {
848 let predecessor_id: Option<CommitId>;
849 let rewrite_source_ids: &[CommitId];
850 if let Some(old_commit_ids) = old_referenced_change_to_commit_ids.get(change_id) {
851 predecessor_id = Some(old_commit_ids[0].clone());
854 rewrite_source_ids = old_commit_ids;
855 } else {
856 predecessor_id = None;
858 rewrite_source_ids = &[];
859 }
860 for new_commit_id in new_commit_ids
865 .iter()
866 .filter(|&id| imported_commit_ids.contains(id))
867 {
868 mut_repo.set_predecessors(new_commit_id.clone(), predecessor_id.as_slice().to_vec());
869 }
870 let rewrite_source_ids = rewrite_source_ids
871 .iter()
872 .filter(|id| rewritable_commit_ids.contains(id));
873 if let [new_commit_id] = &**new_commit_ids {
874 for old_commit_id in rewrite_source_ids {
875 mut_repo.set_rewritten_commit(old_commit_id.clone(), new_commit_id.clone());
876 rewritten_commit_ids.insert(old_commit_id.clone());
877 }
878 } else {
879 for old_commit_id in rewrite_source_ids {
880 mut_repo
881 .set_divergent_rewrite(old_commit_id.clone(), new_commit_ids.iter().cloned());
882 rewritten_commit_ids.insert(old_commit_id.clone());
883 }
884 }
885 }
886
887 Ok(rewritten_commit_ids)
888}
889
890fn diff_refs_to_import(
892 view: &View,
893 git_repo: &gix::Repository,
894 all_remote_tags: bool,
895 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
896) -> Result<RefsToImport, GitImportError> {
897 let mut known_git_refs = view
898 .git_refs()
899 .iter()
900 .filter_map(|(full_name, target)| {
901 let (kind, symbol) =
903 parse_git_ref(full_name).expect("stored git ref should be parsable");
904 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
905 })
906 .collect();
907 let mut known_remote_bookmarks = view
908 .all_remote_bookmarks()
909 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
910 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
911 .collect();
912 let mut known_remote_tags = if all_remote_tags {
913 view.all_remote_tags()
914 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
915 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
916 .collect()
917 } else {
918 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
919 view.remote_tags(remote)
920 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
921 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
922 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
923 .collect()
924 };
925
926 let mut changed_git_refs = Vec::new();
931 let mut changed_remote_bookmarks = Vec::new();
932 let mut changed_remote_tags = Vec::new();
933 let mut failed_ref_names = Vec::new();
934 let actual = git_repo.references().map_err(GitImportError::from_git)?;
935 collect_changed_refs_to_import(
936 actual.local_branches().map_err(GitImportError::from_git)?,
937 &mut known_git_refs,
938 &mut known_remote_bookmarks,
939 &mut changed_git_refs,
940 &mut changed_remote_bookmarks,
941 &mut failed_ref_names,
942 &git_ref_filter,
943 )?;
944 collect_changed_refs_to_import(
945 actual.remote_branches().map_err(GitImportError::from_git)?,
946 &mut known_git_refs,
947 &mut known_remote_bookmarks,
948 &mut changed_git_refs,
949 &mut changed_remote_bookmarks,
950 &mut failed_ref_names,
951 &git_ref_filter,
952 )?;
953 collect_changed_refs_to_import(
954 actual.tags().map_err(GitImportError::from_git)?,
955 &mut known_git_refs,
956 &mut known_remote_tags,
957 &mut changed_git_refs,
958 &mut changed_remote_tags,
959 &mut failed_ref_names,
960 &git_ref_filter,
961 )?;
962 if all_remote_tags {
963 collect_changed_remote_tags_to_import(
964 actual
965 .prefixed(REMOTE_TAG_REF_NAMESPACE)
966 .map_err(GitImportError::from_git)?,
967 &mut known_remote_tags,
968 &mut changed_remote_tags,
969 &mut failed_ref_names,
970 &git_ref_filter,
971 )?;
972 }
973 for full_name in known_git_refs.into_keys() {
974 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
975 }
976 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
977 if old.is_present() {
978 changed_remote_bookmarks.push(GitImportRefUpdate::new(
979 symbol.to_owned(),
980 old.clone(),
981 RefTarget::absent(),
982 ));
983 }
984 }
985 for (RemoteRefKey(symbol), old) in known_remote_tags {
986 if old.is_present() {
987 changed_remote_tags.push(GitImportRefUpdate::new(
988 symbol.to_owned(),
989 old.clone(),
990 RefTarget::absent(),
991 ));
992 }
993 }
994
995 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
997 changed_remote_bookmarks
998 .sort_unstable_by(|update1, update2| update1.symbol.cmp(&update2.symbol));
999 changed_remote_tags.sort_unstable_by(|update1, update2| update1.symbol.cmp(&update2.symbol));
1000 failed_ref_names.sort_unstable();
1001 Ok(RefsToImport {
1002 changed_git_refs,
1003 changed_remote_bookmarks,
1004 changed_remote_tags,
1005 failed_ref_names,
1006 })
1007}
1008
1009fn collect_changed_refs_to_import(
1010 actual_git_refs: gix::reference::iter::Iter,
1011 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
1012 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
1013 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
1014 changed_remote_refs: &mut Vec<GitImportRefUpdate>,
1015 failed_ref_names: &mut Vec<BString>,
1016 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1017) -> Result<(), GitImportError> {
1018 for git_ref in actual_git_refs {
1019 let git_ref = git_ref.map_err(GitImportError::from_git)?;
1020 let full_name_bytes = git_ref.name().as_bstr();
1021 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
1022 failed_ref_names.push(full_name_bytes.to_owned());
1024 continue;
1025 };
1026 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
1027 failed_ref_names.push(full_name_bytes.to_owned());
1028 continue;
1029 }
1030 let full_name = GitRefName::new(full_name);
1031 let Some((kind, symbol)) = parse_git_ref(full_name) else {
1032 continue;
1034 };
1035 if !git_ref_filter(kind, symbol) {
1036 continue;
1037 }
1038 let old_git_target = known_git_refs.get(full_name).copied().flatten();
1039 let old_git_oid = old_git_target.as_normal().map(oid_from_commit_id);
1040 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
1041 continue;
1043 };
1044 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
1045 known_git_refs.remove(full_name);
1046 if new_target != *old_git_target {
1047 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
1048 }
1049 let old_remote_ref = known_remote_refs
1052 .remove(&symbol)
1053 .unwrap_or_else(|| RemoteRef::absent_ref());
1054 if new_target != old_remote_ref.target {
1055 changed_remote_refs.push(GitImportRefUpdate::new(
1056 symbol.to_owned(),
1057 old_remote_ref.clone(),
1058 new_target,
1059 ));
1060 }
1061 }
1062 Ok(())
1063}
1064
1065fn collect_changed_remote_tags_to_import(
1068 actual_git_refs: gix::reference::iter::Iter,
1069 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
1070 changed_remote_refs: &mut Vec<GitImportRefUpdate>,
1071 failed_ref_names: &mut Vec<BString>,
1072 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1073) -> Result<(), GitImportError> {
1074 for git_ref in actual_git_refs {
1075 let git_ref = git_ref.map_err(GitImportError::from_git)?;
1076 let full_name_bytes = git_ref.name().as_bstr();
1077 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
1078 failed_ref_names.push(full_name_bytes.to_owned());
1080 continue;
1081 };
1082 let full_name = GitRefName::new(full_name);
1083 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
1084 continue;
1086 };
1087 if !git_ref_filter(kind, symbol) {
1088 continue;
1089 }
1090 let old_remote_ref = known_remote_refs
1091 .get(&symbol)
1092 .copied()
1093 .unwrap_or_else(|| RemoteRef::absent_ref());
1094 let old_git_oid = old_remote_ref.target.as_normal().map(oid_from_commit_id);
1095 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
1096 continue;
1098 };
1099 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
1100 known_remote_refs.remove(&symbol);
1101 if new_target != old_remote_ref.target {
1102 changed_remote_refs.push(GitImportRefUpdate::new(
1103 symbol.to_owned(),
1104 old_remote_ref.clone(),
1105 new_target,
1106 ));
1107 }
1108 }
1109 Ok(())
1110}
1111
1112fn default_remote_ref_state_for(
1113 kind: GitRefKind,
1114 symbol: RemoteRefSymbol<'_>,
1115 options: &GitImportOptions,
1116) -> RemoteRefState {
1117 match kind {
1118 GitRefKind::Bookmark => {
1119 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1120 || options
1121 .remote_auto_track_bookmarks
1122 .get(symbol.remote)
1123 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
1124 {
1125 RemoteRefState::Tracked
1126 } else {
1127 RemoteRefState::New
1128 }
1129 }
1130 GitRefKind::Tag => RemoteRefState::Tracked,
1132 }
1133}
1134
1135fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
1141 itertools::chain(view.local_bookmarks(), view.local_tags())
1142 .flat_map(|(_, target)| target.added_ids())
1143 .cloned()
1144 .collect()
1145}
1146
1147fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
1154 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
1155 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
1156 .map(|(_, remote_ref)| &remote_ref.target)
1157 .flat_map(|target| target.added_ids())
1158 .cloned()
1159 .collect()
1160}
1161
1162pub async fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
1170 let store = mut_repo.store();
1171 let git_backend = get_git_backend(store)?;
1172 let git_repo = git_backend.git_repo();
1173
1174 let old_git_head = mut_repo.view().git_head();
1175 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1176 Some(CommitId::from_bytes(oid.as_bytes()))
1177 } else {
1178 None
1179 };
1180 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1181 return Ok(());
1182 }
1183
1184 if let Some(head_id) = &new_git_head_id {
1186 let index = mut_repo.index();
1187 if !index.has_id(head_id)? {
1188 git_backend.import_head_commits([head_id]).map_err(|err| {
1189 GitImportError::MissingHeadTarget {
1190 id: head_id.clone(),
1191 err,
1192 }
1193 })?;
1194 }
1195 let commit = store.get_commit_async(head_id).await?;
1198 mut_repo.add_head(&commit).await?;
1199 }
1200
1201 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1202 Ok(())
1203}
1204
1205#[derive(Error, Debug)]
1206pub enum GitExportError {
1207 #[error(transparent)]
1208 Git(Box<dyn std::error::Error + Send + Sync>),
1209 #[error(transparent)]
1210 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1211}
1212
1213impl GitExportError {
1214 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1215 Self::Git(source.into())
1216 }
1217}
1218
1219#[derive(Debug, Error)]
1221pub enum FailedRefExportReason {
1222 #[error("Name is not allowed in Git")]
1224 InvalidGitName,
1225 #[error("Ref was in a conflicted state from the last import")]
1228 ConflictedOldState,
1229 #[error("Ref cannot point to the root commit in Git")]
1231 OnRootCommit,
1232 #[error("Deleted ref had been modified in Git")]
1234 DeletedInJjModifiedInGit,
1235 #[error("Added ref had been added with a different target in Git")]
1237 AddedInJjAddedInGit,
1238 #[error("Modified ref had been deleted in Git")]
1240 ModifiedInJjDeletedInGit,
1241 #[error("Failed to delete")]
1243 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1244 #[error("Failed to set")]
1246 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1247}
1248
1249#[derive(Debug)]
1251pub struct GitExportStats {
1252 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1254 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1258}
1259
1260#[derive(Debug)]
1261struct AllRefsToExport {
1262 bookmarks: RefsToExport,
1263 tags: RefsToExport,
1264}
1265
1266#[derive(Debug)]
1267struct RefsToExport {
1268 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1270 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1275 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1277}
1278
1279pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1288 export_some_refs(mut_repo, |_, _| true)
1289}
1290
1291pub fn export_some_refs(
1292 mut_repo: &mut MutableRepo,
1293 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1294) -> Result<GitExportStats, GitExportError> {
1295 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1296 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1297 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1298 let (_, value) = &map[index];
1299 Some(value)
1300 }
1301
1302 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1303 mut_repo.view(),
1304 mut_repo.store().root_commit_id(),
1305 &git_ref_filter,
1306 );
1307
1308 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1309 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1310 return Ok(());
1311 };
1312 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1313 if let Some((kind, symbol)) = target_name
1314 .as_ref()
1315 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1316 .and_then(|name| parse_git_ref(name.as_ref()))
1317 {
1318 let old_target = head_ref.inner.target.clone();
1319 let current_oid = match head_ref.into_fully_peeled_id() {
1320 Ok(id) => Some(id.detach()),
1321 Err(gix::reference::peel::Error::ToId(
1322 gix::refs::peel::to_id::Error::FollowToObject(
1323 gix::refs::peel::to_object::Error::Follow(
1324 gix::refs::file::find::existing::Error::NotFound { .. },
1325 ),
1326 ),
1327 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1329 };
1330 let refs = match kind {
1331 GitRefKind::Bookmark => &bookmarks,
1332 GitRefKind::Tag => &tags,
1333 };
1334 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1335 Some(new_oid)
1336 } else if get(&refs.to_delete, symbol).is_some() {
1337 None
1338 } else {
1339 current_oid.as_ref()
1340 };
1341 if new_oid != current_oid.as_ref() {
1342 update_git_head(
1343 git_repo,
1344 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1345 current_oid,
1346 )
1347 .map_err(GitExportError::from_git)?;
1348 }
1349 }
1350 Ok(())
1351 };
1352
1353 let git_repo = get_git_repo(mut_repo.store())?;
1354
1355 check_and_detach_head(&git_repo)?;
1356 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1357 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1358 check_and_detach_head(&worktree_repo)?;
1359 }
1360 }
1361
1362 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1363 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1364
1365 copy_exportable_local_bookmarks_to_remote_view(
1366 mut_repo,
1367 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1368 |name| {
1369 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1370 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1371 },
1372 );
1373 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1374 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1375 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1376 });
1377
1378 Ok(GitExportStats {
1379 failed_bookmarks,
1380 failed_tags,
1381 })
1382}
1383
1384fn export_refs_to_git(
1385 mut_repo: &mut MutableRepo,
1386 git_repo: &gix::Repository,
1387 kind: GitRefKind,
1388 refs: RefsToExport,
1389) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1390 let mut failed = refs.failed;
1391 for (symbol, old_oid) in refs.to_delete {
1392 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1393 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1394 continue;
1395 };
1396 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1397 failed.push((symbol, reason));
1398 } else {
1399 let new_target = RefTarget::absent();
1400 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1401 }
1402 }
1403 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1404 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1405 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1406 continue;
1407 };
1408 let new_ref_oid = match kind {
1409 GitRefKind::Bookmark => None,
1410 GitRefKind::Tag => {
1412 let remote_matcher = StringMatcher::all();
1413 find_git_tag_oid_to_copy(
1414 mut_repo.view(),
1415 git_repo,
1416 &symbol.name,
1417 &remote_matcher,
1418 &new_commit_oid,
1419 )
1420 }
1421 };
1422 if let Err(reason) = update_git_ref(
1423 git_repo,
1424 &git_ref_name,
1425 old_commit_oid,
1426 new_commit_oid,
1427 new_ref_oid,
1428 ) {
1429 failed.push((symbol, reason));
1430 } else {
1431 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1432 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1433 }
1434 }
1435
1436 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1438 failed
1439}
1440
1441fn copy_exportable_local_bookmarks_to_remote_view(
1442 mut_repo: &mut MutableRepo,
1443 remote: &RemoteName,
1444 name_filter: impl Fn(&RefName) -> bool,
1445) {
1446 let new_local_bookmarks = mut_repo
1447 .view()
1448 .local_remote_bookmarks(remote)
1449 .filter_map(|(name, targets)| {
1450 let old_target = &targets.remote_ref.target;
1453 let new_target = targets.local_target;
1454 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1455 })
1456 .filter(|&(name, _)| name_filter(name))
1457 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1458 .collect_vec();
1459 for (name, new_target) in new_local_bookmarks {
1460 let new_remote_ref = RemoteRef {
1461 target: new_target,
1462 state: RemoteRefState::Tracked,
1463 };
1464 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1465 }
1466}
1467
1468fn copy_exportable_local_tags_to_remote_view(
1469 mut_repo: &mut MutableRepo,
1470 remote: &RemoteName,
1471 name_filter: impl Fn(&RefName) -> bool,
1472) {
1473 let new_local_tags = mut_repo
1474 .view()
1475 .local_remote_tags(remote)
1476 .filter_map(|(name, targets)| {
1477 let old_target = &targets.remote_ref.target;
1479 let new_target = targets.local_target;
1480 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1481 })
1482 .filter(|&(name, _)| name_filter(name))
1483 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1484 .collect_vec();
1485 for (name, new_target) in new_local_tags {
1486 let new_remote_ref = RemoteRef {
1487 target: new_target,
1488 state: RemoteRefState::Tracked,
1489 };
1490 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1491 }
1492}
1493
1494fn diff_refs_to_export(
1496 view: &View,
1497 root_commit_id: &CommitId,
1498 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1499) -> AllRefsToExport {
1500 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1503 itertools::chain(
1504 view.local_bookmarks().map(|(name, target)| {
1505 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1506 (symbol, target)
1507 }),
1508 view.all_remote_bookmarks()
1509 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1510 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1511 )
1512 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1513 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1514 .collect();
1515 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1517 .local_tags()
1518 .map(|(name, target)| {
1519 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1520 (symbol, target)
1521 })
1522 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1523 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1524 .collect();
1525 let known_git_refs = view
1526 .git_refs()
1527 .iter()
1528 .map(|(full_name, target)| {
1529 let (kind, symbol) =
1530 parse_git_ref(full_name).expect("stored git ref should be parsable");
1531 ((kind, symbol), target)
1532 })
1533 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1537 for ((kind, symbol), target) in known_git_refs {
1538 let ref_targets = match kind {
1539 GitRefKind::Bookmark => &mut all_bookmark_targets,
1540 GitRefKind::Tag => &mut all_tag_targets,
1541 };
1542 ref_targets
1543 .entry(symbol)
1544 .and_modify(|(old_target, _)| *old_target = target)
1545 .or_insert((target, RefTarget::absent_ref()));
1546 }
1547
1548 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1549 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1550 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1551 AllRefsToExport { bookmarks, tags }
1552}
1553
1554fn collect_changed_refs_to_export(
1555 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1556 root_commit_target: &RefTarget,
1557) -> RefsToExport {
1558 let mut to_update = Vec::new();
1559 let mut to_delete = Vec::new();
1560 let mut failed = Vec::new();
1561 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1562 if new_target == old_target {
1563 continue;
1564 }
1565 if new_target == root_commit_target {
1566 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1568 continue;
1569 }
1570 let old_oid = if let Some(id) = old_target.as_normal() {
1571 Some(owned_oid_from_commit_id(id))
1572 } else if old_target.has_conflict() {
1573 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1576 continue;
1577 } else {
1578 assert!(old_target.is_absent());
1579 None
1580 };
1581 if let Some(id) = new_target.as_normal() {
1582 let new_oid = owned_oid_from_commit_id(id);
1583 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1584 } else if new_target.has_conflict() {
1585 continue;
1587 } else {
1588 assert!(new_target.is_absent());
1589 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1590 }
1591 }
1592
1593 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1595 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1596 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1597 RefsToExport {
1598 to_update,
1599 to_delete,
1600 failed,
1601 }
1602}
1603
1604fn find_git_tag_oid_to_copy(
1607 view: &View,
1608 git_repo: &gix::Repository,
1609 name: &RefName,
1610 remote_matcher: &StringMatcher,
1611 commit_oid: &gix::oid,
1612) -> Option<gix::ObjectId> {
1613 view.remote_tags_matching(&StringMatcher::exact(name), remote_matcher)
1615 .filter(|(_, remote_ref)| {
1616 let maybe_id = remote_ref.tracked_target().as_normal();
1617 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1618 })
1619 .filter_map(|(symbol, _)| {
1621 let git_ref_name = to_git_or_remote_tag_ref_name(symbol);
1622 git_repo.find_reference(git_ref_name.as_str()).ok()
1623 })
1624 .filter(|git_ref| {
1627 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1628 })
1629 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1630}
1631
1632fn delete_git_ref(
1633 git_repo: &gix::Repository,
1634 git_ref_name: &GitRefName,
1635 old_oid: &gix::oid,
1636) -> Result<(), FailedRefExportReason> {
1637 let Some(git_ref) = git_repo
1638 .try_find_reference(git_ref_name.as_str())
1639 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1640 else {
1641 return Ok(());
1643 };
1644 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1645 git_ref
1647 .delete()
1648 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1649 } else {
1650 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1652 }
1653}
1654
1655fn create_git_ref(
1657 git_repo: &gix::Repository,
1658 git_ref_name: &GitRefName,
1659 new_commit_oid: gix::ObjectId,
1660 new_ref_oid: Option<gix::ObjectId>,
1661) -> Result<(), FailedRefExportReason> {
1662 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1663 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1664 let Err(set_err) =
1665 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1666 else {
1667 return Ok(());
1669 };
1670 let Some(git_ref) = git_repo
1671 .try_find_reference(git_ref_name.as_str())
1672 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1673 else {
1674 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1675 };
1676 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1679 Ok(())
1680 } else {
1681 Err(FailedRefExportReason::AddedInJjAddedInGit)
1682 }
1683}
1684
1685fn move_git_ref(
1687 git_repo: &gix::Repository,
1688 git_ref_name: &GitRefName,
1689 old_commit_oid: gix::ObjectId,
1690 new_commit_oid: gix::ObjectId,
1691 new_ref_oid: Option<gix::ObjectId>,
1692) -> Result<(), FailedRefExportReason> {
1693 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1694 let constraint =
1695 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1696 let Err(set_err) =
1697 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1698 else {
1699 return Ok(());
1701 };
1702 let Some(git_ref) = git_repo
1704 .try_find_reference(git_ref_name.as_str())
1705 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1706 else {
1707 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1709 };
1710 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1712 if git_commit_oid == Some(new_commit_oid) {
1713 Ok(())
1714 } else if git_commit_oid == Some(old_commit_oid) {
1715 let constraint =
1717 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1718 git_repo
1719 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1720 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1721 Ok(())
1722 } else {
1723 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1724 }
1725}
1726
1727fn update_git_ref(
1728 git_repo: &gix::Repository,
1729 git_ref_name: &GitRefName,
1730 old_commit_oid: Option<gix::ObjectId>,
1731 new_commit_oid: gix::ObjectId,
1732 new_ref_oid: Option<gix::ObjectId>,
1733) -> Result<(), FailedRefExportReason> {
1734 match old_commit_oid {
1735 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1736 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1737 }
1738}
1739
1740fn update_git_head(
1743 git_repo: &gix::Repository,
1744 expected_ref: gix::refs::transaction::PreviousValue,
1745 new_oid: Option<gix::ObjectId>,
1746) -> Result<(), gix::reference::edit::Error> {
1747 let mut ref_edits = Vec::new();
1748 let new_target = if let Some(oid) = new_oid {
1749 gix::refs::Target::Object(oid)
1750 } else {
1751 ref_edits.push(gix::refs::transaction::RefEdit {
1756 change: gix::refs::transaction::Change::Delete {
1757 expected: gix::refs::transaction::PreviousValue::Any,
1758 log: gix::refs::transaction::RefLog::AndReference,
1759 },
1760 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1761 deref: false,
1762 });
1763 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1764 };
1765 ref_edits.push(gix::refs::transaction::RefEdit {
1766 change: gix::refs::transaction::Change::Update {
1767 log: gix::refs::transaction::LogChange {
1768 message: "export from jj".into(),
1769 ..Default::default()
1770 },
1771 expected: expected_ref,
1772 new: new_target,
1773 },
1774 name: "HEAD".try_into().unwrap(),
1775 deref: false,
1776 });
1777 git_repo.edit_references(ref_edits)?;
1778 Ok(())
1779}
1780
1781#[derive(Debug, Error)]
1782pub enum GitResetHeadError {
1783 #[error(transparent)]
1784 Backend(#[from] BackendError),
1785 #[error(transparent)]
1786 Git(Box<dyn std::error::Error + Send + Sync>),
1787 #[error("Failed to update Git HEAD ref")]
1788 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1789 #[error(transparent)]
1790 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1791}
1792
1793impl GitResetHeadError {
1794 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1795 Self::Git(source.into())
1796 }
1797}
1798
1799pub async fn reset_head(
1802 mut_repo: &mut MutableRepo,
1803 wc_commit: &Commit,
1804) -> Result<(), GitResetHeadError> {
1805 let git_repo = get_git_repo(mut_repo.store())?;
1806
1807 let first_parent_id = &wc_commit.parent_ids()[0];
1808 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1809 RefTarget::normal(first_parent_id.clone())
1810 } else {
1811 RefTarget::absent()
1812 };
1813
1814 let old_head_target = mut_repo.git_head();
1816 if old_head_target != new_head_target {
1817 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1818 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1821 if actual_head.is_detached() {
1822 let id = owned_oid_from_commit_id(id);
1823 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1824 } else {
1825 gix::refs::transaction::PreviousValue::MustExist
1828 }
1829 } else {
1830 gix::refs::transaction::PreviousValue::MustExist
1832 };
1833 let new_oid = new_head_target.as_normal().map(owned_oid_from_commit_id);
1834 update_git_head(&git_repo, expected_ref, new_oid)
1835 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1836 mut_repo.set_git_head_target(new_head_target);
1837 }
1838
1839 if git_repo.state().is_some() {
1842 clear_operation_state(&git_repo)?;
1843 }
1844
1845 reset_index(mut_repo, &git_repo, wc_commit).await
1846}
1847
1848fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1850 const STATE_FILE_NAMES: &[&str] = &[
1854 "MERGE_HEAD",
1855 "MERGE_MODE",
1856 "MERGE_MSG",
1857 "REVERT_HEAD",
1858 "CHERRY_PICK_HEAD",
1859 "BISECT_LOG",
1860 ];
1861 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1862 let handle_err = |err: PathError| match err.source.kind() {
1863 std::io::ErrorKind::NotFound => Ok(()),
1864 _ => Err(GitResetHeadError::from_git(err)),
1865 };
1866 for file_name in STATE_FILE_NAMES {
1867 let path = git_repo.path().join(file_name);
1868 std::fs::remove_file(&path)
1869 .context(&path)
1870 .or_else(handle_err)?;
1871 }
1872 for dir_name in STATE_DIR_NAMES {
1873 let path = git_repo.path().join(dir_name);
1874 std::fs::remove_dir_all(&path)
1875 .context(&path)
1876 .or_else(handle_err)?;
1877 }
1878 Ok(())
1879}
1880
1881async fn reset_index(
1882 repo: &dyn Repo,
1883 git_repo: &gix::Repository,
1884 wc_commit: &Commit,
1885) -> Result<(), GitResetHeadError> {
1886 let parent_tree = wc_commit.parent_tree(repo).await?;
1887 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1891 if tree_id == repo.store().empty_tree_id() {
1892 gix::index::File::from_state(
1896 gix::index::State::new(git_repo.object_hash()),
1897 git_repo.index_path(),
1898 )
1899 } else {
1900 git_repo
1903 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1904 .map_err(GitResetHeadError::from_git)?
1905 }
1906 } else {
1907 build_index_from_merged_tree(git_repo, &parent_tree)?
1908 };
1909
1910 let wc_tree = wc_commit.tree();
1911 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).await?;
1912
1913 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1916 index
1917 .entries_mut_with_paths()
1918 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1919 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1920 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1921 })
1922 .filter_map(|merged| merged.both())
1923 .map(|((entry, _), old_entry)| (entry, old_entry))
1924 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1925 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1926 }
1927
1928 debug_assert!(index.verify_entries().is_ok());
1929
1930 index
1931 .write(gix::index::write::Options::default())
1932 .map_err(GitResetHeadError::from_git)
1933}
1934
1935fn build_index_from_merged_tree(
1936 git_repo: &gix::Repository,
1937 merged_tree: &MergedTree,
1938) -> Result<gix::index::File, GitResetHeadError> {
1939 let mut index = gix::index::File::from_state(
1940 gix::index::State::new(git_repo.object_hash()),
1941 git_repo.index_path(),
1942 );
1943
1944 let mut push_index_entry =
1945 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1946 let Some(entry) = maybe_entry else {
1947 return;
1948 };
1949
1950 let (id, mode) = match entry {
1951 TreeValue::File {
1952 id,
1953 executable,
1954 copy_id: _,
1955 } => {
1956 if *executable {
1957 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1958 } else {
1959 (id.as_bytes(), gix::index::entry::Mode::FILE)
1960 }
1961 }
1962 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1963 TreeValue::Tree(_) => {
1964 return;
1969 }
1970 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1971 };
1972
1973 let path = BStr::new(path.as_internal_file_string());
1974
1975 index.dangerously_push_entry(
1978 gix::index::entry::Stat::default(),
1979 gix::ObjectId::from_bytes_or_panic(id),
1980 gix::index::entry::Flags::from_stage(stage),
1981 mode,
1982 path,
1983 );
1984 };
1985
1986 let mut has_many_sided_conflict = false;
1987
1988 for (path, entry) in merged_tree.entries() {
1989 let entry = entry?;
1990 if let Some(resolved) = entry.as_resolved() {
1991 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1992 continue;
1993 }
1994
1995 let conflict = entry.simplify();
1996 if let [left, base, right] = conflict.as_slice() {
1997 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1999 push_index_entry(&path, base, gix::index::entry::Stage::Base);
2000 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
2001 } else {
2002 has_many_sided_conflict = true;
2010 push_index_entry(
2011 &path,
2012 conflict.first(),
2013 gix::index::entry::Stage::Unconflicted,
2014 );
2015 }
2016 }
2017
2018 index.sort_entries();
2021
2022 if has_many_sided_conflict
2025 && index
2026 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
2027 .is_err()
2028 {
2029 let file_blob = git_repo
2030 .write_blob(
2031 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
2032 )
2033 .map_err(GitResetHeadError::from_git)?;
2034 index.dangerously_push_entry(
2035 gix::index::entry::Stat::default(),
2036 file_blob.detach(),
2037 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
2038 gix::index::entry::Mode::FILE,
2039 INDEX_DUMMY_CONFLICT_FILE.into(),
2040 );
2041 index.sort_entries();
2044 }
2045
2046 Ok(index)
2047}
2048
2049pub async fn update_intent_to_add(
2056 repo: &dyn Repo,
2057 old_tree: &MergedTree,
2058 new_tree: &MergedTree,
2059) -> Result<(), GitResetHeadError> {
2060 let git_repo = get_git_repo(repo.store())?;
2061 let mut index = git_repo
2062 .index_or_empty()
2063 .map_err(GitResetHeadError::from_git)?;
2064 let mut_index = Arc::make_mut(&mut index);
2065 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).await?;
2066 debug_assert!(mut_index.verify_entries().is_ok());
2067 mut_index
2068 .write(gix::index::write::Options::default())
2069 .map_err(GitResetHeadError::from_git)?;
2070
2071 Ok(())
2072}
2073
2074async fn update_intent_to_add_impl(
2075 git_repo: &gix::Repository,
2076 index: &mut gix::index::File,
2077 old_tree: &MergedTree,
2078 new_tree: &MergedTree,
2079) -> Result<(), GitResetHeadError> {
2080 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
2081 let mut added_paths = vec![];
2082 let mut removed_paths = HashSet::new();
2083 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2084 let values = values?;
2085 if values.before.is_absent() {
2086 let executable = match values.after.as_normal() {
2087 Some(TreeValue::File {
2088 id: _,
2089 executable,
2090 copy_id: _,
2091 }) => *executable,
2092 Some(TreeValue::Symlink(_)) => false,
2093 _ => {
2094 continue;
2095 }
2096 };
2097 if index
2098 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
2099 .is_err()
2100 {
2101 added_paths.push((BString::from(path.into_internal_string()), executable));
2102 }
2103 } else if values.after.is_absent() {
2104 removed_paths.insert(BString::from(path.into_internal_string()));
2105 }
2106 }
2107
2108 if added_paths.is_empty() && removed_paths.is_empty() {
2109 return Ok(());
2110 }
2111
2112 if !added_paths.is_empty() {
2113 let empty_blob = git_repo
2115 .write_blob(b"")
2116 .map_err(GitResetHeadError::from_git)?
2117 .detach();
2118 for (path, executable) in added_paths {
2119 index.dangerously_push_entry(
2121 gix::index::entry::Stat::default(),
2122 empty_blob,
2123 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
2124 if executable {
2125 gix::index::entry::Mode::FILE_EXECUTABLE
2126 } else {
2127 gix::index::entry::Mode::FILE
2128 },
2129 path.as_ref(),
2130 );
2131 }
2132 }
2133 if !removed_paths.is_empty() {
2134 index.remove_entries(|_size, path, entry| {
2135 entry
2136 .flags
2137 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
2138 && removed_paths.contains(path)
2139 });
2140 }
2141
2142 index.sort_entries();
2143
2144 Ok(())
2145}
2146
2147#[derive(Debug, Error)]
2148pub enum GitRemoteManagementError {
2149 #[error("No git remote named '{}'", .0.as_symbol())]
2150 NoSuchRemote(RemoteNameBuf),
2151 #[error("Git remote named '{}' already exists", .0.as_symbol())]
2152 RemoteAlreadyExists(RemoteNameBuf),
2153 #[error(transparent)]
2154 RemoteName(#[from] GitRemoteNameError),
2155 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
2156 NonstandardConfiguration(RemoteNameBuf),
2157 #[error("Error saving Git configuration")]
2158 GitConfigSaveError(#[source] std::io::Error),
2159 #[error("Unexpected Git error when managing remotes")]
2160 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
2161 #[error(transparent)]
2162 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2163 #[error(transparent)]
2164 RefExpansionError(#[from] GitRefExpansionError),
2165}
2166
2167impl GitRemoteManagementError {
2168 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
2169 Self::InternalGitError(source.into())
2170 }
2171}
2172
2173fn default_fetch_refspec(remote: &RemoteName) -> String {
2174 format!(
2175 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
2176 remote = remote.as_str()
2177 )
2178}
2179
2180fn add_ref(
2181 name: gix::refs::FullName,
2182 target: gix::refs::Target,
2183 message: BString,
2184) -> gix::refs::transaction::RefEdit {
2185 gix::refs::transaction::RefEdit {
2186 change: gix::refs::transaction::Change::Update {
2187 log: gix::refs::transaction::LogChange {
2188 mode: gix::refs::transaction::RefLog::AndReference,
2189 force_create_reflog: false,
2190 message,
2191 },
2192 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2193 new: target,
2194 },
2195 name,
2196 deref: false,
2197 }
2198}
2199
2200fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2201 gix::refs::transaction::RefEdit {
2202 change: gix::refs::transaction::Change::Delete {
2203 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2204 reference.target().into_owned(),
2205 ),
2206 log: gix::refs::transaction::RefLog::AndReference,
2207 },
2208 name: reference.name().to_owned(),
2209 deref: false,
2210 }
2211}
2212
2213pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2219 let mut config_file = File::create(
2220 config
2221 .meta()
2222 .path
2223 .as_ref()
2224 .expect("Git repository to have a config file"),
2225 )?;
2226 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2227}
2228
2229fn save_remote(
2230 config: &mut gix::config::File<'static>,
2231 remote_name: &RemoteName,
2232 remote: &mut gix::Remote,
2233) -> Result<(), GitRemoteManagementError> {
2234 config
2241 .new_section(
2242 "remote",
2243 Some(Cow::Owned(BString::from(remote_name.as_str()))),
2244 )
2245 .map_err(GitRemoteManagementError::from_git)?;
2246 remote
2247 .save_as_to(remote_name.as_str(), config)
2248 .map_err(GitRemoteManagementError::from_git)?;
2249 Ok(())
2250}
2251
2252fn git_config_branch_section_ids_by_remote(
2253 config: &gix::config::File,
2254 remote_name: &RemoteName,
2255) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2256 config
2257 .sections_by_name("branch")
2258 .into_iter()
2259 .flatten()
2260 .filter_map(|section| {
2261 let remote_values = section.values("remote");
2262 let push_remote_values = section.values("pushRemote");
2263 if !remote_values
2264 .iter()
2265 .chain(push_remote_values.iter())
2266 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2267 {
2268 return None;
2269 }
2270 let is_supported_key = |name: &gix::config::parse::section::ValueName| -> bool {
2272 name.eq_ignore_ascii_case(b"remote")
2273 || name.eq_ignore_ascii_case(b"merge")
2274 || name.eq_ignore_ascii_case(b"rebase")
2275 };
2276 if remote_values.len() > 1
2277 || push_remote_values.len() > 1
2278 || !section.value_names().all(is_supported_key)
2279 {
2280 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2281 remote_name.to_owned(),
2282 )));
2283 }
2284 Some(Ok(section.id()))
2285 })
2286 .collect()
2287}
2288
2289fn rename_remote_in_git_branch_config_sections(
2290 config: &mut gix::config::File,
2291 old_remote_name: &RemoteName,
2292 new_remote_name: &RemoteName,
2293) -> Result<(), GitRemoteManagementError> {
2294 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2295 config
2296 .section_mut_by_id(id)
2297 .expect("found section to exist")
2298 .set(
2299 "remote"
2300 .try_into()
2301 .expect("'remote' to be a valid value name"),
2302 BStr::new(new_remote_name.as_str()),
2303 );
2304 }
2305 Ok(())
2306}
2307
2308fn remove_remote_git_branch_config_sections(
2309 config: &mut gix::config::File,
2310 remote_name: &RemoteName,
2311) -> Result<(), GitRemoteManagementError> {
2312 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2313 config
2314 .remove_section_by_id(id)
2315 .expect("removed section to exist");
2316 }
2317 Ok(())
2318}
2319
2320fn remove_remote_git_config_sections(
2321 config: &mut gix::config::File,
2322 remote_name: &RemoteName,
2323) -> Result<(), GitRemoteManagementError> {
2324 let section_ids_to_remove: Vec<_> = config
2325 .sections_by_name("remote")
2326 .into_iter()
2327 .flatten()
2328 .filter(|section| {
2329 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2330 })
2331 .map(|section| {
2332 if section.value_names().any(|name| {
2333 !name.eq_ignore_ascii_case(b"url")
2334 && !name.eq_ignore_ascii_case(b"fetch")
2335 && !name.eq_ignore_ascii_case(b"tagOpt")
2336 }) {
2337 return Err(GitRemoteManagementError::NonstandardConfiguration(
2338 remote_name.to_owned(),
2339 ));
2340 }
2341 Ok(section.id())
2342 })
2343 .try_collect()?;
2344 for id in section_ids_to_remove {
2345 config
2346 .remove_section_by_id(id)
2347 .expect("removed section to exist");
2348 }
2349 Ok(())
2350}
2351
2352pub fn get_all_remote_names(
2354 store: &Store,
2355) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2356 let git_repo = get_git_repo(store)?;
2357 Ok(iter_remote_names(&git_repo).collect())
2358}
2359
2360fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2361 git_repo
2362 .remote_names()
2363 .into_iter()
2364 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2366 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2368 .map(RemoteNameBuf::from)
2369}
2370
2371pub fn add_remote(
2372 mut_repo: &mut MutableRepo,
2373 remote_name: &RemoteName,
2374 url: &str,
2375 push_url: Option<&str>,
2376) -> Result<(), GitRemoteManagementError> {
2377 let git_repo = get_git_repo(mut_repo.store())?;
2378
2379 validate_remote_name(remote_name)?;
2380
2381 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2382 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2383 remote_name.to_owned(),
2384 ));
2385 }
2386
2387 let mut remote = git_repo
2388 .remote_at(url)
2389 .map_err(GitRemoteManagementError::from_git)?
2390 .with_refspecs(
2391 [default_fetch_refspec(remote_name).as_bytes()],
2392 gix::remote::Direction::Fetch,
2393 )
2394 .expect("default refspec to be valid");
2395
2396 if let Some(push_url) = push_url {
2397 remote = remote
2398 .with_push_url(push_url)
2399 .map_err(GitRemoteManagementError::from_git)?;
2400 }
2401
2402 let mut config = git_repo.config_snapshot().clone();
2403 save_remote(&mut config, remote_name, &mut remote)?;
2404 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2405
2406 mut_repo.ensure_remote(remote_name);
2407
2408 Ok(())
2409}
2410
2411pub fn remove_remote(
2412 mut_repo: &mut MutableRepo,
2413 remote_name: &RemoteName,
2414) -> Result<(), GitRemoteManagementError> {
2415 let mut git_repo = get_git_repo(mut_repo.store())?;
2416
2417 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2418 return Err(GitRemoteManagementError::NoSuchRemote(
2419 remote_name.to_owned(),
2420 ));
2421 }
2422
2423 let mut config = git_repo.config_snapshot().clone();
2424 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2425 remove_remote_git_config_sections(&mut config, remote_name)?;
2426 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2427
2428 remove_remote_git_refs(&mut git_repo, remote_name)
2429 .map_err(GitRemoteManagementError::from_git)?;
2430
2431 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2432 remove_remote_refs(mut_repo, remote_name);
2433 }
2434
2435 Ok(())
2436}
2437
2438fn remove_remote_git_refs(
2439 git_repo: &mut gix::Repository,
2440 remote: &RemoteName,
2441) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2442 let bookmark_prefix = format!(
2443 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2444 remote = remote.as_str()
2445 );
2446 let tag_prefix = format!(
2447 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2448 remote = remote.as_str()
2449 );
2450 let edits: Vec<_> = itertools::chain(
2451 git_repo
2452 .references()?
2453 .prefixed(bookmark_prefix.as_str())?
2454 .map_ok(remove_ref),
2455 git_repo
2456 .references()?
2457 .prefixed(tag_prefix.as_str())?
2458 .map_ok(remove_ref),
2459 )
2460 .try_collect()?;
2461 git_repo.edit_references(edits)?;
2462 Ok(())
2463}
2464
2465fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2466 mut_repo.remove_remote(remote);
2467 let prefix = format!(
2468 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2469 remote = remote.as_str()
2470 );
2471 let git_refs_to_delete = mut_repo
2472 .view()
2473 .git_refs()
2474 .keys()
2475 .filter(|&r| r.as_str().starts_with(&prefix))
2476 .cloned()
2477 .collect_vec();
2478 for git_ref in git_refs_to_delete {
2479 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2480 }
2481}
2482
2483pub fn rename_remote(
2484 mut_repo: &mut MutableRepo,
2485 old_remote_name: &RemoteName,
2486 new_remote_name: &RemoteName,
2487) -> Result<(), GitRemoteManagementError> {
2488 let mut git_repo = get_git_repo(mut_repo.store())?;
2489
2490 validate_remote_name(new_remote_name)?;
2491
2492 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2493 return Err(GitRemoteManagementError::NoSuchRemote(
2494 old_remote_name.to_owned(),
2495 ));
2496 };
2497 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2498
2499 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2500 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2501 new_remote_name.to_owned(),
2502 ));
2503 }
2504
2505 match (
2506 remote.refspecs(gix::remote::Direction::Fetch),
2507 remote.refspecs(gix::remote::Direction::Push),
2508 ) {
2509 ([refspec], [])
2510 if refspec.to_ref().to_bstring()
2511 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2512 _ => {
2513 return Err(GitRemoteManagementError::NonstandardConfiguration(
2514 old_remote_name.to_owned(),
2515 ));
2516 }
2517 }
2518
2519 remote
2520 .replace_refspecs(
2521 [default_fetch_refspec(new_remote_name).as_bytes()],
2522 gix::remote::Direction::Fetch,
2523 )
2524 .expect("default refspec to be valid");
2525
2526 let mut config = git_repo.config_snapshot().clone();
2527 save_remote(&mut config, new_remote_name, &mut remote)?;
2528 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2529 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2530 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2531
2532 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2533 .map_err(GitRemoteManagementError::from_git)?;
2534
2535 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2536 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2537 }
2538
2539 Ok(())
2540}
2541
2542fn rename_remote_git_refs(
2543 git_repo: &mut gix::Repository,
2544 old_remote_name: &RemoteName,
2545 new_remote_name: &RemoteName,
2546) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2547 let to_prefixes = |namespace: &str| {
2548 (
2549 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2550 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2551 )
2552 };
2553 let to_rename_edits = {
2554 let ref_log_message = BString::from(format!(
2555 "renamed remote {old_remote_name} to {new_remote_name}",
2556 old_remote_name = old_remote_name.as_symbol(),
2557 new_remote_name = new_remote_name.as_symbol(),
2558 ));
2559 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2560 let new_name = BString::new(
2561 [
2562 new_prefix.as_bytes(),
2563 &old_ref.name().as_bstr()[old_prefix.len()..],
2564 ]
2565 .concat(),
2566 );
2567 [
2568 add_ref(
2569 new_name.try_into().expect("new ref name to be valid"),
2570 old_ref.target().into_owned(),
2571 ref_log_message.clone(),
2572 ),
2573 remove_ref(old_ref),
2574 ]
2575 }
2576 };
2577
2578 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2579 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2580 let edits: Vec<_> = itertools::chain(
2581 git_repo
2582 .references()?
2583 .prefixed(old_bookmark_prefix.as_str())?
2584 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2585 git_repo
2586 .references()?
2587 .prefixed(old_tag_prefix.as_str())?
2588 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2589 )
2590 .flatten_ok()
2591 .try_collect()?;
2592 git_repo.edit_references(edits)?;
2593 Ok(())
2594}
2595
2596pub fn set_remote_urls(
2600 store: &Store,
2601 remote_name: &RemoteName,
2602 new_url: Option<&str>,
2603 new_push_url: Option<&str>,
2604) -> Result<(), GitRemoteManagementError> {
2605 if new_url.is_none() && new_push_url.is_none() {
2607 return Ok(());
2608 }
2609
2610 let git_repo = get_git_repo(store)?;
2611
2612 validate_remote_name(remote_name)?;
2613
2614 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2615 return Err(GitRemoteManagementError::NoSuchRemote(
2616 remote_name.to_owned(),
2617 ));
2618 };
2619 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2620
2621 if let Some(url) = new_url {
2622 remote = remote
2623 .with_url(url)
2624 .map_err(GitRemoteManagementError::from_git)?;
2625 }
2626
2627 if let Some(url) = new_push_url {
2628 remote = remote
2629 .with_push_url(url)
2630 .map_err(GitRemoteManagementError::from_git)?;
2631 }
2632
2633 let mut config = git_repo.config_snapshot().clone();
2634 save_remote(&mut config, remote_name, &mut remote)?;
2635 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2636
2637 Ok(())
2638}
2639
2640fn rename_remote_refs(
2641 mut_repo: &mut MutableRepo,
2642 old_remote_name: &RemoteName,
2643 new_remote_name: &RemoteName,
2644) {
2645 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2646 let prefix = format!(
2647 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2648 remote = old_remote_name.as_str()
2649 );
2650 let git_refs = mut_repo
2651 .view()
2652 .git_refs()
2653 .iter()
2654 .filter_map(|(old, target)| {
2655 old.as_str().strip_prefix(&prefix).map(|p| {
2656 let new: GitRefNameBuf = format!(
2657 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2658 remote = new_remote_name.as_str()
2659 )
2660 .into();
2661 (old.clone(), new, target.clone())
2662 })
2663 })
2664 .collect_vec();
2665 for (old, new, target) in git_refs {
2666 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2667 mut_repo.set_git_ref_target(&new, target);
2668 }
2669}
2670
2671const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2672
2673#[derive(Error, Debug)]
2674pub enum GitFetchError {
2675 #[error("No git remote named '{}'", .0.as_symbol())]
2676 NoSuchRemote(RemoteNameBuf),
2677 #[error(transparent)]
2678 RemoteName(#[from] GitRemoteNameError),
2679 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2680 RejectedUpdates(Vec<GitRefNameBuf>),
2681 #[error(transparent)]
2682 Subprocess(#[from] GitSubprocessError),
2683}
2684
2685#[derive(Error, Debug)]
2686pub enum GitDefaultRefspecError {
2687 #[error("No git remote named '{}'", .0.as_symbol())]
2688 NoSuchRemote(RemoteNameBuf),
2689 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2690 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2691}
2692
2693struct FetchedRefs {
2694 remote: RemoteNameBuf,
2695 bookmark_matcher: StringMatcher,
2696 tag_matcher: StringMatcher,
2697}
2698
2699#[derive(Clone, Debug)]
2701pub struct GitFetchRefExpression {
2702 pub bookmark: StringExpression,
2704 pub tag: StringExpression,
2710}
2711
2712#[derive(Debug)]
2714pub struct ExpandedFetchRefSpecs {
2715 expr: GitFetchRefExpression,
2717 refspecs: Vec<RefSpec>,
2718 negative_refspecs: Vec<NegativeRefSpec>,
2719}
2720
2721#[derive(Error, Debug)]
2722pub enum GitRefExpansionError {
2723 #[error(transparent)]
2724 Expression(#[from] GitRefExpressionError),
2725 #[error(
2726 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2727 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2728 )]
2729 InvalidBranchPattern(StringPattern),
2730}
2731
2732pub fn expand_fetch_refspecs(
2734 remote: &RemoteName,
2735 expr: GitFetchRefExpression,
2736) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2737 let (positive_bookmarks, negative_bookmarks) =
2738 split_into_positive_negative_patterns(&expr.bookmark)?;
2739 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2740
2741 let refspecs = itertools::chain(
2742 positive_bookmarks
2743 .iter()
2744 .map(|&pattern| pattern_to_refspec_glob(pattern))
2745 .map_ok(|glob| {
2746 RefSpec::forced(
2747 format!("refs/heads/{glob}"),
2748 format!(
2749 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2750 remote = remote.as_str()
2751 ),
2752 )
2753 }),
2754 positive_tags
2755 .iter()
2756 .map(|&pattern| pattern_to_refspec_glob(pattern))
2757 .map_ok(|glob| {
2758 RefSpec::forced(
2759 format!("refs/tags/{glob}"),
2760 format!(
2761 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2762 remote = remote.as_str()
2763 ),
2764 )
2765 }),
2766 )
2767 .try_collect()?;
2768
2769 let negative_refspecs = itertools::chain(
2770 negative_bookmarks
2771 .iter()
2772 .map(|&pattern| pattern_to_refspec_glob(pattern))
2773 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2774 negative_tags
2775 .iter()
2776 .map(|&pattern| pattern_to_refspec_glob(pattern))
2777 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2778 )
2779 .try_collect()?;
2780
2781 Ok(ExpandedFetchRefSpecs {
2782 expr,
2783 refspecs,
2784 negative_refspecs,
2785 })
2786}
2787
2788fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2789 pattern
2790 .to_glob()
2791 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2794 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2795}
2796
2797#[derive(Debug, Error)]
2798pub enum GitRefExpressionError {
2799 #[error("Cannot use `~` in sub expression")]
2800 NestedNotIn,
2801 #[error("Cannot use `&` in sub expression")]
2802 NestedIntersection,
2803 #[error("Cannot use `&` for positive expressions")]
2804 PositiveIntersection,
2805}
2806
2807fn split_into_positive_negative_patterns(
2810 expr: &StringExpression,
2811) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2812 static ALL: StringPattern = StringPattern::all();
2813
2814 fn visit_positive<'a>(
2828 expr: &'a StringExpression,
2829 positives: &mut Vec<&'a StringPattern>,
2830 negatives: &mut Vec<&'a StringPattern>,
2831 ) -> Result<(), GitRefExpressionError> {
2832 match expr {
2833 StringExpression::Pattern(pattern) => {
2834 positives.push(pattern);
2835 Ok(())
2836 }
2837 StringExpression::NotIn(complement) => {
2838 positives.push(&ALL);
2839 visit_negative(complement, negatives)
2840 }
2841 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2842 StringExpression::Intersection(expr1, expr2) => {
2843 match (expr1.as_ref(), expr2.as_ref()) {
2844 (other, StringExpression::NotIn(complement))
2845 | (StringExpression::NotIn(complement), other) => {
2846 visit_positive(other, positives, negatives)?;
2847 visit_negative(complement, negatives)
2848 }
2849 _ => Err(GitRefExpressionError::PositiveIntersection),
2850 }
2851 }
2852 }
2853 }
2854
2855 fn visit_negative<'a>(
2856 expr: &'a StringExpression,
2857 negatives: &mut Vec<&'a StringPattern>,
2858 ) -> Result<(), GitRefExpressionError> {
2859 match expr {
2860 StringExpression::Pattern(pattern) => {
2861 negatives.push(pattern);
2862 Ok(())
2863 }
2864 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2865 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2866 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2867 }
2868 }
2869
2870 fn visit_union<'a>(
2871 expr1: &'a StringExpression,
2872 expr2: &'a StringExpression,
2873 patterns: &mut Vec<&'a StringPattern>,
2874 ) -> Result<(), GitRefExpressionError> {
2875 visit_union_sub(expr1, patterns)?;
2876 visit_union_sub(expr2, patterns)
2877 }
2878
2879 fn visit_union_sub<'a>(
2880 expr: &'a StringExpression,
2881 patterns: &mut Vec<&'a StringPattern>,
2882 ) -> Result<(), GitRefExpressionError> {
2883 match expr {
2884 StringExpression::Pattern(pattern) => {
2885 patterns.push(pattern);
2886 Ok(())
2887 }
2888 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2889 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2890 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2891 }
2892 }
2893
2894 let mut positives = Vec::new();
2895 let mut negatives = Vec::new();
2896 visit_positive(expr, &mut positives, &mut negatives)?;
2897 if positives.iter().all(|pattern| pattern.is_all())
2900 && !negatives.is_empty()
2901 && negatives.iter().all(|pattern| pattern.is_all())
2902 {
2903 Ok((vec![], vec![]))
2904 } else {
2905 Ok((positives, negatives))
2906 }
2907}
2908
2909#[derive(Debug)]
2913#[must_use = "warnings should be surfaced in the UI"]
2914pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2915
2916#[derive(Debug)]
2919pub struct IgnoredRefspec {
2920 pub refspec: BString,
2922 pub reason: &'static str,
2924}
2925
2926#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2927enum FetchRefSpecKind {
2928 Positive,
2929 Negative,
2930}
2931
2932pub fn load_default_fetch_bookmarks(
2934 remote_name: &RemoteName,
2935 git_repo: &gix::Repository,
2936) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2937 let remote = git_repo
2938 .try_find_remote(remote_name.as_str())
2939 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2940 .map_err(|e| {
2941 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2942 })?;
2943
2944 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2945 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2946 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2947 let mut negative_bookmarks = Vec::new();
2948 for refspec in remote_refspecs {
2949 let refspec = refspec.to_ref();
2950 match parse_fetch_refspec(remote_name, refspec) {
2951 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2952 positive_bookmarks.push(StringExpression::pattern(bookmark));
2953 }
2954 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2955 negative_bookmarks.push(StringExpression::pattern(bookmark));
2956 }
2957 Err(reason) => {
2958 let refspec = refspec.to_bstring();
2959 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2960 }
2961 }
2962 }
2963
2964 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2965 if !negative_bookmarks.is_empty() {
2967 bookmark_expr =
2968 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2969 }
2970
2971 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2972}
2973
2974fn parse_fetch_refspec(
2975 remote_name: &RemoteName,
2976 refspec: gix::refspec::RefSpecRef<'_>,
2977) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2978 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2979
2980 let (src, positive_dst) = match refspec.instruction() {
2981 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2982 Instruction::Fetch(fetch) => match fetch {
2983 gix::refspec::instruction::Fetch::Only { src: _ } => {
2984 return Err("fetch-only refspecs are not supported");
2985 }
2986 gix::refspec::instruction::Fetch::AndUpdate {
2987 src,
2988 dst,
2989 allow_non_fast_forward,
2990 } => {
2991 if !allow_non_fast_forward {
2992 return Err("non-forced refspecs are not supported");
2993 }
2994 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2995 }
2996 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2997 },
2998 };
2999
3000 let src_branch = src
3001 .strip_prefix("refs/heads/")
3002 .ok_or("only refs/heads/ is supported for refspec sources")?;
3003 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
3004
3005 if let Some(dst) = positive_dst {
3006 let dst_without_prefix = dst
3007 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
3008 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
3009 let dst_branch = dst_without_prefix
3010 .strip_prefix(remote_name.as_str())
3011 .and_then(|d| d.strip_prefix("/"))
3012 .ok_or("remote renaming not supported")?;
3013 if src_branch != dst_branch {
3014 return Err("renaming is not supported");
3015 }
3016 Ok((FetchRefSpecKind::Positive, branch))
3017 } else {
3018 Ok((FetchRefSpecKind::Negative, branch))
3019 }
3020}
3021
3022pub struct GitFetch<'a> {
3024 mut_repo: &'a mut MutableRepo,
3025 git_repo: Box<gix::Repository>,
3026 git_ctx: GitSubprocessContext,
3027 import_options: &'a GitImportOptions,
3028 fetched: Vec<FetchedRefs>,
3029}
3030
3031impl<'a> GitFetch<'a> {
3032 pub fn new(
3033 mut_repo: &'a mut MutableRepo,
3034 subprocess_options: GitSubprocessOptions,
3035 import_options: &'a GitImportOptions,
3036 ) -> Result<Self, UnexpectedGitBackendError> {
3037 let git_backend = get_git_backend(mut_repo.store())?;
3038 let git_repo = Box::new(git_backend.git_repo());
3039 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3040 Ok(GitFetch {
3041 mut_repo,
3042 git_repo,
3043 git_ctx,
3044 import_options,
3045 fetched: vec![],
3046 })
3047 }
3048
3049 #[tracing::instrument(skip(self, callback))]
3055 pub fn fetch(
3056 &mut self,
3057 remote_name: &RemoteName,
3058 ExpandedFetchRefSpecs {
3059 expr,
3060 refspecs: mut remaining_refspecs,
3061 negative_refspecs,
3062 }: ExpandedFetchRefSpecs,
3063 callback: &mut dyn GitSubprocessCallback,
3064 depth: Option<NonZeroU32>,
3065 ) -> Result<(), GitFetchError> {
3066 validate_remote_name(remote_name)?;
3067
3068 if self
3070 .git_repo
3071 .try_find_remote(remote_name.as_str())
3072 .is_none()
3073 {
3074 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3075 }
3076
3077 if remaining_refspecs.is_empty() {
3078 return Ok(());
3080 }
3081
3082 let mut branches_to_prune = Vec::new();
3083 let updates = loop {
3091 let status = self.git_ctx.spawn_fetch(
3092 remote_name,
3093 &remaining_refspecs,
3094 &negative_refspecs,
3095 callback,
3096 depth,
3097 )?;
3098 let failing_refspec = match status {
3099 GitFetchStatus::Updates(updates) => break updates,
3100 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
3101 };
3102 tracing::debug!(failing_refspec, "failed to fetch ref");
3103 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
3104
3105 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
3106 branches_to_prune.push(format!(
3107 "{remote_name}/{branch_name}",
3108 remote_name = remote_name.as_str()
3109 ));
3110 }
3111 };
3112
3113 if !updates.rejected.is_empty() {
3116 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
3117 return Err(GitFetchError::RejectedUpdates(names));
3118 }
3119
3120 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
3123
3124 self.fetched.push(FetchedRefs {
3125 remote: remote_name.to_owned(),
3126 bookmark_matcher: expr.bookmark.to_matcher(),
3127 tag_matcher: expr.tag.to_matcher(),
3128 });
3129 Ok(())
3130 }
3131
3132 #[tracing::instrument(skip(self))]
3134 pub fn get_default_branch(
3135 &self,
3136 remote_name: &RemoteName,
3137 ) -> Result<Option<RefNameBuf>, GitFetchError> {
3138 if self
3139 .git_repo
3140 .try_find_remote(remote_name.as_str())
3141 .is_none()
3142 {
3143 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3144 }
3145 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
3146 tracing::debug!(?default_branch);
3147 Ok(default_branch)
3148 }
3149
3150 #[tracing::instrument(skip(self))]
3157 pub async fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
3158 tracing::debug!("import_refs");
3159 let all_remote_tags = true;
3160 let refs_to_import = diff_refs_to_import(
3161 self.mut_repo.view(),
3162 &self.git_repo,
3163 all_remote_tags,
3164 |kind, symbol| match kind {
3165 GitRefKind::Bookmark => self
3166 .fetched
3167 .iter()
3168 .filter(|fetched| fetched.remote == symbol.remote)
3169 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3170 GitRefKind::Tag => self
3171 .fetched
3172 .iter()
3173 .filter(|fetched| fetched.remote == symbol.remote)
3174 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str())),
3175 },
3176 )?;
3177 let import_stats =
3178 import_refs_inner(self.mut_repo, refs_to_import, self.import_options).await?;
3179
3180 self.fetched.clear();
3181
3182 Ok(import_stats)
3183 }
3184}
3185
3186#[derive(Error, Debug)]
3187pub enum GitPushError {
3188 #[error("No git remote named '{}'", .0.as_symbol())]
3189 NoSuchRemote(RemoteNameBuf),
3190 #[error(transparent)]
3191 RemoteName(#[from] GitRemoteNameError),
3192 #[error(transparent)]
3193 Subprocess(#[from] GitSubprocessError),
3194 #[error(transparent)]
3195 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3196}
3197
3198#[derive(Clone, Debug, Default)]
3199pub struct GitPushRefTargets {
3200 pub bookmarks: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3202 pub tags: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3204}
3205
3206pub struct GitRefUpdate {
3207 pub qualified_name: GitRefNameBuf,
3208 pub targets: Diff<Option<gix::ObjectId>>,
3213}
3214
3215#[derive(Clone, Debug, Default)]
3217pub struct GitPushOptions {
3218 pub remote_push_options: Vec<String>,
3220}
3221
3222pub fn push_refs(
3224 mut_repo: &mut MutableRepo,
3225 subprocess_options: GitSubprocessOptions,
3226 remote: &RemoteName,
3227 targets: &GitPushRefTargets,
3228 callback: &mut dyn GitSubprocessCallback,
3229 options: &GitPushOptions,
3230) -> Result<GitPushStats, GitPushError> {
3231 validate_remote_name(remote)?;
3232
3233 let git_repo = get_git_repo(mut_repo.store())?;
3234 let to_tag_target = |name: &RefName, remote: &RemoteName, id: &CommitId| {
3235 let remote_matcher = StringMatcher::exact(remote);
3236 let oid = owned_oid_from_commit_id(id);
3237 find_git_tag_oid_to_copy(mut_repo.view(), &git_repo, name, &remote_matcher, &oid)
3238 .unwrap_or(oid)
3239 };
3240 let ref_updates = itertools::chain(
3241 targets.bookmarks.iter().map(|(name, update)| GitRefUpdate {
3242 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3243 targets: update
3244 .as_ref()
3245 .map(|id| id.as_ref().map(owned_oid_from_commit_id)),
3246 }),
3247 targets.tags.iter().map(|(name, update)| GitRefUpdate {
3248 qualified_name: format!("refs/tags/{name}", name = name.as_str()).into(),
3249 targets: Diff {
3250 before: update
3251 .before
3252 .as_ref()
3253 .map(|id| to_tag_target(name, remote, id)),
3254 after: update
3255 .after
3256 .as_ref()
3257 .map(|id| to_tag_target(name, REMOTE_NAME_FOR_LOCAL_GIT_REPO, id)),
3258 },
3259 }),
3260 )
3261 .collect_vec();
3262
3263 let push_stats = push_updates(
3264 mut_repo,
3265 subprocess_options,
3266 remote,
3267 &ref_updates,
3268 callback,
3269 options,
3270 )?;
3271 tracing::debug!(?push_stats);
3272
3273 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3274 let pushed_bookmark_updates = || {
3275 iter::zip(&targets.bookmarks, &ref_updates[..targets.bookmarks.len()])
3276 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3277 .map(|((name, update), _)| (&**name, update))
3278 };
3279 let pushed_tag_updates = || {
3280 iter::zip(&targets.tags, &ref_updates[targets.bookmarks.len()..])
3281 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3282 .map(|((name, update), ref_update)| (&**name, update, ref_update))
3283 };
3284
3285 let unexported_bookmarks = {
3288 let refs = build_pushed_bookmarks_to_export(remote, pushed_bookmark_updates());
3289 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3290 };
3291 for (name, _, ref_update) in pushed_tag_updates() {
3295 let symbol = name.to_remote_symbol(remote);
3296 let edit = to_remote_tag_ref_update(symbol, ref_update.targets.after);
3297 if let Err(err) = git_repo.edit_reference(edit) {
3298 tracing::warn!(?symbol, ?err, "failed to update remote tag ref");
3299 }
3300 }
3301
3302 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3303 let is_exported_bookmark = |name: &RefName| {
3304 unexported_bookmarks
3305 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3306 .is_err()
3307 };
3308 for (name, update) in pushed_bookmark_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3309 let new_remote_ref = RemoteRef {
3310 target: RefTarget::resolved(update.after.clone()),
3311 state: RemoteRefState::Tracked,
3312 };
3313 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3314 }
3315 for (name, update, _) in pushed_tag_updates() {
3316 let new_remote_ref = RemoteRef {
3317 target: RefTarget::resolved(update.after.clone()),
3318 state: RemoteRefState::Tracked,
3319 };
3320 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
3321 }
3322
3323 assert!(push_stats.unexported_bookmarks.is_empty());
3327 let push_stats = GitPushStats {
3328 pushed: push_stats.pushed,
3329 rejected: push_stats.rejected,
3330 remote_rejected: push_stats.remote_rejected,
3331 unexported_bookmarks,
3332 };
3333 Ok(push_stats)
3334}
3335
3336pub fn push_updates(
3338 repo: &dyn Repo,
3339 subprocess_options: GitSubprocessOptions,
3340 remote_name: &RemoteName,
3341 updates: &[GitRefUpdate],
3342 callback: &mut dyn GitSubprocessCallback,
3343 options: &GitPushOptions,
3344) -> Result<GitPushStats, GitPushError> {
3345 let mut qualified_remote_refs_expected_locations = HashMap::new();
3346 let mut refspecs = vec![];
3347 for update in updates {
3348 qualified_remote_refs_expected_locations.insert(
3349 update.qualified_name.as_ref(),
3350 update.targets.before.as_deref(),
3351 );
3352 if let Some(new_target) = &update.targets.after {
3353 refspecs.push(RefSpec::forced(
3357 new_target.to_string(),
3358 &update.qualified_name,
3359 ));
3360 } else {
3361 refspecs.push(RefSpec::delete(&update.qualified_name));
3365 }
3366 }
3367
3368 let git_backend = get_git_backend(repo.store())?;
3369 let git_repo = git_backend.git_repo();
3370 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3371
3372 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3374 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3375 }
3376
3377 let refs_to_push: Vec<RefToPush> = refspecs
3378 .iter()
3379 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3380 .collect();
3381
3382 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3383 push_stats.pushed.sort();
3384 push_stats.rejected.sort();
3385 push_stats.remote_rejected.sort();
3386 Ok(push_stats)
3387}
3388
3389fn build_pushed_bookmarks_to_export<'a>(
3391 remote: &RemoteName,
3392 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a Diff<Option<CommitId>>)>,
3393) -> RefsToExport {
3394 let mut to_update = Vec::new();
3395 let mut to_delete = Vec::new();
3396 for (name, update) in pushed_updates {
3397 let symbol = name.to_remote_symbol(remote);
3398 match (update.before.as_ref(), update.after.as_ref()) {
3399 (old, Some(new)) => {
3400 let old_oid = old.map(owned_oid_from_commit_id);
3401 let new_oid = owned_oid_from_commit_id(new);
3402 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3403 }
3404 (Some(old), None) => {
3405 let old_oid = owned_oid_from_commit_id(old);
3406 to_delete.push((symbol.to_owned(), old_oid));
3407 }
3408 (None, None) => panic!("old/new targets should differ"),
3409 }
3410 }
3411
3412 RefsToExport {
3413 to_update,
3414 to_delete,
3415 failed: vec![],
3416 }
3417}
3418
3419fn to_remote_tag_ref_update(
3421 symbol: RemoteRefSymbol<'_>,
3422 new_oid: Option<gix::ObjectId>,
3423) -> gix::refs::transaction::RefEdit {
3424 let expected = gix::refs::transaction::PreviousValue::Any;
3427 let change = match new_oid {
3428 Some(oid) => gix::refs::transaction::Change::Update {
3429 log: gix::refs::transaction::LogChange::default(),
3430 expected,
3431 new: oid.into(),
3432 },
3433 None => gix::refs::transaction::Change::Delete {
3434 expected,
3435 log: gix::refs::transaction::RefLog::AndReference,
3436 },
3437 };
3438 let name = format!(
3439 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}",
3440 remote = symbol.remote.as_str(),
3441 name = symbol.name.as_str()
3442 );
3443 gix::refs::transaction::RefEdit {
3444 change,
3445 name: name.try_into().expect("pushed ref name should be valid"),
3446 deref: false,
3447 }
3448}
3449
3450#[cfg(test)]
3451mod tests {
3452 use assert_matches::assert_matches;
3453
3454 use super::*;
3455 use crate::revset;
3456 use crate::revset::RevsetDiagnostics;
3457
3458 #[test]
3459 fn test_split_positive_negative_patterns() {
3460 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3461 try_split(text).unwrap()
3462 }
3463
3464 fn try_split(
3465 text: &str,
3466 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3467 let mut diagnostics = RevsetDiagnostics::new();
3468 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3469 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3470 Ok((
3471 positives.into_iter().cloned().collect(),
3472 negatives.into_iter().cloned().collect(),
3473 ))
3474 }
3475
3476 insta::assert_compact_debug_snapshot!(
3477 split("a"),
3478 @r#"([Exact("a")], [])"#);
3479 insta::assert_compact_debug_snapshot!(
3480 split("~a"),
3481 @r#"([Substring("")], [Exact("a")])"#);
3482 insta::assert_compact_debug_snapshot!(
3483 split("~a~b"),
3484 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3485 insta::assert_compact_debug_snapshot!(
3486 split("~(a|b)"),
3487 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3488 insta::assert_compact_debug_snapshot!(
3489 split("a|b"),
3490 @r#"([Exact("a"), Exact("b")], [])"#);
3491 insta::assert_compact_debug_snapshot!(
3492 split("(a|b)&~c"),
3493 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3494 insta::assert_compact_debug_snapshot!(
3495 split("~a&b"),
3496 @r#"([Exact("b")], [Exact("a")])"#);
3497 insta::assert_compact_debug_snapshot!(
3498 split("a&~b&~c"),
3499 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3500 insta::assert_compact_debug_snapshot!(
3501 split("~a&b&~c"),
3502 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3503 insta::assert_compact_debug_snapshot!(
3504 split("a&~(b|c)"),
3505 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3506 insta::assert_compact_debug_snapshot!(
3507 split("((a|b)|c)&~(d|(e|f))"),
3508 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3509 assert_matches!(
3510 try_split("a&b"),
3511 Err(GitRefExpressionError::PositiveIntersection)
3512 );
3513 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3514 assert_matches!(
3515 try_split("a&~(b&~c)"),
3516 Err(GitRefExpressionError::NestedIntersection)
3517 );
3518 assert_matches!(
3519 try_split("(a|b)&c"),
3520 Err(GitRefExpressionError::PositiveIntersection)
3521 );
3522 assert_matches!(
3523 try_split("(a&~b)&(~c&~d)"),
3524 Err(GitRefExpressionError::PositiveIntersection)
3525 );
3526 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3527 assert_matches!(
3528 try_split("a&~b|c&~d"),
3529 Err(GitRefExpressionError::NestedIntersection)
3530 );
3531
3532 insta::assert_compact_debug_snapshot!(
3535 split("*"),
3536 @r#"([Glob(GlobPattern("*"))], [])"#);
3537 insta::assert_compact_debug_snapshot!(
3538 split("~*"),
3539 @"([], [])");
3540 insta::assert_compact_debug_snapshot!(
3541 split("a~*"),
3542 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3543 insta::assert_compact_debug_snapshot!(
3544 split("~(a|*)"),
3545 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3546 }
3547}