1#![expect(missing_docs)]
16
17use std::borrow::Borrow;
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::default::Default;
22use std::ffi::OsString;
23use std::fs::File;
24use std::iter;
25use std::num::NonZeroU32;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use bstr::BStr;
30use bstr::BString;
31use futures::StreamExt as _;
32use futures::TryStreamExt as _;
33use gix::refspec::Instruction;
34use itertools::Itertools as _;
35use thiserror::Error;
36
37use crate::backend::BackendError;
38use crate::backend::BackendResult;
39use crate::backend::CommitId;
40use crate::backend::TreeValue;
41use crate::commit::Commit;
42use crate::config::ConfigGetError;
43use crate::file_util::IoResultExt as _;
44use crate::file_util::PathError;
45use crate::git_backend::GitBackend;
46use crate::git_subprocess::GitFetchStatus;
47pub use crate::git_subprocess::GitProgress;
48pub use crate::git_subprocess::GitSidebandLineTerminator;
49pub use crate::git_subprocess::GitSubprocessCallback;
50use crate::git_subprocess::GitSubprocessContext;
51use crate::git_subprocess::GitSubprocessError;
52use crate::index::IndexError;
53use crate::matchers::EverythingMatcher;
54use crate::merge::Diff;
55use crate::merged_tree::MergedTree;
56use crate::merged_tree::TreeDiffEntry;
57use crate::object_id::ObjectId as _;
58use crate::op_store::RefTarget;
59use crate::op_store::RefTargetOptionExt as _;
60use crate::op_store::RemoteRef;
61use crate::op_store::RemoteRefState;
62use crate::ref_name::GitRefName;
63use crate::ref_name::GitRefNameBuf;
64use crate::ref_name::RefName;
65use crate::ref_name::RefNameBuf;
66use crate::ref_name::RemoteName;
67use crate::ref_name::RemoteNameBuf;
68use crate::ref_name::RemoteRefSymbol;
69use crate::ref_name::RemoteRefSymbolBuf;
70use crate::repo::MutableRepo;
71use crate::repo::Repo;
72use crate::repo_path::RepoPath;
73use crate::revset::RevsetExpression;
74use crate::settings::UserSettings;
75use crate::store::Store;
76use crate::str_util::StringExpression;
77use crate::str_util::StringMatcher;
78use crate::str_util::StringPattern;
79use crate::view::View;
80
81pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
83pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
85const REMOTE_BOOKMARK_REF_NAMESPACE: &str = "refs/remotes/";
87const REMOTE_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";
89const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
91const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
94
95#[derive(Clone, Debug)]
96pub struct GitSettings {
97 pub auto_local_bookmark: bool,
99 pub abandon_unreachable_commits: bool,
100 pub executable_path: PathBuf,
101 pub write_change_id_header: bool,
102}
103
104impl GitSettings {
105 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
106 Ok(Self {
107 auto_local_bookmark: settings.get_bool("git.auto-local-bookmark")?,
108 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
109 executable_path: settings.get("git.executable-path")?,
110 write_change_id_header: settings.get("git.write-change-id-header")?,
111 })
112 }
113
114 pub fn to_subprocess_options(&self) -> GitSubprocessOptions {
115 GitSubprocessOptions {
116 executable_path: self.executable_path.clone(),
117 environment: HashMap::new(),
118 }
119 }
120}
121
122#[derive(Clone, Debug)]
124pub struct GitSubprocessOptions {
125 pub executable_path: PathBuf,
126 pub environment: HashMap<OsString, OsString>,
131}
132
133impl GitSubprocessOptions {
134 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
135 Ok(Self {
136 executable_path: settings.get("git.executable-path")?,
137 environment: HashMap::new(),
138 })
139 }
140}
141
142#[derive(Debug, Error)]
143pub enum GitRemoteNameError {
144 #[error(
145 "Git remote named '{name}' is reserved for local Git repository",
146 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
147 )]
148 ReservedForLocalGitRepo,
149 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
150 WithSlash(RemoteNameBuf),
151}
152
153fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
154 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
155 Err(GitRemoteNameError::ReservedForLocalGitRepo)
156 } else if name.as_str().contains('/') {
157 Err(GitRemoteNameError::WithSlash(name.to_owned()))
158 } else {
159 Ok(())
160 }
161}
162
163#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
165pub enum GitRefKind {
166 Bookmark,
167 Tag,
168}
169
170#[derive(Debug, Default)]
172pub struct GitPushStats {
173 pub pushed: Vec<GitRefNameBuf>,
175 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
177 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
179 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
181}
182
183impl GitPushStats {
184 pub fn all_ok(&self) -> bool {
185 self.rejected.is_empty()
186 && self.remote_rejected.is_empty()
187 && self.unexported_bookmarks.is_empty()
188 }
189
190 pub fn some_exported(&self) -> bool {
193 self.pushed.len() > self.unexported_bookmarks.len()
194 }
195}
196
197#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
202
203impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
204 fn borrow(&self) -> &RemoteRefSymbol<'b> {
205 &self.0
206 }
207}
208
209#[derive(Debug, Hash, PartialEq, Eq)]
215pub(crate) struct RefSpec {
216 forced: bool,
217 source: Option<String>,
220 destination: String,
221}
222
223impl RefSpec {
224 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
225 Self {
226 forced: true,
227 source: Some(source.into()),
228 destination: destination.into(),
229 }
230 }
231
232 fn delete(destination: impl Into<String>) -> Self {
233 Self {
235 forced: false,
236 source: None,
237 destination: destination.into(),
238 }
239 }
240
241 pub(crate) fn to_git_format(&self) -> String {
242 format!(
243 "{}{}",
244 if self.forced { "+" } else { "" },
245 self.to_git_format_not_forced()
246 )
247 }
248
249 pub(crate) fn to_git_format_not_forced(&self) -> String {
255 if let Some(s) = &self.source {
256 format!("{}:{}", s, self.destination)
257 } else {
258 format!(":{}", self.destination)
259 }
260 }
261}
262
263#[derive(Debug)]
265#[repr(transparent)]
266pub(crate) struct NegativeRefSpec {
267 source: String,
268}
269
270impl NegativeRefSpec {
271 fn new(source: impl Into<String>) -> Self {
272 Self {
273 source: source.into(),
274 }
275 }
276
277 pub(crate) fn to_git_format(&self) -> String {
278 format!("^{}", self.source)
279 }
280}
281
282pub(crate) struct RefToPush<'a> {
285 pub(crate) refspec: &'a RefSpec,
286 pub(crate) expected_location: Option<&'a CommitId>,
287}
288
289impl<'a> RefToPush<'a> {
290 fn new(
291 refspec: &'a RefSpec,
292 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
293 ) -> Self {
294 let expected_location = *expected_locations
295 .get(GitRefName::new(&refspec.destination))
296 .expect(
297 "The refspecs and the expected locations were both constructed from the same \
298 source of truth. This means the lookup should always work.",
299 );
300
301 Self {
302 refspec,
303 expected_location,
304 }
305 }
306
307 pub(crate) fn to_git_lease(&self) -> String {
308 format!(
309 "{}:{}",
310 self.refspec.destination,
311 self.expected_location
312 .map(|x| x.to_string())
313 .as_deref()
314 .unwrap_or("")
315 )
316 }
317}
318
319pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
322 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
323 if name == "HEAD" {
325 return None;
326 }
327 let name = RefName::new(name);
328 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
329 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
330 } else if let Some(remote_and_name) = full_name
331 .as_str()
332 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
333 {
334 let (remote, name) = remote_and_name.split_once('/')?;
335 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
337 return None;
338 }
339 let name = RefName::new(name);
340 let remote = RemoteName::new(remote);
341 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
342 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
343 let name = RefName::new(name);
344 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
345 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
346 } else {
347 None
348 }
349}
350
351fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
352 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
353 let (remote, name) = remote_and_name.split_once('/')?;
354 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
355 return None;
356 }
357 let name = RefName::new(name);
358 let remote = RemoteName::new(remote);
359 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
360}
361
362fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
363 let RemoteRefSymbol { name, remote } = symbol;
364 let name = name.as_str();
365 let remote = remote.as_str();
366 if name.is_empty() || remote.is_empty() {
367 return None;
368 }
369 match kind {
370 GitRefKind::Bookmark => {
371 if name == "HEAD" {
372 return None;
373 }
374 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
375 Some(format!("refs/heads/{name}").into())
376 } else {
377 Some(format!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())
378 }
379 }
380 GitRefKind::Tag => {
381 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
383 }
384 }
385}
386
387fn to_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
388 let RemoteRefSymbol { name, remote } = symbol;
389 let name = name.as_str();
390 let remote = remote.as_str();
391 (remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
392 .then(|| format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}").into())
393}
394
395#[derive(Debug, Error)]
396#[error("The repo is not backed by a Git repo")]
397pub struct UnexpectedGitBackendError;
398
399pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
401 store.backend_impl().ok_or(UnexpectedGitBackendError)
402}
403
404pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
406 get_git_backend(store).map(|backend| backend.git_repo())
407}
408
409fn resolve_git_ref_to_commit_id(
414 git_ref: &gix::Reference,
415 known_commit_oid: Option<&gix::oid>,
416) -> Option<gix::ObjectId> {
417 let mut peeling_ref = Cow::Borrowed(git_ref);
418
419 if let Some(known_oid) = known_commit_oid {
421 let raw_ref = &git_ref.inner;
422 if let Some(oid) = raw_ref.target.try_id()
423 && oid == known_oid
424 {
425 return Some(oid.to_owned());
426 }
427 if let Some(oid) = raw_ref.peeled
428 && oid == known_oid
429 {
430 return Some(oid);
433 }
434 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
438 let maybe_tag = git_ref
439 .try_id()
440 .and_then(|id| id.object().ok())
441 .and_then(|object| object.try_into_tag().ok());
442 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
443 let oid = oid.detach();
444 if oid == known_oid {
445 return Some(oid);
447 }
448 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
451 }
452 }
453 }
454
455 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
459 let is_commit = peeled_id
460 .object()
461 .is_ok_and(|object| object.kind.is_commit());
462 is_commit.then_some(peeled_id.detach())
463}
464
465#[derive(Error, Debug)]
466pub enum GitImportError {
467 #[error("Failed to read Git HEAD target commit {id}")]
468 MissingHeadTarget {
469 id: CommitId,
470 #[source]
471 err: BackendError,
472 },
473 #[error("Ancestor of Git ref {symbol} is missing")]
474 MissingRefAncestor {
475 symbol: RemoteRefSymbolBuf,
476 #[source]
477 err: BackendError,
478 },
479 #[error(transparent)]
480 Backend(#[from] BackendError),
481 #[error(transparent)]
482 Index(#[from] IndexError),
483 #[error(transparent)]
484 Git(Box<dyn std::error::Error + Send + Sync>),
485 #[error(transparent)]
486 UnexpectedBackend(#[from] UnexpectedGitBackendError),
487}
488
489impl GitImportError {
490 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
491 Self::Git(source.into())
492 }
493}
494
495#[derive(Debug)]
497pub struct GitImportOptions {
498 pub auto_local_bookmark: bool,
500 pub abandon_unreachable_commits: bool,
502 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
504}
505
506#[derive(Clone, Debug, Eq, PartialEq, Default)]
508pub struct GitImportStats {
509 pub abandoned_commits: Vec<CommitId>,
511 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
514 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
517 pub failed_ref_names: Vec<BString>,
522}
523
524#[derive(Debug)]
525struct RefsToImport {
526 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
529 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
532 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
535 failed_ref_names: Vec<BString>,
537}
538
539pub async fn import_refs(
544 mut_repo: &mut MutableRepo,
545 options: &GitImportOptions,
546) -> Result<GitImportStats, GitImportError> {
547 import_some_refs(mut_repo, options, |_, _| true).await
548}
549
550pub async fn import_some_refs(
555 mut_repo: &mut MutableRepo,
556 options: &GitImportOptions,
557 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
558) -> Result<GitImportStats, GitImportError> {
559 let git_repo = get_git_repo(mut_repo.store())?;
560
561 for remote_name in iter_remote_names(&git_repo) {
565 mut_repo.ensure_remote(&remote_name);
566 }
567
568 let all_remote_tags = false;
570 let refs_to_import =
571 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
572 import_refs_inner(mut_repo, refs_to_import, options).await
573}
574
575async fn import_refs_inner(
576 mut_repo: &mut MutableRepo,
577 refs_to_import: RefsToImport,
578 options: &GitImportOptions,
579) -> Result<GitImportStats, GitImportError> {
580 let store = mut_repo.store();
581 let git_backend = get_git_backend(store).expect("backend type should have been tested");
582
583 let RefsToImport {
584 changed_git_refs,
585 changed_remote_bookmarks,
586 changed_remote_tags,
587 failed_ref_names,
588 } = refs_to_import;
589
590 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
596 let index = mut_repo.index();
597 let missing_head_ids: Vec<&CommitId> = iter_changed_refs()
598 .flat_map(|(_, (_, new_target))| new_target.added_ids())
599 .filter_map(|id| match index.has_id(id) {
600 Ok(false) => Some(Ok(id)),
601 Ok(true) => None,
602 Err(e) => Some(Err(e)),
603 })
604 .try_collect()?;
605 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
606
607 let mut head_commits = Vec::new();
609 let get_commit = async |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
610 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
611 symbol: symbol.clone(),
612 err,
613 };
614 if !heads_imported && !index.has_id(id).map_err(GitImportError::Index)? {
616 git_backend
617 .import_head_commits([id])
618 .map_err(missing_ref_err)?;
619 }
620 store.get_commit_async(id).await.map_err(missing_ref_err)
621 };
622 for (symbol, (_, new_target)) in iter_changed_refs() {
623 for id in new_target.added_ids() {
624 let commit = get_commit(id, symbol).await?;
625 head_commits.push(commit);
626 }
627 }
628 mut_repo
631 .add_heads(&head_commits)
632 .await
633 .map_err(GitImportError::Backend)?;
634
635 for (full_name, new_target) in changed_git_refs {
637 mut_repo.set_git_ref_target(&full_name, new_target);
638 }
639 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
640 let symbol = symbol.as_ref();
641 let base_target = old_remote_ref.tracked_target();
642 let new_remote_ref = RemoteRef {
643 target: new_target.clone(),
644 state: if old_remote_ref != RemoteRef::absent_ref() {
645 old_remote_ref.state
646 } else {
647 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
648 },
649 };
650 if new_remote_ref.is_tracked() {
651 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
652 }
653 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
656 }
657 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
658 let symbol = symbol.as_ref();
659 let base_target = old_remote_ref.tracked_target();
660 let new_remote_ref = RemoteRef {
661 target: new_target.clone(),
662 state: if old_remote_ref != RemoteRef::absent_ref() {
663 old_remote_ref.state
664 } else {
665 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
666 },
667 };
668 if new_remote_ref.is_tracked() {
669 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
670 }
671 mut_repo.set_remote_tag(symbol, new_remote_ref);
674 }
675
676 let abandoned_commits = if options.abandon_unreachable_commits {
677 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
678 .await
679 .map_err(GitImportError::Backend)?
680 } else {
681 vec![]
682 };
683 let stats = GitImportStats {
684 abandoned_commits,
685 changed_remote_bookmarks,
686 changed_remote_tags,
687 failed_ref_names,
688 };
689 Ok(stats)
690}
691
692async fn abandon_unreachable_commits(
695 mut_repo: &mut MutableRepo,
696 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
697 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
698) -> BackendResult<Vec<CommitId>> {
699 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
700 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
701 .cloned()
702 .collect_vec();
703 if hidable_git_heads.is_empty() {
704 return Ok(vec![]);
705 }
706 let pinned_expression = RevsetExpression::union_all(&[
707 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
709 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
710 .intersection(&RevsetExpression::visible_heads().ancestors()),
712 RevsetExpression::root(),
713 ]);
714 let abandoned_expression = pinned_expression
715 .range(&RevsetExpression::commits(hidable_git_heads))
716 .intersection(&RevsetExpression::visible_heads().ancestors());
718 let abandoned_commit_ids: Vec<_> = abandoned_expression
719 .evaluate(mut_repo)
720 .map_err(|err| err.into_backend_error())?
721 .stream()
722 .try_collect()
723 .await
724 .map_err(|err| err.into_backend_error())?;
725 for id in &abandoned_commit_ids {
726 let commit = mut_repo.store().get_commit_async(id).await?;
727 mut_repo.record_abandoned_commit(&commit);
728 }
729 Ok(abandoned_commit_ids)
730}
731
732fn diff_refs_to_import(
734 view: &View,
735 git_repo: &gix::Repository,
736 all_remote_tags: bool,
737 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
738) -> Result<RefsToImport, GitImportError> {
739 let mut known_git_refs = view
740 .git_refs()
741 .iter()
742 .filter_map(|(full_name, target)| {
743 let (kind, symbol) =
745 parse_git_ref(full_name).expect("stored git ref should be parsable");
746 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
747 })
748 .collect();
749 let mut known_remote_bookmarks = view
750 .all_remote_bookmarks()
751 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
752 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
753 .collect();
754 let mut known_remote_tags = if all_remote_tags {
755 view.all_remote_tags()
756 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
757 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
758 .collect()
759 } else {
760 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
761 view.remote_tags(remote)
762 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
763 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
764 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
765 .collect()
766 };
767
768 let mut changed_git_refs = Vec::new();
773 let mut changed_remote_bookmarks = Vec::new();
774 let mut changed_remote_tags = Vec::new();
775 let mut failed_ref_names = Vec::new();
776 let actual = git_repo.references().map_err(GitImportError::from_git)?;
777 collect_changed_refs_to_import(
778 actual.local_branches().map_err(GitImportError::from_git)?,
779 &mut known_git_refs,
780 &mut known_remote_bookmarks,
781 &mut changed_git_refs,
782 &mut changed_remote_bookmarks,
783 &mut failed_ref_names,
784 &git_ref_filter,
785 )?;
786 collect_changed_refs_to_import(
787 actual.remote_branches().map_err(GitImportError::from_git)?,
788 &mut known_git_refs,
789 &mut known_remote_bookmarks,
790 &mut changed_git_refs,
791 &mut changed_remote_bookmarks,
792 &mut failed_ref_names,
793 &git_ref_filter,
794 )?;
795 collect_changed_refs_to_import(
796 actual.tags().map_err(GitImportError::from_git)?,
797 &mut known_git_refs,
798 &mut known_remote_tags,
799 &mut changed_git_refs,
800 &mut changed_remote_tags,
801 &mut failed_ref_names,
802 &git_ref_filter,
803 )?;
804 if all_remote_tags {
805 collect_changed_remote_tags_to_import(
806 actual
807 .prefixed(REMOTE_TAG_REF_NAMESPACE)
808 .map_err(GitImportError::from_git)?,
809 &mut known_remote_tags,
810 &mut changed_remote_tags,
811 &mut failed_ref_names,
812 &git_ref_filter,
813 )?;
814 }
815 for full_name in known_git_refs.into_keys() {
816 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
817 }
818 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
819 if old.is_present() {
820 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
821 }
822 }
823 for (RemoteRefKey(symbol), old) in known_remote_tags {
824 if old.is_present() {
825 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
826 }
827 }
828
829 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
831 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
832 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
833 failed_ref_names.sort_unstable();
834 Ok(RefsToImport {
835 changed_git_refs,
836 changed_remote_bookmarks,
837 changed_remote_tags,
838 failed_ref_names,
839 })
840}
841
842fn collect_changed_refs_to_import(
843 actual_git_refs: gix::reference::iter::Iter,
844 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
845 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
846 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
847 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
848 failed_ref_names: &mut Vec<BString>,
849 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
850) -> Result<(), GitImportError> {
851 for git_ref in actual_git_refs {
852 let git_ref = git_ref.map_err(GitImportError::from_git)?;
853 let full_name_bytes = git_ref.name().as_bstr();
854 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
855 failed_ref_names.push(full_name_bytes.to_owned());
857 continue;
858 };
859 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
860 failed_ref_names.push(full_name_bytes.to_owned());
861 continue;
862 }
863 let full_name = GitRefName::new(full_name);
864 let Some((kind, symbol)) = parse_git_ref(full_name) else {
865 continue;
867 };
868 if !git_ref_filter(kind, symbol) {
869 continue;
870 }
871 let old_git_target = known_git_refs.get(full_name).copied().flatten();
872 let old_git_oid = old_git_target
873 .as_normal()
874 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
875 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
876 continue;
878 };
879 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
880 known_git_refs.remove(full_name);
881 if new_target != *old_git_target {
882 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
883 }
884 let old_remote_ref = known_remote_refs
887 .remove(&symbol)
888 .unwrap_or_else(|| RemoteRef::absent_ref());
889 if new_target != old_remote_ref.target {
890 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
891 }
892 }
893 Ok(())
894}
895
896fn collect_changed_remote_tags_to_import(
899 actual_git_refs: gix::reference::iter::Iter,
900 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
901 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
902 failed_ref_names: &mut Vec<BString>,
903 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
904) -> Result<(), GitImportError> {
905 for git_ref in actual_git_refs {
906 let git_ref = git_ref.map_err(GitImportError::from_git)?;
907 let full_name_bytes = git_ref.name().as_bstr();
908 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
909 failed_ref_names.push(full_name_bytes.to_owned());
911 continue;
912 };
913 let full_name = GitRefName::new(full_name);
914 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
915 continue;
917 };
918 if !git_ref_filter(kind, symbol) {
919 continue;
920 }
921 let old_remote_ref = known_remote_refs
922 .get(&symbol)
923 .copied()
924 .unwrap_or_else(|| RemoteRef::absent_ref());
925 let old_git_oid = old_remote_ref
926 .target
927 .as_normal()
928 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
929 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
930 continue;
932 };
933 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
934 known_remote_refs.remove(&symbol);
935 if new_target != old_remote_ref.target {
936 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
937 }
938 }
939 Ok(())
940}
941
942fn default_remote_ref_state_for(
943 kind: GitRefKind,
944 symbol: RemoteRefSymbol<'_>,
945 options: &GitImportOptions,
946) -> RemoteRefState {
947 match kind {
948 GitRefKind::Bookmark => {
949 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
950 || options.auto_local_bookmark
951 || options
952 .remote_auto_track_bookmarks
953 .get(symbol.remote)
954 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
955 {
956 RemoteRefState::Tracked
957 } else {
958 RemoteRefState::New
959 }
960 }
961 GitRefKind::Tag => RemoteRefState::Tracked,
963 }
964}
965
966fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
972 itertools::chain(view.local_bookmarks(), view.local_tags())
973 .flat_map(|(_, target)| target.added_ids())
974 .cloned()
975 .collect()
976}
977
978fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
985 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
986 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
987 .map(|(_, remote_ref)| &remote_ref.target)
988 .flat_map(|target| target.added_ids())
989 .cloned()
990 .collect()
991}
992
993pub async fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
1001 let store = mut_repo.store();
1002 let git_backend = get_git_backend(store)?;
1003 let git_repo = git_backend.git_repo();
1004
1005 let old_git_head = mut_repo.view().git_head();
1006 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1007 Some(CommitId::from_bytes(oid.as_bytes()))
1008 } else {
1009 None
1010 };
1011 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1012 return Ok(());
1013 }
1014
1015 if let Some(head_id) = &new_git_head_id {
1017 let index = mut_repo.index();
1018 if !index.has_id(head_id)? {
1019 git_backend.import_head_commits([head_id]).map_err(|err| {
1020 GitImportError::MissingHeadTarget {
1021 id: head_id.clone(),
1022 err,
1023 }
1024 })?;
1025 }
1026 let commit = store
1029 .get_commit_async(head_id)
1030 .await
1031 .map_err(GitImportError::Backend)?;
1032 mut_repo
1033 .add_head(&commit)
1034 .await
1035 .map_err(GitImportError::Backend)?;
1036 }
1037
1038 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1039 Ok(())
1040}
1041
1042#[derive(Error, Debug)]
1043pub enum GitExportError {
1044 #[error(transparent)]
1045 Git(Box<dyn std::error::Error + Send + Sync>),
1046 #[error(transparent)]
1047 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1048}
1049
1050impl GitExportError {
1051 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1052 Self::Git(source.into())
1053 }
1054}
1055
1056#[derive(Debug, Error)]
1058pub enum FailedRefExportReason {
1059 #[error("Name is not allowed in Git")]
1061 InvalidGitName,
1062 #[error("Ref was in a conflicted state from the last import")]
1065 ConflictedOldState,
1066 #[error("Ref cannot point to the root commit in Git")]
1068 OnRootCommit,
1069 #[error("Deleted ref had been modified in Git")]
1071 DeletedInJjModifiedInGit,
1072 #[error("Added ref had been added with a different target in Git")]
1074 AddedInJjAddedInGit,
1075 #[error("Modified ref had been deleted in Git")]
1077 ModifiedInJjDeletedInGit,
1078 #[error("Failed to delete")]
1080 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1081 #[error("Failed to set")]
1083 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1084}
1085
1086#[derive(Debug)]
1088pub struct GitExportStats {
1089 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1091 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1095}
1096
1097#[derive(Debug)]
1098struct AllRefsToExport {
1099 bookmarks: RefsToExport,
1100 tags: RefsToExport,
1101}
1102
1103#[derive(Debug)]
1104struct RefsToExport {
1105 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1107 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1112 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1114}
1115
1116pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1125 export_some_refs(mut_repo, |_, _| true)
1126}
1127
1128pub fn export_some_refs(
1129 mut_repo: &mut MutableRepo,
1130 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1131) -> Result<GitExportStats, GitExportError> {
1132 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1133 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1134 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1135 let (_, value) = &map[index];
1136 Some(value)
1137 }
1138
1139 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1140 mut_repo.view(),
1141 mut_repo.store().root_commit_id(),
1142 &git_ref_filter,
1143 );
1144
1145 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1146 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1147 return Ok(());
1148 };
1149 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1150 if let Some((kind, symbol)) = target_name
1151 .as_ref()
1152 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1153 .and_then(|name| parse_git_ref(name.as_ref()))
1154 {
1155 let old_target = head_ref.inner.target.clone();
1156 let current_oid = match head_ref.into_fully_peeled_id() {
1157 Ok(id) => Some(id.detach()),
1158 Err(gix::reference::peel::Error::ToId(
1159 gix::refs::peel::to_id::Error::FollowToObject(
1160 gix::refs::peel::to_object::Error::Follow(
1161 gix::refs::file::find::existing::Error::NotFound { .. },
1162 ),
1163 ),
1164 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1166 };
1167 let refs = match kind {
1168 GitRefKind::Bookmark => &bookmarks,
1169 GitRefKind::Tag => &tags,
1170 };
1171 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1172 Some(new_oid)
1173 } else if get(&refs.to_delete, symbol).is_some() {
1174 None
1175 } else {
1176 current_oid.as_ref()
1177 };
1178 if new_oid != current_oid.as_ref() {
1179 update_git_head(
1180 git_repo,
1181 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1182 current_oid,
1183 )
1184 .map_err(GitExportError::from_git)?;
1185 }
1186 }
1187 Ok(())
1188 };
1189
1190 let git_repo = get_git_repo(mut_repo.store())?;
1191
1192 check_and_detach_head(&git_repo)?;
1193 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1194 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1195 check_and_detach_head(&worktree_repo)?;
1196 }
1197 }
1198
1199 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1200 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1201
1202 copy_exportable_local_bookmarks_to_remote_view(
1203 mut_repo,
1204 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1205 |name| {
1206 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1207 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1208 },
1209 );
1210 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1211 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1212 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1213 });
1214
1215 Ok(GitExportStats {
1216 failed_bookmarks,
1217 failed_tags,
1218 })
1219}
1220
1221fn export_refs_to_git(
1222 mut_repo: &mut MutableRepo,
1223 git_repo: &gix::Repository,
1224 kind: GitRefKind,
1225 refs: RefsToExport,
1226) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1227 let mut failed = refs.failed;
1228 for (symbol, old_oid) in refs.to_delete {
1229 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1230 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1231 continue;
1232 };
1233 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1234 failed.push((symbol, reason));
1235 } else {
1236 let new_target = RefTarget::absent();
1237 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1238 }
1239 }
1240 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1241 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1242 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1243 continue;
1244 };
1245 let new_ref_oid = match kind {
1246 GitRefKind::Bookmark => None,
1247 GitRefKind::Tag => {
1249 find_git_tag_oid_to_copy(mut_repo.view(), git_repo, &symbol.name, &new_commit_oid)
1250 }
1251 };
1252 if let Err(reason) = update_git_ref(
1253 git_repo,
1254 &git_ref_name,
1255 old_commit_oid,
1256 new_commit_oid,
1257 new_ref_oid,
1258 ) {
1259 failed.push((symbol, reason));
1260 } else {
1261 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1262 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1263 }
1264 }
1265
1266 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1268 failed
1269}
1270
1271fn copy_exportable_local_bookmarks_to_remote_view(
1272 mut_repo: &mut MutableRepo,
1273 remote: &RemoteName,
1274 name_filter: impl Fn(&RefName) -> bool,
1275) {
1276 let new_local_bookmarks = mut_repo
1277 .view()
1278 .local_remote_bookmarks(remote)
1279 .filter_map(|(name, targets)| {
1280 let old_target = &targets.remote_ref.target;
1283 let new_target = targets.local_target;
1284 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1285 })
1286 .filter(|&(name, _)| name_filter(name))
1287 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1288 .collect_vec();
1289 for (name, new_target) in new_local_bookmarks {
1290 let new_remote_ref = RemoteRef {
1291 target: new_target,
1292 state: RemoteRefState::Tracked,
1293 };
1294 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1295 }
1296}
1297
1298fn copy_exportable_local_tags_to_remote_view(
1299 mut_repo: &mut MutableRepo,
1300 remote: &RemoteName,
1301 name_filter: impl Fn(&RefName) -> bool,
1302) {
1303 let new_local_tags = mut_repo
1304 .view()
1305 .local_remote_tags(remote)
1306 .filter_map(|(name, targets)| {
1307 let old_target = &targets.remote_ref.target;
1309 let new_target = targets.local_target;
1310 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1311 })
1312 .filter(|&(name, _)| name_filter(name))
1313 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1314 .collect_vec();
1315 for (name, new_target) in new_local_tags {
1316 let new_remote_ref = RemoteRef {
1317 target: new_target,
1318 state: RemoteRefState::Tracked,
1319 };
1320 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1321 }
1322}
1323
1324fn diff_refs_to_export(
1326 view: &View,
1327 root_commit_id: &CommitId,
1328 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1329) -> AllRefsToExport {
1330 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1333 itertools::chain(
1334 view.local_bookmarks().map(|(name, target)| {
1335 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1336 (symbol, target)
1337 }),
1338 view.all_remote_bookmarks()
1339 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1340 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1341 )
1342 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1343 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1344 .collect();
1345 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1347 .local_tags()
1348 .map(|(name, target)| {
1349 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1350 (symbol, target)
1351 })
1352 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1353 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1354 .collect();
1355 let known_git_refs = view
1356 .git_refs()
1357 .iter()
1358 .map(|(full_name, target)| {
1359 let (kind, symbol) =
1360 parse_git_ref(full_name).expect("stored git ref should be parsable");
1361 ((kind, symbol), target)
1362 })
1363 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1367 for ((kind, symbol), target) in known_git_refs {
1368 let ref_targets = match kind {
1369 GitRefKind::Bookmark => &mut all_bookmark_targets,
1370 GitRefKind::Tag => &mut all_tag_targets,
1371 };
1372 ref_targets
1373 .entry(symbol)
1374 .and_modify(|(old_target, _)| *old_target = target)
1375 .or_insert((target, RefTarget::absent_ref()));
1376 }
1377
1378 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1379 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1380 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1381 AllRefsToExport { bookmarks, tags }
1382}
1383
1384fn collect_changed_refs_to_export(
1385 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1386 root_commit_target: &RefTarget,
1387) -> RefsToExport {
1388 let mut to_update = Vec::new();
1389 let mut to_delete = Vec::new();
1390 let mut failed = Vec::new();
1391 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1392 if new_target == old_target {
1393 continue;
1394 }
1395 if new_target == root_commit_target {
1396 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1398 continue;
1399 }
1400 let old_oid = if let Some(id) = old_target.as_normal() {
1401 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1402 } else if old_target.has_conflict() {
1403 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1406 continue;
1407 } else {
1408 assert!(old_target.is_absent());
1409 None
1410 };
1411 if let Some(id) = new_target.as_normal() {
1412 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1413 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1414 } else if new_target.has_conflict() {
1415 continue;
1417 } else {
1418 assert!(new_target.is_absent());
1419 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1420 }
1421 }
1422
1423 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1425 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1426 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1427 RefsToExport {
1428 to_update,
1429 to_delete,
1430 failed,
1431 }
1432}
1433
1434fn find_git_tag_oid_to_copy(
1437 view: &View,
1438 git_repo: &gix::Repository,
1439 name: &RefName,
1440 commit_oid: &gix::oid,
1441) -> Option<gix::ObjectId> {
1442 view.remote_tags_matching(&StringMatcher::exact(name), &StringMatcher::all())
1444 .filter(|(_, remote_ref)| {
1445 let maybe_id = remote_ref.tracked_target().as_normal();
1446 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1447 })
1448 .filter_map(|(symbol, _)| {
1450 let git_ref_name = to_remote_tag_ref_name(symbol)?;
1451 git_repo.find_reference(git_ref_name.as_str()).ok()
1452 })
1453 .filter(|git_ref| {
1456 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1457 })
1458 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1459}
1460
1461fn delete_git_ref(
1462 git_repo: &gix::Repository,
1463 git_ref_name: &GitRefName,
1464 old_oid: &gix::oid,
1465) -> Result<(), FailedRefExportReason> {
1466 let Some(git_ref) = git_repo
1467 .try_find_reference(git_ref_name.as_str())
1468 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1469 else {
1470 return Ok(());
1472 };
1473 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1474 git_ref
1476 .delete()
1477 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1478 } else {
1479 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1481 }
1482}
1483
1484fn create_git_ref(
1486 git_repo: &gix::Repository,
1487 git_ref_name: &GitRefName,
1488 new_commit_oid: gix::ObjectId,
1489 new_ref_oid: Option<gix::ObjectId>,
1490) -> Result<(), FailedRefExportReason> {
1491 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1492 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1493 let Err(set_err) =
1494 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1495 else {
1496 return Ok(());
1498 };
1499 let Some(git_ref) = git_repo
1500 .try_find_reference(git_ref_name.as_str())
1501 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1502 else {
1503 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1504 };
1505 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1508 Ok(())
1509 } else {
1510 Err(FailedRefExportReason::AddedInJjAddedInGit)
1511 }
1512}
1513
1514fn move_git_ref(
1516 git_repo: &gix::Repository,
1517 git_ref_name: &GitRefName,
1518 old_commit_oid: gix::ObjectId,
1519 new_commit_oid: gix::ObjectId,
1520 new_ref_oid: Option<gix::ObjectId>,
1521) -> Result<(), FailedRefExportReason> {
1522 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1523 let constraint =
1524 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1525 let Err(set_err) =
1526 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1527 else {
1528 return Ok(());
1530 };
1531 let Some(git_ref) = git_repo
1533 .try_find_reference(git_ref_name.as_str())
1534 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1535 else {
1536 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1538 };
1539 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1541 if git_commit_oid == Some(new_commit_oid) {
1542 Ok(())
1543 } else if git_commit_oid == Some(old_commit_oid) {
1544 let constraint =
1546 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1547 git_repo
1548 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1549 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1550 Ok(())
1551 } else {
1552 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1553 }
1554}
1555
1556fn update_git_ref(
1557 git_repo: &gix::Repository,
1558 git_ref_name: &GitRefName,
1559 old_commit_oid: Option<gix::ObjectId>,
1560 new_commit_oid: gix::ObjectId,
1561 new_ref_oid: Option<gix::ObjectId>,
1562) -> Result<(), FailedRefExportReason> {
1563 match old_commit_oid {
1564 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1565 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1566 }
1567}
1568
1569fn update_git_head(
1572 git_repo: &gix::Repository,
1573 expected_ref: gix::refs::transaction::PreviousValue,
1574 new_oid: Option<gix::ObjectId>,
1575) -> Result<(), gix::reference::edit::Error> {
1576 let mut ref_edits = Vec::new();
1577 let new_target = if let Some(oid) = new_oid {
1578 gix::refs::Target::Object(oid)
1579 } else {
1580 ref_edits.push(gix::refs::transaction::RefEdit {
1585 change: gix::refs::transaction::Change::Delete {
1586 expected: gix::refs::transaction::PreviousValue::Any,
1587 log: gix::refs::transaction::RefLog::AndReference,
1588 },
1589 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1590 deref: false,
1591 });
1592 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1593 };
1594 ref_edits.push(gix::refs::transaction::RefEdit {
1595 change: gix::refs::transaction::Change::Update {
1596 log: gix::refs::transaction::LogChange {
1597 message: "export from jj".into(),
1598 ..Default::default()
1599 },
1600 expected: expected_ref,
1601 new: new_target,
1602 },
1603 name: "HEAD".try_into().unwrap(),
1604 deref: false,
1605 });
1606 git_repo.edit_references(ref_edits)?;
1607 Ok(())
1608}
1609
1610#[derive(Debug, Error)]
1611pub enum GitResetHeadError {
1612 #[error(transparent)]
1613 Backend(#[from] BackendError),
1614 #[error(transparent)]
1615 Git(Box<dyn std::error::Error + Send + Sync>),
1616 #[error("Failed to update Git HEAD ref")]
1617 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1618 #[error(transparent)]
1619 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1620}
1621
1622impl GitResetHeadError {
1623 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1624 Self::Git(source.into())
1625 }
1626}
1627
1628pub async fn reset_head(
1631 mut_repo: &mut MutableRepo,
1632 wc_commit: &Commit,
1633) -> Result<(), GitResetHeadError> {
1634 let git_repo = get_git_repo(mut_repo.store())?;
1635
1636 let first_parent_id = &wc_commit.parent_ids()[0];
1637 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1638 RefTarget::normal(first_parent_id.clone())
1639 } else {
1640 RefTarget::absent()
1641 };
1642
1643 let old_head_target = mut_repo.git_head();
1645 if old_head_target != new_head_target {
1646 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1647 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1650 if actual_head.is_detached() {
1651 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1652 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1653 } else {
1654 gix::refs::transaction::PreviousValue::MustExist
1657 }
1658 } else {
1659 gix::refs::transaction::PreviousValue::MustExist
1661 };
1662 let new_oid = new_head_target
1663 .as_normal()
1664 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1665 update_git_head(&git_repo, expected_ref, new_oid)
1666 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1667 mut_repo.set_git_head_target(new_head_target);
1668 }
1669
1670 if git_repo.state().is_some() {
1673 clear_operation_state(&git_repo)?;
1674 }
1675
1676 reset_index(mut_repo, &git_repo, wc_commit).await
1677}
1678
1679fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1681 const STATE_FILE_NAMES: &[&str] = &[
1685 "MERGE_HEAD",
1686 "MERGE_MODE",
1687 "MERGE_MSG",
1688 "REVERT_HEAD",
1689 "CHERRY_PICK_HEAD",
1690 "BISECT_LOG",
1691 ];
1692 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1693 let handle_err = |err: PathError| match err.source.kind() {
1694 std::io::ErrorKind::NotFound => Ok(()),
1695 _ => Err(GitResetHeadError::from_git(err)),
1696 };
1697 for file_name in STATE_FILE_NAMES {
1698 let path = git_repo.path().join(file_name);
1699 std::fs::remove_file(&path)
1700 .context(&path)
1701 .or_else(handle_err)?;
1702 }
1703 for dir_name in STATE_DIR_NAMES {
1704 let path = git_repo.path().join(dir_name);
1705 std::fs::remove_dir_all(&path)
1706 .context(&path)
1707 .or_else(handle_err)?;
1708 }
1709 Ok(())
1710}
1711
1712async fn reset_index(
1713 repo: &dyn Repo,
1714 git_repo: &gix::Repository,
1715 wc_commit: &Commit,
1716) -> Result<(), GitResetHeadError> {
1717 let parent_tree = wc_commit.parent_tree(repo).await?;
1718 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1722 if tree_id == repo.store().empty_tree_id() {
1723 gix::index::File::from_state(
1727 gix::index::State::new(git_repo.object_hash()),
1728 git_repo.index_path(),
1729 )
1730 } else {
1731 git_repo
1734 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1735 .map_err(GitResetHeadError::from_git)?
1736 }
1737 } else {
1738 build_index_from_merged_tree(git_repo, &parent_tree)?
1739 };
1740
1741 let wc_tree = wc_commit.tree();
1742 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).await?;
1743
1744 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1747 index
1748 .entries_mut_with_paths()
1749 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1750 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1751 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1752 })
1753 .filter_map(|merged| merged.both())
1754 .map(|((entry, _), old_entry)| (entry, old_entry))
1755 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1756 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1757 }
1758
1759 debug_assert!(index.verify_entries().is_ok());
1760
1761 index
1762 .write(gix::index::write::Options::default())
1763 .map_err(GitResetHeadError::from_git)
1764}
1765
1766fn build_index_from_merged_tree(
1767 git_repo: &gix::Repository,
1768 merged_tree: &MergedTree,
1769) -> Result<gix::index::File, GitResetHeadError> {
1770 let mut index = gix::index::File::from_state(
1771 gix::index::State::new(git_repo.object_hash()),
1772 git_repo.index_path(),
1773 );
1774
1775 let mut push_index_entry =
1776 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1777 let Some(entry) = maybe_entry else {
1778 return;
1779 };
1780
1781 let (id, mode) = match entry {
1782 TreeValue::File {
1783 id,
1784 executable,
1785 copy_id: _,
1786 } => {
1787 if *executable {
1788 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1789 } else {
1790 (id.as_bytes(), gix::index::entry::Mode::FILE)
1791 }
1792 }
1793 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1794 TreeValue::Tree(_) => {
1795 return;
1800 }
1801 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1802 };
1803
1804 let path = BStr::new(path.as_internal_file_string());
1805
1806 index.dangerously_push_entry(
1809 gix::index::entry::Stat::default(),
1810 gix::ObjectId::from_bytes_or_panic(id),
1811 gix::index::entry::Flags::from_stage(stage),
1812 mode,
1813 path,
1814 );
1815 };
1816
1817 let mut has_many_sided_conflict = false;
1818
1819 for (path, entry) in merged_tree.entries() {
1820 let entry = entry?;
1821 if let Some(resolved) = entry.as_resolved() {
1822 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1823 continue;
1824 }
1825
1826 let conflict = entry.simplify();
1827 if let [left, base, right] = conflict.as_slice() {
1828 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1830 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1831 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1832 } else {
1833 has_many_sided_conflict = true;
1841 push_index_entry(
1842 &path,
1843 conflict.first(),
1844 gix::index::entry::Stage::Unconflicted,
1845 );
1846 }
1847 }
1848
1849 index.sort_entries();
1852
1853 if has_many_sided_conflict
1856 && index
1857 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1858 .is_err()
1859 {
1860 let file_blob = git_repo
1861 .write_blob(
1862 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1863 )
1864 .map_err(GitResetHeadError::from_git)?;
1865 index.dangerously_push_entry(
1866 gix::index::entry::Stat::default(),
1867 file_blob.detach(),
1868 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1869 gix::index::entry::Mode::FILE,
1870 INDEX_DUMMY_CONFLICT_FILE.into(),
1871 );
1872 index.sort_entries();
1875 }
1876
1877 Ok(index)
1878}
1879
1880pub async fn update_intent_to_add(
1887 repo: &dyn Repo,
1888 old_tree: &MergedTree,
1889 new_tree: &MergedTree,
1890) -> Result<(), GitResetHeadError> {
1891 let git_repo = get_git_repo(repo.store())?;
1892 let mut index = git_repo
1893 .index_or_empty()
1894 .map_err(GitResetHeadError::from_git)?;
1895 let mut_index = Arc::make_mut(&mut index);
1896 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).await?;
1897 debug_assert!(mut_index.verify_entries().is_ok());
1898 mut_index
1899 .write(gix::index::write::Options::default())
1900 .map_err(GitResetHeadError::from_git)?;
1901
1902 Ok(())
1903}
1904
1905async fn update_intent_to_add_impl(
1906 git_repo: &gix::Repository,
1907 index: &mut gix::index::File,
1908 old_tree: &MergedTree,
1909 new_tree: &MergedTree,
1910) -> Result<(), GitResetHeadError> {
1911 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1912 let mut added_paths = vec![];
1913 let mut removed_paths = HashSet::new();
1914 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1915 let values = values?;
1916 if values.before.is_absent() {
1917 let executable = match values.after.as_normal() {
1918 Some(TreeValue::File {
1919 id: _,
1920 executable,
1921 copy_id: _,
1922 }) => *executable,
1923 Some(TreeValue::Symlink(_)) => false,
1924 _ => {
1925 continue;
1926 }
1927 };
1928 if index
1929 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1930 .is_err()
1931 {
1932 added_paths.push((BString::from(path.into_internal_string()), executable));
1933 }
1934 } else if values.after.is_absent() {
1935 removed_paths.insert(BString::from(path.into_internal_string()));
1936 }
1937 }
1938
1939 if added_paths.is_empty() && removed_paths.is_empty() {
1940 return Ok(());
1941 }
1942
1943 if !added_paths.is_empty() {
1944 let empty_blob = git_repo
1946 .write_blob(b"")
1947 .map_err(GitResetHeadError::from_git)?
1948 .detach();
1949 for (path, executable) in added_paths {
1950 index.dangerously_push_entry(
1952 gix::index::entry::Stat::default(),
1953 empty_blob,
1954 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1955 if executable {
1956 gix::index::entry::Mode::FILE_EXECUTABLE
1957 } else {
1958 gix::index::entry::Mode::FILE
1959 },
1960 path.as_ref(),
1961 );
1962 }
1963 }
1964 if !removed_paths.is_empty() {
1965 index.remove_entries(|_size, path, entry| {
1966 entry
1967 .flags
1968 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1969 && removed_paths.contains(path)
1970 });
1971 }
1972
1973 index.sort_entries();
1974
1975 Ok(())
1976}
1977
1978#[derive(Debug, Error)]
1979pub enum GitRemoteManagementError {
1980 #[error("No git remote named '{}'", .0.as_symbol())]
1981 NoSuchRemote(RemoteNameBuf),
1982 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1983 RemoteAlreadyExists(RemoteNameBuf),
1984 #[error(transparent)]
1985 RemoteName(#[from] GitRemoteNameError),
1986 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1987 NonstandardConfiguration(RemoteNameBuf),
1988 #[error("Error saving Git configuration")]
1989 GitConfigSaveError(#[source] std::io::Error),
1990 #[error("Unexpected Git error when managing remotes")]
1991 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1992 #[error(transparent)]
1993 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1994 #[error(transparent)]
1995 RefExpansionError(#[from] GitRefExpansionError),
1996}
1997
1998impl GitRemoteManagementError {
1999 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
2000 Self::InternalGitError(source.into())
2001 }
2002}
2003
2004fn default_fetch_refspec(remote: &RemoteName) -> String {
2005 format!(
2006 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
2007 remote = remote.as_str()
2008 )
2009}
2010
2011fn add_ref(
2012 name: gix::refs::FullName,
2013 target: gix::refs::Target,
2014 message: BString,
2015) -> gix::refs::transaction::RefEdit {
2016 gix::refs::transaction::RefEdit {
2017 change: gix::refs::transaction::Change::Update {
2018 log: gix::refs::transaction::LogChange {
2019 mode: gix::refs::transaction::RefLog::AndReference,
2020 force_create_reflog: false,
2021 message,
2022 },
2023 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2024 new: target,
2025 },
2026 name,
2027 deref: false,
2028 }
2029}
2030
2031fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2032 gix::refs::transaction::RefEdit {
2033 change: gix::refs::transaction::Change::Delete {
2034 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2035 reference.target().into_owned(),
2036 ),
2037 log: gix::refs::transaction::RefLog::AndReference,
2038 },
2039 name: reference.name().to_owned(),
2040 deref: false,
2041 }
2042}
2043
2044pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2050 let mut config_file = File::create(
2051 config
2052 .meta()
2053 .path
2054 .as_ref()
2055 .expect("Git repository to have a config file"),
2056 )?;
2057 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2058}
2059
2060fn save_remote(
2061 config: &mut gix::config::File<'static>,
2062 remote_name: &RemoteName,
2063 remote: &mut gix::Remote,
2064) -> Result<(), GitRemoteManagementError> {
2065 config
2072 .new_section(
2073 "remote",
2074 Some(Cow::Owned(BString::from(remote_name.as_str()))),
2075 )
2076 .map_err(GitRemoteManagementError::from_git)?;
2077 remote
2078 .save_as_to(remote_name.as_str(), config)
2079 .map_err(GitRemoteManagementError::from_git)?;
2080 Ok(())
2081}
2082
2083fn git_config_branch_section_ids_by_remote(
2084 config: &gix::config::File,
2085 remote_name: &RemoteName,
2086) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2087 config
2088 .sections_by_name("branch")
2089 .into_iter()
2090 .flatten()
2091 .filter_map(|section| {
2092 let remote_values = section.values("remote");
2093 let push_remote_values = section.values("pushRemote");
2094 if !remote_values
2095 .iter()
2096 .chain(push_remote_values.iter())
2097 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2098 {
2099 return None;
2100 }
2101 if remote_values.len() > 1
2102 || push_remote_values.len() > 1
2103 || section.value_names().any(|name| {
2104 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
2105 })
2106 {
2107 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2108 remote_name.to_owned(),
2109 )));
2110 }
2111 Some(Ok(section.id()))
2112 })
2113 .collect()
2114}
2115
2116fn rename_remote_in_git_branch_config_sections(
2117 config: &mut gix::config::File,
2118 old_remote_name: &RemoteName,
2119 new_remote_name: &RemoteName,
2120) -> Result<(), GitRemoteManagementError> {
2121 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2122 config
2123 .section_mut_by_id(id)
2124 .expect("found section to exist")
2125 .set(
2126 "remote"
2127 .try_into()
2128 .expect("'remote' to be a valid value name"),
2129 BStr::new(new_remote_name.as_str()),
2130 );
2131 }
2132 Ok(())
2133}
2134
2135fn remove_remote_git_branch_config_sections(
2136 config: &mut gix::config::File,
2137 remote_name: &RemoteName,
2138) -> Result<(), GitRemoteManagementError> {
2139 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2140 config
2141 .remove_section_by_id(id)
2142 .expect("removed section to exist");
2143 }
2144 Ok(())
2145}
2146
2147fn remove_remote_git_config_sections(
2148 config: &mut gix::config::File,
2149 remote_name: &RemoteName,
2150) -> Result<(), GitRemoteManagementError> {
2151 let section_ids_to_remove: Vec<_> = config
2152 .sections_by_name("remote")
2153 .into_iter()
2154 .flatten()
2155 .filter(|section| {
2156 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2157 })
2158 .map(|section| {
2159 if section.value_names().any(|name| {
2160 !name.eq_ignore_ascii_case(b"url")
2161 && !name.eq_ignore_ascii_case(b"fetch")
2162 && !name.eq_ignore_ascii_case(b"tagOpt")
2163 }) {
2164 return Err(GitRemoteManagementError::NonstandardConfiguration(
2165 remote_name.to_owned(),
2166 ));
2167 }
2168 Ok(section.id())
2169 })
2170 .try_collect()?;
2171 for id in section_ids_to_remove {
2172 config
2173 .remove_section_by_id(id)
2174 .expect("removed section to exist");
2175 }
2176 Ok(())
2177}
2178
2179pub fn get_all_remote_names(
2181 store: &Store,
2182) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2183 let git_repo = get_git_repo(store)?;
2184 Ok(iter_remote_names(&git_repo).collect())
2185}
2186
2187fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2188 git_repo
2189 .remote_names()
2190 .into_iter()
2191 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2193 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2195 .map(RemoteNameBuf::from)
2196}
2197
2198pub fn add_remote(
2199 mut_repo: &mut MutableRepo,
2200 remote_name: &RemoteName,
2201 url: &str,
2202 push_url: Option<&str>,
2203 fetch_tags: gix::remote::fetch::Tags,
2204 bookmark_expr: &StringExpression,
2205) -> Result<(), GitRemoteManagementError> {
2206 let git_repo = get_git_repo(mut_repo.store())?;
2207
2208 validate_remote_name(remote_name)?;
2209
2210 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2211 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2212 remote_name.to_owned(),
2213 ));
2214 }
2215
2216 let ref_expr = GitFetchRefExpression {
2217 bookmark: bookmark_expr.clone(),
2218 tag: StringExpression::none(),
2221 };
2222 let ExpandedFetchRefSpecs {
2223 expr: _,
2224 refspecs,
2225 negative_refspecs,
2226 } = expand_fetch_refspecs(remote_name, ref_expr)?;
2227 let fetch_refspecs = itertools::chain(
2228 refspecs.iter().map(|spec| spec.to_git_format()),
2229 negative_refspecs.iter().map(|spec| spec.to_git_format()),
2230 )
2231 .map(BString::from);
2232
2233 let mut remote = git_repo
2234 .remote_at(url)
2235 .map_err(GitRemoteManagementError::from_git)?
2236 .with_fetch_tags(fetch_tags)
2237 .with_refspecs(fetch_refspecs, gix::remote::Direction::Fetch)
2238 .expect("previously-parsed refspecs to be valid");
2239
2240 if let Some(push_url) = push_url {
2241 remote = remote
2242 .with_push_url(push_url)
2243 .map_err(GitRemoteManagementError::from_git)?;
2244 }
2245
2246 let mut config = git_repo.config_snapshot().clone();
2247 save_remote(&mut config, remote_name, &mut remote)?;
2248 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2249
2250 mut_repo.ensure_remote(remote_name);
2251
2252 Ok(())
2253}
2254
2255pub fn remove_remote(
2256 mut_repo: &mut MutableRepo,
2257 remote_name: &RemoteName,
2258) -> Result<(), GitRemoteManagementError> {
2259 let mut git_repo = get_git_repo(mut_repo.store())?;
2260
2261 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2262 return Err(GitRemoteManagementError::NoSuchRemote(
2263 remote_name.to_owned(),
2264 ));
2265 }
2266
2267 let mut config = git_repo.config_snapshot().clone();
2268 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2269 remove_remote_git_config_sections(&mut config, remote_name)?;
2270 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2271
2272 remove_remote_git_refs(&mut git_repo, remote_name)
2273 .map_err(GitRemoteManagementError::from_git)?;
2274
2275 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2276 remove_remote_refs(mut_repo, remote_name);
2277 }
2278
2279 Ok(())
2280}
2281
2282fn remove_remote_git_refs(
2283 git_repo: &mut gix::Repository,
2284 remote: &RemoteName,
2285) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2286 let bookmark_prefix = format!(
2287 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2288 remote = remote.as_str()
2289 );
2290 let tag_prefix = format!(
2291 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2292 remote = remote.as_str()
2293 );
2294 let edits: Vec<_> = itertools::chain(
2295 git_repo
2296 .references()?
2297 .prefixed(bookmark_prefix.as_str())?
2298 .map_ok(remove_ref),
2299 git_repo
2300 .references()?
2301 .prefixed(tag_prefix.as_str())?
2302 .map_ok(remove_ref),
2303 )
2304 .try_collect()?;
2305 git_repo.edit_references(edits)?;
2306 Ok(())
2307}
2308
2309fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2310 mut_repo.remove_remote(remote);
2311 let prefix = format!(
2312 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2313 remote = remote.as_str()
2314 );
2315 let git_refs_to_delete = mut_repo
2316 .view()
2317 .git_refs()
2318 .keys()
2319 .filter(|&r| r.as_str().starts_with(&prefix))
2320 .cloned()
2321 .collect_vec();
2322 for git_ref in git_refs_to_delete {
2323 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2324 }
2325}
2326
2327pub fn rename_remote(
2328 mut_repo: &mut MutableRepo,
2329 old_remote_name: &RemoteName,
2330 new_remote_name: &RemoteName,
2331) -> Result<(), GitRemoteManagementError> {
2332 let mut git_repo = get_git_repo(mut_repo.store())?;
2333
2334 validate_remote_name(new_remote_name)?;
2335
2336 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2337 return Err(GitRemoteManagementError::NoSuchRemote(
2338 old_remote_name.to_owned(),
2339 ));
2340 };
2341 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2342
2343 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2344 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2345 new_remote_name.to_owned(),
2346 ));
2347 }
2348
2349 match (
2350 remote.refspecs(gix::remote::Direction::Fetch),
2351 remote.refspecs(gix::remote::Direction::Push),
2352 ) {
2353 ([refspec], [])
2354 if refspec.to_ref().to_bstring()
2355 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2356 _ => {
2357 return Err(GitRemoteManagementError::NonstandardConfiguration(
2358 old_remote_name.to_owned(),
2359 ));
2360 }
2361 }
2362
2363 remote
2364 .replace_refspecs(
2365 [default_fetch_refspec(new_remote_name).as_bytes()],
2366 gix::remote::Direction::Fetch,
2367 )
2368 .expect("default refspec to be valid");
2369
2370 let mut config = git_repo.config_snapshot().clone();
2371 save_remote(&mut config, new_remote_name, &mut remote)?;
2372 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2373 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2374 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2375
2376 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2377 .map_err(GitRemoteManagementError::from_git)?;
2378
2379 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2380 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2381 }
2382
2383 Ok(())
2384}
2385
2386fn rename_remote_git_refs(
2387 git_repo: &mut gix::Repository,
2388 old_remote_name: &RemoteName,
2389 new_remote_name: &RemoteName,
2390) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2391 let to_prefixes = |namespace: &str| {
2392 (
2393 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2394 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2395 )
2396 };
2397 let to_rename_edits = {
2398 let ref_log_message = BString::from(format!(
2399 "renamed remote {old_remote_name} to {new_remote_name}",
2400 old_remote_name = old_remote_name.as_symbol(),
2401 new_remote_name = new_remote_name.as_symbol(),
2402 ));
2403 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2404 let new_name = BString::new(
2405 [
2406 new_prefix.as_bytes(),
2407 &old_ref.name().as_bstr()[old_prefix.len()..],
2408 ]
2409 .concat(),
2410 );
2411 [
2412 add_ref(
2413 new_name.try_into().expect("new ref name to be valid"),
2414 old_ref.target().into_owned(),
2415 ref_log_message.clone(),
2416 ),
2417 remove_ref(old_ref),
2418 ]
2419 }
2420 };
2421
2422 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2423 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2424 let edits: Vec<_> = itertools::chain(
2425 git_repo
2426 .references()?
2427 .prefixed(old_bookmark_prefix.as_str())?
2428 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2429 git_repo
2430 .references()?
2431 .prefixed(old_tag_prefix.as_str())?
2432 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2433 )
2434 .flatten_ok()
2435 .try_collect()?;
2436 git_repo.edit_references(edits)?;
2437 Ok(())
2438}
2439
2440pub fn set_remote_urls(
2444 store: &Store,
2445 remote_name: &RemoteName,
2446 new_url: Option<&str>,
2447 new_push_url: Option<&str>,
2448) -> Result<(), GitRemoteManagementError> {
2449 if new_url.is_none() && new_push_url.is_none() {
2451 return Ok(());
2452 }
2453
2454 let git_repo = get_git_repo(store)?;
2455
2456 validate_remote_name(remote_name)?;
2457
2458 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2459 return Err(GitRemoteManagementError::NoSuchRemote(
2460 remote_name.to_owned(),
2461 ));
2462 };
2463 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2464
2465 if let Some(url) = new_url {
2466 remote = remote
2467 .with_url(url)
2468 .map_err(GitRemoteManagementError::from_git)?;
2469 }
2470
2471 if let Some(url) = new_push_url {
2472 remote = remote
2473 .with_push_url(url)
2474 .map_err(GitRemoteManagementError::from_git)?;
2475 }
2476
2477 let mut config = git_repo.config_snapshot().clone();
2478 save_remote(&mut config, remote_name, &mut remote)?;
2479 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2480
2481 Ok(())
2482}
2483
2484fn rename_remote_refs(
2485 mut_repo: &mut MutableRepo,
2486 old_remote_name: &RemoteName,
2487 new_remote_name: &RemoteName,
2488) {
2489 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2490 let prefix = format!(
2491 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2492 remote = old_remote_name.as_str()
2493 );
2494 let git_refs = mut_repo
2495 .view()
2496 .git_refs()
2497 .iter()
2498 .filter_map(|(old, target)| {
2499 old.as_str().strip_prefix(&prefix).map(|p| {
2500 let new: GitRefNameBuf = format!(
2501 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2502 remote = new_remote_name.as_str()
2503 )
2504 .into();
2505 (old.clone(), new, target.clone())
2506 })
2507 })
2508 .collect_vec();
2509 for (old, new, target) in git_refs {
2510 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2511 mut_repo.set_git_ref_target(&new, target);
2512 }
2513}
2514
2515const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2516
2517#[derive(Error, Debug)]
2518pub enum GitFetchError {
2519 #[error("No git remote named '{}'", .0.as_symbol())]
2520 NoSuchRemote(RemoteNameBuf),
2521 #[error(transparent)]
2522 RemoteName(#[from] GitRemoteNameError),
2523 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2524 RejectedUpdates(Vec<GitRefNameBuf>),
2525 #[error(transparent)]
2526 Subprocess(#[from] GitSubprocessError),
2527}
2528
2529#[derive(Error, Debug)]
2530pub enum GitDefaultRefspecError {
2531 #[error("No git remote named '{}'", .0.as_symbol())]
2532 NoSuchRemote(RemoteNameBuf),
2533 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2534 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2535}
2536
2537struct FetchedRefs {
2538 remote: RemoteNameBuf,
2539 bookmark_matcher: StringMatcher,
2540 tag_matcher: StringMatcher,
2541}
2542
2543#[derive(Clone, Debug)]
2545pub struct GitFetchRefExpression {
2546 pub bookmark: StringExpression,
2548 pub tag: StringExpression,
2554}
2555
2556#[derive(Debug)]
2558pub struct ExpandedFetchRefSpecs {
2559 expr: GitFetchRefExpression,
2561 refspecs: Vec<RefSpec>,
2562 negative_refspecs: Vec<NegativeRefSpec>,
2563}
2564
2565#[derive(Error, Debug)]
2566pub enum GitRefExpansionError {
2567 #[error(transparent)]
2568 Expression(#[from] GitRefExpressionError),
2569 #[error(
2570 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2571 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2572 )]
2573 InvalidBranchPattern(StringPattern),
2574}
2575
2576pub fn expand_fetch_refspecs(
2578 remote: &RemoteName,
2579 expr: GitFetchRefExpression,
2580) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2581 let (positive_bookmarks, negative_bookmarks) =
2582 split_into_positive_negative_patterns(&expr.bookmark)?;
2583 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2584
2585 let refspecs = itertools::chain(
2586 positive_bookmarks
2587 .iter()
2588 .map(|&pattern| pattern_to_refspec_glob(pattern))
2589 .map_ok(|glob| {
2590 RefSpec::forced(
2591 format!("refs/heads/{glob}"),
2592 format!(
2593 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2594 remote = remote.as_str()
2595 ),
2596 )
2597 }),
2598 positive_tags
2599 .iter()
2600 .map(|&pattern| pattern_to_refspec_glob(pattern))
2601 .map_ok(|glob| {
2602 RefSpec::forced(
2603 format!("refs/tags/{glob}"),
2604 format!(
2605 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2606 remote = remote.as_str()
2607 ),
2608 )
2609 }),
2610 )
2611 .try_collect()?;
2612
2613 let negative_refspecs = itertools::chain(
2614 negative_bookmarks
2615 .iter()
2616 .map(|&pattern| pattern_to_refspec_glob(pattern))
2617 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2618 negative_tags
2619 .iter()
2620 .map(|&pattern| pattern_to_refspec_glob(pattern))
2621 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2622 )
2623 .try_collect()?;
2624
2625 Ok(ExpandedFetchRefSpecs {
2626 expr,
2627 refspecs,
2628 negative_refspecs,
2629 })
2630}
2631
2632fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2633 pattern
2634 .to_glob()
2635 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2638 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2639}
2640
2641#[derive(Debug, Error)]
2642pub enum GitRefExpressionError {
2643 #[error("Cannot use `~` in sub expression")]
2644 NestedNotIn,
2645 #[error("Cannot use `&` in sub expression")]
2646 NestedIntersection,
2647 #[error("Cannot use `&` for positive expressions")]
2648 PositiveIntersection,
2649}
2650
2651fn split_into_positive_negative_patterns(
2654 expr: &StringExpression,
2655) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2656 static ALL: StringPattern = StringPattern::all();
2657
2658 fn visit_positive<'a>(
2672 expr: &'a StringExpression,
2673 positives: &mut Vec<&'a StringPattern>,
2674 negatives: &mut Vec<&'a StringPattern>,
2675 ) -> Result<(), GitRefExpressionError> {
2676 match expr {
2677 StringExpression::Pattern(pattern) => {
2678 positives.push(pattern);
2679 Ok(())
2680 }
2681 StringExpression::NotIn(complement) => {
2682 positives.push(&ALL);
2683 visit_negative(complement, negatives)
2684 }
2685 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2686 StringExpression::Intersection(expr1, expr2) => {
2687 match (expr1.as_ref(), expr2.as_ref()) {
2688 (other, StringExpression::NotIn(complement))
2689 | (StringExpression::NotIn(complement), other) => {
2690 visit_positive(other, positives, negatives)?;
2691 visit_negative(complement, negatives)
2692 }
2693 _ => Err(GitRefExpressionError::PositiveIntersection),
2694 }
2695 }
2696 }
2697 }
2698
2699 fn visit_negative<'a>(
2700 expr: &'a StringExpression,
2701 negatives: &mut Vec<&'a StringPattern>,
2702 ) -> Result<(), GitRefExpressionError> {
2703 match expr {
2704 StringExpression::Pattern(pattern) => {
2705 negatives.push(pattern);
2706 Ok(())
2707 }
2708 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2709 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2710 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2711 }
2712 }
2713
2714 fn visit_union<'a>(
2715 expr1: &'a StringExpression,
2716 expr2: &'a StringExpression,
2717 patterns: &mut Vec<&'a StringPattern>,
2718 ) -> Result<(), GitRefExpressionError> {
2719 visit_union_sub(expr1, patterns)?;
2720 visit_union_sub(expr2, patterns)
2721 }
2722
2723 fn visit_union_sub<'a>(
2724 expr: &'a StringExpression,
2725 patterns: &mut Vec<&'a StringPattern>,
2726 ) -> Result<(), GitRefExpressionError> {
2727 match expr {
2728 StringExpression::Pattern(pattern) => {
2729 patterns.push(pattern);
2730 Ok(())
2731 }
2732 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2733 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2734 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2735 }
2736 }
2737
2738 let mut positives = Vec::new();
2739 let mut negatives = Vec::new();
2740 visit_positive(expr, &mut positives, &mut negatives)?;
2741 if positives.iter().all(|pattern| pattern.is_all())
2744 && !negatives.is_empty()
2745 && negatives.iter().all(|pattern| pattern.is_all())
2746 {
2747 Ok((vec![], vec![]))
2748 } else {
2749 Ok((positives, negatives))
2750 }
2751}
2752
2753#[derive(Debug)]
2757#[must_use = "warnings should be surfaced in the UI"]
2758pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2759
2760#[derive(Debug)]
2763pub struct IgnoredRefspec {
2764 pub refspec: BString,
2766 pub reason: &'static str,
2768}
2769
2770#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2771enum FetchRefSpecKind {
2772 Positive,
2773 Negative,
2774}
2775
2776pub fn load_default_fetch_bookmarks(
2778 remote_name: &RemoteName,
2779 git_repo: &gix::Repository,
2780) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2781 let remote = git_repo
2782 .try_find_remote(remote_name.as_str())
2783 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2784 .map_err(|e| {
2785 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2786 })?;
2787
2788 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2789 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2790 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2791 let mut negative_bookmarks = Vec::new();
2792 for refspec in remote_refspecs {
2793 let refspec = refspec.to_ref();
2794 match parse_fetch_refspec(remote_name, refspec) {
2795 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2796 positive_bookmarks.push(StringExpression::pattern(bookmark));
2797 }
2798 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2799 negative_bookmarks.push(StringExpression::pattern(bookmark));
2800 }
2801 Err(reason) => {
2802 let refspec = refspec.to_bstring();
2803 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2804 }
2805 }
2806 }
2807
2808 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2809 if !negative_bookmarks.is_empty() {
2811 bookmark_expr =
2812 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2813 }
2814
2815 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2816}
2817
2818fn parse_fetch_refspec(
2819 remote_name: &RemoteName,
2820 refspec: gix::refspec::RefSpecRef<'_>,
2821) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2822 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2823
2824 let (src, positive_dst) = match refspec.instruction() {
2825 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2826 Instruction::Fetch(fetch) => match fetch {
2827 gix::refspec::instruction::Fetch::Only { src: _ } => {
2828 return Err("fetch-only refspecs are not supported");
2829 }
2830 gix::refspec::instruction::Fetch::AndUpdate {
2831 src,
2832 dst,
2833 allow_non_fast_forward,
2834 } => {
2835 if !allow_non_fast_forward {
2836 return Err("non-forced refspecs are not supported");
2837 }
2838 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2839 }
2840 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2841 },
2842 };
2843
2844 let src_branch = src
2845 .strip_prefix("refs/heads/")
2846 .ok_or("only refs/heads/ is supported for refspec sources")?;
2847 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2848
2849 if let Some(dst) = positive_dst {
2850 let dst_without_prefix = dst
2851 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
2852 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2853 let dst_branch = dst_without_prefix
2854 .strip_prefix(remote_name.as_str())
2855 .and_then(|d| d.strip_prefix("/"))
2856 .ok_or("remote renaming not supported")?;
2857 if src_branch != dst_branch {
2858 return Err("renaming is not supported");
2859 }
2860 Ok((FetchRefSpecKind::Positive, branch))
2861 } else {
2862 Ok((FetchRefSpecKind::Negative, branch))
2863 }
2864}
2865
2866pub struct GitFetch<'a> {
2868 mut_repo: &'a mut MutableRepo,
2869 git_repo: Box<gix::Repository>,
2870 git_ctx: GitSubprocessContext,
2871 import_options: &'a GitImportOptions,
2872 fetched: Vec<FetchedRefs>,
2873}
2874
2875impl<'a> GitFetch<'a> {
2876 pub fn new(
2877 mut_repo: &'a mut MutableRepo,
2878 subprocess_options: GitSubprocessOptions,
2879 import_options: &'a GitImportOptions,
2880 ) -> Result<Self, UnexpectedGitBackendError> {
2881 let git_backend = get_git_backend(mut_repo.store())?;
2882 let git_repo = Box::new(git_backend.git_repo());
2883 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
2884 Ok(GitFetch {
2885 mut_repo,
2886 git_repo,
2887 git_ctx,
2888 import_options,
2889 fetched: vec![],
2890 })
2891 }
2892
2893 #[tracing::instrument(skip(self, callback))]
2899 pub fn fetch(
2900 &mut self,
2901 remote_name: &RemoteName,
2902 ExpandedFetchRefSpecs {
2903 expr,
2904 refspecs: mut remaining_refspecs,
2905 negative_refspecs,
2906 }: ExpandedFetchRefSpecs,
2907 callback: &mut dyn GitSubprocessCallback,
2908 depth: Option<NonZeroU32>,
2909 fetch_tags_override: Option<FetchTagsOverride>,
2910 ) -> Result<(), GitFetchError> {
2911 validate_remote_name(remote_name)?;
2912
2913 if self
2915 .git_repo
2916 .try_find_remote(remote_name.as_str())
2917 .is_none()
2918 {
2919 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2920 }
2921
2922 if remaining_refspecs.is_empty() {
2923 return Ok(());
2925 }
2926
2927 let mut branches_to_prune = Vec::new();
2928 let updates = loop {
2936 let status = self.git_ctx.spawn_fetch(
2937 remote_name,
2938 &remaining_refspecs,
2939 &negative_refspecs,
2940 callback,
2941 depth,
2942 fetch_tags_override,
2943 )?;
2944 let failing_refspec = match status {
2945 GitFetchStatus::Updates(updates) => break updates,
2946 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
2947 };
2948 tracing::debug!(failing_refspec, "failed to fetch ref");
2949 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2950
2951 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2952 branches_to_prune.push(format!(
2953 "{remote_name}/{branch_name}",
2954 remote_name = remote_name.as_str()
2955 ));
2956 }
2957 };
2958
2959 if !updates.rejected.is_empty() {
2962 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
2963 return Err(GitFetchError::RejectedUpdates(names));
2964 }
2965
2966 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2969
2970 self.fetched.push(FetchedRefs {
2971 remote: remote_name.to_owned(),
2972 bookmark_matcher: expr.bookmark.to_matcher(),
2973 tag_matcher: expr.tag.to_matcher(),
2974 });
2975 Ok(())
2976 }
2977
2978 #[tracing::instrument(skip(self))]
2980 pub fn get_default_branch(
2981 &self,
2982 remote_name: &RemoteName,
2983 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2984 if self
2985 .git_repo
2986 .try_find_remote(remote_name.as_str())
2987 .is_none()
2988 {
2989 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2990 }
2991 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2992 tracing::debug!(?default_branch);
2993 Ok(default_branch)
2994 }
2995
2996 #[tracing::instrument(skip(self))]
3003 pub async fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
3004 tracing::debug!("import_refs");
3005 let all_remote_tags = true;
3006 let refs_to_import = diff_refs_to_import(
3007 self.mut_repo.view(),
3008 &self.git_repo,
3009 all_remote_tags,
3010 |kind, symbol| match kind {
3011 GitRefKind::Bookmark => self
3012 .fetched
3013 .iter()
3014 .filter(|fetched| fetched.remote == symbol.remote)
3015 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3016 GitRefKind::Tag => {
3017 symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
3021 || self
3022 .fetched
3023 .iter()
3024 .filter(|fetched| fetched.remote == symbol.remote)
3025 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str()))
3026 }
3027 },
3028 )?;
3029 let import_stats =
3030 import_refs_inner(self.mut_repo, refs_to_import, self.import_options).await?;
3031
3032 self.fetched.clear();
3033
3034 Ok(import_stats)
3035 }
3036}
3037
3038#[derive(Error, Debug)]
3039pub enum GitPushError {
3040 #[error("No git remote named '{}'", .0.as_symbol())]
3041 NoSuchRemote(RemoteNameBuf),
3042 #[error(transparent)]
3043 RemoteName(#[from] GitRemoteNameError),
3044 #[error(transparent)]
3045 Subprocess(#[from] GitSubprocessError),
3046 #[error(transparent)]
3047 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3048}
3049
3050#[derive(Clone, Debug)]
3051pub struct GitPushRefTargets {
3052 pub bookmarks: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3054}
3055
3056pub struct GitRefUpdate {
3057 pub qualified_name: GitRefNameBuf,
3058 pub targets: Diff<Option<CommitId>>,
3063}
3064
3065#[derive(Clone, Debug, Default)]
3067pub struct GitPushOptions {
3068 pub extra_args: Vec<String>,
3070 pub remote_push_options: Vec<String>,
3072}
3073
3074pub fn push_refs(
3076 mut_repo: &mut MutableRepo,
3077 subprocess_options: GitSubprocessOptions,
3078 remote: &RemoteName,
3079 targets: &GitPushRefTargets,
3080 callback: &mut dyn GitSubprocessCallback,
3081 options: &GitPushOptions,
3082) -> Result<GitPushStats, GitPushError> {
3083 validate_remote_name(remote)?;
3084
3085 let ref_updates = targets
3086 .bookmarks
3087 .iter()
3088 .map(|(name, update)| GitRefUpdate {
3089 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3090 targets: update.clone(),
3091 })
3092 .collect_vec();
3093
3094 let push_stats = push_updates(
3095 mut_repo,
3096 subprocess_options,
3097 remote,
3098 &ref_updates,
3099 callback,
3100 options,
3101 )?;
3102 tracing::debug!(?push_stats);
3103
3104 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3105 let pushed_bookmark_updates = || {
3106 iter::zip(&targets.bookmarks, &ref_updates)
3107 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3108 .map(|((name, update), _)| (name.as_ref(), update))
3109 };
3110
3111 let unexported_bookmarks = {
3114 let git_repo =
3115 get_git_repo(mut_repo.store()).expect("backend type should have been tested");
3116 let refs = build_pushed_bookmarks_to_export(remote, pushed_bookmark_updates());
3117 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3118 };
3119
3120 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3121 let is_exported_bookmark = |name: &RefName| {
3122 unexported_bookmarks
3123 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3124 .is_err()
3125 };
3126 for (name, update) in pushed_bookmark_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3127 let new_remote_ref = RemoteRef {
3128 target: RefTarget::resolved(update.after.clone()),
3129 state: RemoteRefState::Tracked,
3130 };
3131 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3132 }
3133
3134 assert!(push_stats.unexported_bookmarks.is_empty());
3138 let push_stats = GitPushStats {
3139 pushed: push_stats.pushed,
3140 rejected: push_stats.rejected,
3141 remote_rejected: push_stats.remote_rejected,
3142 unexported_bookmarks,
3143 };
3144 Ok(push_stats)
3145}
3146
3147pub fn push_updates(
3149 repo: &dyn Repo,
3150 subprocess_options: GitSubprocessOptions,
3151 remote_name: &RemoteName,
3152 updates: &[GitRefUpdate],
3153 callback: &mut dyn GitSubprocessCallback,
3154 options: &GitPushOptions,
3155) -> Result<GitPushStats, GitPushError> {
3156 let mut qualified_remote_refs_expected_locations = HashMap::new();
3157 let mut refspecs = vec![];
3158 for update in updates {
3159 qualified_remote_refs_expected_locations.insert(
3160 update.qualified_name.as_ref(),
3161 update.targets.before.as_ref(),
3162 );
3163 if let Some(new_target) = &update.targets.after {
3164 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
3168 } else {
3169 refspecs.push(RefSpec::delete(&update.qualified_name));
3173 }
3174 }
3175
3176 let git_backend = get_git_backend(repo.store())?;
3177 let git_repo = git_backend.git_repo();
3178 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3179
3180 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3182 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3183 }
3184
3185 let refs_to_push: Vec<RefToPush> = refspecs
3186 .iter()
3187 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3188 .collect();
3189
3190 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3191 push_stats.pushed.sort();
3192 push_stats.rejected.sort();
3193 push_stats.remote_rejected.sort();
3194 Ok(push_stats)
3195}
3196
3197fn build_pushed_bookmarks_to_export<'a>(
3199 remote: &RemoteName,
3200 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a Diff<Option<CommitId>>)>,
3201) -> RefsToExport {
3202 let mut to_update = Vec::new();
3203 let mut to_delete = Vec::new();
3204 for (name, update) in pushed_updates {
3205 let symbol = name.to_remote_symbol(remote);
3206 match (update.before.as_ref(), update.after.as_ref()) {
3207 (old, Some(new)) => {
3208 let old_oid = old.map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
3209 let new_oid = gix::ObjectId::from_bytes_or_panic(new.as_bytes());
3210 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3211 }
3212 (Some(old), None) => {
3213 let old_oid = gix::ObjectId::from_bytes_or_panic(old.as_bytes());
3214 to_delete.push((symbol.to_owned(), old_oid));
3215 }
3216 (None, None) => panic!("old/new targets should differ"),
3217 }
3218 }
3219
3220 RefsToExport {
3221 to_update,
3222 to_delete,
3223 failed: vec![],
3224 }
3225}
3226
3227#[derive(Copy, Clone, Debug)]
3230pub enum FetchTagsOverride {
3231 AllTags,
3234 NoTags,
3237}
3238
3239#[cfg(test)]
3240mod tests {
3241 use assert_matches::assert_matches;
3242
3243 use super::*;
3244 use crate::revset;
3245 use crate::revset::RevsetDiagnostics;
3246
3247 #[test]
3248 fn test_split_positive_negative_patterns() {
3249 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3250 try_split(text).unwrap()
3251 }
3252
3253 fn try_split(
3254 text: &str,
3255 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3256 let mut diagnostics = RevsetDiagnostics::new();
3257 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3258 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3259 Ok((
3260 positives.into_iter().cloned().collect(),
3261 negatives.into_iter().cloned().collect(),
3262 ))
3263 }
3264
3265 insta::assert_compact_debug_snapshot!(
3266 split("a"),
3267 @r#"([Exact("a")], [])"#);
3268 insta::assert_compact_debug_snapshot!(
3269 split("~a"),
3270 @r#"([Substring("")], [Exact("a")])"#);
3271 insta::assert_compact_debug_snapshot!(
3272 split("~a~b"),
3273 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3274 insta::assert_compact_debug_snapshot!(
3275 split("~(a|b)"),
3276 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3277 insta::assert_compact_debug_snapshot!(
3278 split("a|b"),
3279 @r#"([Exact("a"), Exact("b")], [])"#);
3280 insta::assert_compact_debug_snapshot!(
3281 split("(a|b)&~c"),
3282 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3283 insta::assert_compact_debug_snapshot!(
3284 split("~a&b"),
3285 @r#"([Exact("b")], [Exact("a")])"#);
3286 insta::assert_compact_debug_snapshot!(
3287 split("a&~b&~c"),
3288 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3289 insta::assert_compact_debug_snapshot!(
3290 split("~a&b&~c"),
3291 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3292 insta::assert_compact_debug_snapshot!(
3293 split("a&~(b|c)"),
3294 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3295 insta::assert_compact_debug_snapshot!(
3296 split("((a|b)|c)&~(d|(e|f))"),
3297 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3298 assert_matches!(
3299 try_split("a&b"),
3300 Err(GitRefExpressionError::PositiveIntersection)
3301 );
3302 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3303 assert_matches!(
3304 try_split("a&~(b&~c)"),
3305 Err(GitRefExpressionError::NestedIntersection)
3306 );
3307 assert_matches!(
3308 try_split("(a|b)&c"),
3309 Err(GitRefExpressionError::PositiveIntersection)
3310 );
3311 assert_matches!(
3312 try_split("(a&~b)&(~c&~d)"),
3313 Err(GitRefExpressionError::PositiveIntersection)
3314 );
3315 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3316 assert_matches!(
3317 try_split("a&~b|c&~d"),
3318 Err(GitRefExpressionError::NestedIntersection)
3319 );
3320
3321 insta::assert_compact_debug_snapshot!(
3324 split("*"),
3325 @r#"([Glob(GlobPattern("*"))], [])"#);
3326 insta::assert_compact_debug_snapshot!(
3327 split("~*"),
3328 @"([], [])");
3329 insta::assert_compact_debug_snapshot!(
3330 split("a~*"),
3331 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3332 insta::assert_compact_debug_snapshot!(
3333 split("~(a|*)"),
3334 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3335 }
3336}