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::fs::File;
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::sync::Arc;
26
27use bstr::BStr;
28use bstr::BString;
29use futures::StreamExt as _;
30use gix::refspec::Instruction;
31use itertools::Itertools as _;
32use pollster::FutureExt as _;
33use thiserror::Error;
34
35use crate::backend::BackendError;
36use crate::backend::BackendResult;
37use crate::backend::CommitId;
38use crate::backend::TreeValue;
39use crate::commit::Commit;
40use crate::config::ConfigGetError;
41use crate::file_util::IoResultExt as _;
42use crate::file_util::PathError;
43use crate::git_backend::GitBackend;
44use crate::git_subprocess::GitSubprocessContext;
45use crate::git_subprocess::GitSubprocessError;
46use crate::index::IndexError;
47use crate::matchers::EverythingMatcher;
48use crate::merged_tree::MergedTree;
49use crate::merged_tree::TreeDiffEntry;
50use crate::object_id::ObjectId as _;
51use crate::op_store::RefTarget;
52use crate::op_store::RefTargetOptionExt as _;
53use crate::op_store::RemoteRef;
54use crate::op_store::RemoteRefState;
55use crate::ref_name::GitRefName;
56use crate::ref_name::GitRefNameBuf;
57use crate::ref_name::RefName;
58use crate::ref_name::RefNameBuf;
59use crate::ref_name::RemoteName;
60use crate::ref_name::RemoteNameBuf;
61use crate::ref_name::RemoteRefSymbol;
62use crate::ref_name::RemoteRefSymbolBuf;
63use crate::refs::BookmarkPushUpdate;
64use crate::repo::MutableRepo;
65use crate::repo::Repo;
66use crate::repo_path::RepoPath;
67use crate::revset::RevsetExpression;
68use crate::settings::RemoteSettings;
69use crate::settings::UserSettings;
70use crate::store::Store;
71use crate::str_util::StringExpression;
72use crate::str_util::StringMatcher;
73use crate::str_util::StringPattern;
74use crate::view::View;
75
76pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
78pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
80const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
82const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
85
86#[derive(Clone, Debug)]
87pub struct GitSettings {
88 pub auto_local_bookmark: bool,
90 pub abandon_unreachable_commits: bool,
91 pub executable_path: PathBuf,
92 pub write_change_id_header: bool,
93 pub remotes: HashMap<RemoteNameBuf, RemoteSettings>,
94}
95
96impl GitSettings {
97 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
98 Ok(Self {
99 auto_local_bookmark: settings.get_bool("git.auto-local-bookmark")?,
100 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
101 executable_path: settings.get("git.executable-path")?,
102 write_change_id_header: settings.get("git.write-change-id-header")?,
103 remotes: RemoteSettings::table_from_settings(settings)?,
104 })
105 }
106}
107
108#[derive(Debug, Error)]
109pub enum GitRemoteNameError {
110 #[error(
111 "Git remote named '{name}' is reserved for local Git repository",
112 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
113 )]
114 ReservedForLocalGitRepo,
115 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
116 WithSlash(RemoteNameBuf),
117}
118
119fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
120 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
121 Err(GitRemoteNameError::ReservedForLocalGitRepo)
122 } else if name.as_str().contains("/") {
123 Err(GitRemoteNameError::WithSlash(name.to_owned()))
124 } else {
125 Ok(())
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
131pub enum GitRefKind {
132 Bookmark,
133 Tag,
134}
135
136#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct GitPushStats {
139 pub pushed: Vec<GitRefNameBuf>,
141 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
143 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
145}
146
147impl GitPushStats {
148 pub fn all_ok(&self) -> bool {
149 self.rejected.is_empty() && self.remote_rejected.is_empty()
150 }
151}
152
153#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
157struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
158
159impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
160 fn borrow(&self) -> &RemoteRefSymbol<'b> {
161 &self.0
162 }
163}
164
165#[derive(Debug, Hash, PartialEq, Eq)]
171pub(crate) struct RefSpec {
172 forced: bool,
173 source: Option<String>,
176 destination: String,
177}
178
179impl RefSpec {
180 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
181 Self {
182 forced: true,
183 source: Some(source.into()),
184 destination: destination.into(),
185 }
186 }
187
188 fn delete(destination: impl Into<String>) -> Self {
189 Self {
191 forced: false,
192 source: None,
193 destination: destination.into(),
194 }
195 }
196
197 pub(crate) fn to_git_format(&self) -> String {
198 format!(
199 "{}{}",
200 if self.forced { "+" } else { "" },
201 self.to_git_format_not_forced()
202 )
203 }
204
205 pub(crate) fn to_git_format_not_forced(&self) -> String {
211 if let Some(s) = &self.source {
212 format!("{}:{}", s, self.destination)
213 } else {
214 format!(":{}", self.destination)
215 }
216 }
217}
218
219#[derive(Debug)]
221#[repr(transparent)]
222pub(crate) struct NegativeRefSpec {
223 source: String,
224}
225
226impl NegativeRefSpec {
227 fn new(source: impl Into<String>) -> Self {
228 Self {
229 source: source.into(),
230 }
231 }
232
233 pub(crate) fn to_git_format(&self) -> String {
234 format!("^{}", self.source)
235 }
236}
237
238pub(crate) struct RefToPush<'a> {
241 pub(crate) refspec: &'a RefSpec,
242 pub(crate) expected_location: Option<&'a CommitId>,
243}
244
245impl<'a> RefToPush<'a> {
246 fn new(
247 refspec: &'a RefSpec,
248 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
249 ) -> Self {
250 let expected_location = *expected_locations
251 .get(GitRefName::new(&refspec.destination))
252 .expect(
253 "The refspecs and the expected locations were both constructed from the same \
254 source of truth. This means the lookup should always work.",
255 );
256
257 Self {
258 refspec,
259 expected_location,
260 }
261 }
262
263 pub(crate) fn to_git_lease(&self) -> String {
264 format!(
265 "{}:{}",
266 self.refspec.destination,
267 self.expected_location
268 .map(|x| x.to_string())
269 .as_deref()
270 .unwrap_or("")
271 )
272 }
273}
274
275pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
278 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
279 if name == "HEAD" {
281 return None;
282 }
283 let name = RefName::new(name);
284 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
285 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
286 } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
287 let (remote, name) = remote_and_name.split_once('/')?;
288 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
290 return None;
291 }
292 let name = RefName::new(name);
293 let remote = RemoteName::new(remote);
294 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
295 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
296 let name = RefName::new(name);
297 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
298 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
299 } else {
300 None
301 }
302}
303
304fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
305 let RemoteRefSymbol { name, remote } = symbol;
306 let name = name.as_str();
307 let remote = remote.as_str();
308 if name.is_empty() || remote.is_empty() {
309 return None;
310 }
311 match kind {
312 GitRefKind::Bookmark => {
313 if name == "HEAD" {
314 return None;
315 }
316 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
317 Some(format!("refs/heads/{name}").into())
318 } else {
319 Some(format!("refs/remotes/{remote}/{name}").into())
320 }
321 }
322 GitRefKind::Tag => {
323 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
324 }
325 }
326}
327
328#[derive(Debug, Error)]
329#[error("The repo is not backed by a Git repo")]
330pub struct UnexpectedGitBackendError;
331
332pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
334 store.backend_impl().ok_or(UnexpectedGitBackendError)
335}
336
337pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
339 get_git_backend(store).map(|backend| backend.git_repo())
340}
341
342fn resolve_git_ref_to_commit_id(
347 git_ref: &gix::Reference,
348 known_target: &RefTarget,
349) -> Option<CommitId> {
350 let mut peeling_ref = Cow::Borrowed(git_ref);
351
352 if let Some(id) = known_target.as_normal() {
354 let raw_ref = &git_ref.inner;
355 if let Some(oid) = raw_ref.target.try_id()
356 && oid.as_bytes() == id.as_bytes()
357 {
358 return Some(id.clone());
359 }
360 if let Some(oid) = raw_ref.peeled
361 && oid.as_bytes() == id.as_bytes()
362 {
363 return Some(id.clone());
366 }
367 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
371 let maybe_tag = git_ref
372 .try_id()
373 .and_then(|id| id.object().ok())
374 .and_then(|object| object.try_into_tag().ok());
375 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
376 if oid.as_bytes() == id.as_bytes() {
377 return Some(id.clone());
379 }
380 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());
383 }
384 }
385 }
386
387 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
391 let is_commit = peeled_id
392 .object()
393 .is_ok_and(|object| object.kind.is_commit());
394 is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
395}
396
397#[derive(Error, Debug)]
398pub enum GitImportError {
399 #[error("Failed to read Git HEAD target commit {id}")]
400 MissingHeadTarget {
401 id: CommitId,
402 #[source]
403 err: BackendError,
404 },
405 #[error("Ancestor of Git ref {symbol} is missing")]
406 MissingRefAncestor {
407 symbol: RemoteRefSymbolBuf,
408 #[source]
409 err: BackendError,
410 },
411 #[error(transparent)]
412 Backend(#[from] BackendError),
413 #[error(transparent)]
414 Index(#[from] IndexError),
415 #[error(transparent)]
416 Git(Box<dyn std::error::Error + Send + Sync>),
417 #[error(transparent)]
418 UnexpectedBackend(#[from] UnexpectedGitBackendError),
419}
420
421impl GitImportError {
422 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
423 Self::Git(source.into())
424 }
425}
426
427#[derive(Clone, Debug, Eq, PartialEq, Default)]
429pub struct GitImportStats {
430 pub abandoned_commits: Vec<CommitId>,
432 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
435 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
438 pub failed_ref_names: Vec<BString>,
443}
444
445#[derive(Debug)]
446struct RefsToImport {
447 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
450 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
453 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
456 failed_ref_names: Vec<BString>,
458}
459
460pub fn import_refs(
465 mut_repo: &mut MutableRepo,
466 git_settings: &GitSettings,
467) -> Result<GitImportStats, GitImportError> {
468 import_some_refs(mut_repo, git_settings, |_, _| true)
469}
470
471pub fn import_some_refs(
476 mut_repo: &mut MutableRepo,
477 git_settings: &GitSettings,
478 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
479) -> Result<GitImportStats, GitImportError> {
480 let store = mut_repo.store();
481 let git_backend = get_git_backend(store)?;
482 let git_repo = git_backend.git_repo();
483
484 let RefsToImport {
485 changed_git_refs,
486 changed_remote_bookmarks,
487 changed_remote_tags,
488 failed_ref_names,
489 } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;
490
491 let index = mut_repo.index();
498 let missing_head_ids: Vec<&CommitId> = changed_git_refs
499 .iter()
500 .flat_map(|(_, new_target)| new_target.added_ids())
501 .filter_map(|id| match index.has_id(id) {
502 Ok(false) => Some(Ok(id)),
503 Ok(true) => None,
504 Err(e) => Some(Err(e)),
505 })
506 .try_collect()?;
507 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
508
509 let mut head_commits = Vec::new();
511 let get_commit = |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
512 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
513 symbol: symbol.clone(),
514 err,
515 };
516 if !heads_imported && !index.has_id(id).map_err(GitImportError::Index)? {
518 git_backend
519 .import_head_commits([id])
520 .map_err(missing_ref_err)?;
521 }
522 store.get_commit(id).map_err(missing_ref_err)
523 };
524 for (symbol, (_, new_target)) in
525 itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)
526 {
527 for id in new_target.added_ids() {
528 let commit = get_commit(id, symbol)?;
529 head_commits.push(commit);
530 }
531 }
532 mut_repo
535 .add_heads(&head_commits)
536 .map_err(GitImportError::Backend)?;
537
538 for remote_name in iter_remote_names(&git_repo) {
542 mut_repo.ensure_remote(&remote_name);
543 }
544
545 for (full_name, new_target) in changed_git_refs {
547 mut_repo.set_git_ref_target(&full_name, new_target);
548 }
549 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
550 let symbol = symbol.as_ref();
551 let base_target = old_remote_ref.tracked_target();
552 let new_remote_ref = RemoteRef {
553 target: new_target.clone(),
554 state: if old_remote_ref != RemoteRef::absent_ref() {
555 old_remote_ref.state
556 } else {
557 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)
558 },
559 };
560 if new_remote_ref.is_tracked() {
561 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
562 }
563 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
566 }
567 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
568 let symbol = symbol.as_ref();
569 let base_target = old_remote_ref.tracked_target();
570 let new_remote_ref = RemoteRef {
571 target: new_target.clone(),
572 state: if old_remote_ref != RemoteRef::absent_ref() {
573 old_remote_ref.state
574 } else {
575 default_remote_ref_state_for(GitRefKind::Tag, symbol, git_settings)
576 },
577 };
578 if new_remote_ref.is_tracked() {
579 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
580 }
581 mut_repo.set_remote_tag(symbol, new_remote_ref);
584 }
585
586 let abandoned_commits = if git_settings.abandon_unreachable_commits {
587 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
588 .map_err(GitImportError::Backend)?
589 } else {
590 vec![]
591 };
592 let stats = GitImportStats {
593 abandoned_commits,
594 changed_remote_bookmarks,
595 changed_remote_tags,
596 failed_ref_names,
597 };
598 Ok(stats)
599}
600
601fn abandon_unreachable_commits(
604 mut_repo: &mut MutableRepo,
605 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
606 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
607) -> BackendResult<Vec<CommitId>> {
608 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
609 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
610 .cloned()
611 .collect_vec();
612 if hidable_git_heads.is_empty() {
613 return Ok(vec![]);
614 }
615 let pinned_expression = RevsetExpression::union_all(&[
616 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
618 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
619 .intersection(&RevsetExpression::visible_heads().ancestors()),
621 RevsetExpression::root(),
622 ]);
623 let abandoned_expression = pinned_expression
624 .range(&RevsetExpression::commits(hidable_git_heads))
625 .intersection(&RevsetExpression::visible_heads().ancestors());
627 let abandoned_commit_ids: Vec<_> = abandoned_expression
628 .evaluate(mut_repo)
629 .map_err(|err| err.into_backend_error())?
630 .iter()
631 .try_collect()
632 .map_err(|err| err.into_backend_error())?;
633 for id in &abandoned_commit_ids {
634 let commit = mut_repo.store().get_commit(id)?;
635 mut_repo.record_abandoned_commit(&commit);
636 }
637 Ok(abandoned_commit_ids)
638}
639
640fn diff_refs_to_import(
642 view: &View,
643 git_repo: &gix::Repository,
644 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
645) -> Result<RefsToImport, GitImportError> {
646 let mut known_git_refs = view
647 .git_refs()
648 .iter()
649 .filter_map(|(full_name, target)| {
650 let (kind, symbol) =
652 parse_git_ref(full_name).expect("stored git ref should be parsable");
653 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
654 })
655 .collect();
656 let mut known_remote_bookmarks = view
657 .all_remote_bookmarks()
658 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
659 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
660 .collect();
661 let mut known_remote_tags = view
662 .all_remote_tags()
663 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
664 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
665 .collect();
666
667 let mut changed_git_refs = Vec::new();
668 let mut changed_remote_bookmarks = Vec::new();
669 let mut changed_remote_tags = Vec::new();
670 let mut failed_ref_names = Vec::new();
671 let actual = git_repo.references().map_err(GitImportError::from_git)?;
672 collect_changed_refs_to_import(
673 actual.local_branches().map_err(GitImportError::from_git)?,
674 &mut known_git_refs,
675 &mut known_remote_bookmarks,
676 &mut changed_git_refs,
677 &mut changed_remote_bookmarks,
678 &mut failed_ref_names,
679 &git_ref_filter,
680 )?;
681 collect_changed_refs_to_import(
682 actual.remote_branches().map_err(GitImportError::from_git)?,
683 &mut known_git_refs,
684 &mut known_remote_bookmarks,
685 &mut changed_git_refs,
686 &mut changed_remote_bookmarks,
687 &mut failed_ref_names,
688 &git_ref_filter,
689 )?;
690 collect_changed_refs_to_import(
691 actual.tags().map_err(GitImportError::from_git)?,
692 &mut known_git_refs,
693 &mut known_remote_tags,
694 &mut changed_git_refs,
695 &mut changed_remote_tags,
696 &mut failed_ref_names,
697 &git_ref_filter,
698 )?;
699 for full_name in known_git_refs.into_keys() {
700 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
701 }
702 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
703 if old.is_present() {
704 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
705 }
706 }
707 for (RemoteRefKey(symbol), old) in known_remote_tags {
708 if old.is_present() {
709 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
710 }
711 }
712
713 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
715 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
716 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
717 failed_ref_names.sort_unstable();
718 Ok(RefsToImport {
719 changed_git_refs,
720 changed_remote_bookmarks,
721 changed_remote_tags,
722 failed_ref_names,
723 })
724}
725
726fn collect_changed_refs_to_import(
727 actual_git_refs: gix::reference::iter::Iter,
728 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
729 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
730 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
731 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
732 failed_ref_names: &mut Vec<BString>,
733 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
734) -> Result<(), GitImportError> {
735 for git_ref in actual_git_refs {
736 let git_ref = git_ref.map_err(GitImportError::from_git)?;
737 let full_name_bytes = git_ref.name().as_bstr();
738 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
739 failed_ref_names.push(full_name_bytes.to_owned());
741 continue;
742 };
743 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
744 failed_ref_names.push(full_name_bytes.to_owned());
745 continue;
746 }
747 let full_name = GitRefName::new(full_name);
748 let Some((kind, symbol)) = parse_git_ref(full_name) else {
749 continue;
751 };
752 if !git_ref_filter(kind, symbol) {
753 continue;
754 }
755 let old_git_target = known_git_refs.get(full_name).copied().flatten();
756 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
757 continue;
759 };
760 let new_target = RefTarget::normal(id);
761 known_git_refs.remove(full_name);
762 if new_target != *old_git_target {
763 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
764 }
765 let old_remote_ref = known_remote_refs
768 .remove(&symbol)
769 .unwrap_or_else(|| RemoteRef::absent_ref());
770 if new_target != old_remote_ref.target {
771 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
772 }
773 }
774 Ok(())
775}
776
777fn default_remote_ref_state_for(
778 kind: GitRefKind,
779 symbol: RemoteRefSymbol<'_>,
780 git_settings: &GitSettings,
781) -> RemoteRefState {
782 match kind {
783 GitRefKind::Bookmark => {
784 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
785 || git_settings.auto_local_bookmark
786 || git_settings
787 .remotes
788 .get(symbol.remote)
789 .is_some_and(|remote_settings| {
790 remote_settings
791 .auto_track_bookmarks
792 .is_match(symbol.name.as_str())
793 })
794 {
795 RemoteRefState::Tracked
796 } else {
797 RemoteRefState::New
798 }
799 }
800 GitRefKind::Tag => RemoteRefState::Tracked,
802 }
803}
804
805fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
811 itertools::chain(view.local_bookmarks(), view.local_tags())
812 .flat_map(|(_, target)| target.added_ids())
813 .cloned()
814 .collect()
815}
816
817fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
824 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
825 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
826 .map(|(_, remote_ref)| &remote_ref.target)
827 .flat_map(|target| target.added_ids())
828 .cloned()
829 .collect()
830}
831
832pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
840 let store = mut_repo.store();
841 let git_backend = get_git_backend(store)?;
842 let git_repo = git_backend.git_repo();
843
844 let old_git_head = mut_repo.view().git_head();
845 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
846 Some(CommitId::from_bytes(oid.as_bytes()))
847 } else {
848 None
849 };
850 if old_git_head.as_resolved() == Some(&new_git_head_id) {
851 return Ok(());
852 }
853
854 if let Some(head_id) = &new_git_head_id {
856 let index = mut_repo.index();
857 if !index.has_id(head_id)? {
858 git_backend.import_head_commits([head_id]).map_err(|err| {
859 GitImportError::MissingHeadTarget {
860 id: head_id.clone(),
861 err,
862 }
863 })?;
864 }
865 store
868 .get_commit(head_id)
869 .and_then(|commit| mut_repo.add_head(&commit))
870 .map_err(GitImportError::Backend)?;
871 }
872
873 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
874 Ok(())
875}
876
877#[derive(Error, Debug)]
878pub enum GitExportError {
879 #[error(transparent)]
880 Git(Box<dyn std::error::Error + Send + Sync>),
881 #[error(transparent)]
882 UnexpectedBackend(#[from] UnexpectedGitBackendError),
883}
884
885impl GitExportError {
886 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
887 Self::Git(source.into())
888 }
889}
890
891#[derive(Debug, Error)]
893pub enum FailedRefExportReason {
894 #[error("Name is not allowed in Git")]
896 InvalidGitName,
897 #[error("Ref was in a conflicted state from the last import")]
900 ConflictedOldState,
901 #[error("Ref cannot point to the root commit in Git")]
903 OnRootCommit,
904 #[error("Deleted ref had been modified in Git")]
906 DeletedInJjModifiedInGit,
907 #[error("Added ref had been added with a different target in Git")]
909 AddedInJjAddedInGit,
910 #[error("Modified ref had been deleted in Git")]
912 ModifiedInJjDeletedInGit,
913 #[error("Failed to delete")]
915 FailedToDelete(#[source] Box<gix::reference::edit::Error>),
916 #[error("Failed to set")]
918 FailedToSet(#[source] Box<gix::reference::edit::Error>),
919}
920
921#[derive(Debug)]
923pub struct GitExportStats {
924 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
926 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
930}
931
932#[derive(Debug)]
933struct AllRefsToExport {
934 bookmarks: RefsToExport,
935 tags: RefsToExport,
936}
937
938#[derive(Debug)]
939struct RefsToExport {
940 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
942 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
947 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
949}
950
951pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
960 export_some_refs(mut_repo, |_, _| true)
961}
962
963pub fn export_some_refs(
964 mut_repo: &mut MutableRepo,
965 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
966) -> Result<GitExportStats, GitExportError> {
967 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
968 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
969 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
970 let (_, value) = &map[index];
971 Some(value)
972 }
973
974 let git_repo = get_git_repo(mut_repo.store())?;
975
976 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
977 mut_repo.view(),
978 mut_repo.store().root_commit_id(),
979 &git_ref_filter,
980 );
981
982 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
984 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
985 if let Some((kind, symbol)) = target_name
986 .as_ref()
987 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
988 .and_then(|name| parse_git_ref(name.as_ref()))
989 {
990 let old_target = head_ref.inner.target.clone();
991 let current_oid = match head_ref.into_fully_peeled_id() {
992 Ok(id) => Some(id.detach()),
993 Err(gix::reference::peel::Error::ToId(
994 gix::refs::peel::to_id::Error::FollowToObject(
995 gix::refs::peel::to_object::Error::Follow(
996 gix::refs::file::find::existing::Error::NotFound { .. },
997 ),
998 ),
999 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1001 };
1002 let refs = match kind {
1003 GitRefKind::Bookmark => &bookmarks,
1004 GitRefKind::Tag => &tags,
1005 };
1006 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1007 Some(new_oid)
1008 } else if get(&refs.to_delete, symbol).is_some() {
1009 None
1010 } else {
1011 current_oid.as_ref()
1012 };
1013 if new_oid != current_oid.as_ref() {
1014 update_git_head(
1015 &git_repo,
1016 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1017 current_oid,
1018 )
1019 .map_err(GitExportError::from_git)?;
1020 }
1021 }
1022 }
1023
1024 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1025 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1026
1027 copy_exportable_local_bookmarks_to_remote_view(
1028 mut_repo,
1029 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1030 |name| {
1031 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1032 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1033 },
1034 );
1035 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1036 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1037 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1038 });
1039
1040 Ok(GitExportStats {
1041 failed_bookmarks,
1042 failed_tags,
1043 })
1044}
1045
1046fn export_refs_to_git(
1047 mut_repo: &mut MutableRepo,
1048 git_repo: &gix::Repository,
1049 kind: GitRefKind,
1050 refs: RefsToExport,
1051) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1052 let mut failed = refs.failed;
1053 for (symbol, old_oid) in refs.to_delete {
1054 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1055 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1056 continue;
1057 };
1058 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1059 failed.push((symbol, reason));
1060 } else {
1061 let new_target = RefTarget::absent();
1062 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1063 }
1064 }
1065 for (symbol, (old_oid, new_oid)) in refs.to_update {
1066 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1067 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1068 continue;
1069 };
1070 if let Err(reason) = update_git_ref(git_repo, &git_ref_name, old_oid, new_oid) {
1071 failed.push((symbol, reason));
1072 } else {
1073 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
1074 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1075 }
1076 }
1077
1078 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1080 failed
1081}
1082
1083fn copy_exportable_local_bookmarks_to_remote_view(
1084 mut_repo: &mut MutableRepo,
1085 remote: &RemoteName,
1086 name_filter: impl Fn(&RefName) -> bool,
1087) {
1088 let new_local_bookmarks = mut_repo
1089 .view()
1090 .local_remote_bookmarks(remote)
1091 .filter_map(|(name, targets)| {
1092 let old_target = &targets.remote_ref.target;
1095 let new_target = targets.local_target;
1096 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1097 })
1098 .filter(|&(name, _)| name_filter(name))
1099 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1100 .collect_vec();
1101 for (name, new_target) in new_local_bookmarks {
1102 let new_remote_ref = RemoteRef {
1103 target: new_target,
1104 state: RemoteRefState::Tracked,
1105 };
1106 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1107 }
1108}
1109
1110fn copy_exportable_local_tags_to_remote_view(
1111 mut_repo: &mut MutableRepo,
1112 remote: &RemoteName,
1113 name_filter: impl Fn(&RefName) -> bool,
1114) {
1115 let new_local_tags = mut_repo
1116 .view()
1117 .local_remote_tags(remote)
1118 .filter_map(|(name, targets)| {
1119 let old_target = &targets.remote_ref.target;
1121 let new_target = targets.local_target;
1122 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1123 })
1124 .filter(|&(name, _)| name_filter(name))
1125 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1126 .collect_vec();
1127 for (name, new_target) in new_local_tags {
1128 let new_remote_ref = RemoteRef {
1129 target: new_target,
1130 state: RemoteRefState::Tracked,
1131 };
1132 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1133 }
1134}
1135
1136fn diff_refs_to_export(
1138 view: &View,
1139 root_commit_id: &CommitId,
1140 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1141) -> AllRefsToExport {
1142 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1145 itertools::chain(
1146 view.local_bookmarks().map(|(name, target)| {
1147 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1148 (symbol, target)
1149 }),
1150 view.all_remote_bookmarks()
1151 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1152 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1153 )
1154 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1155 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1156 .collect();
1157 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1159 .local_tags()
1160 .map(|(name, target)| {
1161 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1162 (symbol, target)
1163 })
1164 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1165 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1166 .collect();
1167 let known_git_refs = view
1168 .git_refs()
1169 .iter()
1170 .map(|(full_name, target)| {
1171 let (kind, symbol) =
1172 parse_git_ref(full_name).expect("stored git ref should be parsable");
1173 ((kind, symbol), target)
1174 })
1175 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1179 for ((kind, symbol), target) in known_git_refs {
1180 let ref_targets = match kind {
1181 GitRefKind::Bookmark => &mut all_bookmark_targets,
1182 GitRefKind::Tag => &mut all_tag_targets,
1183 };
1184 ref_targets
1185 .entry(symbol)
1186 .and_modify(|(old_target, _)| *old_target = target)
1187 .or_insert((target, RefTarget::absent_ref()));
1188 }
1189
1190 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1191 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1192 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1193 AllRefsToExport { bookmarks, tags }
1194}
1195
1196fn collect_changed_refs_to_export(
1197 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1198 root_commit_target: &RefTarget,
1199) -> RefsToExport {
1200 let mut to_update = Vec::new();
1201 let mut to_delete = Vec::new();
1202 let mut failed = Vec::new();
1203 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1204 if new_target == old_target {
1205 continue;
1206 }
1207 if new_target == root_commit_target {
1208 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1210 continue;
1211 }
1212 let old_oid = if let Some(id) = old_target.as_normal() {
1213 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1214 } else if old_target.has_conflict() {
1215 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1218 continue;
1219 } else {
1220 assert!(old_target.is_absent());
1221 None
1222 };
1223 if let Some(id) = new_target.as_normal() {
1224 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1225 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1226 } else if new_target.has_conflict() {
1227 continue;
1229 } else {
1230 assert!(new_target.is_absent());
1231 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1232 }
1233 }
1234
1235 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1237 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1238 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1239 RefsToExport {
1240 to_update,
1241 to_delete,
1242 failed,
1243 }
1244}
1245
1246fn delete_git_ref(
1247 git_repo: &gix::Repository,
1248 git_ref_name: &GitRefName,
1249 old_oid: &gix::oid,
1250) -> Result<(), FailedRefExportReason> {
1251 if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1252 if git_ref.inner.target.try_id() == Some(old_oid) {
1253 git_ref
1255 .delete()
1256 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1257 } else {
1258 return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1260 }
1261 } else {
1262 }
1264 Ok(())
1265}
1266
1267fn update_git_ref(
1268 git_repo: &gix::Repository,
1269 git_ref_name: &GitRefName,
1270 old_oid: Option<gix::ObjectId>,
1271 new_oid: gix::ObjectId,
1272) -> Result<(), FailedRefExportReason> {
1273 match old_oid {
1274 None => {
1275 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1276 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1279 return Err(FailedRefExportReason::AddedInJjAddedInGit);
1280 }
1281 } else {
1282 git_repo
1284 .reference(
1285 git_ref_name.as_str(),
1286 new_oid,
1287 gix::refs::transaction::PreviousValue::MustNotExist,
1288 "export from jj",
1289 )
1290 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1291 }
1292 }
1293 Some(old_oid) => {
1294 if let Err(err) = git_repo.reference(
1296 git_ref_name.as_str(),
1297 new_oid,
1298 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1299 "export from jj",
1300 ) {
1301 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1303 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1305 return Err(FailedRefExportReason::FailedToSet(err.into()));
1306 }
1307 } else {
1308 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1310 }
1311 } else {
1312 }
1315 }
1316 }
1317 Ok(())
1318}
1319
1320fn update_git_head(
1323 git_repo: &gix::Repository,
1324 expected_ref: gix::refs::transaction::PreviousValue,
1325 new_oid: Option<gix::ObjectId>,
1326) -> Result<(), gix::reference::edit::Error> {
1327 let mut ref_edits = Vec::new();
1328 let new_target = if let Some(oid) = new_oid {
1329 gix::refs::Target::Object(oid)
1330 } else {
1331 ref_edits.push(gix::refs::transaction::RefEdit {
1336 change: gix::refs::transaction::Change::Delete {
1337 expected: gix::refs::transaction::PreviousValue::Any,
1338 log: gix::refs::transaction::RefLog::AndReference,
1339 },
1340 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1341 deref: false,
1342 });
1343 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1344 };
1345 ref_edits.push(gix::refs::transaction::RefEdit {
1346 change: gix::refs::transaction::Change::Update {
1347 log: gix::refs::transaction::LogChange {
1348 message: "export from jj".into(),
1349 ..Default::default()
1350 },
1351 expected: expected_ref,
1352 new: new_target,
1353 },
1354 name: "HEAD".try_into().unwrap(),
1355 deref: false,
1356 });
1357 git_repo.edit_references(ref_edits)?;
1358 Ok(())
1359}
1360
1361#[derive(Debug, Error)]
1362pub enum GitResetHeadError {
1363 #[error(transparent)]
1364 Backend(#[from] BackendError),
1365 #[error(transparent)]
1366 Git(Box<dyn std::error::Error + Send + Sync>),
1367 #[error("Failed to update Git HEAD ref")]
1368 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1369 #[error(transparent)]
1370 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1371}
1372
1373impl GitResetHeadError {
1374 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1375 Self::Git(source.into())
1376 }
1377}
1378
1379pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1382 let git_repo = get_git_repo(mut_repo.store())?;
1383
1384 let first_parent_id = &wc_commit.parent_ids()[0];
1385 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1386 RefTarget::normal(first_parent_id.clone())
1387 } else {
1388 RefTarget::absent()
1389 };
1390
1391 let old_head_target = mut_repo.git_head();
1393 if old_head_target != new_head_target {
1394 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1395 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1398 if actual_head.is_detached() {
1399 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1400 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1401 } else {
1402 gix::refs::transaction::PreviousValue::MustExist
1405 }
1406 } else {
1407 gix::refs::transaction::PreviousValue::MustExist
1409 };
1410 let new_oid = new_head_target
1411 .as_normal()
1412 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1413 update_git_head(&git_repo, expected_ref, new_oid)
1414 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1415 mut_repo.set_git_head_target(new_head_target);
1416 }
1417
1418 if git_repo.state().is_some() {
1421 clear_operation_state(&git_repo)?;
1422 }
1423
1424 reset_index(mut_repo, &git_repo, wc_commit)
1425}
1426
1427fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1429 const STATE_FILE_NAMES: &[&str] = &[
1433 "MERGE_HEAD",
1434 "MERGE_MODE",
1435 "MERGE_MSG",
1436 "REVERT_HEAD",
1437 "CHERRY_PICK_HEAD",
1438 "BISECT_LOG",
1439 ];
1440 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1441 let handle_err = |err: PathError| match err.source.kind() {
1442 std::io::ErrorKind::NotFound => Ok(()),
1443 _ => Err(GitResetHeadError::from_git(err)),
1444 };
1445 for file_name in STATE_FILE_NAMES {
1446 let path = git_repo.path().join(file_name);
1447 std::fs::remove_file(&path)
1448 .context(&path)
1449 .or_else(handle_err)?;
1450 }
1451 for dir_name in STATE_DIR_NAMES {
1452 let path = git_repo.path().join(dir_name);
1453 std::fs::remove_dir_all(&path)
1454 .context(&path)
1455 .or_else(handle_err)?;
1456 }
1457 Ok(())
1458}
1459
1460fn reset_index(
1461 repo: &dyn Repo,
1462 git_repo: &gix::Repository,
1463 wc_commit: &Commit,
1464) -> Result<(), GitResetHeadError> {
1465 let parent_tree = wc_commit.parent_tree(repo)?;
1466 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1470 if tree_id == repo.store().empty_tree_id() {
1471 gix::index::File::from_state(
1475 gix::index::State::new(git_repo.object_hash()),
1476 git_repo.index_path(),
1477 )
1478 } else {
1479 git_repo
1482 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1483 .map_err(GitResetHeadError::from_git)?
1484 }
1485 } else {
1486 build_index_from_merged_tree(git_repo, &parent_tree)?
1487 };
1488
1489 let wc_tree = wc_commit.tree();
1490 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1491
1492 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1495 index
1496 .entries_mut_with_paths()
1497 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1498 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1499 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1500 })
1501 .filter_map(|merged| merged.both())
1502 .map(|((entry, _), old_entry)| (entry, old_entry))
1503 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1504 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1505 }
1506
1507 debug_assert!(index.verify_entries().is_ok());
1508
1509 index
1510 .write(gix::index::write::Options::default())
1511 .map_err(GitResetHeadError::from_git)
1512}
1513
1514fn build_index_from_merged_tree(
1515 git_repo: &gix::Repository,
1516 merged_tree: &MergedTree,
1517) -> Result<gix::index::File, GitResetHeadError> {
1518 let mut index = gix::index::File::from_state(
1519 gix::index::State::new(git_repo.object_hash()),
1520 git_repo.index_path(),
1521 );
1522
1523 let mut push_index_entry =
1524 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1525 let Some(entry) = maybe_entry else {
1526 return;
1527 };
1528
1529 let (id, mode) = match entry {
1530 TreeValue::File {
1531 id,
1532 executable,
1533 copy_id: _,
1534 } => {
1535 if *executable {
1536 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1537 } else {
1538 (id.as_bytes(), gix::index::entry::Mode::FILE)
1539 }
1540 }
1541 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1542 TreeValue::Tree(_) => {
1543 return;
1548 }
1549 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1550 };
1551
1552 let path = BStr::new(path.as_internal_file_string());
1553
1554 index.dangerously_push_entry(
1557 gix::index::entry::Stat::default(),
1558 gix::ObjectId::from_bytes_or_panic(id),
1559 gix::index::entry::Flags::from_stage(stage),
1560 mode,
1561 path,
1562 );
1563 };
1564
1565 let mut has_many_sided_conflict = false;
1566
1567 for (path, entry) in merged_tree.entries() {
1568 let entry = entry?;
1569 if let Some(resolved) = entry.as_resolved() {
1570 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1571 continue;
1572 }
1573
1574 let conflict = entry.simplify();
1575 if let [left, base, right] = conflict.as_slice() {
1576 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1578 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1579 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1580 } else {
1581 has_many_sided_conflict = true;
1589 push_index_entry(
1590 &path,
1591 conflict.first(),
1592 gix::index::entry::Stage::Unconflicted,
1593 );
1594 }
1595 }
1596
1597 index.sort_entries();
1600
1601 if has_many_sided_conflict
1604 && index
1605 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1606 .is_err()
1607 {
1608 let file_blob = git_repo
1609 .write_blob(
1610 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1611 )
1612 .map_err(GitResetHeadError::from_git)?;
1613 index.dangerously_push_entry(
1614 gix::index::entry::Stat::default(),
1615 file_blob.detach(),
1616 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1617 gix::index::entry::Mode::FILE,
1618 INDEX_DUMMY_CONFLICT_FILE.into(),
1619 );
1620 index.sort_entries();
1623 }
1624
1625 Ok(index)
1626}
1627
1628pub fn update_intent_to_add(
1635 repo: &dyn Repo,
1636 old_tree: &MergedTree,
1637 new_tree: &MergedTree,
1638) -> Result<(), GitResetHeadError> {
1639 let git_repo = get_git_repo(repo.store())?;
1640 let mut index = git_repo
1641 .index_or_empty()
1642 .map_err(GitResetHeadError::from_git)?;
1643 let mut_index = Arc::make_mut(&mut index);
1644 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1645 debug_assert!(mut_index.verify_entries().is_ok());
1646 mut_index
1647 .write(gix::index::write::Options::default())
1648 .map_err(GitResetHeadError::from_git)?;
1649
1650 Ok(())
1651}
1652
1653async fn update_intent_to_add_impl(
1654 git_repo: &gix::Repository,
1655 index: &mut gix::index::File,
1656 old_tree: &MergedTree,
1657 new_tree: &MergedTree,
1658) -> Result<(), GitResetHeadError> {
1659 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1660 let mut added_paths = vec![];
1661 let mut removed_paths = HashSet::new();
1662 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1663 let values = values?;
1664 if values.before.is_absent() {
1665 let executable = match values.after.as_normal() {
1666 Some(TreeValue::File {
1667 id: _,
1668 executable,
1669 copy_id: _,
1670 }) => *executable,
1671 Some(TreeValue::Symlink(_)) => false,
1672 _ => {
1673 continue;
1674 }
1675 };
1676 if index
1677 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1678 .is_err()
1679 {
1680 added_paths.push((BString::from(path.into_internal_string()), executable));
1681 }
1682 } else if values.after.is_absent() {
1683 removed_paths.insert(BString::from(path.into_internal_string()));
1684 }
1685 }
1686
1687 if added_paths.is_empty() && removed_paths.is_empty() {
1688 return Ok(());
1689 }
1690
1691 if !added_paths.is_empty() {
1692 let empty_blob = git_repo
1694 .write_blob(b"")
1695 .map_err(GitResetHeadError::from_git)?
1696 .detach();
1697 for (path, executable) in added_paths {
1698 index.dangerously_push_entry(
1700 gix::index::entry::Stat::default(),
1701 empty_blob,
1702 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1703 if executable {
1704 gix::index::entry::Mode::FILE_EXECUTABLE
1705 } else {
1706 gix::index::entry::Mode::FILE
1707 },
1708 path.as_ref(),
1709 );
1710 }
1711 }
1712 if !removed_paths.is_empty() {
1713 index.remove_entries(|_size, path, entry| {
1714 entry
1715 .flags
1716 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1717 && removed_paths.contains(path)
1718 });
1719 }
1720
1721 index.sort_entries();
1722
1723 Ok(())
1724}
1725
1726#[derive(Debug, Error)]
1727pub enum GitRemoteManagementError {
1728 #[error("No git remote named '{}'", .0.as_symbol())]
1729 NoSuchRemote(RemoteNameBuf),
1730 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1731 RemoteAlreadyExists(RemoteNameBuf),
1732 #[error(transparent)]
1733 RemoteName(#[from] GitRemoteNameError),
1734 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1735 NonstandardConfiguration(RemoteNameBuf),
1736 #[error("Error saving Git configuration")]
1737 GitConfigSaveError(#[source] std::io::Error),
1738 #[error("Unexpected Git error when managing remotes")]
1739 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1740 #[error(transparent)]
1741 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1742 #[error(transparent)]
1743 RefExpansionError(#[from] GitRefExpansionError),
1744}
1745
1746impl GitRemoteManagementError {
1747 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1748 Self::InternalGitError(source.into())
1749 }
1750}
1751
1752fn default_fetch_refspec(remote: &RemoteName) -> String {
1753 format!(
1754 "+refs/heads/*:refs/remotes/{remote}/*",
1755 remote = remote.as_str()
1756 )
1757}
1758
1759fn add_ref(
1760 name: gix::refs::FullName,
1761 target: gix::refs::Target,
1762 message: BString,
1763) -> gix::refs::transaction::RefEdit {
1764 gix::refs::transaction::RefEdit {
1765 change: gix::refs::transaction::Change::Update {
1766 log: gix::refs::transaction::LogChange {
1767 mode: gix::refs::transaction::RefLog::AndReference,
1768 force_create_reflog: false,
1769 message,
1770 },
1771 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1772 new: target,
1773 },
1774 name,
1775 deref: false,
1776 }
1777}
1778
1779fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1780 gix::refs::transaction::RefEdit {
1781 change: gix::refs::transaction::Change::Delete {
1782 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1783 reference.target().into_owned(),
1784 ),
1785 log: gix::refs::transaction::RefLog::AndReference,
1786 },
1787 name: reference.name().to_owned(),
1788 deref: false,
1789 }
1790}
1791
1792fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1798 let mut config_file = File::create(
1799 config
1800 .meta()
1801 .path
1802 .as_ref()
1803 .expect("Git repository to have a config file"),
1804 )?;
1805 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1806}
1807
1808fn save_remote(
1809 config: &mut gix::config::File<'static>,
1810 remote_name: &RemoteName,
1811 remote: &mut gix::Remote,
1812) -> Result<(), GitRemoteManagementError> {
1813 config
1820 .new_section(
1821 "remote",
1822 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1823 )
1824 .map_err(GitRemoteManagementError::from_git)?;
1825 remote
1826 .save_as_to(remote_name.as_str(), config)
1827 .map_err(GitRemoteManagementError::from_git)?;
1828 Ok(())
1829}
1830
1831fn git_config_branch_section_ids_by_remote(
1832 config: &gix::config::File,
1833 remote_name: &RemoteName,
1834) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1835 config
1836 .sections_by_name("branch")
1837 .into_iter()
1838 .flatten()
1839 .filter_map(|section| {
1840 let remote_values = section.values("remote");
1841 let push_remote_values = section.values("pushRemote");
1842 if !remote_values
1843 .iter()
1844 .chain(push_remote_values.iter())
1845 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1846 {
1847 return None;
1848 }
1849 if remote_values.len() > 1
1850 || push_remote_values.len() > 1
1851 || section.value_names().any(|name| {
1852 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1853 })
1854 {
1855 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1856 remote_name.to_owned(),
1857 )));
1858 }
1859 Some(Ok(section.id()))
1860 })
1861 .collect()
1862}
1863
1864fn rename_remote_in_git_branch_config_sections(
1865 config: &mut gix::config::File,
1866 old_remote_name: &RemoteName,
1867 new_remote_name: &RemoteName,
1868) -> Result<(), GitRemoteManagementError> {
1869 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1870 config
1871 .section_mut_by_id(id)
1872 .expect("found section to exist")
1873 .set(
1874 "remote"
1875 .try_into()
1876 .expect("'remote' to be a valid value name"),
1877 BStr::new(new_remote_name.as_str()),
1878 );
1879 }
1880 Ok(())
1881}
1882
1883fn remove_remote_git_branch_config_sections(
1884 config: &mut gix::config::File,
1885 remote_name: &RemoteName,
1886) -> Result<(), GitRemoteManagementError> {
1887 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1888 config
1889 .remove_section_by_id(id)
1890 .expect("removed section to exist");
1891 }
1892 Ok(())
1893}
1894
1895fn remove_remote_git_config_sections(
1896 config: &mut gix::config::File,
1897 remote_name: &RemoteName,
1898) -> Result<(), GitRemoteManagementError> {
1899 let section_ids_to_remove: Vec<_> = config
1900 .sections_by_name("remote")
1901 .into_iter()
1902 .flatten()
1903 .filter(|section| {
1904 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1905 })
1906 .map(|section| {
1907 if section.value_names().any(|name| {
1908 !name.eq_ignore_ascii_case(b"url")
1909 && !name.eq_ignore_ascii_case(b"fetch")
1910 && !name.eq_ignore_ascii_case(b"tagOpt")
1911 }) {
1912 return Err(GitRemoteManagementError::NonstandardConfiguration(
1913 remote_name.to_owned(),
1914 ));
1915 }
1916 Ok(section.id())
1917 })
1918 .try_collect()?;
1919 for id in section_ids_to_remove {
1920 config
1921 .remove_section_by_id(id)
1922 .expect("removed section to exist");
1923 }
1924 Ok(())
1925}
1926
1927pub fn get_all_remote_names(
1929 store: &Store,
1930) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1931 let git_repo = get_git_repo(store)?;
1932 Ok(iter_remote_names(&git_repo).collect())
1933}
1934
1935fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
1936 git_repo
1937 .remote_names()
1938 .into_iter()
1939 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1941 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1943 .map(RemoteNameBuf::from)
1944}
1945
1946pub fn add_remote(
1947 mut_repo: &mut MutableRepo,
1948 remote_name: &RemoteName,
1949 url: &str,
1950 fetch_tags: gix::remote::fetch::Tags,
1951 bookmark_expr: &StringExpression,
1952) -> Result<(), GitRemoteManagementError> {
1953 let git_repo = get_git_repo(mut_repo.store())?;
1954
1955 validate_remote_name(remote_name)?;
1956
1957 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1958 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1959 remote_name.to_owned(),
1960 ));
1961 }
1962
1963 let ExpandedFetchRefSpecs {
1964 bookmark_expr: _,
1965 refspecs,
1966 negative_refspecs,
1967 } = expand_fetch_refspecs(remote_name, bookmark_expr.clone())?;
1968 let fetch_refspecs = itertools::chain(
1969 refspecs.iter().map(|spec| spec.to_git_format()),
1970 negative_refspecs.iter().map(|spec| spec.to_git_format()),
1971 )
1972 .map(BString::from);
1973
1974 let mut remote = git_repo
1975 .remote_at(url)
1976 .map_err(GitRemoteManagementError::from_git)?
1977 .with_fetch_tags(fetch_tags)
1978 .with_refspecs(fetch_refspecs, gix::remote::Direction::Fetch)
1979 .expect("previously-parsed refspecs to be valid");
1980 let mut config = git_repo.config_snapshot().clone();
1981 save_remote(&mut config, remote_name, &mut remote)?;
1982 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1983
1984 mut_repo.ensure_remote(remote_name);
1985
1986 Ok(())
1987}
1988
1989pub fn remove_remote(
1990 mut_repo: &mut MutableRepo,
1991 remote_name: &RemoteName,
1992) -> Result<(), GitRemoteManagementError> {
1993 let mut git_repo = get_git_repo(mut_repo.store())?;
1994
1995 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1996 return Err(GitRemoteManagementError::NoSuchRemote(
1997 remote_name.to_owned(),
1998 ));
1999 };
2000
2001 let mut config = git_repo.config_snapshot().clone();
2002 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2003 remove_remote_git_config_sections(&mut config, remote_name)?;
2004 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2005
2006 remove_remote_git_refs(&mut git_repo, remote_name)
2007 .map_err(GitRemoteManagementError::from_git)?;
2008
2009 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2010 remove_remote_refs(mut_repo, remote_name);
2011 }
2012
2013 Ok(())
2014}
2015
2016fn remove_remote_git_refs(
2017 git_repo: &mut gix::Repository,
2018 remote: &RemoteName,
2019) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2020 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
2021 let edits: Vec<_> = git_repo
2022 .references()?
2023 .prefixed(prefix.as_str())?
2024 .map_ok(remove_ref)
2025 .try_collect()?;
2026 git_repo.edit_references(edits)?;
2027 Ok(())
2028}
2029
2030fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2031 mut_repo.remove_remote(remote);
2032 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
2033 let git_refs_to_delete = mut_repo
2034 .view()
2035 .git_refs()
2036 .keys()
2037 .filter(|&r| r.as_str().starts_with(&prefix))
2038 .cloned()
2039 .collect_vec();
2040 for git_ref in git_refs_to_delete {
2041 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2042 }
2043}
2044
2045pub fn rename_remote(
2046 mut_repo: &mut MutableRepo,
2047 old_remote_name: &RemoteName,
2048 new_remote_name: &RemoteName,
2049) -> Result<(), GitRemoteManagementError> {
2050 let mut git_repo = get_git_repo(mut_repo.store())?;
2051
2052 validate_remote_name(new_remote_name)?;
2053
2054 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2055 return Err(GitRemoteManagementError::NoSuchRemote(
2056 old_remote_name.to_owned(),
2057 ));
2058 };
2059 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2060
2061 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2062 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2063 new_remote_name.to_owned(),
2064 ));
2065 }
2066
2067 match (
2068 remote.refspecs(gix::remote::Direction::Fetch),
2069 remote.refspecs(gix::remote::Direction::Push),
2070 ) {
2071 ([refspec], [])
2072 if refspec.to_ref().to_bstring()
2073 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2074 _ => {
2075 return Err(GitRemoteManagementError::NonstandardConfiguration(
2076 old_remote_name.to_owned(),
2077 ));
2078 }
2079 }
2080
2081 remote
2082 .replace_refspecs(
2083 [default_fetch_refspec(new_remote_name).as_bytes()],
2084 gix::remote::Direction::Fetch,
2085 )
2086 .expect("default refspec to be valid");
2087
2088 let mut config = git_repo.config_snapshot().clone();
2089 save_remote(&mut config, new_remote_name, &mut remote)?;
2090 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2091 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2092 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2093
2094 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2095 .map_err(GitRemoteManagementError::from_git)?;
2096
2097 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2098 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2099 }
2100
2101 Ok(())
2102}
2103
2104fn rename_remote_git_refs(
2105 git_repo: &mut gix::Repository,
2106 old_remote_name: &RemoteName,
2107 new_remote_name: &RemoteName,
2108) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2109 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2110 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
2111 let ref_log_message = BString::from(format!(
2112 "renamed remote {old_remote_name} to {new_remote_name}",
2113 old_remote_name = old_remote_name.as_symbol(),
2114 new_remote_name = new_remote_name.as_symbol(),
2115 ));
2116
2117 let edits: Vec<_> = git_repo
2118 .references()?
2119 .prefixed(old_prefix.as_str())?
2120 .map_ok(|old_ref| {
2121 let new_name = BString::new(
2122 [
2123 new_prefix.as_bytes(),
2124 &old_ref.name().as_bstr()[old_prefix.len()..],
2125 ]
2126 .concat(),
2127 );
2128 [
2129 add_ref(
2130 new_name.try_into().expect("new ref name to be valid"),
2131 old_ref.target().into_owned(),
2132 ref_log_message.clone(),
2133 ),
2134 remove_ref(old_ref),
2135 ]
2136 })
2137 .flatten_ok()
2138 .try_collect()?;
2139 git_repo.edit_references(edits)?;
2140 Ok(())
2141}
2142
2143fn gix_remote_with_fetch_url<Url, E>(
2149 remote: gix::Remote,
2150 url: Url,
2151) -> Result<gix::Remote, gix::remote::init::Error>
2152where
2153 Url: TryInto<gix::Url, Error = E>,
2154 gix::url::parse::Error: From<E>,
2155{
2156 let mut new_remote = remote.repo().remote_at(url)?;
2157 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2163 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2164 new_remote
2165 .replace_refspecs(
2166 remote
2167 .refspecs(direction)
2168 .iter()
2169 .map(|refspec| refspec.to_ref().to_bstring()),
2170 direction,
2171 )
2172 .expect("existing refspecs to be valid");
2173 }
2174 Ok(new_remote)
2175}
2176
2177pub fn set_remote_url(
2178 store: &Store,
2179 remote_name: &RemoteName,
2180 new_remote_url: &str,
2181) -> Result<(), GitRemoteManagementError> {
2182 let git_repo = get_git_repo(store)?;
2183
2184 validate_remote_name(remote_name)?;
2185
2186 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2187 return Err(GitRemoteManagementError::NoSuchRemote(
2188 remote_name.to_owned(),
2189 ));
2190 };
2191 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2192
2193 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2194 return Err(GitRemoteManagementError::NonstandardConfiguration(
2195 remote_name.to_owned(),
2196 ));
2197 }
2198
2199 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2200 .map_err(GitRemoteManagementError::from_git)?;
2201
2202 let mut config = git_repo.config_snapshot().clone();
2203 save_remote(&mut config, remote_name, &mut remote)?;
2204 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2205
2206 Ok(())
2207}
2208
2209fn rename_remote_refs(
2210 mut_repo: &mut MutableRepo,
2211 old_remote_name: &RemoteName,
2212 new_remote_name: &RemoteName,
2213) {
2214 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2215 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2216 let git_refs = mut_repo
2217 .view()
2218 .git_refs()
2219 .iter()
2220 .filter_map(|(old, target)| {
2221 old.as_str().strip_prefix(&prefix).map(|p| {
2222 let new: GitRefNameBuf =
2223 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2224 (old.clone(), new, target.clone())
2225 })
2226 })
2227 .collect_vec();
2228 for (old, new, target) in git_refs {
2229 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2230 mut_repo.set_git_ref_target(&new, target);
2231 }
2232}
2233
2234const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2235
2236#[derive(Error, Debug)]
2237pub enum GitFetchError {
2238 #[error("No git remote named '{}'", .0.as_symbol())]
2239 NoSuchRemote(RemoteNameBuf),
2240 #[error(transparent)]
2241 RemoteName(#[from] GitRemoteNameError),
2242 #[error(transparent)]
2243 Subprocess(#[from] GitSubprocessError),
2244}
2245
2246#[derive(Error, Debug)]
2247pub enum GitDefaultRefspecError {
2248 #[error("No git remote named '{}'", .0.as_symbol())]
2249 NoSuchRemote(RemoteNameBuf),
2250 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2251 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2252}
2253
2254struct FetchedBranches {
2255 remote: RemoteNameBuf,
2256 bookmark_matcher: StringMatcher,
2257}
2258
2259#[derive(Debug)]
2261pub struct ExpandedFetchRefSpecs {
2262 bookmark_expr: StringExpression,
2264 refspecs: Vec<RefSpec>,
2265 negative_refspecs: Vec<NegativeRefSpec>,
2266}
2267
2268#[derive(Error, Debug)]
2269pub enum GitRefExpansionError {
2270 #[error(transparent)]
2271 Expression(#[from] GitRefExpressionError),
2272 #[error(
2273 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2274 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2275 )]
2276 InvalidBranchPattern(StringPattern),
2277}
2278
2279pub fn expand_fetch_refspecs(
2281 remote: &RemoteName,
2282 bookmark_expr: StringExpression,
2283) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2284 let (positive_bookmarks, negative_bookmarks) =
2285 split_into_positive_negative_patterns(&bookmark_expr)?;
2286
2287 let refspecs = positive_bookmarks
2288 .iter()
2289 .map(|&pattern| {
2290 pattern
2291 .to_glob()
2292 .filter(
2293 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2296 )
2297 .map(|glob| {
2298 RefSpec::forced(
2299 format!("refs/heads/{glob}"),
2300 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2301 )
2302 })
2303 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2304 })
2305 .try_collect()?;
2306
2307 let negative_refspecs = negative_bookmarks
2308 .iter()
2309 .map(|&pattern| {
2310 pattern
2311 .to_glob()
2312 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2313 .map(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}")))
2314 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2315 })
2316 .try_collect()?;
2317
2318 Ok(ExpandedFetchRefSpecs {
2319 bookmark_expr,
2320 refspecs,
2321 negative_refspecs,
2322 })
2323}
2324
2325#[derive(Debug, Error)]
2326pub enum GitRefExpressionError {
2327 #[error("Cannot use `~` in sub expression")]
2328 NestedNotIn,
2329 #[error("Cannot use `&` in sub expression")]
2330 NestedIntersection,
2331 #[error("Cannot use `&` for positive expressions")]
2332 PositiveIntersection,
2333}
2334
2335fn split_into_positive_negative_patterns(
2338 expr: &StringExpression,
2339) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2340 static ALL: StringPattern = StringPattern::all();
2341
2342 fn visit_positive<'a>(
2356 expr: &'a StringExpression,
2357 positives: &mut Vec<&'a StringPattern>,
2358 negatives: &mut Vec<&'a StringPattern>,
2359 ) -> Result<(), GitRefExpressionError> {
2360 match expr {
2361 StringExpression::Pattern(pattern) => {
2362 positives.push(pattern);
2363 Ok(())
2364 }
2365 StringExpression::NotIn(complement) => {
2366 positives.push(&ALL);
2367 visit_negative(complement, negatives)
2368 }
2369 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2370 StringExpression::Intersection(expr1, expr2) => {
2371 match (expr1.as_ref(), expr2.as_ref()) {
2372 (other, StringExpression::NotIn(complement))
2373 | (StringExpression::NotIn(complement), other) => {
2374 visit_positive(other, positives, negatives)?;
2375 visit_negative(complement, negatives)
2376 }
2377 _ => Err(GitRefExpressionError::PositiveIntersection),
2378 }
2379 }
2380 }
2381 }
2382
2383 fn visit_negative<'a>(
2384 expr: &'a StringExpression,
2385 negatives: &mut Vec<&'a StringPattern>,
2386 ) -> Result<(), GitRefExpressionError> {
2387 match expr {
2388 StringExpression::Pattern(pattern) => {
2389 negatives.push(pattern);
2390 Ok(())
2391 }
2392 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2393 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2394 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2395 }
2396 }
2397
2398 fn visit_union<'a>(
2399 expr1: &'a StringExpression,
2400 expr2: &'a StringExpression,
2401 patterns: &mut Vec<&'a StringPattern>,
2402 ) -> Result<(), GitRefExpressionError> {
2403 visit_union_sub(expr1, patterns)?;
2404 visit_union_sub(expr2, patterns)
2405 }
2406
2407 fn visit_union_sub<'a>(
2408 expr: &'a StringExpression,
2409 patterns: &mut Vec<&'a StringPattern>,
2410 ) -> Result<(), GitRefExpressionError> {
2411 match expr {
2412 StringExpression::Pattern(pattern) => {
2413 patterns.push(pattern);
2414 Ok(())
2415 }
2416 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2417 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2418 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2419 }
2420 }
2421
2422 let mut positives = Vec::new();
2423 let mut negatives = Vec::new();
2424 visit_positive(expr, &mut positives, &mut negatives)?;
2425 Ok((positives, negatives))
2426}
2427
2428#[derive(Debug)]
2432#[must_use = "warnings should be surfaced in the UI"]
2433pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2434
2435#[derive(Debug)]
2438pub struct IgnoredRefspec {
2439 pub refspec: BString,
2441 pub reason: &'static str,
2443}
2444
2445#[derive(Debug)]
2446enum FetchRefSpec {
2447 Positive(RefSpec),
2448 Negative(NegativeRefSpec),
2449}
2450
2451pub fn expand_default_fetch_refspecs(
2453 remote_name: &RemoteName,
2454 git_repo: &gix::Repository,
2455) -> Result<(IgnoredRefspecs, ExpandedFetchRefSpecs), GitDefaultRefspecError> {
2456 let remote = git_repo
2457 .try_find_remote(remote_name.as_str())
2458 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2459 .map_err(|e| {
2460 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2461 })?;
2462
2463 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2464 let mut refspecs = Vec::with_capacity(remote_refspecs.len());
2465 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2466 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2467 let mut negative_refspecs = Vec::new();
2468 let mut negative_bookmarks = Vec::new();
2469 for refspec in remote_refspecs {
2470 let refspec = refspec.to_ref();
2471 match parse_fetch_refspec(remote_name, refspec) {
2472 Ok((FetchRefSpec::Positive(refspec), bookmark)) => {
2473 refspecs.push(refspec);
2474 positive_bookmarks.push(StringExpression::pattern(bookmark));
2475 }
2476 Ok((FetchRefSpec::Negative(refspec), bookmark)) => {
2477 negative_refspecs.push(refspec);
2478 negative_bookmarks.push(StringExpression::pattern(bookmark));
2479 }
2480 Err(reason) => {
2481 let refspec = refspec.to_bstring();
2482 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2483 }
2484 }
2485 }
2486
2487 let bookmark_expr = StringExpression::union_all(positive_bookmarks)
2488 .intersection(StringExpression::union_all(negative_bookmarks).negated());
2489
2490 Ok((
2491 IgnoredRefspecs(ignored_refspecs),
2492 ExpandedFetchRefSpecs {
2493 bookmark_expr,
2494 refspecs,
2495 negative_refspecs,
2496 },
2497 ))
2498}
2499
2500fn parse_fetch_refspec(
2501 remote_name: &RemoteName,
2502 refspec: gix::refspec::RefSpecRef<'_>,
2503) -> Result<(FetchRefSpec, StringPattern), &'static str> {
2504 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2505
2506 let (src, positive_dst) = match refspec.instruction() {
2507 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2508 Instruction::Fetch(fetch) => match fetch {
2509 gix::refspec::instruction::Fetch::Only { src: _ } => {
2510 return Err("fetch-only refspecs are not supported");
2511 }
2512 gix::refspec::instruction::Fetch::AndUpdate {
2513 src,
2514 dst,
2515 allow_non_fast_forward,
2516 } => {
2517 if !allow_non_fast_forward {
2518 return Err("non-forced refspecs are not supported");
2519 }
2520 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2521 }
2522 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2523 },
2524 };
2525
2526 let src_branch = src
2527 .strip_prefix("refs/heads/")
2528 .ok_or("only refs/heads/ is supported for refspec sources")?;
2529 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2530
2531 if let Some(dst) = positive_dst {
2532 let dst_without_prefix = dst
2533 .strip_prefix("refs/remotes/")
2534 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2535 let dst_branch = dst_without_prefix
2536 .strip_prefix(remote_name.as_str())
2537 .and_then(|d| d.strip_prefix("/"))
2538 .ok_or("remote renaming not supported")?;
2539 if src_branch != dst_branch {
2540 return Err("renaming is not supported");
2541 }
2542 Ok((FetchRefSpec::Positive(RefSpec::forced(src, dst)), branch))
2543 } else {
2544 Ok((FetchRefSpec::Negative(NegativeRefSpec::new(src)), branch))
2545 }
2546}
2547
2548pub struct GitFetch<'a> {
2550 mut_repo: &'a mut MutableRepo,
2551 git_repo: Box<gix::Repository>,
2552 git_ctx: GitSubprocessContext<'a>,
2553 git_settings: &'a GitSettings,
2554 fetched: Vec<FetchedBranches>,
2555}
2556
2557impl<'a> GitFetch<'a> {
2558 pub fn new(
2559 mut_repo: &'a mut MutableRepo,
2560 git_settings: &'a GitSettings,
2561 ) -> Result<Self, UnexpectedGitBackendError> {
2562 let git_backend = get_git_backend(mut_repo.store())?;
2563 let git_repo = Box::new(git_backend.git_repo());
2564 let git_ctx =
2565 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2566 Ok(GitFetch {
2567 mut_repo,
2568 git_repo,
2569 git_ctx,
2570 git_settings,
2571 fetched: vec![],
2572 })
2573 }
2574
2575 #[tracing::instrument(skip(self, callbacks))]
2581 pub fn fetch(
2582 &mut self,
2583 remote_name: &RemoteName,
2584 ExpandedFetchRefSpecs {
2585 bookmark_expr,
2586 refspecs: mut remaining_refspecs,
2587 negative_refspecs,
2588 }: ExpandedFetchRefSpecs,
2589 mut callbacks: RemoteCallbacks,
2590 depth: Option<NonZeroU32>,
2591 fetch_tags_override: Option<FetchTagsOverride>,
2592 ) -> Result<(), GitFetchError> {
2593 validate_remote_name(remote_name)?;
2594
2595 if self
2597 .git_repo
2598 .try_find_remote(remote_name.as_str())
2599 .is_none()
2600 {
2601 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2602 }
2603
2604 if remaining_refspecs.is_empty() {
2605 return Ok(());
2607 }
2608
2609 let mut branches_to_prune = Vec::new();
2610 while let Some(failing_refspec) = self.git_ctx.spawn_fetch(
2618 remote_name,
2619 &remaining_refspecs,
2620 &negative_refspecs,
2621 &mut callbacks,
2622 depth,
2623 fetch_tags_override,
2624 )? {
2625 tracing::debug!(failing_refspec, "failed to fetch ref");
2626 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2627
2628 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2629 branches_to_prune.push(format!(
2630 "{remote_name}/{branch_name}",
2631 remote_name = remote_name.as_str()
2632 ));
2633 }
2634 }
2635
2636 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2639
2640 self.fetched.push(FetchedBranches {
2641 remote: remote_name.to_owned(),
2642 bookmark_matcher: bookmark_expr.to_matcher(),
2643 });
2644 Ok(())
2645 }
2646
2647 #[tracing::instrument(skip(self))]
2649 pub fn get_default_branch(
2650 &self,
2651 remote_name: &RemoteName,
2652 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2653 if self
2654 .git_repo
2655 .try_find_remote(remote_name.as_str())
2656 .is_none()
2657 {
2658 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2659 }
2660 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2661 tracing::debug!(?default_branch);
2662 Ok(default_branch)
2663 }
2664
2665 #[tracing::instrument(skip(self))]
2673 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2674 tracing::debug!("import_refs");
2675 let import_stats =
2676 import_some_refs(
2677 self.mut_repo,
2678 self.git_settings,
2679 |kind, symbol| match kind {
2680 GitRefKind::Bookmark => self
2681 .fetched
2682 .iter()
2683 .filter(|fetched| fetched.remote == symbol.remote)
2684 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
2685 GitRefKind::Tag => true,
2686 },
2687 )?;
2688
2689 self.fetched.clear();
2690
2691 Ok(import_stats)
2692 }
2693}
2694
2695#[derive(Error, Debug)]
2696pub enum GitPushError {
2697 #[error("No git remote named '{}'", .0.as_symbol())]
2698 NoSuchRemote(RemoteNameBuf),
2699 #[error(transparent)]
2700 RemoteName(#[from] GitRemoteNameError),
2701 #[error(transparent)]
2702 Subprocess(#[from] GitSubprocessError),
2703 #[error(transparent)]
2704 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2705}
2706
2707#[derive(Clone, Debug)]
2708pub struct GitBranchPushTargets {
2709 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2710}
2711
2712pub struct GitRefUpdate {
2713 pub qualified_name: GitRefNameBuf,
2714 pub expected_current_target: Option<CommitId>,
2719 pub new_target: Option<CommitId>,
2720}
2721
2722pub fn push_branches(
2724 mut_repo: &mut MutableRepo,
2725 git_settings: &GitSettings,
2726 remote: &RemoteName,
2727 targets: &GitBranchPushTargets,
2728 callbacks: RemoteCallbacks,
2729) -> Result<GitPushStats, GitPushError> {
2730 validate_remote_name(remote)?;
2731
2732 let ref_updates = targets
2733 .branch_updates
2734 .iter()
2735 .map(|(name, update)| GitRefUpdate {
2736 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2737 expected_current_target: update.old_target.clone(),
2738 new_target: update.new_target.clone(),
2739 })
2740 .collect_vec();
2741
2742 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2743 tracing::debug!(?push_stats);
2744
2745 if push_stats.all_ok() {
2749 for (name, update) in &targets.branch_updates {
2750 let git_ref_name: GitRefNameBuf = format!(
2751 "refs/remotes/{remote}/{name}",
2752 remote = remote.as_str(),
2753 name = name.as_str()
2754 )
2755 .into();
2756 let new_remote_ref = RemoteRef {
2757 target: RefTarget::resolved(update.new_target.clone()),
2758 state: RemoteRefState::Tracked,
2759 };
2760 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2761 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2762 }
2763 }
2764
2765 Ok(push_stats)
2766}
2767
2768pub fn push_updates(
2770 repo: &dyn Repo,
2771 git_settings: &GitSettings,
2772 remote_name: &RemoteName,
2773 updates: &[GitRefUpdate],
2774 mut callbacks: RemoteCallbacks,
2775) -> Result<GitPushStats, GitPushError> {
2776 let mut qualified_remote_refs_expected_locations = HashMap::new();
2777 let mut refspecs = vec![];
2778 for update in updates {
2779 qualified_remote_refs_expected_locations.insert(
2780 update.qualified_name.as_ref(),
2781 update.expected_current_target.as_ref(),
2782 );
2783 if let Some(new_target) = &update.new_target {
2784 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2788 } else {
2789 refspecs.push(RefSpec::delete(&update.qualified_name));
2793 }
2794 }
2795
2796 let git_backend = get_git_backend(repo.store())?;
2797 let git_repo = git_backend.git_repo();
2798 let git_ctx =
2799 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2800
2801 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2803 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2804 }
2805
2806 let refs_to_push: Vec<RefToPush> = refspecs
2807 .iter()
2808 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2809 .collect();
2810
2811 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2812 push_stats.pushed.sort();
2813 push_stats.rejected.sort();
2814 push_stats.remote_rejected.sort();
2815 Ok(push_stats)
2816}
2817
2818#[non_exhaustive]
2819#[derive(Default)]
2820#[expect(clippy::type_complexity)]
2821pub struct RemoteCallbacks<'a> {
2822 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2823 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2824 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2825 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2826 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2827}
2828
2829#[derive(Clone, Debug)]
2830pub struct Progress {
2831 pub bytes_downloaded: Option<u64>,
2833 pub overall: f32,
2834}
2835
2836#[derive(Copy, Clone, Debug)]
2839pub enum FetchTagsOverride {
2840 AllTags,
2843 NoTags,
2846}
2847
2848#[cfg(test)]
2849mod tests {
2850 use assert_matches::assert_matches;
2851
2852 use super::*;
2853 use crate::revset;
2854 use crate::revset::RevsetDiagnostics;
2855
2856 #[test]
2857 fn test_split_positive_negative_patterns() {
2858 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
2859 try_split(text).unwrap()
2860 }
2861
2862 fn try_split(
2863 text: &str,
2864 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
2865 let mut diagnostics = RevsetDiagnostics::new();
2866 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
2867 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
2868 Ok((
2869 positives.into_iter().cloned().collect(),
2870 negatives.into_iter().cloned().collect(),
2871 ))
2872 }
2873
2874 insta::assert_compact_debug_snapshot!(
2875 split("a"),
2876 @r#"([Exact("a")], [])"#);
2877 insta::assert_compact_debug_snapshot!(
2878 split("~a"),
2879 @r#"([Substring("")], [Exact("a")])"#);
2880 insta::assert_compact_debug_snapshot!(
2881 split("~a~b"),
2882 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
2883 insta::assert_compact_debug_snapshot!(
2884 split("~(a|b)"),
2885 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
2886 insta::assert_compact_debug_snapshot!(
2887 split("a|b"),
2888 @r#"([Exact("a"), Exact("b")], [])"#);
2889 insta::assert_compact_debug_snapshot!(
2890 split("(a|b)&~c"),
2891 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
2892 insta::assert_compact_debug_snapshot!(
2893 split("~a&b"),
2894 @r#"([Exact("b")], [Exact("a")])"#);
2895 insta::assert_compact_debug_snapshot!(
2896 split("a&~b&~c"),
2897 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
2898 insta::assert_compact_debug_snapshot!(
2899 split("~a&b&~c"),
2900 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
2901 insta::assert_compact_debug_snapshot!(
2902 split("a&~(b|c)"),
2903 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
2904 insta::assert_compact_debug_snapshot!(
2905 split("((a|b)|c)&~(d|(e|f))"),
2906 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
2907 assert_matches!(
2908 try_split("a&b"),
2909 Err(GitRefExpressionError::PositiveIntersection)
2910 );
2911 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
2912 assert_matches!(
2913 try_split("a&~(b&~c)"),
2914 Err(GitRefExpressionError::NestedIntersection)
2915 );
2916 assert_matches!(
2917 try_split("(a|b)&c"),
2918 Err(GitRefExpressionError::PositiveIntersection)
2919 );
2920 assert_matches!(
2921 try_split("(a&~b)&(~c&~d)"),
2922 Err(GitRefExpressionError::PositiveIntersection)
2923 );
2924 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
2925 assert_matches!(
2926 try_split("a&~b|c&~d"),
2927 Err(GitRefExpressionError::NestedIntersection)
2928 );
2929 }
2930}