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
163fn oid_from_commit_id(id: &CommitId) -> &gix::oid {
165 gix::oid::from_bytes_unchecked(id.as_bytes())
166}
167
168fn owned_oid_from_commit_id(id: &CommitId) -> gix::ObjectId {
170 gix::ObjectId::from_bytes_or_panic(id.as_bytes())
171}
172
173#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
175pub enum GitRefKind {
176 Bookmark,
177 Tag,
178}
179
180#[derive(Debug, Default)]
182pub struct GitPushStats {
183 pub pushed: Vec<GitRefNameBuf>,
185 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
187 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
189 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
191}
192
193impl GitPushStats {
194 pub fn all_ok(&self) -> bool {
195 self.rejected.is_empty()
196 && self.remote_rejected.is_empty()
197 && self.unexported_bookmarks.is_empty()
198 }
199
200 pub fn some_exported(&self) -> bool {
203 self.pushed.len() > self.unexported_bookmarks.len()
204 }
205}
206
207#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
211struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
212
213impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
214 fn borrow(&self) -> &RemoteRefSymbol<'b> {
215 &self.0
216 }
217}
218
219#[derive(Debug, Hash, PartialEq, Eq)]
225pub(crate) struct RefSpec {
226 forced: bool,
227 source: Option<String>,
230 destination: String,
231}
232
233impl RefSpec {
234 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
235 Self {
236 forced: true,
237 source: Some(source.into()),
238 destination: destination.into(),
239 }
240 }
241
242 fn delete(destination: impl Into<String>) -> Self {
243 Self {
245 forced: false,
246 source: None,
247 destination: destination.into(),
248 }
249 }
250
251 pub(crate) fn to_git_format(&self) -> String {
252 format!(
253 "{}{}",
254 if self.forced { "+" } else { "" },
255 self.to_git_format_not_forced()
256 )
257 }
258
259 pub(crate) fn to_git_format_not_forced(&self) -> String {
265 if let Some(s) = &self.source {
266 format!("{}:{}", s, self.destination)
267 } else {
268 format!(":{}", self.destination)
269 }
270 }
271}
272
273#[derive(Debug)]
275#[repr(transparent)]
276pub(crate) struct NegativeRefSpec {
277 source: String,
278}
279
280impl NegativeRefSpec {
281 fn new(source: impl Into<String>) -> Self {
282 Self {
283 source: source.into(),
284 }
285 }
286
287 pub(crate) fn to_git_format(&self) -> String {
288 format!("^{}", self.source)
289 }
290}
291
292pub(crate) struct RefToPush<'a> {
295 pub(crate) refspec: &'a RefSpec,
296 pub(crate) expected_location: Option<&'a gix::oid>,
297}
298
299impl<'a> RefToPush<'a> {
300 fn new(
301 refspec: &'a RefSpec,
302 expected_locations: &'a HashMap<&GitRefName, Option<&gix::oid>>,
303 ) -> Self {
304 let expected_location = *expected_locations
305 .get(GitRefName::new(&refspec.destination))
306 .expect(
307 "The refspecs and the expected locations were both constructed from the same \
308 source of truth. This means the lookup should always work.",
309 );
310
311 Self {
312 refspec,
313 expected_location,
314 }
315 }
316
317 pub(crate) fn to_git_lease(&self) -> String {
318 format!(
319 "{}:{}",
320 self.refspec.destination,
321 self.expected_location
322 .map(|x| x.to_string())
323 .as_deref()
324 .unwrap_or("")
325 )
326 }
327}
328
329pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
332 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
333 if name == "HEAD" {
335 return None;
336 }
337 let name = RefName::new(name);
338 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
339 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
340 } else if let Some(remote_and_name) = full_name
341 .as_str()
342 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
343 {
344 let (remote, name) = remote_and_name.split_once('/')?;
345 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
347 return None;
348 }
349 let name = RefName::new(name);
350 let remote = RemoteName::new(remote);
351 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
352 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
353 let name = RefName::new(name);
354 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
355 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
356 } else {
357 None
358 }
359}
360
361fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
362 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
363 let (remote, name) = remote_and_name.split_once('/')?;
364 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
365 return None;
366 }
367 let name = RefName::new(name);
368 let remote = RemoteName::new(remote);
369 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
370}
371
372fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
373 let RemoteRefSymbol { name, remote } = symbol;
374 let name = name.as_str();
375 let remote = remote.as_str();
376 if name.is_empty() || remote.is_empty() {
377 return None;
378 }
379 match kind {
380 GitRefKind::Bookmark => {
381 if name == "HEAD" {
382 return None;
383 }
384 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
385 Some(format!("refs/heads/{name}").into())
386 } else {
387 Some(format!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())
388 }
389 }
390 GitRefKind::Tag => {
391 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
393 }
394 }
395}
396
397fn to_git_or_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> GitRefNameBuf {
398 let RemoteRefSymbol { name, remote } = symbol;
399 let name = name.as_str();
400 let remote = remote.as_str();
401 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
402 format!("refs/tags/{name}").into()
403 } else {
404 format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}").into()
405 }
406}
407
408#[derive(Debug, Error)]
409#[error("The repo is not backed by a Git repo")]
410pub struct UnexpectedGitBackendError;
411
412pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
414 store.backend_impl().ok_or(UnexpectedGitBackendError)
415}
416
417pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
419 get_git_backend(store).map(|backend| backend.git_repo())
420}
421
422fn resolve_git_ref_to_commit_id(
427 git_ref: &gix::Reference,
428 known_commit_oid: Option<&gix::oid>,
429) -> Option<gix::ObjectId> {
430 let mut peeling_ref = Cow::Borrowed(git_ref);
431
432 if let Some(known_oid) = known_commit_oid {
434 let raw_ref = &git_ref.inner;
435 if let Some(oid) = raw_ref.target.try_id()
436 && oid == known_oid
437 {
438 return Some(oid.to_owned());
439 }
440 if let Some(oid) = raw_ref.peeled
441 && oid == known_oid
442 {
443 return Some(oid);
446 }
447 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
451 let maybe_tag = git_ref
452 .try_id()
453 .and_then(|id| id.object().ok())
454 .and_then(|object| object.try_into_tag().ok());
455 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
456 let oid = oid.detach();
457 if oid == known_oid {
458 return Some(oid);
460 }
461 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
464 }
465 }
466 }
467
468 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
472 let is_commit = peeled_id
473 .object()
474 .is_ok_and(|object| object.kind.is_commit());
475 is_commit.then_some(peeled_id.detach())
476}
477
478#[derive(Error, Debug)]
479pub enum GitImportError {
480 #[error("Failed to read Git HEAD target commit {id}")]
481 MissingHeadTarget {
482 id: CommitId,
483 #[source]
484 err: BackendError,
485 },
486 #[error("Ancestor of Git ref {symbol} is missing")]
487 MissingRefAncestor {
488 symbol: RemoteRefSymbolBuf,
489 #[source]
490 err: BackendError,
491 },
492 #[error(transparent)]
493 Backend(#[from] BackendError),
494 #[error(transparent)]
495 Index(#[from] IndexError),
496 #[error(transparent)]
497 Git(Box<dyn std::error::Error + Send + Sync>),
498 #[error(transparent)]
499 UnexpectedBackend(#[from] UnexpectedGitBackendError),
500}
501
502impl GitImportError {
503 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
504 Self::Git(source.into())
505 }
506}
507
508#[derive(Debug)]
510pub struct GitImportOptions {
511 pub auto_local_bookmark: bool,
513 pub abandon_unreachable_commits: bool,
515 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
517}
518
519#[derive(Clone, Debug, Eq, PartialEq, Default)]
521pub struct GitImportStats {
522 pub abandoned_commits: Vec<CommitId>,
524 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
527 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
530 pub failed_ref_names: Vec<BString>,
535}
536
537#[derive(Debug)]
538struct RefsToImport {
539 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
542 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
545 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
548 failed_ref_names: Vec<BString>,
550}
551
552pub async fn import_refs(
557 mut_repo: &mut MutableRepo,
558 options: &GitImportOptions,
559) -> Result<GitImportStats, GitImportError> {
560 import_some_refs(mut_repo, options, |_, _| true).await
561}
562
563pub async fn import_some_refs(
568 mut_repo: &mut MutableRepo,
569 options: &GitImportOptions,
570 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
571) -> Result<GitImportStats, GitImportError> {
572 let git_repo = get_git_repo(mut_repo.store())?;
573
574 for remote_name in iter_remote_names(&git_repo) {
578 mut_repo.ensure_remote(&remote_name);
579 }
580
581 let all_remote_tags = false;
583 let refs_to_import =
584 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
585 import_refs_inner(mut_repo, refs_to_import, options).await
586}
587
588async fn import_refs_inner(
589 mut_repo: &mut MutableRepo,
590 refs_to_import: RefsToImport,
591 options: &GitImportOptions,
592) -> Result<GitImportStats, GitImportError> {
593 let store = mut_repo.store();
594 let git_backend = get_git_backend(store).expect("backend type should have been tested");
595
596 let RefsToImport {
597 changed_git_refs,
598 changed_remote_bookmarks,
599 changed_remote_tags,
600 failed_ref_names,
601 } = refs_to_import;
602
603 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
609 let index = mut_repo.index();
610 let missing_head_ids: Vec<&CommitId> = iter_changed_refs()
611 .flat_map(|(_, (_, new_target))| new_target.added_ids())
612 .filter_map(|id| match index.has_id(id) {
613 Ok(false) => Some(Ok(id)),
614 Ok(true) => None,
615 Err(e) => Some(Err(e)),
616 })
617 .try_collect()?;
618 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
619
620 let mut head_commits = Vec::new();
622 let get_commit = async |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
623 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
624 symbol: symbol.clone(),
625 err,
626 };
627 if !heads_imported && !index.has_id(id).map_err(GitImportError::Index)? {
629 git_backend
630 .import_head_commits([id])
631 .map_err(missing_ref_err)?;
632 }
633 store.get_commit_async(id).await.map_err(missing_ref_err)
634 };
635 for (symbol, (_, new_target)) in iter_changed_refs() {
636 for id in new_target.added_ids() {
637 let commit = get_commit(id, symbol).await?;
638 head_commits.push(commit);
639 }
640 }
641 mut_repo
644 .add_heads(&head_commits)
645 .await
646 .map_err(GitImportError::Backend)?;
647
648 for (full_name, new_target) in changed_git_refs {
650 mut_repo.set_git_ref_target(&full_name, new_target);
651 }
652 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
653 let symbol = symbol.as_ref();
654 let base_target = old_remote_ref.tracked_target();
655 let new_remote_ref = RemoteRef {
656 target: new_target.clone(),
657 state: if old_remote_ref != RemoteRef::absent_ref() {
658 old_remote_ref.state
659 } else {
660 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
661 },
662 };
663 if new_remote_ref.is_tracked() {
664 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
665 }
666 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
669 }
670 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
671 let symbol = symbol.as_ref();
672 let base_target = old_remote_ref.tracked_target();
673 let new_remote_ref = RemoteRef {
674 target: new_target.clone(),
675 state: if old_remote_ref != RemoteRef::absent_ref() {
676 old_remote_ref.state
677 } else {
678 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
679 },
680 };
681 if new_remote_ref.is_tracked() {
682 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
683 }
684 mut_repo.set_remote_tag(symbol, new_remote_ref);
687 }
688
689 let abandoned_commits = if options.abandon_unreachable_commits {
690 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
691 .await
692 .map_err(GitImportError::Backend)?
693 } else {
694 vec![]
695 };
696 let stats = GitImportStats {
697 abandoned_commits,
698 changed_remote_bookmarks,
699 changed_remote_tags,
700 failed_ref_names,
701 };
702 Ok(stats)
703}
704
705async fn abandon_unreachable_commits(
708 mut_repo: &mut MutableRepo,
709 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
710 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
711) -> BackendResult<Vec<CommitId>> {
712 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
713 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
714 .cloned()
715 .collect_vec();
716 if hidable_git_heads.is_empty() {
717 return Ok(vec![]);
718 }
719 let pinned_expression = RevsetExpression::union_all(&[
720 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
722 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
723 .intersection(&RevsetExpression::visible_heads().ancestors()),
725 RevsetExpression::root(),
726 ]);
727 let abandoned_expression = pinned_expression
728 .range(&RevsetExpression::commits(hidable_git_heads))
729 .intersection(&RevsetExpression::visible_heads().ancestors());
731 let abandoned_commit_ids: Vec<_> = abandoned_expression
732 .evaluate(mut_repo)
733 .map_err(|err| err.into_backend_error())?
734 .stream()
735 .try_collect()
736 .await
737 .map_err(|err| err.into_backend_error())?;
738 for id in &abandoned_commit_ids {
739 let commit = mut_repo.store().get_commit_async(id).await?;
740 mut_repo.record_abandoned_commit(&commit);
741 }
742 Ok(abandoned_commit_ids)
743}
744
745fn diff_refs_to_import(
747 view: &View,
748 git_repo: &gix::Repository,
749 all_remote_tags: bool,
750 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
751) -> Result<RefsToImport, GitImportError> {
752 let mut known_git_refs = view
753 .git_refs()
754 .iter()
755 .filter_map(|(full_name, target)| {
756 let (kind, symbol) =
758 parse_git_ref(full_name).expect("stored git ref should be parsable");
759 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
760 })
761 .collect();
762 let mut known_remote_bookmarks = view
763 .all_remote_bookmarks()
764 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
765 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
766 .collect();
767 let mut known_remote_tags = if all_remote_tags {
768 view.all_remote_tags()
769 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
770 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
771 .collect()
772 } else {
773 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
774 view.remote_tags(remote)
775 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
776 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
777 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
778 .collect()
779 };
780
781 let mut changed_git_refs = Vec::new();
786 let mut changed_remote_bookmarks = Vec::new();
787 let mut changed_remote_tags = Vec::new();
788 let mut failed_ref_names = Vec::new();
789 let actual = git_repo.references().map_err(GitImportError::from_git)?;
790 collect_changed_refs_to_import(
791 actual.local_branches().map_err(GitImportError::from_git)?,
792 &mut known_git_refs,
793 &mut known_remote_bookmarks,
794 &mut changed_git_refs,
795 &mut changed_remote_bookmarks,
796 &mut failed_ref_names,
797 &git_ref_filter,
798 )?;
799 collect_changed_refs_to_import(
800 actual.remote_branches().map_err(GitImportError::from_git)?,
801 &mut known_git_refs,
802 &mut known_remote_bookmarks,
803 &mut changed_git_refs,
804 &mut changed_remote_bookmarks,
805 &mut failed_ref_names,
806 &git_ref_filter,
807 )?;
808 collect_changed_refs_to_import(
809 actual.tags().map_err(GitImportError::from_git)?,
810 &mut known_git_refs,
811 &mut known_remote_tags,
812 &mut changed_git_refs,
813 &mut changed_remote_tags,
814 &mut failed_ref_names,
815 &git_ref_filter,
816 )?;
817 if all_remote_tags {
818 collect_changed_remote_tags_to_import(
819 actual
820 .prefixed(REMOTE_TAG_REF_NAMESPACE)
821 .map_err(GitImportError::from_git)?,
822 &mut known_remote_tags,
823 &mut changed_remote_tags,
824 &mut failed_ref_names,
825 &git_ref_filter,
826 )?;
827 }
828 for full_name in known_git_refs.into_keys() {
829 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
830 }
831 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
832 if old.is_present() {
833 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
834 }
835 }
836 for (RemoteRefKey(symbol), old) in known_remote_tags {
837 if old.is_present() {
838 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
839 }
840 }
841
842 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
844 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
845 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
846 failed_ref_names.sort_unstable();
847 Ok(RefsToImport {
848 changed_git_refs,
849 changed_remote_bookmarks,
850 changed_remote_tags,
851 failed_ref_names,
852 })
853}
854
855fn collect_changed_refs_to_import(
856 actual_git_refs: gix::reference::iter::Iter,
857 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
858 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
859 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
860 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
861 failed_ref_names: &mut Vec<BString>,
862 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
863) -> Result<(), GitImportError> {
864 for git_ref in actual_git_refs {
865 let git_ref = git_ref.map_err(GitImportError::from_git)?;
866 let full_name_bytes = git_ref.name().as_bstr();
867 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
868 failed_ref_names.push(full_name_bytes.to_owned());
870 continue;
871 };
872 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
873 failed_ref_names.push(full_name_bytes.to_owned());
874 continue;
875 }
876 let full_name = GitRefName::new(full_name);
877 let Some((kind, symbol)) = parse_git_ref(full_name) else {
878 continue;
880 };
881 if !git_ref_filter(kind, symbol) {
882 continue;
883 }
884 let old_git_target = known_git_refs.get(full_name).copied().flatten();
885 let old_git_oid = old_git_target.as_normal().map(oid_from_commit_id);
886 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
887 continue;
889 };
890 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
891 known_git_refs.remove(full_name);
892 if new_target != *old_git_target {
893 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
894 }
895 let old_remote_ref = known_remote_refs
898 .remove(&symbol)
899 .unwrap_or_else(|| RemoteRef::absent_ref());
900 if new_target != old_remote_ref.target {
901 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
902 }
903 }
904 Ok(())
905}
906
907fn collect_changed_remote_tags_to_import(
910 actual_git_refs: gix::reference::iter::Iter,
911 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
912 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
913 failed_ref_names: &mut Vec<BString>,
914 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
915) -> Result<(), GitImportError> {
916 for git_ref in actual_git_refs {
917 let git_ref = git_ref.map_err(GitImportError::from_git)?;
918 let full_name_bytes = git_ref.name().as_bstr();
919 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
920 failed_ref_names.push(full_name_bytes.to_owned());
922 continue;
923 };
924 let full_name = GitRefName::new(full_name);
925 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
926 continue;
928 };
929 if !git_ref_filter(kind, symbol) {
930 continue;
931 }
932 let old_remote_ref = known_remote_refs
933 .get(&symbol)
934 .copied()
935 .unwrap_or_else(|| RemoteRef::absent_ref());
936 let old_git_oid = old_remote_ref.target.as_normal().map(oid_from_commit_id);
937 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
938 continue;
940 };
941 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
942 known_remote_refs.remove(&symbol);
943 if new_target != old_remote_ref.target {
944 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
945 }
946 }
947 Ok(())
948}
949
950fn default_remote_ref_state_for(
951 kind: GitRefKind,
952 symbol: RemoteRefSymbol<'_>,
953 options: &GitImportOptions,
954) -> RemoteRefState {
955 match kind {
956 GitRefKind::Bookmark => {
957 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
958 || options.auto_local_bookmark
959 || options
960 .remote_auto_track_bookmarks
961 .get(symbol.remote)
962 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
963 {
964 RemoteRefState::Tracked
965 } else {
966 RemoteRefState::New
967 }
968 }
969 GitRefKind::Tag => RemoteRefState::Tracked,
971 }
972}
973
974fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
980 itertools::chain(view.local_bookmarks(), view.local_tags())
981 .flat_map(|(_, target)| target.added_ids())
982 .cloned()
983 .collect()
984}
985
986fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
993 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
994 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
995 .map(|(_, remote_ref)| &remote_ref.target)
996 .flat_map(|target| target.added_ids())
997 .cloned()
998 .collect()
999}
1000
1001pub async fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
1009 let store = mut_repo.store();
1010 let git_backend = get_git_backend(store)?;
1011 let git_repo = git_backend.git_repo();
1012
1013 let old_git_head = mut_repo.view().git_head();
1014 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1015 Some(CommitId::from_bytes(oid.as_bytes()))
1016 } else {
1017 None
1018 };
1019 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1020 return Ok(());
1021 }
1022
1023 if let Some(head_id) = &new_git_head_id {
1025 let index = mut_repo.index();
1026 if !index.has_id(head_id)? {
1027 git_backend.import_head_commits([head_id]).map_err(|err| {
1028 GitImportError::MissingHeadTarget {
1029 id: head_id.clone(),
1030 err,
1031 }
1032 })?;
1033 }
1034 let commit = store
1037 .get_commit_async(head_id)
1038 .await
1039 .map_err(GitImportError::Backend)?;
1040 mut_repo
1041 .add_head(&commit)
1042 .await
1043 .map_err(GitImportError::Backend)?;
1044 }
1045
1046 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1047 Ok(())
1048}
1049
1050#[derive(Error, Debug)]
1051pub enum GitExportError {
1052 #[error(transparent)]
1053 Git(Box<dyn std::error::Error + Send + Sync>),
1054 #[error(transparent)]
1055 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1056}
1057
1058impl GitExportError {
1059 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1060 Self::Git(source.into())
1061 }
1062}
1063
1064#[derive(Debug, Error)]
1066pub enum FailedRefExportReason {
1067 #[error("Name is not allowed in Git")]
1069 InvalidGitName,
1070 #[error("Ref was in a conflicted state from the last import")]
1073 ConflictedOldState,
1074 #[error("Ref cannot point to the root commit in Git")]
1076 OnRootCommit,
1077 #[error("Deleted ref had been modified in Git")]
1079 DeletedInJjModifiedInGit,
1080 #[error("Added ref had been added with a different target in Git")]
1082 AddedInJjAddedInGit,
1083 #[error("Modified ref had been deleted in Git")]
1085 ModifiedInJjDeletedInGit,
1086 #[error("Failed to delete")]
1088 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1089 #[error("Failed to set")]
1091 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1092}
1093
1094#[derive(Debug)]
1096pub struct GitExportStats {
1097 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1099 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1103}
1104
1105#[derive(Debug)]
1106struct AllRefsToExport {
1107 bookmarks: RefsToExport,
1108 tags: RefsToExport,
1109}
1110
1111#[derive(Debug)]
1112struct RefsToExport {
1113 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1115 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1120 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1122}
1123
1124pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1133 export_some_refs(mut_repo, |_, _| true)
1134}
1135
1136pub fn export_some_refs(
1137 mut_repo: &mut MutableRepo,
1138 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1139) -> Result<GitExportStats, GitExportError> {
1140 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1141 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1142 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1143 let (_, value) = &map[index];
1144 Some(value)
1145 }
1146
1147 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1148 mut_repo.view(),
1149 mut_repo.store().root_commit_id(),
1150 &git_ref_filter,
1151 );
1152
1153 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1154 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1155 return Ok(());
1156 };
1157 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1158 if let Some((kind, symbol)) = target_name
1159 .as_ref()
1160 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1161 .and_then(|name| parse_git_ref(name.as_ref()))
1162 {
1163 let old_target = head_ref.inner.target.clone();
1164 let current_oid = match head_ref.into_fully_peeled_id() {
1165 Ok(id) => Some(id.detach()),
1166 Err(gix::reference::peel::Error::ToId(
1167 gix::refs::peel::to_id::Error::FollowToObject(
1168 gix::refs::peel::to_object::Error::Follow(
1169 gix::refs::file::find::existing::Error::NotFound { .. },
1170 ),
1171 ),
1172 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1174 };
1175 let refs = match kind {
1176 GitRefKind::Bookmark => &bookmarks,
1177 GitRefKind::Tag => &tags,
1178 };
1179 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1180 Some(new_oid)
1181 } else if get(&refs.to_delete, symbol).is_some() {
1182 None
1183 } else {
1184 current_oid.as_ref()
1185 };
1186 if new_oid != current_oid.as_ref() {
1187 update_git_head(
1188 git_repo,
1189 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1190 current_oid,
1191 )
1192 .map_err(GitExportError::from_git)?;
1193 }
1194 }
1195 Ok(())
1196 };
1197
1198 let git_repo = get_git_repo(mut_repo.store())?;
1199
1200 check_and_detach_head(&git_repo)?;
1201 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1202 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1203 check_and_detach_head(&worktree_repo)?;
1204 }
1205 }
1206
1207 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1208 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1209
1210 copy_exportable_local_bookmarks_to_remote_view(
1211 mut_repo,
1212 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1213 |name| {
1214 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1215 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1216 },
1217 );
1218 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1219 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1220 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1221 });
1222
1223 Ok(GitExportStats {
1224 failed_bookmarks,
1225 failed_tags,
1226 })
1227}
1228
1229fn export_refs_to_git(
1230 mut_repo: &mut MutableRepo,
1231 git_repo: &gix::Repository,
1232 kind: GitRefKind,
1233 refs: RefsToExport,
1234) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1235 let mut failed = refs.failed;
1236 for (symbol, old_oid) in refs.to_delete {
1237 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1238 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1239 continue;
1240 };
1241 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1242 failed.push((symbol, reason));
1243 } else {
1244 let new_target = RefTarget::absent();
1245 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1246 }
1247 }
1248 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1249 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1250 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1251 continue;
1252 };
1253 let new_ref_oid = match kind {
1254 GitRefKind::Bookmark => None,
1255 GitRefKind::Tag => {
1257 let remote_matcher = StringMatcher::all();
1258 find_git_tag_oid_to_copy(
1259 mut_repo.view(),
1260 git_repo,
1261 &symbol.name,
1262 &remote_matcher,
1263 &new_commit_oid,
1264 )
1265 }
1266 };
1267 if let Err(reason) = update_git_ref(
1268 git_repo,
1269 &git_ref_name,
1270 old_commit_oid,
1271 new_commit_oid,
1272 new_ref_oid,
1273 ) {
1274 failed.push((symbol, reason));
1275 } else {
1276 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1277 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1278 }
1279 }
1280
1281 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1283 failed
1284}
1285
1286fn copy_exportable_local_bookmarks_to_remote_view(
1287 mut_repo: &mut MutableRepo,
1288 remote: &RemoteName,
1289 name_filter: impl Fn(&RefName) -> bool,
1290) {
1291 let new_local_bookmarks = mut_repo
1292 .view()
1293 .local_remote_bookmarks(remote)
1294 .filter_map(|(name, targets)| {
1295 let old_target = &targets.remote_ref.target;
1298 let new_target = targets.local_target;
1299 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1300 })
1301 .filter(|&(name, _)| name_filter(name))
1302 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1303 .collect_vec();
1304 for (name, new_target) in new_local_bookmarks {
1305 let new_remote_ref = RemoteRef {
1306 target: new_target,
1307 state: RemoteRefState::Tracked,
1308 };
1309 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1310 }
1311}
1312
1313fn copy_exportable_local_tags_to_remote_view(
1314 mut_repo: &mut MutableRepo,
1315 remote: &RemoteName,
1316 name_filter: impl Fn(&RefName) -> bool,
1317) {
1318 let new_local_tags = mut_repo
1319 .view()
1320 .local_remote_tags(remote)
1321 .filter_map(|(name, targets)| {
1322 let old_target = &targets.remote_ref.target;
1324 let new_target = targets.local_target;
1325 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1326 })
1327 .filter(|&(name, _)| name_filter(name))
1328 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1329 .collect_vec();
1330 for (name, new_target) in new_local_tags {
1331 let new_remote_ref = RemoteRef {
1332 target: new_target,
1333 state: RemoteRefState::Tracked,
1334 };
1335 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1336 }
1337}
1338
1339fn diff_refs_to_export(
1341 view: &View,
1342 root_commit_id: &CommitId,
1343 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1344) -> AllRefsToExport {
1345 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1348 itertools::chain(
1349 view.local_bookmarks().map(|(name, target)| {
1350 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1351 (symbol, target)
1352 }),
1353 view.all_remote_bookmarks()
1354 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1355 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1356 )
1357 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1358 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1359 .collect();
1360 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1362 .local_tags()
1363 .map(|(name, target)| {
1364 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1365 (symbol, target)
1366 })
1367 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1368 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1369 .collect();
1370 let known_git_refs = view
1371 .git_refs()
1372 .iter()
1373 .map(|(full_name, target)| {
1374 let (kind, symbol) =
1375 parse_git_ref(full_name).expect("stored git ref should be parsable");
1376 ((kind, symbol), target)
1377 })
1378 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1382 for ((kind, symbol), target) in known_git_refs {
1383 let ref_targets = match kind {
1384 GitRefKind::Bookmark => &mut all_bookmark_targets,
1385 GitRefKind::Tag => &mut all_tag_targets,
1386 };
1387 ref_targets
1388 .entry(symbol)
1389 .and_modify(|(old_target, _)| *old_target = target)
1390 .or_insert((target, RefTarget::absent_ref()));
1391 }
1392
1393 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1394 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1395 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1396 AllRefsToExport { bookmarks, tags }
1397}
1398
1399fn collect_changed_refs_to_export(
1400 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1401 root_commit_target: &RefTarget,
1402) -> RefsToExport {
1403 let mut to_update = Vec::new();
1404 let mut to_delete = Vec::new();
1405 let mut failed = Vec::new();
1406 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1407 if new_target == old_target {
1408 continue;
1409 }
1410 if new_target == root_commit_target {
1411 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1413 continue;
1414 }
1415 let old_oid = if let Some(id) = old_target.as_normal() {
1416 Some(owned_oid_from_commit_id(id))
1417 } else if old_target.has_conflict() {
1418 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1421 continue;
1422 } else {
1423 assert!(old_target.is_absent());
1424 None
1425 };
1426 if let Some(id) = new_target.as_normal() {
1427 let new_oid = owned_oid_from_commit_id(id);
1428 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1429 } else if new_target.has_conflict() {
1430 continue;
1432 } else {
1433 assert!(new_target.is_absent());
1434 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1435 }
1436 }
1437
1438 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1440 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1441 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1442 RefsToExport {
1443 to_update,
1444 to_delete,
1445 failed,
1446 }
1447}
1448
1449fn find_git_tag_oid_to_copy(
1452 view: &View,
1453 git_repo: &gix::Repository,
1454 name: &RefName,
1455 remote_matcher: &StringMatcher,
1456 commit_oid: &gix::oid,
1457) -> Option<gix::ObjectId> {
1458 view.remote_tags_matching(&StringMatcher::exact(name), remote_matcher)
1460 .filter(|(_, remote_ref)| {
1461 let maybe_id = remote_ref.tracked_target().as_normal();
1462 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1463 })
1464 .filter_map(|(symbol, _)| {
1466 let git_ref_name = to_git_or_remote_tag_ref_name(symbol);
1467 git_repo.find_reference(git_ref_name.as_str()).ok()
1468 })
1469 .filter(|git_ref| {
1472 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1473 })
1474 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1475}
1476
1477fn delete_git_ref(
1478 git_repo: &gix::Repository,
1479 git_ref_name: &GitRefName,
1480 old_oid: &gix::oid,
1481) -> Result<(), FailedRefExportReason> {
1482 let Some(git_ref) = git_repo
1483 .try_find_reference(git_ref_name.as_str())
1484 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1485 else {
1486 return Ok(());
1488 };
1489 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1490 git_ref
1492 .delete()
1493 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1494 } else {
1495 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1497 }
1498}
1499
1500fn create_git_ref(
1502 git_repo: &gix::Repository,
1503 git_ref_name: &GitRefName,
1504 new_commit_oid: gix::ObjectId,
1505 new_ref_oid: Option<gix::ObjectId>,
1506) -> Result<(), FailedRefExportReason> {
1507 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1508 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1509 let Err(set_err) =
1510 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1511 else {
1512 return Ok(());
1514 };
1515 let Some(git_ref) = git_repo
1516 .try_find_reference(git_ref_name.as_str())
1517 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1518 else {
1519 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1520 };
1521 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1524 Ok(())
1525 } else {
1526 Err(FailedRefExportReason::AddedInJjAddedInGit)
1527 }
1528}
1529
1530fn move_git_ref(
1532 git_repo: &gix::Repository,
1533 git_ref_name: &GitRefName,
1534 old_commit_oid: gix::ObjectId,
1535 new_commit_oid: gix::ObjectId,
1536 new_ref_oid: Option<gix::ObjectId>,
1537) -> Result<(), FailedRefExportReason> {
1538 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1539 let constraint =
1540 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1541 let Err(set_err) =
1542 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1543 else {
1544 return Ok(());
1546 };
1547 let Some(git_ref) = git_repo
1549 .try_find_reference(git_ref_name.as_str())
1550 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1551 else {
1552 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1554 };
1555 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1557 if git_commit_oid == Some(new_commit_oid) {
1558 Ok(())
1559 } else if git_commit_oid == Some(old_commit_oid) {
1560 let constraint =
1562 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1563 git_repo
1564 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1565 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1566 Ok(())
1567 } else {
1568 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1569 }
1570}
1571
1572fn update_git_ref(
1573 git_repo: &gix::Repository,
1574 git_ref_name: &GitRefName,
1575 old_commit_oid: Option<gix::ObjectId>,
1576 new_commit_oid: gix::ObjectId,
1577 new_ref_oid: Option<gix::ObjectId>,
1578) -> Result<(), FailedRefExportReason> {
1579 match old_commit_oid {
1580 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1581 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1582 }
1583}
1584
1585fn update_git_head(
1588 git_repo: &gix::Repository,
1589 expected_ref: gix::refs::transaction::PreviousValue,
1590 new_oid: Option<gix::ObjectId>,
1591) -> Result<(), gix::reference::edit::Error> {
1592 let mut ref_edits = Vec::new();
1593 let new_target = if let Some(oid) = new_oid {
1594 gix::refs::Target::Object(oid)
1595 } else {
1596 ref_edits.push(gix::refs::transaction::RefEdit {
1601 change: gix::refs::transaction::Change::Delete {
1602 expected: gix::refs::transaction::PreviousValue::Any,
1603 log: gix::refs::transaction::RefLog::AndReference,
1604 },
1605 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1606 deref: false,
1607 });
1608 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1609 };
1610 ref_edits.push(gix::refs::transaction::RefEdit {
1611 change: gix::refs::transaction::Change::Update {
1612 log: gix::refs::transaction::LogChange {
1613 message: "export from jj".into(),
1614 ..Default::default()
1615 },
1616 expected: expected_ref,
1617 new: new_target,
1618 },
1619 name: "HEAD".try_into().unwrap(),
1620 deref: false,
1621 });
1622 git_repo.edit_references(ref_edits)?;
1623 Ok(())
1624}
1625
1626#[derive(Debug, Error)]
1627pub enum GitResetHeadError {
1628 #[error(transparent)]
1629 Backend(#[from] BackendError),
1630 #[error(transparent)]
1631 Git(Box<dyn std::error::Error + Send + Sync>),
1632 #[error("Failed to update Git HEAD ref")]
1633 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1634 #[error(transparent)]
1635 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1636}
1637
1638impl GitResetHeadError {
1639 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1640 Self::Git(source.into())
1641 }
1642}
1643
1644pub async fn reset_head(
1647 mut_repo: &mut MutableRepo,
1648 wc_commit: &Commit,
1649) -> Result<(), GitResetHeadError> {
1650 let git_repo = get_git_repo(mut_repo.store())?;
1651
1652 let first_parent_id = &wc_commit.parent_ids()[0];
1653 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1654 RefTarget::normal(first_parent_id.clone())
1655 } else {
1656 RefTarget::absent()
1657 };
1658
1659 let old_head_target = mut_repo.git_head();
1661 if old_head_target != new_head_target {
1662 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1663 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1666 if actual_head.is_detached() {
1667 let id = owned_oid_from_commit_id(id);
1668 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1669 } else {
1670 gix::refs::transaction::PreviousValue::MustExist
1673 }
1674 } else {
1675 gix::refs::transaction::PreviousValue::MustExist
1677 };
1678 let new_oid = new_head_target.as_normal().map(owned_oid_from_commit_id);
1679 update_git_head(&git_repo, expected_ref, new_oid)
1680 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1681 mut_repo.set_git_head_target(new_head_target);
1682 }
1683
1684 if git_repo.state().is_some() {
1687 clear_operation_state(&git_repo)?;
1688 }
1689
1690 reset_index(mut_repo, &git_repo, wc_commit).await
1691}
1692
1693fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1695 const STATE_FILE_NAMES: &[&str] = &[
1699 "MERGE_HEAD",
1700 "MERGE_MODE",
1701 "MERGE_MSG",
1702 "REVERT_HEAD",
1703 "CHERRY_PICK_HEAD",
1704 "BISECT_LOG",
1705 ];
1706 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1707 let handle_err = |err: PathError| match err.source.kind() {
1708 std::io::ErrorKind::NotFound => Ok(()),
1709 _ => Err(GitResetHeadError::from_git(err)),
1710 };
1711 for file_name in STATE_FILE_NAMES {
1712 let path = git_repo.path().join(file_name);
1713 std::fs::remove_file(&path)
1714 .context(&path)
1715 .or_else(handle_err)?;
1716 }
1717 for dir_name in STATE_DIR_NAMES {
1718 let path = git_repo.path().join(dir_name);
1719 std::fs::remove_dir_all(&path)
1720 .context(&path)
1721 .or_else(handle_err)?;
1722 }
1723 Ok(())
1724}
1725
1726async fn reset_index(
1727 repo: &dyn Repo,
1728 git_repo: &gix::Repository,
1729 wc_commit: &Commit,
1730) -> Result<(), GitResetHeadError> {
1731 let parent_tree = wc_commit.parent_tree(repo).await?;
1732 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1736 if tree_id == repo.store().empty_tree_id() {
1737 gix::index::File::from_state(
1741 gix::index::State::new(git_repo.object_hash()),
1742 git_repo.index_path(),
1743 )
1744 } else {
1745 git_repo
1748 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1749 .map_err(GitResetHeadError::from_git)?
1750 }
1751 } else {
1752 build_index_from_merged_tree(git_repo, &parent_tree)?
1753 };
1754
1755 let wc_tree = wc_commit.tree();
1756 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).await?;
1757
1758 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1761 index
1762 .entries_mut_with_paths()
1763 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1764 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1765 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1766 })
1767 .filter_map(|merged| merged.both())
1768 .map(|((entry, _), old_entry)| (entry, old_entry))
1769 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1770 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1771 }
1772
1773 debug_assert!(index.verify_entries().is_ok());
1774
1775 index
1776 .write(gix::index::write::Options::default())
1777 .map_err(GitResetHeadError::from_git)
1778}
1779
1780fn build_index_from_merged_tree(
1781 git_repo: &gix::Repository,
1782 merged_tree: &MergedTree,
1783) -> Result<gix::index::File, GitResetHeadError> {
1784 let mut index = gix::index::File::from_state(
1785 gix::index::State::new(git_repo.object_hash()),
1786 git_repo.index_path(),
1787 );
1788
1789 let mut push_index_entry =
1790 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1791 let Some(entry) = maybe_entry else {
1792 return;
1793 };
1794
1795 let (id, mode) = match entry {
1796 TreeValue::File {
1797 id,
1798 executable,
1799 copy_id: _,
1800 } => {
1801 if *executable {
1802 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1803 } else {
1804 (id.as_bytes(), gix::index::entry::Mode::FILE)
1805 }
1806 }
1807 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1808 TreeValue::Tree(_) => {
1809 return;
1814 }
1815 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1816 };
1817
1818 let path = BStr::new(path.as_internal_file_string());
1819
1820 index.dangerously_push_entry(
1823 gix::index::entry::Stat::default(),
1824 gix::ObjectId::from_bytes_or_panic(id),
1825 gix::index::entry::Flags::from_stage(stage),
1826 mode,
1827 path,
1828 );
1829 };
1830
1831 let mut has_many_sided_conflict = false;
1832
1833 for (path, entry) in merged_tree.entries() {
1834 let entry = entry?;
1835 if let Some(resolved) = entry.as_resolved() {
1836 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1837 continue;
1838 }
1839
1840 let conflict = entry.simplify();
1841 if let [left, base, right] = conflict.as_slice() {
1842 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1844 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1845 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1846 } else {
1847 has_many_sided_conflict = true;
1855 push_index_entry(
1856 &path,
1857 conflict.first(),
1858 gix::index::entry::Stage::Unconflicted,
1859 );
1860 }
1861 }
1862
1863 index.sort_entries();
1866
1867 if has_many_sided_conflict
1870 && index
1871 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1872 .is_err()
1873 {
1874 let file_blob = git_repo
1875 .write_blob(
1876 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1877 )
1878 .map_err(GitResetHeadError::from_git)?;
1879 index.dangerously_push_entry(
1880 gix::index::entry::Stat::default(),
1881 file_blob.detach(),
1882 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1883 gix::index::entry::Mode::FILE,
1884 INDEX_DUMMY_CONFLICT_FILE.into(),
1885 );
1886 index.sort_entries();
1889 }
1890
1891 Ok(index)
1892}
1893
1894pub async fn update_intent_to_add(
1901 repo: &dyn Repo,
1902 old_tree: &MergedTree,
1903 new_tree: &MergedTree,
1904) -> Result<(), GitResetHeadError> {
1905 let git_repo = get_git_repo(repo.store())?;
1906 let mut index = git_repo
1907 .index_or_empty()
1908 .map_err(GitResetHeadError::from_git)?;
1909 let mut_index = Arc::make_mut(&mut index);
1910 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).await?;
1911 debug_assert!(mut_index.verify_entries().is_ok());
1912 mut_index
1913 .write(gix::index::write::Options::default())
1914 .map_err(GitResetHeadError::from_git)?;
1915
1916 Ok(())
1917}
1918
1919async fn update_intent_to_add_impl(
1920 git_repo: &gix::Repository,
1921 index: &mut gix::index::File,
1922 old_tree: &MergedTree,
1923 new_tree: &MergedTree,
1924) -> Result<(), GitResetHeadError> {
1925 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1926 let mut added_paths = vec![];
1927 let mut removed_paths = HashSet::new();
1928 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1929 let values = values?;
1930 if values.before.is_absent() {
1931 let executable = match values.after.as_normal() {
1932 Some(TreeValue::File {
1933 id: _,
1934 executable,
1935 copy_id: _,
1936 }) => *executable,
1937 Some(TreeValue::Symlink(_)) => false,
1938 _ => {
1939 continue;
1940 }
1941 };
1942 if index
1943 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1944 .is_err()
1945 {
1946 added_paths.push((BString::from(path.into_internal_string()), executable));
1947 }
1948 } else if values.after.is_absent() {
1949 removed_paths.insert(BString::from(path.into_internal_string()));
1950 }
1951 }
1952
1953 if added_paths.is_empty() && removed_paths.is_empty() {
1954 return Ok(());
1955 }
1956
1957 if !added_paths.is_empty() {
1958 let empty_blob = git_repo
1960 .write_blob(b"")
1961 .map_err(GitResetHeadError::from_git)?
1962 .detach();
1963 for (path, executable) in added_paths {
1964 index.dangerously_push_entry(
1966 gix::index::entry::Stat::default(),
1967 empty_blob,
1968 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1969 if executable {
1970 gix::index::entry::Mode::FILE_EXECUTABLE
1971 } else {
1972 gix::index::entry::Mode::FILE
1973 },
1974 path.as_ref(),
1975 );
1976 }
1977 }
1978 if !removed_paths.is_empty() {
1979 index.remove_entries(|_size, path, entry| {
1980 entry
1981 .flags
1982 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1983 && removed_paths.contains(path)
1984 });
1985 }
1986
1987 index.sort_entries();
1988
1989 Ok(())
1990}
1991
1992#[derive(Debug, Error)]
1993pub enum GitRemoteManagementError {
1994 #[error("No git remote named '{}'", .0.as_symbol())]
1995 NoSuchRemote(RemoteNameBuf),
1996 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1997 RemoteAlreadyExists(RemoteNameBuf),
1998 #[error(transparent)]
1999 RemoteName(#[from] GitRemoteNameError),
2000 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
2001 NonstandardConfiguration(RemoteNameBuf),
2002 #[error("Error saving Git configuration")]
2003 GitConfigSaveError(#[source] std::io::Error),
2004 #[error("Unexpected Git error when managing remotes")]
2005 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
2006 #[error(transparent)]
2007 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2008 #[error(transparent)]
2009 RefExpansionError(#[from] GitRefExpansionError),
2010}
2011
2012impl GitRemoteManagementError {
2013 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
2014 Self::InternalGitError(source.into())
2015 }
2016}
2017
2018fn default_fetch_refspec(remote: &RemoteName) -> String {
2019 format!(
2020 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
2021 remote = remote.as_str()
2022 )
2023}
2024
2025fn add_ref(
2026 name: gix::refs::FullName,
2027 target: gix::refs::Target,
2028 message: BString,
2029) -> gix::refs::transaction::RefEdit {
2030 gix::refs::transaction::RefEdit {
2031 change: gix::refs::transaction::Change::Update {
2032 log: gix::refs::transaction::LogChange {
2033 mode: gix::refs::transaction::RefLog::AndReference,
2034 force_create_reflog: false,
2035 message,
2036 },
2037 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2038 new: target,
2039 },
2040 name,
2041 deref: false,
2042 }
2043}
2044
2045fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2046 gix::refs::transaction::RefEdit {
2047 change: gix::refs::transaction::Change::Delete {
2048 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2049 reference.target().into_owned(),
2050 ),
2051 log: gix::refs::transaction::RefLog::AndReference,
2052 },
2053 name: reference.name().to_owned(),
2054 deref: false,
2055 }
2056}
2057
2058pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2064 let mut config_file = File::create(
2065 config
2066 .meta()
2067 .path
2068 .as_ref()
2069 .expect("Git repository to have a config file"),
2070 )?;
2071 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2072}
2073
2074fn save_remote(
2075 config: &mut gix::config::File<'static>,
2076 remote_name: &RemoteName,
2077 remote: &mut gix::Remote,
2078) -> Result<(), GitRemoteManagementError> {
2079 config
2086 .new_section(
2087 "remote",
2088 Some(Cow::Owned(BString::from(remote_name.as_str()))),
2089 )
2090 .map_err(GitRemoteManagementError::from_git)?;
2091 remote
2092 .save_as_to(remote_name.as_str(), config)
2093 .map_err(GitRemoteManagementError::from_git)?;
2094 Ok(())
2095}
2096
2097fn git_config_branch_section_ids_by_remote(
2098 config: &gix::config::File,
2099 remote_name: &RemoteName,
2100) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2101 config
2102 .sections_by_name("branch")
2103 .into_iter()
2104 .flatten()
2105 .filter_map(|section| {
2106 let remote_values = section.values("remote");
2107 let push_remote_values = section.values("pushRemote");
2108 if !remote_values
2109 .iter()
2110 .chain(push_remote_values.iter())
2111 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2112 {
2113 return None;
2114 }
2115 let is_supported_key = |name: &gix::config::parse::section::ValueName| -> bool {
2117 name.eq_ignore_ascii_case(b"remote")
2118 || name.eq_ignore_ascii_case(b"merge")
2119 || name.eq_ignore_ascii_case(b"rebase")
2120 };
2121 if remote_values.len() > 1
2122 || push_remote_values.len() > 1
2123 || !section.value_names().all(is_supported_key)
2124 {
2125 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2126 remote_name.to_owned(),
2127 )));
2128 }
2129 Some(Ok(section.id()))
2130 })
2131 .collect()
2132}
2133
2134fn rename_remote_in_git_branch_config_sections(
2135 config: &mut gix::config::File,
2136 old_remote_name: &RemoteName,
2137 new_remote_name: &RemoteName,
2138) -> Result<(), GitRemoteManagementError> {
2139 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2140 config
2141 .section_mut_by_id(id)
2142 .expect("found section to exist")
2143 .set(
2144 "remote"
2145 .try_into()
2146 .expect("'remote' to be a valid value name"),
2147 BStr::new(new_remote_name.as_str()),
2148 );
2149 }
2150 Ok(())
2151}
2152
2153fn remove_remote_git_branch_config_sections(
2154 config: &mut gix::config::File,
2155 remote_name: &RemoteName,
2156) -> Result<(), GitRemoteManagementError> {
2157 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2158 config
2159 .remove_section_by_id(id)
2160 .expect("removed section to exist");
2161 }
2162 Ok(())
2163}
2164
2165fn remove_remote_git_config_sections(
2166 config: &mut gix::config::File,
2167 remote_name: &RemoteName,
2168) -> Result<(), GitRemoteManagementError> {
2169 let section_ids_to_remove: Vec<_> = config
2170 .sections_by_name("remote")
2171 .into_iter()
2172 .flatten()
2173 .filter(|section| {
2174 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2175 })
2176 .map(|section| {
2177 if section.value_names().any(|name| {
2178 !name.eq_ignore_ascii_case(b"url")
2179 && !name.eq_ignore_ascii_case(b"fetch")
2180 && !name.eq_ignore_ascii_case(b"tagOpt")
2181 }) {
2182 return Err(GitRemoteManagementError::NonstandardConfiguration(
2183 remote_name.to_owned(),
2184 ));
2185 }
2186 Ok(section.id())
2187 })
2188 .try_collect()?;
2189 for id in section_ids_to_remove {
2190 config
2191 .remove_section_by_id(id)
2192 .expect("removed section to exist");
2193 }
2194 Ok(())
2195}
2196
2197pub fn get_all_remote_names(
2199 store: &Store,
2200) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2201 let git_repo = get_git_repo(store)?;
2202 Ok(iter_remote_names(&git_repo).collect())
2203}
2204
2205fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2206 git_repo
2207 .remote_names()
2208 .into_iter()
2209 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2211 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2213 .map(RemoteNameBuf::from)
2214}
2215
2216pub fn add_remote(
2217 mut_repo: &mut MutableRepo,
2218 remote_name: &RemoteName,
2219 url: &str,
2220 push_url: Option<&str>,
2221 fetch_tags: gix::remote::fetch::Tags,
2222) -> Result<(), GitRemoteManagementError> {
2223 let git_repo = get_git_repo(mut_repo.store())?;
2224
2225 validate_remote_name(remote_name)?;
2226
2227 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2228 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2229 remote_name.to_owned(),
2230 ));
2231 }
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(
2238 [default_fetch_refspec(remote_name).as_bytes()],
2239 gix::remote::Direction::Fetch,
2240 )
2241 .expect("default refspec to be valid");
2242
2243 if let Some(push_url) = push_url {
2244 remote = remote
2245 .with_push_url(push_url)
2246 .map_err(GitRemoteManagementError::from_git)?;
2247 }
2248
2249 let mut config = git_repo.config_snapshot().clone();
2250 save_remote(&mut config, remote_name, &mut remote)?;
2251 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2252
2253 mut_repo.ensure_remote(remote_name);
2254
2255 Ok(())
2256}
2257
2258pub fn remove_remote(
2259 mut_repo: &mut MutableRepo,
2260 remote_name: &RemoteName,
2261) -> Result<(), GitRemoteManagementError> {
2262 let mut git_repo = get_git_repo(mut_repo.store())?;
2263
2264 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2265 return Err(GitRemoteManagementError::NoSuchRemote(
2266 remote_name.to_owned(),
2267 ));
2268 }
2269
2270 let mut config = git_repo.config_snapshot().clone();
2271 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2272 remove_remote_git_config_sections(&mut config, remote_name)?;
2273 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2274
2275 remove_remote_git_refs(&mut git_repo, remote_name)
2276 .map_err(GitRemoteManagementError::from_git)?;
2277
2278 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2279 remove_remote_refs(mut_repo, remote_name);
2280 }
2281
2282 Ok(())
2283}
2284
2285fn remove_remote_git_refs(
2286 git_repo: &mut gix::Repository,
2287 remote: &RemoteName,
2288) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2289 let bookmark_prefix = format!(
2290 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2291 remote = remote.as_str()
2292 );
2293 let tag_prefix = format!(
2294 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2295 remote = remote.as_str()
2296 );
2297 let edits: Vec<_> = itertools::chain(
2298 git_repo
2299 .references()?
2300 .prefixed(bookmark_prefix.as_str())?
2301 .map_ok(remove_ref),
2302 git_repo
2303 .references()?
2304 .prefixed(tag_prefix.as_str())?
2305 .map_ok(remove_ref),
2306 )
2307 .try_collect()?;
2308 git_repo.edit_references(edits)?;
2309 Ok(())
2310}
2311
2312fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2313 mut_repo.remove_remote(remote);
2314 let prefix = format!(
2315 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2316 remote = remote.as_str()
2317 );
2318 let git_refs_to_delete = mut_repo
2319 .view()
2320 .git_refs()
2321 .keys()
2322 .filter(|&r| r.as_str().starts_with(&prefix))
2323 .cloned()
2324 .collect_vec();
2325 for git_ref in git_refs_to_delete {
2326 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2327 }
2328}
2329
2330pub fn rename_remote(
2331 mut_repo: &mut MutableRepo,
2332 old_remote_name: &RemoteName,
2333 new_remote_name: &RemoteName,
2334) -> Result<(), GitRemoteManagementError> {
2335 let mut git_repo = get_git_repo(mut_repo.store())?;
2336
2337 validate_remote_name(new_remote_name)?;
2338
2339 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2340 return Err(GitRemoteManagementError::NoSuchRemote(
2341 old_remote_name.to_owned(),
2342 ));
2343 };
2344 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2345
2346 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2347 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2348 new_remote_name.to_owned(),
2349 ));
2350 }
2351
2352 match (
2353 remote.refspecs(gix::remote::Direction::Fetch),
2354 remote.refspecs(gix::remote::Direction::Push),
2355 ) {
2356 ([refspec], [])
2357 if refspec.to_ref().to_bstring()
2358 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2359 _ => {
2360 return Err(GitRemoteManagementError::NonstandardConfiguration(
2361 old_remote_name.to_owned(),
2362 ));
2363 }
2364 }
2365
2366 remote
2367 .replace_refspecs(
2368 [default_fetch_refspec(new_remote_name).as_bytes()],
2369 gix::remote::Direction::Fetch,
2370 )
2371 .expect("default refspec to be valid");
2372
2373 let mut config = git_repo.config_snapshot().clone();
2374 save_remote(&mut config, new_remote_name, &mut remote)?;
2375 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2376 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2377 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2378
2379 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2380 .map_err(GitRemoteManagementError::from_git)?;
2381
2382 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2383 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2384 }
2385
2386 Ok(())
2387}
2388
2389fn rename_remote_git_refs(
2390 git_repo: &mut gix::Repository,
2391 old_remote_name: &RemoteName,
2392 new_remote_name: &RemoteName,
2393) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2394 let to_prefixes = |namespace: &str| {
2395 (
2396 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2397 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2398 )
2399 };
2400 let to_rename_edits = {
2401 let ref_log_message = BString::from(format!(
2402 "renamed remote {old_remote_name} to {new_remote_name}",
2403 old_remote_name = old_remote_name.as_symbol(),
2404 new_remote_name = new_remote_name.as_symbol(),
2405 ));
2406 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2407 let new_name = BString::new(
2408 [
2409 new_prefix.as_bytes(),
2410 &old_ref.name().as_bstr()[old_prefix.len()..],
2411 ]
2412 .concat(),
2413 );
2414 [
2415 add_ref(
2416 new_name.try_into().expect("new ref name to be valid"),
2417 old_ref.target().into_owned(),
2418 ref_log_message.clone(),
2419 ),
2420 remove_ref(old_ref),
2421 ]
2422 }
2423 };
2424
2425 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2426 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2427 let edits: Vec<_> = itertools::chain(
2428 git_repo
2429 .references()?
2430 .prefixed(old_bookmark_prefix.as_str())?
2431 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2432 git_repo
2433 .references()?
2434 .prefixed(old_tag_prefix.as_str())?
2435 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2436 )
2437 .flatten_ok()
2438 .try_collect()?;
2439 git_repo.edit_references(edits)?;
2440 Ok(())
2441}
2442
2443pub fn set_remote_urls(
2447 store: &Store,
2448 remote_name: &RemoteName,
2449 new_url: Option<&str>,
2450 new_push_url: Option<&str>,
2451) -> Result<(), GitRemoteManagementError> {
2452 if new_url.is_none() && new_push_url.is_none() {
2454 return Ok(());
2455 }
2456
2457 let git_repo = get_git_repo(store)?;
2458
2459 validate_remote_name(remote_name)?;
2460
2461 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2462 return Err(GitRemoteManagementError::NoSuchRemote(
2463 remote_name.to_owned(),
2464 ));
2465 };
2466 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2467
2468 if let Some(url) = new_url {
2469 remote = remote
2470 .with_url(url)
2471 .map_err(GitRemoteManagementError::from_git)?;
2472 }
2473
2474 if let Some(url) = new_push_url {
2475 remote = remote
2476 .with_push_url(url)
2477 .map_err(GitRemoteManagementError::from_git)?;
2478 }
2479
2480 let mut config = git_repo.config_snapshot().clone();
2481 save_remote(&mut config, remote_name, &mut remote)?;
2482 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2483
2484 Ok(())
2485}
2486
2487fn rename_remote_refs(
2488 mut_repo: &mut MutableRepo,
2489 old_remote_name: &RemoteName,
2490 new_remote_name: &RemoteName,
2491) {
2492 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2493 let prefix = format!(
2494 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2495 remote = old_remote_name.as_str()
2496 );
2497 let git_refs = mut_repo
2498 .view()
2499 .git_refs()
2500 .iter()
2501 .filter_map(|(old, target)| {
2502 old.as_str().strip_prefix(&prefix).map(|p| {
2503 let new: GitRefNameBuf = format!(
2504 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2505 remote = new_remote_name.as_str()
2506 )
2507 .into();
2508 (old.clone(), new, target.clone())
2509 })
2510 })
2511 .collect_vec();
2512 for (old, new, target) in git_refs {
2513 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2514 mut_repo.set_git_ref_target(&new, target);
2515 }
2516}
2517
2518const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2519
2520#[derive(Error, Debug)]
2521pub enum GitFetchError {
2522 #[error("No git remote named '{}'", .0.as_symbol())]
2523 NoSuchRemote(RemoteNameBuf),
2524 #[error(transparent)]
2525 RemoteName(#[from] GitRemoteNameError),
2526 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2527 RejectedUpdates(Vec<GitRefNameBuf>),
2528 #[error(transparent)]
2529 Subprocess(#[from] GitSubprocessError),
2530}
2531
2532#[derive(Error, Debug)]
2533pub enum GitDefaultRefspecError {
2534 #[error("No git remote named '{}'", .0.as_symbol())]
2535 NoSuchRemote(RemoteNameBuf),
2536 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2537 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2538}
2539
2540struct FetchedRefs {
2541 remote: RemoteNameBuf,
2542 bookmark_matcher: StringMatcher,
2543 tag_matcher: StringMatcher,
2544}
2545
2546#[derive(Clone, Debug)]
2548pub struct GitFetchRefExpression {
2549 pub bookmark: StringExpression,
2551 pub tag: StringExpression,
2557}
2558
2559#[derive(Debug)]
2561pub struct ExpandedFetchRefSpecs {
2562 expr: GitFetchRefExpression,
2564 refspecs: Vec<RefSpec>,
2565 negative_refspecs: Vec<NegativeRefSpec>,
2566}
2567
2568#[derive(Error, Debug)]
2569pub enum GitRefExpansionError {
2570 #[error(transparent)]
2571 Expression(#[from] GitRefExpressionError),
2572 #[error(
2573 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2574 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2575 )]
2576 InvalidBranchPattern(StringPattern),
2577}
2578
2579pub fn expand_fetch_refspecs(
2581 remote: &RemoteName,
2582 expr: GitFetchRefExpression,
2583) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2584 let (positive_bookmarks, negative_bookmarks) =
2585 split_into_positive_negative_patterns(&expr.bookmark)?;
2586 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2587
2588 let refspecs = itertools::chain(
2589 positive_bookmarks
2590 .iter()
2591 .map(|&pattern| pattern_to_refspec_glob(pattern))
2592 .map_ok(|glob| {
2593 RefSpec::forced(
2594 format!("refs/heads/{glob}"),
2595 format!(
2596 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2597 remote = remote.as_str()
2598 ),
2599 )
2600 }),
2601 positive_tags
2602 .iter()
2603 .map(|&pattern| pattern_to_refspec_glob(pattern))
2604 .map_ok(|glob| {
2605 RefSpec::forced(
2606 format!("refs/tags/{glob}"),
2607 format!(
2608 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2609 remote = remote.as_str()
2610 ),
2611 )
2612 }),
2613 )
2614 .try_collect()?;
2615
2616 let negative_refspecs = itertools::chain(
2617 negative_bookmarks
2618 .iter()
2619 .map(|&pattern| pattern_to_refspec_glob(pattern))
2620 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2621 negative_tags
2622 .iter()
2623 .map(|&pattern| pattern_to_refspec_glob(pattern))
2624 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2625 )
2626 .try_collect()?;
2627
2628 Ok(ExpandedFetchRefSpecs {
2629 expr,
2630 refspecs,
2631 negative_refspecs,
2632 })
2633}
2634
2635fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2636 pattern
2637 .to_glob()
2638 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2641 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2642}
2643
2644#[derive(Debug, Error)]
2645pub enum GitRefExpressionError {
2646 #[error("Cannot use `~` in sub expression")]
2647 NestedNotIn,
2648 #[error("Cannot use `&` in sub expression")]
2649 NestedIntersection,
2650 #[error("Cannot use `&` for positive expressions")]
2651 PositiveIntersection,
2652}
2653
2654fn split_into_positive_negative_patterns(
2657 expr: &StringExpression,
2658) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2659 static ALL: StringPattern = StringPattern::all();
2660
2661 fn visit_positive<'a>(
2675 expr: &'a StringExpression,
2676 positives: &mut Vec<&'a StringPattern>,
2677 negatives: &mut Vec<&'a StringPattern>,
2678 ) -> Result<(), GitRefExpressionError> {
2679 match expr {
2680 StringExpression::Pattern(pattern) => {
2681 positives.push(pattern);
2682 Ok(())
2683 }
2684 StringExpression::NotIn(complement) => {
2685 positives.push(&ALL);
2686 visit_negative(complement, negatives)
2687 }
2688 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2689 StringExpression::Intersection(expr1, expr2) => {
2690 match (expr1.as_ref(), expr2.as_ref()) {
2691 (other, StringExpression::NotIn(complement))
2692 | (StringExpression::NotIn(complement), other) => {
2693 visit_positive(other, positives, negatives)?;
2694 visit_negative(complement, negatives)
2695 }
2696 _ => Err(GitRefExpressionError::PositiveIntersection),
2697 }
2698 }
2699 }
2700 }
2701
2702 fn visit_negative<'a>(
2703 expr: &'a StringExpression,
2704 negatives: &mut Vec<&'a StringPattern>,
2705 ) -> Result<(), GitRefExpressionError> {
2706 match expr {
2707 StringExpression::Pattern(pattern) => {
2708 negatives.push(pattern);
2709 Ok(())
2710 }
2711 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2712 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2713 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2714 }
2715 }
2716
2717 fn visit_union<'a>(
2718 expr1: &'a StringExpression,
2719 expr2: &'a StringExpression,
2720 patterns: &mut Vec<&'a StringPattern>,
2721 ) -> Result<(), GitRefExpressionError> {
2722 visit_union_sub(expr1, patterns)?;
2723 visit_union_sub(expr2, patterns)
2724 }
2725
2726 fn visit_union_sub<'a>(
2727 expr: &'a StringExpression,
2728 patterns: &mut Vec<&'a StringPattern>,
2729 ) -> Result<(), GitRefExpressionError> {
2730 match expr {
2731 StringExpression::Pattern(pattern) => {
2732 patterns.push(pattern);
2733 Ok(())
2734 }
2735 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2736 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2737 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2738 }
2739 }
2740
2741 let mut positives = Vec::new();
2742 let mut negatives = Vec::new();
2743 visit_positive(expr, &mut positives, &mut negatives)?;
2744 if positives.iter().all(|pattern| pattern.is_all())
2747 && !negatives.is_empty()
2748 && negatives.iter().all(|pattern| pattern.is_all())
2749 {
2750 Ok((vec![], vec![]))
2751 } else {
2752 Ok((positives, negatives))
2753 }
2754}
2755
2756#[derive(Debug)]
2760#[must_use = "warnings should be surfaced in the UI"]
2761pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2762
2763#[derive(Debug)]
2766pub struct IgnoredRefspec {
2767 pub refspec: BString,
2769 pub reason: &'static str,
2771}
2772
2773#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2774enum FetchRefSpecKind {
2775 Positive,
2776 Negative,
2777}
2778
2779pub fn load_default_fetch_bookmarks(
2781 remote_name: &RemoteName,
2782 git_repo: &gix::Repository,
2783) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2784 let remote = git_repo
2785 .try_find_remote(remote_name.as_str())
2786 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2787 .map_err(|e| {
2788 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2789 })?;
2790
2791 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2792 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2793 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2794 let mut negative_bookmarks = Vec::new();
2795 for refspec in remote_refspecs {
2796 let refspec = refspec.to_ref();
2797 match parse_fetch_refspec(remote_name, refspec) {
2798 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2799 positive_bookmarks.push(StringExpression::pattern(bookmark));
2800 }
2801 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2802 negative_bookmarks.push(StringExpression::pattern(bookmark));
2803 }
2804 Err(reason) => {
2805 let refspec = refspec.to_bstring();
2806 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2807 }
2808 }
2809 }
2810
2811 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2812 if !negative_bookmarks.is_empty() {
2814 bookmark_expr =
2815 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2816 }
2817
2818 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2819}
2820
2821fn parse_fetch_refspec(
2822 remote_name: &RemoteName,
2823 refspec: gix::refspec::RefSpecRef<'_>,
2824) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2825 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2826
2827 let (src, positive_dst) = match refspec.instruction() {
2828 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2829 Instruction::Fetch(fetch) => match fetch {
2830 gix::refspec::instruction::Fetch::Only { src: _ } => {
2831 return Err("fetch-only refspecs are not supported");
2832 }
2833 gix::refspec::instruction::Fetch::AndUpdate {
2834 src,
2835 dst,
2836 allow_non_fast_forward,
2837 } => {
2838 if !allow_non_fast_forward {
2839 return Err("non-forced refspecs are not supported");
2840 }
2841 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2842 }
2843 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2844 },
2845 };
2846
2847 let src_branch = src
2848 .strip_prefix("refs/heads/")
2849 .ok_or("only refs/heads/ is supported for refspec sources")?;
2850 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2851
2852 if let Some(dst) = positive_dst {
2853 let dst_without_prefix = dst
2854 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
2855 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2856 let dst_branch = dst_without_prefix
2857 .strip_prefix(remote_name.as_str())
2858 .and_then(|d| d.strip_prefix("/"))
2859 .ok_or("remote renaming not supported")?;
2860 if src_branch != dst_branch {
2861 return Err("renaming is not supported");
2862 }
2863 Ok((FetchRefSpecKind::Positive, branch))
2864 } else {
2865 Ok((FetchRefSpecKind::Negative, branch))
2866 }
2867}
2868
2869pub struct GitFetch<'a> {
2871 mut_repo: &'a mut MutableRepo,
2872 git_repo: Box<gix::Repository>,
2873 git_ctx: GitSubprocessContext,
2874 import_options: &'a GitImportOptions,
2875 fetched: Vec<FetchedRefs>,
2876}
2877
2878impl<'a> GitFetch<'a> {
2879 pub fn new(
2880 mut_repo: &'a mut MutableRepo,
2881 subprocess_options: GitSubprocessOptions,
2882 import_options: &'a GitImportOptions,
2883 ) -> Result<Self, UnexpectedGitBackendError> {
2884 let git_backend = get_git_backend(mut_repo.store())?;
2885 let git_repo = Box::new(git_backend.git_repo());
2886 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
2887 Ok(GitFetch {
2888 mut_repo,
2889 git_repo,
2890 git_ctx,
2891 import_options,
2892 fetched: vec![],
2893 })
2894 }
2895
2896 #[tracing::instrument(skip(self, callback))]
2902 pub fn fetch(
2903 &mut self,
2904 remote_name: &RemoteName,
2905 ExpandedFetchRefSpecs {
2906 expr,
2907 refspecs: mut remaining_refspecs,
2908 negative_refspecs,
2909 }: ExpandedFetchRefSpecs,
2910 callback: &mut dyn GitSubprocessCallback,
2911 depth: Option<NonZeroU32>,
2912 fetch_tags_override: Option<FetchTagsOverride>,
2913 ) -> Result<(), GitFetchError> {
2914 validate_remote_name(remote_name)?;
2915
2916 if self
2918 .git_repo
2919 .try_find_remote(remote_name.as_str())
2920 .is_none()
2921 {
2922 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2923 }
2924
2925 if remaining_refspecs.is_empty() {
2926 return Ok(());
2928 }
2929
2930 let mut branches_to_prune = Vec::new();
2931 let updates = loop {
2939 let status = self.git_ctx.spawn_fetch(
2940 remote_name,
2941 &remaining_refspecs,
2942 &negative_refspecs,
2943 callback,
2944 depth,
2945 fetch_tags_override,
2946 )?;
2947 let failing_refspec = match status {
2948 GitFetchStatus::Updates(updates) => break updates,
2949 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
2950 };
2951 tracing::debug!(failing_refspec, "failed to fetch ref");
2952 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2953
2954 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2955 branches_to_prune.push(format!(
2956 "{remote_name}/{branch_name}",
2957 remote_name = remote_name.as_str()
2958 ));
2959 }
2960 };
2961
2962 if !updates.rejected.is_empty() {
2965 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
2966 return Err(GitFetchError::RejectedUpdates(names));
2967 }
2968
2969 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2972
2973 self.fetched.push(FetchedRefs {
2974 remote: remote_name.to_owned(),
2975 bookmark_matcher: expr.bookmark.to_matcher(),
2976 tag_matcher: expr.tag.to_matcher(),
2977 });
2978 Ok(())
2979 }
2980
2981 #[tracing::instrument(skip(self))]
2983 pub fn get_default_branch(
2984 &self,
2985 remote_name: &RemoteName,
2986 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2987 if self
2988 .git_repo
2989 .try_find_remote(remote_name.as_str())
2990 .is_none()
2991 {
2992 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2993 }
2994 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2995 tracing::debug!(?default_branch);
2996 Ok(default_branch)
2997 }
2998
2999 #[tracing::instrument(skip(self))]
3006 pub async fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
3007 tracing::debug!("import_refs");
3008 let all_remote_tags = true;
3009 let refs_to_import = diff_refs_to_import(
3010 self.mut_repo.view(),
3011 &self.git_repo,
3012 all_remote_tags,
3013 |kind, symbol| match kind {
3014 GitRefKind::Bookmark => self
3015 .fetched
3016 .iter()
3017 .filter(|fetched| fetched.remote == symbol.remote)
3018 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3019 GitRefKind::Tag => {
3020 symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
3024 || self
3025 .fetched
3026 .iter()
3027 .filter(|fetched| fetched.remote == symbol.remote)
3028 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str()))
3029 }
3030 },
3031 )?;
3032 let import_stats =
3033 import_refs_inner(self.mut_repo, refs_to_import, self.import_options).await?;
3034
3035 self.fetched.clear();
3036
3037 Ok(import_stats)
3038 }
3039}
3040
3041#[derive(Error, Debug)]
3042pub enum GitPushError {
3043 #[error("No git remote named '{}'", .0.as_symbol())]
3044 NoSuchRemote(RemoteNameBuf),
3045 #[error(transparent)]
3046 RemoteName(#[from] GitRemoteNameError),
3047 #[error(transparent)]
3048 Subprocess(#[from] GitSubprocessError),
3049 #[error(transparent)]
3050 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3051}
3052
3053#[derive(Clone, Debug, Default)]
3054pub struct GitPushRefTargets {
3055 pub bookmarks: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3057 pub tags: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3059}
3060
3061pub struct GitRefUpdate {
3062 pub qualified_name: GitRefNameBuf,
3063 pub targets: Diff<Option<gix::ObjectId>>,
3068}
3069
3070#[derive(Clone, Debug, Default)]
3072pub struct GitPushOptions {
3073 pub remote_push_options: Vec<String>,
3075}
3076
3077pub fn push_refs(
3079 mut_repo: &mut MutableRepo,
3080 subprocess_options: GitSubprocessOptions,
3081 remote: &RemoteName,
3082 targets: &GitPushRefTargets,
3083 callback: &mut dyn GitSubprocessCallback,
3084 options: &GitPushOptions,
3085) -> Result<GitPushStats, GitPushError> {
3086 validate_remote_name(remote)?;
3087
3088 let git_repo = get_git_repo(mut_repo.store())?;
3089 let to_tag_target = |name: &RefName, remote: &RemoteName, id: &CommitId| {
3090 let remote_matcher = StringMatcher::exact(remote);
3091 let oid = owned_oid_from_commit_id(id);
3092 find_git_tag_oid_to_copy(mut_repo.view(), &git_repo, name, &remote_matcher, &oid)
3093 .unwrap_or(oid)
3094 };
3095 let ref_updates = itertools::chain(
3096 targets.bookmarks.iter().map(|(name, update)| GitRefUpdate {
3097 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3098 targets: update
3099 .as_ref()
3100 .map(|id| id.as_ref().map(owned_oid_from_commit_id)),
3101 }),
3102 targets.tags.iter().map(|(name, update)| GitRefUpdate {
3103 qualified_name: format!("refs/tags/{name}", name = name.as_str()).into(),
3104 targets: Diff {
3105 before: update
3106 .before
3107 .as_ref()
3108 .map(|id| to_tag_target(name, remote, id)),
3109 after: update
3110 .after
3111 .as_ref()
3112 .map(|id| to_tag_target(name, REMOTE_NAME_FOR_LOCAL_GIT_REPO, id)),
3113 },
3114 }),
3115 )
3116 .collect_vec();
3117
3118 let push_stats = push_updates(
3119 mut_repo,
3120 subprocess_options,
3121 remote,
3122 &ref_updates,
3123 callback,
3124 options,
3125 )?;
3126 tracing::debug!(?push_stats);
3127
3128 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3129 let pushed_bookmark_updates = || {
3130 iter::zip(&targets.bookmarks, &ref_updates[..targets.bookmarks.len()])
3131 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3132 .map(|((name, update), _)| (&**name, update))
3133 };
3134 let pushed_tag_updates = || {
3135 iter::zip(&targets.tags, &ref_updates[targets.bookmarks.len()..])
3136 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3137 .map(|((name, update), ref_update)| (&**name, update, ref_update))
3138 };
3139
3140 let unexported_bookmarks = {
3143 let refs = build_pushed_bookmarks_to_export(remote, pushed_bookmark_updates());
3144 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3145 };
3146 for (name, _, ref_update) in pushed_tag_updates() {
3150 let symbol = name.to_remote_symbol(remote);
3151 let edit = to_remote_tag_ref_update(symbol, ref_update.targets.after);
3152 if let Err(err) = git_repo.edit_reference(edit) {
3153 tracing::warn!(?symbol, ?err, "failed to update remote tag ref");
3154 }
3155 }
3156
3157 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3158 let is_exported_bookmark = |name: &RefName| {
3159 unexported_bookmarks
3160 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3161 .is_err()
3162 };
3163 for (name, update) in pushed_bookmark_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3164 let new_remote_ref = RemoteRef {
3165 target: RefTarget::resolved(update.after.clone()),
3166 state: RemoteRefState::Tracked,
3167 };
3168 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3169 }
3170 for (name, update, _) in pushed_tag_updates() {
3171 let new_remote_ref = RemoteRef {
3172 target: RefTarget::resolved(update.after.clone()),
3173 state: RemoteRefState::Tracked,
3174 };
3175 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
3176 }
3177
3178 assert!(push_stats.unexported_bookmarks.is_empty());
3182 let push_stats = GitPushStats {
3183 pushed: push_stats.pushed,
3184 rejected: push_stats.rejected,
3185 remote_rejected: push_stats.remote_rejected,
3186 unexported_bookmarks,
3187 };
3188 Ok(push_stats)
3189}
3190
3191pub fn push_updates(
3193 repo: &dyn Repo,
3194 subprocess_options: GitSubprocessOptions,
3195 remote_name: &RemoteName,
3196 updates: &[GitRefUpdate],
3197 callback: &mut dyn GitSubprocessCallback,
3198 options: &GitPushOptions,
3199) -> Result<GitPushStats, GitPushError> {
3200 let mut qualified_remote_refs_expected_locations = HashMap::new();
3201 let mut refspecs = vec![];
3202 for update in updates {
3203 qualified_remote_refs_expected_locations.insert(
3204 update.qualified_name.as_ref(),
3205 update.targets.before.as_deref(),
3206 );
3207 if let Some(new_target) = &update.targets.after {
3208 refspecs.push(RefSpec::forced(
3212 new_target.to_string(),
3213 &update.qualified_name,
3214 ));
3215 } else {
3216 refspecs.push(RefSpec::delete(&update.qualified_name));
3220 }
3221 }
3222
3223 let git_backend = get_git_backend(repo.store())?;
3224 let git_repo = git_backend.git_repo();
3225 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3226
3227 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3229 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3230 }
3231
3232 let refs_to_push: Vec<RefToPush> = refspecs
3233 .iter()
3234 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3235 .collect();
3236
3237 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3238 push_stats.pushed.sort();
3239 push_stats.rejected.sort();
3240 push_stats.remote_rejected.sort();
3241 Ok(push_stats)
3242}
3243
3244fn build_pushed_bookmarks_to_export<'a>(
3246 remote: &RemoteName,
3247 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a Diff<Option<CommitId>>)>,
3248) -> RefsToExport {
3249 let mut to_update = Vec::new();
3250 let mut to_delete = Vec::new();
3251 for (name, update) in pushed_updates {
3252 let symbol = name.to_remote_symbol(remote);
3253 match (update.before.as_ref(), update.after.as_ref()) {
3254 (old, Some(new)) => {
3255 let old_oid = old.map(owned_oid_from_commit_id);
3256 let new_oid = owned_oid_from_commit_id(new);
3257 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3258 }
3259 (Some(old), None) => {
3260 let old_oid = owned_oid_from_commit_id(old);
3261 to_delete.push((symbol.to_owned(), old_oid));
3262 }
3263 (None, None) => panic!("old/new targets should differ"),
3264 }
3265 }
3266
3267 RefsToExport {
3268 to_update,
3269 to_delete,
3270 failed: vec![],
3271 }
3272}
3273
3274fn to_remote_tag_ref_update(
3276 symbol: RemoteRefSymbol<'_>,
3277 new_oid: Option<gix::ObjectId>,
3278) -> gix::refs::transaction::RefEdit {
3279 let expected = gix::refs::transaction::PreviousValue::Any;
3282 let change = match new_oid {
3283 Some(oid) => gix::refs::transaction::Change::Update {
3284 log: gix::refs::transaction::LogChange::default(),
3285 expected,
3286 new: oid.into(),
3287 },
3288 None => gix::refs::transaction::Change::Delete {
3289 expected,
3290 log: gix::refs::transaction::RefLog::AndReference,
3291 },
3292 };
3293 let name = format!(
3294 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}",
3295 remote = symbol.remote.as_str(),
3296 name = symbol.name.as_str()
3297 );
3298 gix::refs::transaction::RefEdit {
3299 change,
3300 name: name.try_into().expect("pushed ref name should be valid"),
3301 deref: false,
3302 }
3303}
3304
3305#[derive(Copy, Clone, Debug)]
3308pub enum FetchTagsOverride {
3309 AllTags,
3312 NoTags,
3315}
3316
3317#[cfg(test)]
3318mod tests {
3319 use assert_matches::assert_matches;
3320
3321 use super::*;
3322 use crate::revset;
3323 use crate::revset::RevsetDiagnostics;
3324
3325 #[test]
3326 fn test_split_positive_negative_patterns() {
3327 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3328 try_split(text).unwrap()
3329 }
3330
3331 fn try_split(
3332 text: &str,
3333 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3334 let mut diagnostics = RevsetDiagnostics::new();
3335 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3336 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3337 Ok((
3338 positives.into_iter().cloned().collect(),
3339 negatives.into_iter().cloned().collect(),
3340 ))
3341 }
3342
3343 insta::assert_compact_debug_snapshot!(
3344 split("a"),
3345 @r#"([Exact("a")], [])"#);
3346 insta::assert_compact_debug_snapshot!(
3347 split("~a"),
3348 @r#"([Substring("")], [Exact("a")])"#);
3349 insta::assert_compact_debug_snapshot!(
3350 split("~a~b"),
3351 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3352 insta::assert_compact_debug_snapshot!(
3353 split("~(a|b)"),
3354 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3355 insta::assert_compact_debug_snapshot!(
3356 split("a|b"),
3357 @r#"([Exact("a"), Exact("b")], [])"#);
3358 insta::assert_compact_debug_snapshot!(
3359 split("(a|b)&~c"),
3360 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3361 insta::assert_compact_debug_snapshot!(
3362 split("~a&b"),
3363 @r#"([Exact("b")], [Exact("a")])"#);
3364 insta::assert_compact_debug_snapshot!(
3365 split("a&~b&~c"),
3366 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3367 insta::assert_compact_debug_snapshot!(
3368 split("~a&b&~c"),
3369 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3370 insta::assert_compact_debug_snapshot!(
3371 split("a&~(b|c)"),
3372 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3373 insta::assert_compact_debug_snapshot!(
3374 split("((a|b)|c)&~(d|(e|f))"),
3375 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3376 assert_matches!(
3377 try_split("a&b"),
3378 Err(GitRefExpressionError::PositiveIntersection)
3379 );
3380 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3381 assert_matches!(
3382 try_split("a&~(b&~c)"),
3383 Err(GitRefExpressionError::NestedIntersection)
3384 );
3385 assert_matches!(
3386 try_split("(a|b)&c"),
3387 Err(GitRefExpressionError::PositiveIntersection)
3388 );
3389 assert_matches!(
3390 try_split("(a&~b)&(~c&~d)"),
3391 Err(GitRefExpressionError::PositiveIntersection)
3392 );
3393 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3394 assert_matches!(
3395 try_split("a&~b|c&~d"),
3396 Err(GitRefExpressionError::NestedIntersection)
3397 );
3398
3399 insta::assert_compact_debug_snapshot!(
3402 split("*"),
3403 @r#"([Glob(GlobPattern("*"))], [])"#);
3404 insta::assert_compact_debug_snapshot!(
3405 split("~*"),
3406 @"([], [])");
3407 insta::assert_compact_debug_snapshot!(
3408 split("a~*"),
3409 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3410 insta::assert_compact_debug_snapshot!(
3411 split("~(a|*)"),
3412 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3413 }
3414}