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 gix::refspec::Instruction;
33use itertools::Itertools as _;
34use pollster::FutureExt 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::merged_tree::MergedTree;
55use crate::merged_tree::TreeDiffEntry;
56use crate::object_id::ObjectId as _;
57use crate::op_store::RefTarget;
58use crate::op_store::RefTargetOptionExt as _;
59use crate::op_store::RemoteRef;
60use crate::op_store::RemoteRefState;
61use crate::ref_name::GitRefName;
62use crate::ref_name::GitRefNameBuf;
63use crate::ref_name::RefName;
64use crate::ref_name::RefNameBuf;
65use crate::ref_name::RemoteName;
66use crate::ref_name::RemoteNameBuf;
67use crate::ref_name::RemoteRefSymbol;
68use crate::ref_name::RemoteRefSymbolBuf;
69use crate::refs::BookmarkPushUpdate;
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_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";
87const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
89const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
92
93#[derive(Clone, Debug)]
94pub struct GitSettings {
95 pub auto_local_bookmark: bool,
97 pub abandon_unreachable_commits: bool,
98 pub executable_path: PathBuf,
99 pub write_change_id_header: bool,
100}
101
102impl GitSettings {
103 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
104 Ok(Self {
105 auto_local_bookmark: settings.get_bool("git.auto-local-bookmark")?,
106 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
107 executable_path: settings.get("git.executable-path")?,
108 write_change_id_header: settings.get("git.write-change-id-header")?,
109 })
110 }
111
112 pub fn to_subprocess_options(&self) -> GitSubprocessOptions {
113 GitSubprocessOptions {
114 executable_path: self.executable_path.clone(),
115 environment: HashMap::new(),
116 }
117 }
118}
119
120#[derive(Clone, Debug)]
122pub struct GitSubprocessOptions {
123 pub executable_path: PathBuf,
124 pub environment: HashMap<OsString, OsString>,
129}
130
131impl GitSubprocessOptions {
132 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
133 Ok(Self {
134 executable_path: settings.get("git.executable-path")?,
135 environment: HashMap::new(),
136 })
137 }
138}
139
140#[derive(Debug, Error)]
141pub enum GitRemoteNameError {
142 #[error(
143 "Git remote named '{name}' is reserved for local Git repository",
144 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
145 )]
146 ReservedForLocalGitRepo,
147 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
148 WithSlash(RemoteNameBuf),
149}
150
151fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
152 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
153 Err(GitRemoteNameError::ReservedForLocalGitRepo)
154 } else if name.as_str().contains('/') {
155 Err(GitRemoteNameError::WithSlash(name.to_owned()))
156 } else {
157 Ok(())
158 }
159}
160
161#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub enum GitRefKind {
164 Bookmark,
165 Tag,
166}
167
168#[derive(Debug, Default)]
170pub struct GitPushStats {
171 pub pushed: Vec<GitRefNameBuf>,
173 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
175 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
177 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
179}
180
181impl GitPushStats {
182 pub fn all_ok(&self) -> bool {
183 self.rejected.is_empty()
184 && self.remote_rejected.is_empty()
185 && self.unexported_bookmarks.is_empty()
186 }
187
188 pub fn some_exported(&self) -> bool {
191 self.pushed.len() > self.unexported_bookmarks.len()
192 }
193}
194
195#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
199struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
200
201impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
202 fn borrow(&self) -> &RemoteRefSymbol<'b> {
203 &self.0
204 }
205}
206
207#[derive(Debug, Hash, PartialEq, Eq)]
213pub(crate) struct RefSpec {
214 forced: bool,
215 source: Option<String>,
218 destination: String,
219}
220
221impl RefSpec {
222 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
223 Self {
224 forced: true,
225 source: Some(source.into()),
226 destination: destination.into(),
227 }
228 }
229
230 fn delete(destination: impl Into<String>) -> Self {
231 Self {
233 forced: false,
234 source: None,
235 destination: destination.into(),
236 }
237 }
238
239 pub(crate) fn to_git_format(&self) -> String {
240 format!(
241 "{}{}",
242 if self.forced { "+" } else { "" },
243 self.to_git_format_not_forced()
244 )
245 }
246
247 pub(crate) fn to_git_format_not_forced(&self) -> String {
253 if let Some(s) = &self.source {
254 format!("{}:{}", s, self.destination)
255 } else {
256 format!(":{}", self.destination)
257 }
258 }
259}
260
261#[derive(Debug)]
263#[repr(transparent)]
264pub(crate) struct NegativeRefSpec {
265 source: String,
266}
267
268impl NegativeRefSpec {
269 fn new(source: impl Into<String>) -> Self {
270 Self {
271 source: source.into(),
272 }
273 }
274
275 pub(crate) fn to_git_format(&self) -> String {
276 format!("^{}", self.source)
277 }
278}
279
280pub(crate) struct RefToPush<'a> {
283 pub(crate) refspec: &'a RefSpec,
284 pub(crate) expected_location: Option<&'a CommitId>,
285}
286
287impl<'a> RefToPush<'a> {
288 fn new(
289 refspec: &'a RefSpec,
290 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
291 ) -> Self {
292 let expected_location = *expected_locations
293 .get(GitRefName::new(&refspec.destination))
294 .expect(
295 "The refspecs and the expected locations were both constructed from the same \
296 source of truth. This means the lookup should always work.",
297 );
298
299 Self {
300 refspec,
301 expected_location,
302 }
303 }
304
305 pub(crate) fn to_git_lease(&self) -> String {
306 format!(
307 "{}:{}",
308 self.refspec.destination,
309 self.expected_location
310 .map(|x| x.to_string())
311 .as_deref()
312 .unwrap_or("")
313 )
314 }
315}
316
317pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
320 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
321 if name == "HEAD" {
323 return None;
324 }
325 let name = RefName::new(name);
326 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
327 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
328 } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
329 let (remote, name) = remote_and_name.split_once('/')?;
330 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
332 return None;
333 }
334 let name = RefName::new(name);
335 let remote = RemoteName::new(remote);
336 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
337 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
338 let name = RefName::new(name);
339 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
340 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
341 } else {
342 None
343 }
344}
345
346fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
347 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
348 let (remote, name) = remote_and_name.split_once('/')?;
349 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
350 return None;
351 }
352 let name = RefName::new(name);
353 let remote = RemoteName::new(remote);
354 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
355}
356
357fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
358 let RemoteRefSymbol { name, remote } = symbol;
359 let name = name.as_str();
360 let remote = remote.as_str();
361 if name.is_empty() || remote.is_empty() {
362 return None;
363 }
364 match kind {
365 GitRefKind::Bookmark => {
366 if name == "HEAD" {
367 return None;
368 }
369 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
370 Some(format!("refs/heads/{name}").into())
371 } else {
372 Some(format!("refs/remotes/{remote}/{name}").into())
373 }
374 }
375 GitRefKind::Tag => {
376 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
377 }
378 }
379}
380
381#[derive(Debug, Error)]
382#[error("The repo is not backed by a Git repo")]
383pub struct UnexpectedGitBackendError;
384
385pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
387 store.backend_impl().ok_or(UnexpectedGitBackendError)
388}
389
390pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
392 get_git_backend(store).map(|backend| backend.git_repo())
393}
394
395fn resolve_git_ref_to_commit_id(
400 git_ref: &gix::Reference,
401 known_commit_oid: Option<&gix::oid>,
402) -> Option<gix::ObjectId> {
403 let mut peeling_ref = Cow::Borrowed(git_ref);
404
405 if let Some(known_oid) = known_commit_oid {
407 let raw_ref = &git_ref.inner;
408 if let Some(oid) = raw_ref.target.try_id()
409 && oid == known_oid
410 {
411 return Some(oid.to_owned());
412 }
413 if let Some(oid) = raw_ref.peeled
414 && oid == known_oid
415 {
416 return Some(oid);
419 }
420 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
424 let maybe_tag = git_ref
425 .try_id()
426 .and_then(|id| id.object().ok())
427 .and_then(|object| object.try_into_tag().ok());
428 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
429 let oid = oid.detach();
430 if oid == known_oid {
431 return Some(oid);
433 }
434 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
437 }
438 }
439 }
440
441 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
445 let is_commit = peeled_id
446 .object()
447 .is_ok_and(|object| object.kind.is_commit());
448 is_commit.then_some(peeled_id.detach())
449}
450
451#[derive(Error, Debug)]
452pub enum GitImportError {
453 #[error("Failed to read Git HEAD target commit {id}")]
454 MissingHeadTarget {
455 id: CommitId,
456 #[source]
457 err: BackendError,
458 },
459 #[error("Ancestor of Git ref {symbol} is missing")]
460 MissingRefAncestor {
461 symbol: RemoteRefSymbolBuf,
462 #[source]
463 err: BackendError,
464 },
465 #[error(transparent)]
466 Backend(#[from] BackendError),
467 #[error(transparent)]
468 Index(#[from] IndexError),
469 #[error(transparent)]
470 Git(Box<dyn std::error::Error + Send + Sync>),
471 #[error(transparent)]
472 UnexpectedBackend(#[from] UnexpectedGitBackendError),
473}
474
475impl GitImportError {
476 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
477 Self::Git(source.into())
478 }
479}
480
481#[derive(Debug)]
483pub struct GitImportOptions {
484 pub auto_local_bookmark: bool,
486 pub abandon_unreachable_commits: bool,
488 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
490}
491
492#[derive(Clone, Debug, Eq, PartialEq, Default)]
494pub struct GitImportStats {
495 pub abandoned_commits: Vec<CommitId>,
497 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
500 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
503 pub failed_ref_names: Vec<BString>,
508}
509
510#[derive(Debug)]
511struct RefsToImport {
512 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
515 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
518 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
521 failed_ref_names: Vec<BString>,
523}
524
525pub fn import_refs(
530 mut_repo: &mut MutableRepo,
531 options: &GitImportOptions,
532) -> Result<GitImportStats, GitImportError> {
533 import_some_refs(mut_repo, options, |_, _| true)
534}
535
536pub fn import_some_refs(
541 mut_repo: &mut MutableRepo,
542 options: &GitImportOptions,
543 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
544) -> Result<GitImportStats, GitImportError> {
545 let git_repo = get_git_repo(mut_repo.store())?;
546
547 for remote_name in iter_remote_names(&git_repo) {
551 mut_repo.ensure_remote(&remote_name);
552 }
553
554 let all_remote_tags = false;
556 let refs_to_import =
557 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
558 import_refs_inner(mut_repo, refs_to_import, options)
559}
560
561fn import_refs_inner(
562 mut_repo: &mut MutableRepo,
563 refs_to_import: RefsToImport,
564 options: &GitImportOptions,
565) -> Result<GitImportStats, GitImportError> {
566 let store = mut_repo.store();
567 let git_backend = get_git_backend(store).expect("backend type should have been tested");
568
569 let RefsToImport {
570 changed_git_refs,
571 changed_remote_bookmarks,
572 changed_remote_tags,
573 failed_ref_names,
574 } = refs_to_import;
575
576 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
582 let index = mut_repo.index();
583 let missing_head_ids: Vec<&CommitId> = iter_changed_refs()
584 .flat_map(|(_, (_, new_target))| new_target.added_ids())
585 .filter_map(|id| match index.has_id(id) {
586 Ok(false) => Some(Ok(id)),
587 Ok(true) => None,
588 Err(e) => Some(Err(e)),
589 })
590 .try_collect()?;
591 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
592
593 let mut head_commits = Vec::new();
595 let get_commit = |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
596 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
597 symbol: symbol.clone(),
598 err,
599 };
600 if !heads_imported && !index.has_id(id).map_err(GitImportError::Index)? {
602 git_backend
603 .import_head_commits([id])
604 .map_err(missing_ref_err)?;
605 }
606 store.get_commit(id).map_err(missing_ref_err)
607 };
608 for (symbol, (_, new_target)) in iter_changed_refs() {
609 for id in new_target.added_ids() {
610 let commit = get_commit(id, symbol)?;
611 head_commits.push(commit);
612 }
613 }
614 mut_repo
617 .add_heads(&head_commits)
618 .map_err(GitImportError::Backend)?;
619
620 for (full_name, new_target) in changed_git_refs {
622 mut_repo.set_git_ref_target(&full_name, new_target);
623 }
624 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
625 let symbol = symbol.as_ref();
626 let base_target = old_remote_ref.tracked_target();
627 let new_remote_ref = RemoteRef {
628 target: new_target.clone(),
629 state: if old_remote_ref != RemoteRef::absent_ref() {
630 old_remote_ref.state
631 } else {
632 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
633 },
634 };
635 if new_remote_ref.is_tracked() {
636 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)?;
637 }
638 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
641 }
642 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
643 let symbol = symbol.as_ref();
644 let base_target = old_remote_ref.tracked_target();
645 let new_remote_ref = RemoteRef {
646 target: new_target.clone(),
647 state: if old_remote_ref != RemoteRef::absent_ref() {
648 old_remote_ref.state
649 } else {
650 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
651 },
652 };
653 if new_remote_ref.is_tracked() {
654 mut_repo.merge_local_tag(symbol.name, base_target, &new_remote_ref.target)?;
655 }
656 mut_repo.set_remote_tag(symbol, new_remote_ref);
659 }
660
661 let abandoned_commits = if options.abandon_unreachable_commits {
662 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
663 .map_err(GitImportError::Backend)?
664 } else {
665 vec![]
666 };
667 let stats = GitImportStats {
668 abandoned_commits,
669 changed_remote_bookmarks,
670 changed_remote_tags,
671 failed_ref_names,
672 };
673 Ok(stats)
674}
675
676fn abandon_unreachable_commits(
679 mut_repo: &mut MutableRepo,
680 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
681 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
682) -> BackendResult<Vec<CommitId>> {
683 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
684 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
685 .cloned()
686 .collect_vec();
687 if hidable_git_heads.is_empty() {
688 return Ok(vec![]);
689 }
690 let pinned_expression = RevsetExpression::union_all(&[
691 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
693 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
694 .intersection(&RevsetExpression::visible_heads().ancestors()),
696 RevsetExpression::root(),
697 ]);
698 let abandoned_expression = pinned_expression
699 .range(&RevsetExpression::commits(hidable_git_heads))
700 .intersection(&RevsetExpression::visible_heads().ancestors());
702 let abandoned_commit_ids: Vec<_> = abandoned_expression
703 .evaluate(mut_repo)
704 .map_err(|err| err.into_backend_error())?
705 .iter()
706 .try_collect()
707 .map_err(|err| err.into_backend_error())?;
708 for id in &abandoned_commit_ids {
709 let commit = mut_repo.store().get_commit(id)?;
710 mut_repo.record_abandoned_commit(&commit);
711 }
712 Ok(abandoned_commit_ids)
713}
714
715fn diff_refs_to_import(
717 view: &View,
718 git_repo: &gix::Repository,
719 all_remote_tags: bool,
720 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
721) -> Result<RefsToImport, GitImportError> {
722 let mut known_git_refs = view
723 .git_refs()
724 .iter()
725 .filter_map(|(full_name, target)| {
726 let (kind, symbol) =
728 parse_git_ref(full_name).expect("stored git ref should be parsable");
729 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
730 })
731 .collect();
732 let mut known_remote_bookmarks = view
733 .all_remote_bookmarks()
734 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
735 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
736 .collect();
737 let mut known_remote_tags = if all_remote_tags {
738 view.all_remote_tags()
739 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
740 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
741 .collect()
742 } else {
743 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
744 view.remote_tags(remote)
745 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
746 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
747 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
748 .collect()
749 };
750
751 let mut changed_git_refs = Vec::new();
756 let mut changed_remote_bookmarks = Vec::new();
757 let mut changed_remote_tags = Vec::new();
758 let mut failed_ref_names = Vec::new();
759 let actual = git_repo.references().map_err(GitImportError::from_git)?;
760 collect_changed_refs_to_import(
761 actual.local_branches().map_err(GitImportError::from_git)?,
762 &mut known_git_refs,
763 &mut known_remote_bookmarks,
764 &mut changed_git_refs,
765 &mut changed_remote_bookmarks,
766 &mut failed_ref_names,
767 &git_ref_filter,
768 )?;
769 collect_changed_refs_to_import(
770 actual.remote_branches().map_err(GitImportError::from_git)?,
771 &mut known_git_refs,
772 &mut known_remote_bookmarks,
773 &mut changed_git_refs,
774 &mut changed_remote_bookmarks,
775 &mut failed_ref_names,
776 &git_ref_filter,
777 )?;
778 collect_changed_refs_to_import(
779 actual.tags().map_err(GitImportError::from_git)?,
780 &mut known_git_refs,
781 &mut known_remote_tags,
782 &mut changed_git_refs,
783 &mut changed_remote_tags,
784 &mut failed_ref_names,
785 &git_ref_filter,
786 )?;
787 if all_remote_tags {
788 collect_changed_remote_tags_to_import(
789 actual
790 .prefixed(REMOTE_TAG_REF_NAMESPACE)
791 .map_err(GitImportError::from_git)?,
792 &mut known_remote_tags,
793 &mut changed_remote_tags,
794 &mut failed_ref_names,
795 &git_ref_filter,
796 )?;
797 }
798 for full_name in known_git_refs.into_keys() {
799 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
800 }
801 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
802 if old.is_present() {
803 changed_remote_bookmarks.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
804 }
805 }
806 for (RemoteRefKey(symbol), old) in known_remote_tags {
807 if old.is_present() {
808 changed_remote_tags.push((symbol.to_owned(), (old.clone(), RefTarget::absent())));
809 }
810 }
811
812 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
814 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
815 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
816 failed_ref_names.sort_unstable();
817 Ok(RefsToImport {
818 changed_git_refs,
819 changed_remote_bookmarks,
820 changed_remote_tags,
821 failed_ref_names,
822 })
823}
824
825fn collect_changed_refs_to_import(
826 actual_git_refs: gix::reference::iter::Iter,
827 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
828 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
829 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
830 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
831 failed_ref_names: &mut Vec<BString>,
832 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
833) -> Result<(), GitImportError> {
834 for git_ref in actual_git_refs {
835 let git_ref = git_ref.map_err(GitImportError::from_git)?;
836 let full_name_bytes = git_ref.name().as_bstr();
837 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
838 failed_ref_names.push(full_name_bytes.to_owned());
840 continue;
841 };
842 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
843 failed_ref_names.push(full_name_bytes.to_owned());
844 continue;
845 }
846 let full_name = GitRefName::new(full_name);
847 let Some((kind, symbol)) = parse_git_ref(full_name) else {
848 continue;
850 };
851 if !git_ref_filter(kind, symbol) {
852 continue;
853 }
854 let old_git_target = known_git_refs.get(full_name).copied().flatten();
855 let old_git_oid = old_git_target
856 .as_normal()
857 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
858 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
859 continue;
861 };
862 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
863 known_git_refs.remove(full_name);
864 if new_target != *old_git_target {
865 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
866 }
867 let old_remote_ref = known_remote_refs
870 .remove(&symbol)
871 .unwrap_or_else(|| RemoteRef::absent_ref());
872 if new_target != old_remote_ref.target {
873 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
874 }
875 }
876 Ok(())
877}
878
879fn collect_changed_remote_tags_to_import(
882 actual_git_refs: gix::reference::iter::Iter,
883 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
884 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
885 failed_ref_names: &mut Vec<BString>,
886 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
887) -> Result<(), GitImportError> {
888 for git_ref in actual_git_refs {
889 let git_ref = git_ref.map_err(GitImportError::from_git)?;
890 let full_name_bytes = git_ref.name().as_bstr();
891 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
892 failed_ref_names.push(full_name_bytes.to_owned());
894 continue;
895 };
896 let full_name = GitRefName::new(full_name);
897 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
898 continue;
900 };
901 if !git_ref_filter(kind, symbol) {
902 continue;
903 }
904 let old_remote_ref = known_remote_refs
905 .get(&symbol)
906 .copied()
907 .unwrap_or_else(|| RemoteRef::absent_ref());
908 let old_git_oid = old_remote_ref
909 .target
910 .as_normal()
911 .map(|id| gix::oid::from_bytes_unchecked(id.as_bytes()));
912 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
913 continue;
915 };
916 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
917 known_remote_refs.remove(&symbol);
918 if new_target != old_remote_ref.target {
919 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref.clone(), new_target)));
920 }
921 }
922 Ok(())
923}
924
925fn default_remote_ref_state_for(
926 kind: GitRefKind,
927 symbol: RemoteRefSymbol<'_>,
928 options: &GitImportOptions,
929) -> RemoteRefState {
930 match kind {
931 GitRefKind::Bookmark => {
932 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
933 || options.auto_local_bookmark
934 || options
935 .remote_auto_track_bookmarks
936 .get(symbol.remote)
937 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
938 {
939 RemoteRefState::Tracked
940 } else {
941 RemoteRefState::New
942 }
943 }
944 GitRefKind::Tag => RemoteRefState::Tracked,
946 }
947}
948
949fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
955 itertools::chain(view.local_bookmarks(), view.local_tags())
956 .flat_map(|(_, target)| target.added_ids())
957 .cloned()
958 .collect()
959}
960
961fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
968 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
969 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
970 .map(|(_, remote_ref)| &remote_ref.target)
971 .flat_map(|target| target.added_ids())
972 .cloned()
973 .collect()
974}
975
976pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
984 let store = mut_repo.store();
985 let git_backend = get_git_backend(store)?;
986 let git_repo = git_backend.git_repo();
987
988 let old_git_head = mut_repo.view().git_head();
989 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
990 Some(CommitId::from_bytes(oid.as_bytes()))
991 } else {
992 None
993 };
994 if old_git_head.as_resolved() == Some(&new_git_head_id) {
995 return Ok(());
996 }
997
998 if let Some(head_id) = &new_git_head_id {
1000 let index = mut_repo.index();
1001 if !index.has_id(head_id)? {
1002 git_backend.import_head_commits([head_id]).map_err(|err| {
1003 GitImportError::MissingHeadTarget {
1004 id: head_id.clone(),
1005 err,
1006 }
1007 })?;
1008 }
1009 store
1012 .get_commit(head_id)
1013 .and_then(|commit| mut_repo.add_head(&commit))
1014 .map_err(GitImportError::Backend)?;
1015 }
1016
1017 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
1018 Ok(())
1019}
1020
1021#[derive(Error, Debug)]
1022pub enum GitExportError {
1023 #[error(transparent)]
1024 Git(Box<dyn std::error::Error + Send + Sync>),
1025 #[error(transparent)]
1026 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1027}
1028
1029impl GitExportError {
1030 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1031 Self::Git(source.into())
1032 }
1033}
1034
1035#[derive(Debug, Error)]
1037pub enum FailedRefExportReason {
1038 #[error("Name is not allowed in Git")]
1040 InvalidGitName,
1041 #[error("Ref was in a conflicted state from the last import")]
1044 ConflictedOldState,
1045 #[error("Ref cannot point to the root commit in Git")]
1047 OnRootCommit,
1048 #[error("Deleted ref had been modified in Git")]
1050 DeletedInJjModifiedInGit,
1051 #[error("Added ref had been added with a different target in Git")]
1053 AddedInJjAddedInGit,
1054 #[error("Modified ref had been deleted in Git")]
1056 ModifiedInJjDeletedInGit,
1057 #[error("Failed to delete")]
1059 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1060 #[error("Failed to set")]
1062 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1063}
1064
1065#[derive(Debug)]
1067pub struct GitExportStats {
1068 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1070 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1074}
1075
1076#[derive(Debug)]
1077struct AllRefsToExport {
1078 bookmarks: RefsToExport,
1079 tags: RefsToExport,
1080}
1081
1082#[derive(Debug)]
1083struct RefsToExport {
1084 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1086 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1091 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1093}
1094
1095pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1104 export_some_refs(mut_repo, |_, _| true)
1105}
1106
1107pub fn export_some_refs(
1108 mut_repo: &mut MutableRepo,
1109 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1110) -> Result<GitExportStats, GitExportError> {
1111 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1112 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1113 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1114 let (_, value) = &map[index];
1115 Some(value)
1116 }
1117
1118 let git_repo = get_git_repo(mut_repo.store())?;
1119
1120 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1121 mut_repo.view(),
1122 mut_repo.store().root_commit_id(),
1123 &git_ref_filter,
1124 );
1125
1126 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
1128 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
1129 if let Some((kind, symbol)) = target_name
1130 .as_ref()
1131 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
1132 .and_then(|name| parse_git_ref(name.as_ref()))
1133 {
1134 let old_target = head_ref.inner.target.clone();
1135 let current_oid = match head_ref.into_fully_peeled_id() {
1136 Ok(id) => Some(id.detach()),
1137 Err(gix::reference::peel::Error::ToId(
1138 gix::refs::peel::to_id::Error::FollowToObject(
1139 gix::refs::peel::to_object::Error::Follow(
1140 gix::refs::file::find::existing::Error::NotFound { .. },
1141 ),
1142 ),
1143 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
1145 };
1146 let refs = match kind {
1147 GitRefKind::Bookmark => &bookmarks,
1148 GitRefKind::Tag => &tags,
1149 };
1150 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1151 Some(new_oid)
1152 } else if get(&refs.to_delete, symbol).is_some() {
1153 None
1154 } else {
1155 current_oid.as_ref()
1156 };
1157 if new_oid != current_oid.as_ref() {
1158 update_git_head(
1159 &git_repo,
1160 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1161 current_oid,
1162 )
1163 .map_err(GitExportError::from_git)?;
1164 }
1165 }
1166 }
1167
1168 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1169 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1170
1171 copy_exportable_local_bookmarks_to_remote_view(
1172 mut_repo,
1173 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1174 |name| {
1175 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1176 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1177 },
1178 );
1179 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1180 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1181 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1182 });
1183
1184 Ok(GitExportStats {
1185 failed_bookmarks,
1186 failed_tags,
1187 })
1188}
1189
1190fn export_refs_to_git(
1191 mut_repo: &mut MutableRepo,
1192 git_repo: &gix::Repository,
1193 kind: GitRefKind,
1194 refs: RefsToExport,
1195) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1196 let mut failed = refs.failed;
1197 for (symbol, old_oid) in refs.to_delete {
1198 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1199 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1200 continue;
1201 };
1202 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1203 failed.push((symbol, reason));
1204 } else {
1205 let new_target = RefTarget::absent();
1206 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1207 }
1208 }
1209 for (symbol, (old_oid, new_oid)) in refs.to_update {
1210 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1211 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1212 continue;
1213 };
1214 if let Err(reason) = update_git_ref(git_repo, &git_ref_name, old_oid, new_oid) {
1215 failed.push((symbol, reason));
1216 } else {
1217 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
1218 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1219 }
1220 }
1221
1222 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1224 failed
1225}
1226
1227fn copy_exportable_local_bookmarks_to_remote_view(
1228 mut_repo: &mut MutableRepo,
1229 remote: &RemoteName,
1230 name_filter: impl Fn(&RefName) -> bool,
1231) {
1232 let new_local_bookmarks = mut_repo
1233 .view()
1234 .local_remote_bookmarks(remote)
1235 .filter_map(|(name, targets)| {
1236 let old_target = &targets.remote_ref.target;
1239 let new_target = targets.local_target;
1240 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1241 })
1242 .filter(|&(name, _)| name_filter(name))
1243 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1244 .collect_vec();
1245 for (name, new_target) in new_local_bookmarks {
1246 let new_remote_ref = RemoteRef {
1247 target: new_target,
1248 state: RemoteRefState::Tracked,
1249 };
1250 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1251 }
1252}
1253
1254fn copy_exportable_local_tags_to_remote_view(
1255 mut_repo: &mut MutableRepo,
1256 remote: &RemoteName,
1257 name_filter: impl Fn(&RefName) -> bool,
1258) {
1259 let new_local_tags = mut_repo
1260 .view()
1261 .local_remote_tags(remote)
1262 .filter_map(|(name, targets)| {
1263 let old_target = &targets.remote_ref.target;
1265 let new_target = targets.local_target;
1266 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1267 })
1268 .filter(|&(name, _)| name_filter(name))
1269 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1270 .collect_vec();
1271 for (name, new_target) in new_local_tags {
1272 let new_remote_ref = RemoteRef {
1273 target: new_target,
1274 state: RemoteRefState::Tracked,
1275 };
1276 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1277 }
1278}
1279
1280fn diff_refs_to_export(
1282 view: &View,
1283 root_commit_id: &CommitId,
1284 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1285) -> AllRefsToExport {
1286 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1289 itertools::chain(
1290 view.local_bookmarks().map(|(name, target)| {
1291 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1292 (symbol, target)
1293 }),
1294 view.all_remote_bookmarks()
1295 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1296 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1297 )
1298 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1299 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1300 .collect();
1301 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1303 .local_tags()
1304 .map(|(name, target)| {
1305 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1306 (symbol, target)
1307 })
1308 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1309 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1310 .collect();
1311 let known_git_refs = view
1312 .git_refs()
1313 .iter()
1314 .map(|(full_name, target)| {
1315 let (kind, symbol) =
1316 parse_git_ref(full_name).expect("stored git ref should be parsable");
1317 ((kind, symbol), target)
1318 })
1319 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1323 for ((kind, symbol), target) in known_git_refs {
1324 let ref_targets = match kind {
1325 GitRefKind::Bookmark => &mut all_bookmark_targets,
1326 GitRefKind::Tag => &mut all_tag_targets,
1327 };
1328 ref_targets
1329 .entry(symbol)
1330 .and_modify(|(old_target, _)| *old_target = target)
1331 .or_insert((target, RefTarget::absent_ref()));
1332 }
1333
1334 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1335 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1336 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1337 AllRefsToExport { bookmarks, tags }
1338}
1339
1340fn collect_changed_refs_to_export(
1341 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1342 root_commit_target: &RefTarget,
1343) -> RefsToExport {
1344 let mut to_update = Vec::new();
1345 let mut to_delete = Vec::new();
1346 let mut failed = Vec::new();
1347 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1348 if new_target == old_target {
1349 continue;
1350 }
1351 if new_target == root_commit_target {
1352 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1354 continue;
1355 }
1356 let old_oid = if let Some(id) = old_target.as_normal() {
1357 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1358 } else if old_target.has_conflict() {
1359 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1362 continue;
1363 } else {
1364 assert!(old_target.is_absent());
1365 None
1366 };
1367 if let Some(id) = new_target.as_normal() {
1368 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1369 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1370 } else if new_target.has_conflict() {
1371 continue;
1373 } else {
1374 assert!(new_target.is_absent());
1375 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1376 }
1377 }
1378
1379 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1381 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1382 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1383 RefsToExport {
1384 to_update,
1385 to_delete,
1386 failed,
1387 }
1388}
1389
1390fn delete_git_ref(
1391 git_repo: &gix::Repository,
1392 git_ref_name: &GitRefName,
1393 old_oid: &gix::oid,
1394) -> Result<(), FailedRefExportReason> {
1395 let Some(git_ref) = git_repo
1396 .try_find_reference(git_ref_name.as_str())
1397 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1398 else {
1399 return Ok(());
1401 };
1402 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1403 git_ref
1405 .delete()
1406 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1407 } else {
1408 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1410 }
1411}
1412
1413fn create_git_ref(
1414 git_repo: &gix::Repository,
1415 git_ref_name: &GitRefName,
1416 new_oid: gix::ObjectId,
1417) -> Result<(), FailedRefExportReason> {
1418 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1419 let Err(set_err) =
1420 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1421 else {
1422 return Ok(());
1424 };
1425 let Some(git_ref) = git_repo
1426 .try_find_reference(git_ref_name.as_str())
1427 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1428 else {
1429 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1430 };
1431 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_oid) {
1434 Ok(())
1435 } else {
1436 Err(FailedRefExportReason::AddedInJjAddedInGit)
1437 }
1438}
1439
1440fn move_git_ref(
1441 git_repo: &gix::Repository,
1442 git_ref_name: &GitRefName,
1443 old_oid: gix::ObjectId,
1444 new_oid: gix::ObjectId,
1445) -> Result<(), FailedRefExportReason> {
1446 let constraint = gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into());
1447 let Err(set_err) =
1448 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1449 else {
1450 return Ok(());
1452 };
1453 let Some(git_ref) = git_repo
1455 .try_find_reference(git_ref_name.as_str())
1456 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1457 else {
1458 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1460 };
1461 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_oid));
1463 if git_commit_oid == Some(new_oid) {
1464 Ok(())
1465 } else if git_commit_oid == Some(old_oid) {
1466 let constraint =
1468 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1469 git_repo
1470 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1471 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1472 Ok(())
1473 } else {
1474 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1475 }
1476}
1477
1478fn update_git_ref(
1479 git_repo: &gix::Repository,
1480 git_ref_name: &GitRefName,
1481 old_oid: Option<gix::ObjectId>,
1482 new_oid: gix::ObjectId,
1483) -> Result<(), FailedRefExportReason> {
1484 match old_oid {
1485 None => create_git_ref(git_repo, git_ref_name, new_oid),
1486 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_oid),
1487 }
1488}
1489
1490fn update_git_head(
1493 git_repo: &gix::Repository,
1494 expected_ref: gix::refs::transaction::PreviousValue,
1495 new_oid: Option<gix::ObjectId>,
1496) -> Result<(), gix::reference::edit::Error> {
1497 let mut ref_edits = Vec::new();
1498 let new_target = if let Some(oid) = new_oid {
1499 gix::refs::Target::Object(oid)
1500 } else {
1501 ref_edits.push(gix::refs::transaction::RefEdit {
1506 change: gix::refs::transaction::Change::Delete {
1507 expected: gix::refs::transaction::PreviousValue::Any,
1508 log: gix::refs::transaction::RefLog::AndReference,
1509 },
1510 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1511 deref: false,
1512 });
1513 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1514 };
1515 ref_edits.push(gix::refs::transaction::RefEdit {
1516 change: gix::refs::transaction::Change::Update {
1517 log: gix::refs::transaction::LogChange {
1518 message: "export from jj".into(),
1519 ..Default::default()
1520 },
1521 expected: expected_ref,
1522 new: new_target,
1523 },
1524 name: "HEAD".try_into().unwrap(),
1525 deref: false,
1526 });
1527 git_repo.edit_references(ref_edits)?;
1528 Ok(())
1529}
1530
1531#[derive(Debug, Error)]
1532pub enum GitResetHeadError {
1533 #[error(transparent)]
1534 Backend(#[from] BackendError),
1535 #[error(transparent)]
1536 Git(Box<dyn std::error::Error + Send + Sync>),
1537 #[error("Failed to update Git HEAD ref")]
1538 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1539 #[error(transparent)]
1540 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1541}
1542
1543impl GitResetHeadError {
1544 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1545 Self::Git(source.into())
1546 }
1547}
1548
1549pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1552 let git_repo = get_git_repo(mut_repo.store())?;
1553
1554 let first_parent_id = &wc_commit.parent_ids()[0];
1555 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1556 RefTarget::normal(first_parent_id.clone())
1557 } else {
1558 RefTarget::absent()
1559 };
1560
1561 let old_head_target = mut_repo.git_head();
1563 if old_head_target != new_head_target {
1564 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1565 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1568 if actual_head.is_detached() {
1569 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1570 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1571 } else {
1572 gix::refs::transaction::PreviousValue::MustExist
1575 }
1576 } else {
1577 gix::refs::transaction::PreviousValue::MustExist
1579 };
1580 let new_oid = new_head_target
1581 .as_normal()
1582 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1583 update_git_head(&git_repo, expected_ref, new_oid)
1584 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1585 mut_repo.set_git_head_target(new_head_target);
1586 }
1587
1588 if git_repo.state().is_some() {
1591 clear_operation_state(&git_repo)?;
1592 }
1593
1594 reset_index(mut_repo, &git_repo, wc_commit)
1595}
1596
1597fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
1599 const STATE_FILE_NAMES: &[&str] = &[
1603 "MERGE_HEAD",
1604 "MERGE_MODE",
1605 "MERGE_MSG",
1606 "REVERT_HEAD",
1607 "CHERRY_PICK_HEAD",
1608 "BISECT_LOG",
1609 ];
1610 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1611 let handle_err = |err: PathError| match err.source.kind() {
1612 std::io::ErrorKind::NotFound => Ok(()),
1613 _ => Err(GitResetHeadError::from_git(err)),
1614 };
1615 for file_name in STATE_FILE_NAMES {
1616 let path = git_repo.path().join(file_name);
1617 std::fs::remove_file(&path)
1618 .context(&path)
1619 .or_else(handle_err)?;
1620 }
1621 for dir_name in STATE_DIR_NAMES {
1622 let path = git_repo.path().join(dir_name);
1623 std::fs::remove_dir_all(&path)
1624 .context(&path)
1625 .or_else(handle_err)?;
1626 }
1627 Ok(())
1628}
1629
1630fn reset_index(
1631 repo: &dyn Repo,
1632 git_repo: &gix::Repository,
1633 wc_commit: &Commit,
1634) -> Result<(), GitResetHeadError> {
1635 let parent_tree = wc_commit.parent_tree(repo)?;
1636 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
1640 if tree_id == repo.store().empty_tree_id() {
1641 gix::index::File::from_state(
1645 gix::index::State::new(git_repo.object_hash()),
1646 git_repo.index_path(),
1647 )
1648 } else {
1649 git_repo
1652 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
1653 .map_err(GitResetHeadError::from_git)?
1654 }
1655 } else {
1656 build_index_from_merged_tree(git_repo, &parent_tree)?
1657 };
1658
1659 let wc_tree = wc_commit.tree();
1660 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1661
1662 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1665 index
1666 .entries_mut_with_paths()
1667 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1668 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1669 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1670 })
1671 .filter_map(|merged| merged.both())
1672 .map(|((entry, _), old_entry)| (entry, old_entry))
1673 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1674 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1675 }
1676
1677 debug_assert!(index.verify_entries().is_ok());
1678
1679 index
1680 .write(gix::index::write::Options::default())
1681 .map_err(GitResetHeadError::from_git)
1682}
1683
1684fn build_index_from_merged_tree(
1685 git_repo: &gix::Repository,
1686 merged_tree: &MergedTree,
1687) -> Result<gix::index::File, GitResetHeadError> {
1688 let mut index = gix::index::File::from_state(
1689 gix::index::State::new(git_repo.object_hash()),
1690 git_repo.index_path(),
1691 );
1692
1693 let mut push_index_entry =
1694 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1695 let Some(entry) = maybe_entry else {
1696 return;
1697 };
1698
1699 let (id, mode) = match entry {
1700 TreeValue::File {
1701 id,
1702 executable,
1703 copy_id: _,
1704 } => {
1705 if *executable {
1706 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1707 } else {
1708 (id.as_bytes(), gix::index::entry::Mode::FILE)
1709 }
1710 }
1711 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1712 TreeValue::Tree(_) => {
1713 return;
1718 }
1719 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1720 };
1721
1722 let path = BStr::new(path.as_internal_file_string());
1723
1724 index.dangerously_push_entry(
1727 gix::index::entry::Stat::default(),
1728 gix::ObjectId::from_bytes_or_panic(id),
1729 gix::index::entry::Flags::from_stage(stage),
1730 mode,
1731 path,
1732 );
1733 };
1734
1735 let mut has_many_sided_conflict = false;
1736
1737 for (path, entry) in merged_tree.entries() {
1738 let entry = entry?;
1739 if let Some(resolved) = entry.as_resolved() {
1740 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1741 continue;
1742 }
1743
1744 let conflict = entry.simplify();
1745 if let [left, base, right] = conflict.as_slice() {
1746 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1748 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1749 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1750 } else {
1751 has_many_sided_conflict = true;
1759 push_index_entry(
1760 &path,
1761 conflict.first(),
1762 gix::index::entry::Stage::Unconflicted,
1763 );
1764 }
1765 }
1766
1767 index.sort_entries();
1770
1771 if has_many_sided_conflict
1774 && index
1775 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1776 .is_err()
1777 {
1778 let file_blob = git_repo
1779 .write_blob(
1780 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1781 )
1782 .map_err(GitResetHeadError::from_git)?;
1783 index.dangerously_push_entry(
1784 gix::index::entry::Stat::default(),
1785 file_blob.detach(),
1786 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1787 gix::index::entry::Mode::FILE,
1788 INDEX_DUMMY_CONFLICT_FILE.into(),
1789 );
1790 index.sort_entries();
1793 }
1794
1795 Ok(index)
1796}
1797
1798pub fn update_intent_to_add(
1805 repo: &dyn Repo,
1806 old_tree: &MergedTree,
1807 new_tree: &MergedTree,
1808) -> Result<(), GitResetHeadError> {
1809 let git_repo = get_git_repo(repo.store())?;
1810 let mut index = git_repo
1811 .index_or_empty()
1812 .map_err(GitResetHeadError::from_git)?;
1813 let mut_index = Arc::make_mut(&mut index);
1814 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1815 debug_assert!(mut_index.verify_entries().is_ok());
1816 mut_index
1817 .write(gix::index::write::Options::default())
1818 .map_err(GitResetHeadError::from_git)?;
1819
1820 Ok(())
1821}
1822
1823async fn update_intent_to_add_impl(
1824 git_repo: &gix::Repository,
1825 index: &mut gix::index::File,
1826 old_tree: &MergedTree,
1827 new_tree: &MergedTree,
1828) -> Result<(), GitResetHeadError> {
1829 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1830 let mut added_paths = vec![];
1831 let mut removed_paths = HashSet::new();
1832 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1833 let values = values?;
1834 if values.before.is_absent() {
1835 let executable = match values.after.as_normal() {
1836 Some(TreeValue::File {
1837 id: _,
1838 executable,
1839 copy_id: _,
1840 }) => *executable,
1841 Some(TreeValue::Symlink(_)) => false,
1842 _ => {
1843 continue;
1844 }
1845 };
1846 if index
1847 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1848 .is_err()
1849 {
1850 added_paths.push((BString::from(path.into_internal_string()), executable));
1851 }
1852 } else if values.after.is_absent() {
1853 removed_paths.insert(BString::from(path.into_internal_string()));
1854 }
1855 }
1856
1857 if added_paths.is_empty() && removed_paths.is_empty() {
1858 return Ok(());
1859 }
1860
1861 if !added_paths.is_empty() {
1862 let empty_blob = git_repo
1864 .write_blob(b"")
1865 .map_err(GitResetHeadError::from_git)?
1866 .detach();
1867 for (path, executable) in added_paths {
1868 index.dangerously_push_entry(
1870 gix::index::entry::Stat::default(),
1871 empty_blob,
1872 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1873 if executable {
1874 gix::index::entry::Mode::FILE_EXECUTABLE
1875 } else {
1876 gix::index::entry::Mode::FILE
1877 },
1878 path.as_ref(),
1879 );
1880 }
1881 }
1882 if !removed_paths.is_empty() {
1883 index.remove_entries(|_size, path, entry| {
1884 entry
1885 .flags
1886 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1887 && removed_paths.contains(path)
1888 });
1889 }
1890
1891 index.sort_entries();
1892
1893 Ok(())
1894}
1895
1896#[derive(Debug, Error)]
1897pub enum GitRemoteManagementError {
1898 #[error("No git remote named '{}'", .0.as_symbol())]
1899 NoSuchRemote(RemoteNameBuf),
1900 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1901 RemoteAlreadyExists(RemoteNameBuf),
1902 #[error(transparent)]
1903 RemoteName(#[from] GitRemoteNameError),
1904 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1905 NonstandardConfiguration(RemoteNameBuf),
1906 #[error("Error saving Git configuration")]
1907 GitConfigSaveError(#[source] std::io::Error),
1908 #[error("Unexpected Git error when managing remotes")]
1909 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1910 #[error(transparent)]
1911 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1912 #[error(transparent)]
1913 RefExpansionError(#[from] GitRefExpansionError),
1914}
1915
1916impl GitRemoteManagementError {
1917 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1918 Self::InternalGitError(source.into())
1919 }
1920}
1921
1922fn default_fetch_refspec(remote: &RemoteName) -> String {
1923 format!(
1924 "+refs/heads/*:refs/remotes/{remote}/*",
1925 remote = remote.as_str()
1926 )
1927}
1928
1929fn add_ref(
1930 name: gix::refs::FullName,
1931 target: gix::refs::Target,
1932 message: BString,
1933) -> gix::refs::transaction::RefEdit {
1934 gix::refs::transaction::RefEdit {
1935 change: gix::refs::transaction::Change::Update {
1936 log: gix::refs::transaction::LogChange {
1937 mode: gix::refs::transaction::RefLog::AndReference,
1938 force_create_reflog: false,
1939 message,
1940 },
1941 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1942 new: target,
1943 },
1944 name,
1945 deref: false,
1946 }
1947}
1948
1949fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1950 gix::refs::transaction::RefEdit {
1951 change: gix::refs::transaction::Change::Delete {
1952 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1953 reference.target().into_owned(),
1954 ),
1955 log: gix::refs::transaction::RefLog::AndReference,
1956 },
1957 name: reference.name().to_owned(),
1958 deref: false,
1959 }
1960}
1961
1962pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1968 let mut config_file = File::create(
1969 config
1970 .meta()
1971 .path
1972 .as_ref()
1973 .expect("Git repository to have a config file"),
1974 )?;
1975 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1976}
1977
1978fn save_remote(
1979 config: &mut gix::config::File<'static>,
1980 remote_name: &RemoteName,
1981 remote: &mut gix::Remote,
1982) -> Result<(), GitRemoteManagementError> {
1983 config
1990 .new_section(
1991 "remote",
1992 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1993 )
1994 .map_err(GitRemoteManagementError::from_git)?;
1995 remote
1996 .save_as_to(remote_name.as_str(), config)
1997 .map_err(GitRemoteManagementError::from_git)?;
1998 Ok(())
1999}
2000
2001fn git_config_branch_section_ids_by_remote(
2002 config: &gix::config::File,
2003 remote_name: &RemoteName,
2004) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2005 config
2006 .sections_by_name("branch")
2007 .into_iter()
2008 .flatten()
2009 .filter_map(|section| {
2010 let remote_values = section.values("remote");
2011 let push_remote_values = section.values("pushRemote");
2012 if !remote_values
2013 .iter()
2014 .chain(push_remote_values.iter())
2015 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
2016 {
2017 return None;
2018 }
2019 if remote_values.len() > 1
2020 || push_remote_values.len() > 1
2021 || section.value_names().any(|name| {
2022 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
2023 })
2024 {
2025 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2026 remote_name.to_owned(),
2027 )));
2028 }
2029 Some(Ok(section.id()))
2030 })
2031 .collect()
2032}
2033
2034fn rename_remote_in_git_branch_config_sections(
2035 config: &mut gix::config::File,
2036 old_remote_name: &RemoteName,
2037 new_remote_name: &RemoteName,
2038) -> Result<(), GitRemoteManagementError> {
2039 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2040 config
2041 .section_mut_by_id(id)
2042 .expect("found section to exist")
2043 .set(
2044 "remote"
2045 .try_into()
2046 .expect("'remote' to be a valid value name"),
2047 BStr::new(new_remote_name.as_str()),
2048 );
2049 }
2050 Ok(())
2051}
2052
2053fn remove_remote_git_branch_config_sections(
2054 config: &mut gix::config::File,
2055 remote_name: &RemoteName,
2056) -> Result<(), GitRemoteManagementError> {
2057 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2058 config
2059 .remove_section_by_id(id)
2060 .expect("removed section to exist");
2061 }
2062 Ok(())
2063}
2064
2065fn remove_remote_git_config_sections(
2066 config: &mut gix::config::File,
2067 remote_name: &RemoteName,
2068) -> Result<(), GitRemoteManagementError> {
2069 let section_ids_to_remove: Vec<_> = config
2070 .sections_by_name("remote")
2071 .into_iter()
2072 .flatten()
2073 .filter(|section| {
2074 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2075 })
2076 .map(|section| {
2077 if section.value_names().any(|name| {
2078 !name.eq_ignore_ascii_case(b"url")
2079 && !name.eq_ignore_ascii_case(b"fetch")
2080 && !name.eq_ignore_ascii_case(b"tagOpt")
2081 }) {
2082 return Err(GitRemoteManagementError::NonstandardConfiguration(
2083 remote_name.to_owned(),
2084 ));
2085 }
2086 Ok(section.id())
2087 })
2088 .try_collect()?;
2089 for id in section_ids_to_remove {
2090 config
2091 .remove_section_by_id(id)
2092 .expect("removed section to exist");
2093 }
2094 Ok(())
2095}
2096
2097pub fn get_all_remote_names(
2099 store: &Store,
2100) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2101 let git_repo = get_git_repo(store)?;
2102 Ok(iter_remote_names(&git_repo).collect())
2103}
2104
2105fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2106 git_repo
2107 .remote_names()
2108 .into_iter()
2109 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
2111 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
2113 .map(RemoteNameBuf::from)
2114}
2115
2116pub fn add_remote(
2117 mut_repo: &mut MutableRepo,
2118 remote_name: &RemoteName,
2119 url: &str,
2120 push_url: Option<&str>,
2121 fetch_tags: gix::remote::fetch::Tags,
2122 bookmark_expr: &StringExpression,
2123) -> Result<(), GitRemoteManagementError> {
2124 let git_repo = get_git_repo(mut_repo.store())?;
2125
2126 validate_remote_name(remote_name)?;
2127
2128 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
2129 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2130 remote_name.to_owned(),
2131 ));
2132 }
2133
2134 let ref_expr = GitFetchRefExpression {
2135 bookmark: bookmark_expr.clone(),
2136 tag: StringExpression::none(),
2139 };
2140 let ExpandedFetchRefSpecs {
2141 expr: _,
2142 refspecs,
2143 negative_refspecs,
2144 } = expand_fetch_refspecs(remote_name, ref_expr)?;
2145 let fetch_refspecs = itertools::chain(
2146 refspecs.iter().map(|spec| spec.to_git_format()),
2147 negative_refspecs.iter().map(|spec| spec.to_git_format()),
2148 )
2149 .map(BString::from);
2150
2151 let mut remote = git_repo
2152 .remote_at(url)
2153 .map_err(GitRemoteManagementError::from_git)?
2154 .with_fetch_tags(fetch_tags)
2155 .with_refspecs(fetch_refspecs, gix::remote::Direction::Fetch)
2156 .expect("previously-parsed refspecs to be valid");
2157
2158 if let Some(push_url) = push_url {
2159 remote = remote
2160 .with_push_url(push_url)
2161 .map_err(GitRemoteManagementError::from_git)?;
2162 }
2163
2164 let mut config = git_repo.config_snapshot().clone();
2165 save_remote(&mut config, remote_name, &mut remote)?;
2166 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2167
2168 mut_repo.ensure_remote(remote_name);
2169
2170 Ok(())
2171}
2172
2173pub fn remove_remote(
2174 mut_repo: &mut MutableRepo,
2175 remote_name: &RemoteName,
2176) -> Result<(), GitRemoteManagementError> {
2177 let mut git_repo = get_git_repo(mut_repo.store())?;
2178
2179 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2180 return Err(GitRemoteManagementError::NoSuchRemote(
2181 remote_name.to_owned(),
2182 ));
2183 }
2184
2185 let mut config = git_repo.config_snapshot().clone();
2186 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2187 remove_remote_git_config_sections(&mut config, remote_name)?;
2188 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2189
2190 remove_remote_git_refs(&mut git_repo, remote_name)
2191 .map_err(GitRemoteManagementError::from_git)?;
2192
2193 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2194 remove_remote_refs(mut_repo, remote_name);
2195 }
2196
2197 Ok(())
2198}
2199
2200fn remove_remote_git_refs(
2201 git_repo: &mut gix::Repository,
2202 remote: &RemoteName,
2203) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2204 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
2205 let edits: Vec<_> = git_repo
2206 .references()?
2207 .prefixed(prefix.as_str())?
2208 .map_ok(remove_ref)
2209 .try_collect()?;
2210 git_repo.edit_references(edits)?;
2212 Ok(())
2213}
2214
2215fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2216 mut_repo.remove_remote(remote);
2217 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
2218 let git_refs_to_delete = mut_repo
2219 .view()
2220 .git_refs()
2221 .keys()
2222 .filter(|&r| r.as_str().starts_with(&prefix))
2223 .cloned()
2224 .collect_vec();
2225 for git_ref in git_refs_to_delete {
2226 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2227 }
2228}
2229
2230pub fn rename_remote(
2231 mut_repo: &mut MutableRepo,
2232 old_remote_name: &RemoteName,
2233 new_remote_name: &RemoteName,
2234) -> Result<(), GitRemoteManagementError> {
2235 let mut git_repo = get_git_repo(mut_repo.store())?;
2236
2237 validate_remote_name(new_remote_name)?;
2238
2239 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
2240 return Err(GitRemoteManagementError::NoSuchRemote(
2241 old_remote_name.to_owned(),
2242 ));
2243 };
2244 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2245
2246 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
2247 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2248 new_remote_name.to_owned(),
2249 ));
2250 }
2251
2252 match (
2253 remote.refspecs(gix::remote::Direction::Fetch),
2254 remote.refspecs(gix::remote::Direction::Push),
2255 ) {
2256 ([refspec], [])
2257 if refspec.to_ref().to_bstring()
2258 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2259 _ => {
2260 return Err(GitRemoteManagementError::NonstandardConfiguration(
2261 old_remote_name.to_owned(),
2262 ));
2263 }
2264 }
2265
2266 remote
2267 .replace_refspecs(
2268 [default_fetch_refspec(new_remote_name).as_bytes()],
2269 gix::remote::Direction::Fetch,
2270 )
2271 .expect("default refspec to be valid");
2272
2273 let mut config = git_repo.config_snapshot().clone();
2274 save_remote(&mut config, new_remote_name, &mut remote)?;
2275 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2276 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2277 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2278
2279 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2280 .map_err(GitRemoteManagementError::from_git)?;
2281
2282 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2283 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2284 }
2285
2286 Ok(())
2287}
2288
2289fn rename_remote_git_refs(
2290 git_repo: &mut gix::Repository,
2291 old_remote_name: &RemoteName,
2292 new_remote_name: &RemoteName,
2293) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2294 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2295 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
2296 let ref_log_message = BString::from(format!(
2297 "renamed remote {old_remote_name} to {new_remote_name}",
2298 old_remote_name = old_remote_name.as_symbol(),
2299 new_remote_name = new_remote_name.as_symbol(),
2300 ));
2301
2302 let edits: Vec<_> = git_repo
2303 .references()?
2304 .prefixed(old_prefix.as_str())?
2305 .map_ok(|old_ref| {
2306 let new_name = BString::new(
2307 [
2308 new_prefix.as_bytes(),
2309 &old_ref.name().as_bstr()[old_prefix.len()..],
2310 ]
2311 .concat(),
2312 );
2313 [
2314 add_ref(
2315 new_name.try_into().expect("new ref name to be valid"),
2316 old_ref.target().into_owned(),
2317 ref_log_message.clone(),
2318 ),
2319 remove_ref(old_ref),
2320 ]
2321 })
2322 .flatten_ok()
2323 .try_collect()?;
2324 git_repo.edit_references(edits)?;
2326 Ok(())
2327}
2328
2329pub fn set_remote_urls(
2333 store: &Store,
2334 remote_name: &RemoteName,
2335 new_url: Option<&str>,
2336 new_push_url: Option<&str>,
2337) -> Result<(), GitRemoteManagementError> {
2338 if new_url.is_none() && new_push_url.is_none() {
2340 return Ok(());
2341 }
2342
2343 let git_repo = get_git_repo(store)?;
2344
2345 validate_remote_name(remote_name)?;
2346
2347 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2348 return Err(GitRemoteManagementError::NoSuchRemote(
2349 remote_name.to_owned(),
2350 ));
2351 };
2352 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2353
2354 if let Some(url) = new_url {
2355 remote = remote
2356 .with_url(url)
2357 .map_err(GitRemoteManagementError::from_git)?;
2358 }
2359
2360 if let Some(url) = new_push_url {
2361 remote = remote
2362 .with_push_url(url)
2363 .map_err(GitRemoteManagementError::from_git)?;
2364 }
2365
2366 let mut config = git_repo.config_snapshot().clone();
2367 save_remote(&mut config, remote_name, &mut remote)?;
2368 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2369
2370 Ok(())
2371}
2372
2373fn rename_remote_refs(
2374 mut_repo: &mut MutableRepo,
2375 old_remote_name: &RemoteName,
2376 new_remote_name: &RemoteName,
2377) {
2378 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2379 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2380 let git_refs = mut_repo
2381 .view()
2382 .git_refs()
2383 .iter()
2384 .filter_map(|(old, target)| {
2385 old.as_str().strip_prefix(&prefix).map(|p| {
2386 let new: GitRefNameBuf =
2387 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2388 (old.clone(), new, target.clone())
2389 })
2390 })
2391 .collect_vec();
2392 for (old, new, target) in git_refs {
2393 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2394 mut_repo.set_git_ref_target(&new, target);
2395 }
2396}
2397
2398const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2399
2400#[derive(Error, Debug)]
2401pub enum GitFetchError {
2402 #[error("No git remote named '{}'", .0.as_symbol())]
2403 NoSuchRemote(RemoteNameBuf),
2404 #[error(transparent)]
2405 RemoteName(#[from] GitRemoteNameError),
2406 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2407 RejectedUpdates(Vec<GitRefNameBuf>),
2408 #[error(transparent)]
2409 Subprocess(#[from] GitSubprocessError),
2410}
2411
2412#[derive(Error, Debug)]
2413pub enum GitDefaultRefspecError {
2414 #[error("No git remote named '{}'", .0.as_symbol())]
2415 NoSuchRemote(RemoteNameBuf),
2416 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2417 InvalidRemoteConfiguration(RemoteNameBuf, #[source] Box<gix::remote::find::Error>),
2418}
2419
2420struct FetchedRefs {
2421 remote: RemoteNameBuf,
2422 bookmark_matcher: StringMatcher,
2423 tag_matcher: StringMatcher,
2424}
2425
2426#[derive(Clone, Debug)]
2428pub struct GitFetchRefExpression {
2429 pub bookmark: StringExpression,
2431 pub tag: StringExpression,
2437}
2438
2439#[derive(Debug)]
2441pub struct ExpandedFetchRefSpecs {
2442 expr: GitFetchRefExpression,
2444 refspecs: Vec<RefSpec>,
2445 negative_refspecs: Vec<NegativeRefSpec>,
2446}
2447
2448#[derive(Error, Debug)]
2449pub enum GitRefExpansionError {
2450 #[error(transparent)]
2451 Expression(#[from] GitRefExpressionError),
2452 #[error(
2453 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2454 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2455 )]
2456 InvalidBranchPattern(StringPattern),
2457}
2458
2459pub fn expand_fetch_refspecs(
2461 remote: &RemoteName,
2462 expr: GitFetchRefExpression,
2463) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2464 let (positive_bookmarks, negative_bookmarks) =
2465 split_into_positive_negative_patterns(&expr.bookmark)?;
2466 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2467
2468 let refspecs = itertools::chain(
2469 positive_bookmarks
2470 .iter()
2471 .map(|&pattern| pattern_to_refspec_glob(pattern))
2472 .map_ok(|glob| {
2473 RefSpec::forced(
2474 format!("refs/heads/{glob}"),
2475 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2476 )
2477 }),
2478 positive_tags
2479 .iter()
2480 .map(|&pattern| pattern_to_refspec_glob(pattern))
2481 .map_ok(|glob| {
2482 RefSpec::forced(
2483 format!("refs/tags/{glob}"),
2484 format!(
2485 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2486 remote = remote.as_str()
2487 ),
2488 )
2489 }),
2490 )
2491 .try_collect()?;
2492
2493 let negative_refspecs = itertools::chain(
2494 negative_bookmarks
2495 .iter()
2496 .map(|&pattern| pattern_to_refspec_glob(pattern))
2497 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
2498 negative_tags
2499 .iter()
2500 .map(|&pattern| pattern_to_refspec_glob(pattern))
2501 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
2502 )
2503 .try_collect()?;
2504
2505 Ok(ExpandedFetchRefSpecs {
2506 expr,
2507 refspecs,
2508 negative_refspecs,
2509 })
2510}
2511
2512fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
2513 pattern
2514 .to_glob()
2515 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
2518 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
2519}
2520
2521#[derive(Debug, Error)]
2522pub enum GitRefExpressionError {
2523 #[error("Cannot use `~` in sub expression")]
2524 NestedNotIn,
2525 #[error("Cannot use `&` in sub expression")]
2526 NestedIntersection,
2527 #[error("Cannot use `&` for positive expressions")]
2528 PositiveIntersection,
2529}
2530
2531fn split_into_positive_negative_patterns(
2534 expr: &StringExpression,
2535) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
2536 static ALL: StringPattern = StringPattern::all();
2537
2538 fn visit_positive<'a>(
2552 expr: &'a StringExpression,
2553 positives: &mut Vec<&'a StringPattern>,
2554 negatives: &mut Vec<&'a StringPattern>,
2555 ) -> Result<(), GitRefExpressionError> {
2556 match expr {
2557 StringExpression::Pattern(pattern) => {
2558 positives.push(pattern);
2559 Ok(())
2560 }
2561 StringExpression::NotIn(complement) => {
2562 positives.push(&ALL);
2563 visit_negative(complement, negatives)
2564 }
2565 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
2566 StringExpression::Intersection(expr1, expr2) => {
2567 match (expr1.as_ref(), expr2.as_ref()) {
2568 (other, StringExpression::NotIn(complement))
2569 | (StringExpression::NotIn(complement), other) => {
2570 visit_positive(other, positives, negatives)?;
2571 visit_negative(complement, negatives)
2572 }
2573 _ => Err(GitRefExpressionError::PositiveIntersection),
2574 }
2575 }
2576 }
2577 }
2578
2579 fn visit_negative<'a>(
2580 expr: &'a StringExpression,
2581 negatives: &mut Vec<&'a StringPattern>,
2582 ) -> Result<(), GitRefExpressionError> {
2583 match expr {
2584 StringExpression::Pattern(pattern) => {
2585 negatives.push(pattern);
2586 Ok(())
2587 }
2588 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2589 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
2590 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2591 }
2592 }
2593
2594 fn visit_union<'a>(
2595 expr1: &'a StringExpression,
2596 expr2: &'a StringExpression,
2597 patterns: &mut Vec<&'a StringPattern>,
2598 ) -> Result<(), GitRefExpressionError> {
2599 visit_union_sub(expr1, patterns)?;
2600 visit_union_sub(expr2, patterns)
2601 }
2602
2603 fn visit_union_sub<'a>(
2604 expr: &'a StringExpression,
2605 patterns: &mut Vec<&'a StringPattern>,
2606 ) -> Result<(), GitRefExpressionError> {
2607 match expr {
2608 StringExpression::Pattern(pattern) => {
2609 patterns.push(pattern);
2610 Ok(())
2611 }
2612 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
2613 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
2614 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
2615 }
2616 }
2617
2618 let mut positives = Vec::new();
2619 let mut negatives = Vec::new();
2620 visit_positive(expr, &mut positives, &mut negatives)?;
2621 if positives.iter().all(|pattern| pattern.is_all())
2624 && !negatives.is_empty()
2625 && negatives.iter().all(|pattern| pattern.is_all())
2626 {
2627 Ok((vec![], vec![]))
2628 } else {
2629 Ok((positives, negatives))
2630 }
2631}
2632
2633#[derive(Debug)]
2637#[must_use = "warnings should be surfaced in the UI"]
2638pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
2639
2640#[derive(Debug)]
2643pub struct IgnoredRefspec {
2644 pub refspec: BString,
2646 pub reason: &'static str,
2648}
2649
2650#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2651enum FetchRefSpecKind {
2652 Positive,
2653 Negative,
2654}
2655
2656pub fn load_default_fetch_bookmarks(
2658 remote_name: &RemoteName,
2659 git_repo: &gix::Repository,
2660) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
2661 let remote = git_repo
2662 .try_find_remote(remote_name.as_str())
2663 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
2664 .map_err(|e| {
2665 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), Box::new(e))
2666 })?;
2667
2668 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
2669 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
2670 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
2671 let mut negative_bookmarks = Vec::new();
2672 for refspec in remote_refspecs {
2673 let refspec = refspec.to_ref();
2674 match parse_fetch_refspec(remote_name, refspec) {
2675 Ok((FetchRefSpecKind::Positive, bookmark)) => {
2676 positive_bookmarks.push(StringExpression::pattern(bookmark));
2677 }
2678 Ok((FetchRefSpecKind::Negative, bookmark)) => {
2679 negative_bookmarks.push(StringExpression::pattern(bookmark));
2680 }
2681 Err(reason) => {
2682 let refspec = refspec.to_bstring();
2683 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
2684 }
2685 }
2686 }
2687
2688 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
2689 if !negative_bookmarks.is_empty() {
2691 bookmark_expr =
2692 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
2693 }
2694
2695 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
2696}
2697
2698fn parse_fetch_refspec(
2699 remote_name: &RemoteName,
2700 refspec: gix::refspec::RefSpecRef<'_>,
2701) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
2702 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
2703
2704 let (src, positive_dst) = match refspec.instruction() {
2705 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
2706 Instruction::Fetch(fetch) => match fetch {
2707 gix::refspec::instruction::Fetch::Only { src: _ } => {
2708 return Err("fetch-only refspecs are not supported");
2709 }
2710 gix::refspec::instruction::Fetch::AndUpdate {
2711 src,
2712 dst,
2713 allow_non_fast_forward,
2714 } => {
2715 if !allow_non_fast_forward {
2716 return Err("non-forced refspecs are not supported");
2717 }
2718 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
2719 }
2720 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
2721 },
2722 };
2723
2724 let src_branch = src
2725 .strip_prefix("refs/heads/")
2726 .ok_or("only refs/heads/ is supported for refspec sources")?;
2727 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
2728
2729 if let Some(dst) = positive_dst {
2730 let dst_without_prefix = dst
2731 .strip_prefix("refs/remotes/")
2732 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
2733 let dst_branch = dst_without_prefix
2734 .strip_prefix(remote_name.as_str())
2735 .and_then(|d| d.strip_prefix("/"))
2736 .ok_or("remote renaming not supported")?;
2737 if src_branch != dst_branch {
2738 return Err("renaming is not supported");
2739 }
2740 Ok((FetchRefSpecKind::Positive, branch))
2741 } else {
2742 Ok((FetchRefSpecKind::Negative, branch))
2743 }
2744}
2745
2746pub struct GitFetch<'a> {
2748 mut_repo: &'a mut MutableRepo,
2749 git_repo: Box<gix::Repository>,
2750 git_ctx: GitSubprocessContext,
2751 import_options: &'a GitImportOptions,
2752 fetched: Vec<FetchedRefs>,
2753}
2754
2755impl<'a> GitFetch<'a> {
2756 pub fn new(
2757 mut_repo: &'a mut MutableRepo,
2758 subprocess_options: GitSubprocessOptions,
2759 import_options: &'a GitImportOptions,
2760 ) -> Result<Self, UnexpectedGitBackendError> {
2761 let git_backend = get_git_backend(mut_repo.store())?;
2762 let git_repo = Box::new(git_backend.git_repo());
2763 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
2764 Ok(GitFetch {
2765 mut_repo,
2766 git_repo,
2767 git_ctx,
2768 import_options,
2769 fetched: vec![],
2770 })
2771 }
2772
2773 #[tracing::instrument(skip(self, callback))]
2779 pub fn fetch(
2780 &mut self,
2781 remote_name: &RemoteName,
2782 ExpandedFetchRefSpecs {
2783 expr,
2784 refspecs: mut remaining_refspecs,
2785 negative_refspecs,
2786 }: ExpandedFetchRefSpecs,
2787 callback: &mut dyn GitSubprocessCallback,
2788 depth: Option<NonZeroU32>,
2789 fetch_tags_override: Option<FetchTagsOverride>,
2790 ) -> Result<(), GitFetchError> {
2791 validate_remote_name(remote_name)?;
2792
2793 if self
2795 .git_repo
2796 .try_find_remote(remote_name.as_str())
2797 .is_none()
2798 {
2799 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2800 }
2801
2802 if remaining_refspecs.is_empty() {
2803 return Ok(());
2805 }
2806
2807 let mut branches_to_prune = Vec::new();
2808 let updates = loop {
2816 let status = self.git_ctx.spawn_fetch(
2817 remote_name,
2818 &remaining_refspecs,
2819 &negative_refspecs,
2820 callback,
2821 depth,
2822 fetch_tags_override,
2823 )?;
2824 let failing_refspec = match status {
2825 GitFetchStatus::Updates(updates) => break updates,
2826 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
2827 };
2828 tracing::debug!(failing_refspec, "failed to fetch ref");
2829 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2830
2831 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2832 branches_to_prune.push(format!(
2833 "{remote_name}/{branch_name}",
2834 remote_name = remote_name.as_str()
2835 ));
2836 }
2837 };
2838
2839 if !updates.rejected.is_empty() {
2842 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
2843 return Err(GitFetchError::RejectedUpdates(names));
2844 }
2845
2846 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2849
2850 self.fetched.push(FetchedRefs {
2851 remote: remote_name.to_owned(),
2852 bookmark_matcher: expr.bookmark.to_matcher(),
2853 tag_matcher: expr.tag.to_matcher(),
2854 });
2855 Ok(())
2856 }
2857
2858 #[tracing::instrument(skip(self))]
2860 pub fn get_default_branch(
2861 &self,
2862 remote_name: &RemoteName,
2863 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2864 if self
2865 .git_repo
2866 .try_find_remote(remote_name.as_str())
2867 .is_none()
2868 {
2869 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2870 }
2871 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2872 tracing::debug!(?default_branch);
2873 Ok(default_branch)
2874 }
2875
2876 #[tracing::instrument(skip(self))]
2883 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2884 tracing::debug!("import_refs");
2885 let all_remote_tags = true;
2886 let refs_to_import = diff_refs_to_import(
2887 self.mut_repo.view(),
2888 &self.git_repo,
2889 all_remote_tags,
2890 |kind, symbol| match kind {
2891 GitRefKind::Bookmark => self
2892 .fetched
2893 .iter()
2894 .filter(|fetched| fetched.remote == symbol.remote)
2895 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
2896 GitRefKind::Tag => {
2897 symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
2901 || self
2902 .fetched
2903 .iter()
2904 .filter(|fetched| fetched.remote == symbol.remote)
2905 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str()))
2906 }
2907 },
2908 )?;
2909 let import_stats = import_refs_inner(self.mut_repo, refs_to_import, self.import_options)?;
2910
2911 self.fetched.clear();
2912
2913 Ok(import_stats)
2914 }
2915}
2916
2917#[derive(Error, Debug)]
2918pub enum GitPushError {
2919 #[error("No git remote named '{}'", .0.as_symbol())]
2920 NoSuchRemote(RemoteNameBuf),
2921 #[error(transparent)]
2922 RemoteName(#[from] GitRemoteNameError),
2923 #[error(transparent)]
2924 Subprocess(#[from] GitSubprocessError),
2925 #[error(transparent)]
2926 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2927}
2928
2929#[derive(Clone, Debug)]
2930pub struct GitBranchPushTargets {
2931 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2932}
2933
2934pub struct GitRefUpdate {
2935 pub qualified_name: GitRefNameBuf,
2936 pub expected_current_target: Option<CommitId>,
2941 pub new_target: Option<CommitId>,
2942}
2943
2944pub fn push_branches(
2946 mut_repo: &mut MutableRepo,
2947 subprocess_options: GitSubprocessOptions,
2948 remote: &RemoteName,
2949 targets: &GitBranchPushTargets,
2950 callback: &mut dyn GitSubprocessCallback,
2951) -> Result<GitPushStats, GitPushError> {
2952 validate_remote_name(remote)?;
2953
2954 let ref_updates = targets
2955 .branch_updates
2956 .iter()
2957 .map(|(name, update)| GitRefUpdate {
2958 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2959 expected_current_target: update.old_target.clone(),
2960 new_target: update.new_target.clone(),
2961 })
2962 .collect_vec();
2963
2964 let push_stats = push_updates(mut_repo, subprocess_options, remote, &ref_updates, callback)?;
2965 tracing::debug!(?push_stats);
2966
2967 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
2968 let pushed_branch_updates = || {
2969 iter::zip(&targets.branch_updates, &ref_updates)
2970 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
2971 .map(|((name, update), _)| (name.as_ref(), update))
2972 };
2973
2974 let unexported_bookmarks = {
2977 let git_repo =
2978 get_git_repo(mut_repo.store()).expect("backend type should have been tested");
2979 let refs = build_pushed_bookmarks_to_export(remote, pushed_branch_updates());
2980 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
2981 };
2982
2983 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
2984 let is_exported_bookmark = |name: &RefName| {
2985 unexported_bookmarks
2986 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
2987 .is_err()
2988 };
2989 for (name, update) in pushed_branch_updates().filter(|(name, _)| is_exported_bookmark(name)) {
2990 let new_remote_ref = RemoteRef {
2991 target: RefTarget::resolved(update.new_target.clone()),
2992 state: RemoteRefState::Tracked,
2993 };
2994 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2995 }
2996
2997 assert!(push_stats.unexported_bookmarks.is_empty());
3001 let push_stats = GitPushStats {
3002 pushed: push_stats.pushed,
3003 rejected: push_stats.rejected,
3004 remote_rejected: push_stats.remote_rejected,
3005 unexported_bookmarks,
3006 };
3007 Ok(push_stats)
3008}
3009
3010pub fn push_updates(
3012 repo: &dyn Repo,
3013 subprocess_options: GitSubprocessOptions,
3014 remote_name: &RemoteName,
3015 updates: &[GitRefUpdate],
3016 callback: &mut dyn GitSubprocessCallback,
3017) -> Result<GitPushStats, GitPushError> {
3018 let mut qualified_remote_refs_expected_locations = HashMap::new();
3019 let mut refspecs = vec![];
3020 for update in updates {
3021 qualified_remote_refs_expected_locations.insert(
3022 update.qualified_name.as_ref(),
3023 update.expected_current_target.as_ref(),
3024 );
3025 if let Some(new_target) = &update.new_target {
3026 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
3030 } else {
3031 refspecs.push(RefSpec::delete(&update.qualified_name));
3035 }
3036 }
3037
3038 let git_backend = get_git_backend(repo.store())?;
3039 let git_repo = git_backend.git_repo();
3040 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3041
3042 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
3044 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3045 }
3046
3047 let refs_to_push: Vec<RefToPush> = refspecs
3048 .iter()
3049 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3050 .collect();
3051
3052 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback)?;
3053 push_stats.pushed.sort();
3054 push_stats.rejected.sort();
3055 push_stats.remote_rejected.sort();
3056 Ok(push_stats)
3057}
3058
3059fn build_pushed_bookmarks_to_export<'a>(
3061 remote: &RemoteName,
3062 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a BookmarkPushUpdate)>,
3063) -> RefsToExport {
3064 let mut to_update = Vec::new();
3065 let mut to_delete = Vec::new();
3066 for (name, update) in pushed_updates {
3067 let symbol = name.to_remote_symbol(remote);
3068 match (update.old_target.as_ref(), update.new_target.as_ref()) {
3069 (old, Some(new)) => {
3070 let old_oid = old.map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
3071 let new_oid = gix::ObjectId::from_bytes_or_panic(new.as_bytes());
3072 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3073 }
3074 (Some(old), None) => {
3075 let old_oid = gix::ObjectId::from_bytes_or_panic(old.as_bytes());
3076 to_delete.push((symbol.to_owned(), old_oid));
3077 }
3078 (None, None) => panic!("old/new targets should differ"),
3079 }
3080 }
3081
3082 RefsToExport {
3083 to_update,
3084 to_delete,
3085 failed: vec![],
3086 }
3087}
3088
3089#[derive(Copy, Clone, Debug)]
3092pub enum FetchTagsOverride {
3093 AllTags,
3096 NoTags,
3099}
3100
3101#[cfg(test)]
3102mod tests {
3103 use assert_matches::assert_matches;
3104
3105 use super::*;
3106 use crate::revset;
3107 use crate::revset::RevsetDiagnostics;
3108
3109 #[test]
3110 fn test_split_positive_negative_patterns() {
3111 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3112 try_split(text).unwrap()
3113 }
3114
3115 fn try_split(
3116 text: &str,
3117 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3118 let mut diagnostics = RevsetDiagnostics::new();
3119 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3120 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3121 Ok((
3122 positives.into_iter().cloned().collect(),
3123 negatives.into_iter().cloned().collect(),
3124 ))
3125 }
3126
3127 insta::assert_compact_debug_snapshot!(
3128 split("a"),
3129 @r#"([Exact("a")], [])"#);
3130 insta::assert_compact_debug_snapshot!(
3131 split("~a"),
3132 @r#"([Substring("")], [Exact("a")])"#);
3133 insta::assert_compact_debug_snapshot!(
3134 split("~a~b"),
3135 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3136 insta::assert_compact_debug_snapshot!(
3137 split("~(a|b)"),
3138 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3139 insta::assert_compact_debug_snapshot!(
3140 split("a|b"),
3141 @r#"([Exact("a"), Exact("b")], [])"#);
3142 insta::assert_compact_debug_snapshot!(
3143 split("(a|b)&~c"),
3144 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3145 insta::assert_compact_debug_snapshot!(
3146 split("~a&b"),
3147 @r#"([Exact("b")], [Exact("a")])"#);
3148 insta::assert_compact_debug_snapshot!(
3149 split("a&~b&~c"),
3150 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3151 insta::assert_compact_debug_snapshot!(
3152 split("~a&b&~c"),
3153 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3154 insta::assert_compact_debug_snapshot!(
3155 split("a&~(b|c)"),
3156 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3157 insta::assert_compact_debug_snapshot!(
3158 split("((a|b)|c)&~(d|(e|f))"),
3159 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3160 assert_matches!(
3161 try_split("a&b"),
3162 Err(GitRefExpressionError::PositiveIntersection)
3163 );
3164 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3165 assert_matches!(
3166 try_split("a&~(b&~c)"),
3167 Err(GitRefExpressionError::NestedIntersection)
3168 );
3169 assert_matches!(
3170 try_split("(a|b)&c"),
3171 Err(GitRefExpressionError::PositiveIntersection)
3172 );
3173 assert_matches!(
3174 try_split("(a&~b)&(~c&~d)"),
3175 Err(GitRefExpressionError::PositiveIntersection)
3176 );
3177 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3178 assert_matches!(
3179 try_split("a&~b|c&~d"),
3180 Err(GitRefExpressionError::NestedIntersection)
3181 );
3182
3183 insta::assert_compact_debug_snapshot!(
3186 split("*"),
3187 @r#"([Glob(GlobPattern("*"))], [])"#);
3188 insta::assert_compact_debug_snapshot!(
3189 split("~*"),
3190 @"([], [])");
3191 insta::assert_compact_debug_snapshot!(
3192 split("a~*"),
3193 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3194 insta::assert_compact_debug_snapshot!(
3195 split("~(a|*)"),
3196 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3197 }
3198}