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