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