1#![expect(missing_docs)]
16
17use std::borrow::Borrow;
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::default::Default;
22use std::ffi::OsString;
23use std::fs::File;
24use std::iter;
25use std::num::NonZeroU32;
26use std::path::Path;
27use std::path::PathBuf;
28use std::sync::Arc;
29
30use bstr::BStr;
31use bstr::BString;
32use futures::StreamExt as _;
33use futures::TryStreamExt as _;
34use futures::stream;
35use gix::refspec::Instruction;
36use itertools::Itertools as _;
37use tempfile::TempDir;
38use thiserror::Error;
39
40use crate::backend::BackendError;
41use crate::backend::ChangeId;
42use crate::backend::CommitId;
43use crate::backend::TreeValue;
44use crate::commit::Commit;
45use crate::config::ConfigGetError;
46use crate::file_util::IoResultExt as _;
47use crate::file_util::PathError;
48use crate::file_util::is_empty_dir;
49use crate::git_backend::GitBackend;
50use crate::git_subprocess::GitFetchStatus;
51pub use crate::git_subprocess::GitProgress;
52pub use crate::git_subprocess::GitSidebandLineTerminator;
53pub use crate::git_subprocess::GitSubprocessCallback;
54use crate::git_subprocess::GitSubprocessContext;
55use crate::git_subprocess::GitSubprocessError;
56use crate::index::IndexError;
57use crate::matchers::EverythingMatcher;
58use crate::merge::Diff;
59use crate::merged_tree::MergedTree;
60use crate::merged_tree::TreeDiffEntry;
61use crate::object_id::ObjectId as _;
62use crate::op_store::ABSENT_REF_TARGET;
63use crate::op_store::ABSENT_REMOTE_REF;
64use crate::op_store::RefTarget;
65use crate::op_store::RefTargetOptionExt as _;
66use crate::op_store::RemoteRef;
67use crate::op_store::RemoteRefState;
68use crate::ref_name::GitRefName;
69use crate::ref_name::GitRefNameBuf;
70use crate::ref_name::RefName;
71use crate::ref_name::RefNameBuf;
72use crate::ref_name::RemoteName;
73use crate::ref_name::RemoteNameBuf;
74use crate::ref_name::RemoteRefSymbol;
75use crate::ref_name::RemoteRefSymbolBuf;
76use crate::ref_name::WorkspaceName;
77use crate::repo::MutableRepo;
78use crate::repo::Repo;
79use crate::repo_path::RepoPath;
80use crate::revset::ResolvedRevsetExpression;
81use crate::revset::RevsetEvaluationError;
82use crate::revset::RevsetExpression;
83use crate::revset::RevsetStreamExt as _;
84use crate::settings::UserSettings;
85use crate::store::Store;
86use crate::str_util::StringExpression;
87use crate::str_util::StringMatcher;
88use crate::str_util::StringPattern;
89use crate::view::View;
90
91pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
93pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
95const REMOTE_BOOKMARK_REF_NAMESPACE: &str = "refs/remotes/";
97const REMOTE_TAG_REF_NAMESPACE: &str = "refs/jj/remote-tags/";
99const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
105const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
108
109#[derive(Clone, Debug)]
110pub struct GitSettings {
111 pub abandon_unreachable_commits: bool,
112 pub executable_path: PathBuf,
113 pub record_synthetic_predecessors: bool,
114 pub write_change_id_header: bool,
115}
116
117impl GitSettings {
118 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
119 Ok(Self {
120 abandon_unreachable_commits: settings.get_bool("git.abandon-unreachable-commits")?,
121 executable_path: settings.get("git.executable-path")?,
122 record_synthetic_predecessors: settings
123 .get_bool("git.record-synthetic-predecessors")?,
124 write_change_id_header: settings.get("git.write-change-id-header")?,
125 })
126 }
127
128 pub fn to_subprocess_options(&self) -> GitSubprocessOptions {
129 GitSubprocessOptions {
130 executable_path: self.executable_path.clone(),
131 environment: HashMap::new(),
132 }
133 }
134}
135
136#[derive(Clone, Debug)]
138pub struct GitSubprocessOptions {
139 pub executable_path: PathBuf,
140 pub environment: HashMap<OsString, OsString>,
145}
146
147impl GitSubprocessOptions {
148 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
149 Ok(Self {
150 executable_path: settings.get("git.executable-path")?,
151 environment: HashMap::new(),
152 })
153 }
154}
155
156#[derive(Debug, Error)]
157pub enum GitRemoteNameError {
158 #[error(
159 "Git remote named '{name}' is reserved for local Git repository",
160 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
161 )]
162 ReservedForLocalGitRepo,
163 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
164 WithSlash(RemoteNameBuf),
165 #[error("Invalid Git remote name")]
166 InvalidName(#[from] gix::Error),
167}
168
169fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
170 gix::remote::name::validated(name.as_str())?;
171 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
172 Err(GitRemoteNameError::ReservedForLocalGitRepo)
173 } else if name.as_str().contains('/') {
174 Err(GitRemoteNameError::WithSlash(name.to_owned()))
175 } else {
176 Ok(())
177 }
178}
179
180fn oid_from_commit_id(id: &CommitId) -> &gix::oid {
182 gix::oid::from_bytes_unchecked(id.as_bytes())
183}
184
185fn owned_oid_from_commit_id(id: &CommitId) -> gix::ObjectId {
187 gix::ObjectId::from_bytes_or_panic(id.as_bytes())
188}
189
190#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub enum GitRefKind {
193 Bookmark,
194 Tag,
195}
196
197#[derive(Debug, Default)]
199pub struct GitPushStats {
200 pub pushed: Vec<GitRefNameBuf>,
202 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
204 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
206 pub unexported_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
208}
209
210impl GitPushStats {
211 pub fn all_ok(&self) -> bool {
212 self.rejected.is_empty()
213 && self.remote_rejected.is_empty()
214 && self.unexported_bookmarks.is_empty()
215 }
216
217 pub fn some_exported(&self) -> bool {
220 self.pushed.len() > self.unexported_bookmarks.len()
221 }
222}
223
224#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
228struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
229
230impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
231 fn borrow(&self) -> &RemoteRefSymbol<'b> {
232 &self.0
233 }
234}
235
236#[derive(Debug, Hash, PartialEq, Eq)]
242pub(crate) struct RefSpec {
243 forced: bool,
244 source: Option<String>,
247 destination: String,
248}
249
250impl RefSpec {
251 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
252 Self {
253 forced: true,
254 source: Some(source.into()),
255 destination: destination.into(),
256 }
257 }
258
259 fn delete(destination: impl Into<String>) -> Self {
260 Self {
262 forced: false,
263 source: None,
264 destination: destination.into(),
265 }
266 }
267
268 pub(crate) fn to_git_format(&self) -> String {
269 format!(
270 "{}{}",
271 if self.forced { "+" } else { "" },
272 self.to_git_format_not_forced()
273 )
274 }
275
276 pub(crate) fn to_git_format_not_forced(&self) -> String {
282 if let Some(s) = &self.source {
283 format!("{}:{}", s, self.destination)
284 } else {
285 format!(":{}", self.destination)
286 }
287 }
288}
289
290#[derive(Debug)]
292#[repr(transparent)]
293pub(crate) struct NegativeRefSpec {
294 source: String,
295}
296
297impl NegativeRefSpec {
298 fn new(source: impl Into<String>) -> Self {
299 Self {
300 source: source.into(),
301 }
302 }
303
304 pub(crate) fn to_git_format(&self) -> String {
305 format!("^{}", self.source)
306 }
307}
308
309pub(crate) struct RefToPush<'a> {
312 pub(crate) refspec: &'a RefSpec,
313 pub(crate) expected_location: Option<&'a gix::oid>,
314}
315
316impl<'a> RefToPush<'a> {
317 fn new(
318 refspec: &'a RefSpec,
319 expected_locations: &'a HashMap<&GitRefName, Option<&gix::oid>>,
320 ) -> Self {
321 let expected_location = *expected_locations
322 .get(GitRefName::new(&refspec.destination))
323 .expect(
324 "The refspecs and the expected locations were both constructed from the same \
325 source of truth. This means the lookup should always work.",
326 );
327
328 Self {
329 refspec,
330 expected_location,
331 }
332 }
333
334 pub(crate) fn to_git_lease(&self) -> String {
335 format!(
336 "{}:{}",
337 self.refspec.destination,
338 self.expected_location
339 .map(|x| x.to_string())
340 .as_deref()
341 .unwrap_or("")
342 )
343 }
344}
345
346pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
349 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
350 if name == "HEAD" {
352 return None;
353 }
354 let name = RefName::new(name);
355 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
356 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
357 } else if let Some(remote_and_name) = full_name
358 .as_str()
359 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
360 {
361 let (remote, name) = remote_and_name.split_once('/')?;
362 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
364 return None;
365 }
366 let name = RefName::new(name);
367 let remote = RemoteName::new(remote);
368 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
369 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
370 let name = RefName::new(name);
371 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
372 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
373 } else {
374 None
375 }
376}
377
378fn parse_remote_tag_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
379 let remote_and_name = full_name.as_str().strip_prefix(REMOTE_TAG_REF_NAMESPACE)?;
380 let (remote, name) = remote_and_name.split_once('/')?;
381 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
382 return None;
383 }
384 let name = RefName::new(name);
385 let remote = RemoteName::new(remote);
386 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
387}
388
389fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
390 let RemoteRefSymbol { name, remote } = symbol;
391 let name = name.as_str();
392 let remote = remote.as_str();
393 if name.is_empty() || remote.is_empty() {
394 return None;
395 }
396 match kind {
397 GitRefKind::Bookmark => {
398 if name == "HEAD" {
399 return None;
400 }
401 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
402 Some(format!("refs/heads/{name}").into())
403 } else {
404 Some(format!("{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{name}").into())
405 }
406 }
407 GitRefKind::Tag => {
408 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
410 }
411 }
412}
413
414fn to_git_or_remote_tag_ref_name(symbol: RemoteRefSymbol<'_>) -> GitRefNameBuf {
415 let RemoteRefSymbol { name, remote } = symbol;
416 let name = name.as_str();
417 let remote = remote.as_str();
418 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
419 format!("refs/tags/{name}").into()
420 } else {
421 format!("{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}").into()
422 }
423}
424
425#[derive(Debug, Error)]
426#[error("The repo is not backed by a Git repo")]
427pub struct UnexpectedGitBackendError;
428
429pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
431 store.backend_impl().ok_or(UnexpectedGitBackendError)
432}
433
434pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
436 get_git_backend(store).map(|backend| backend.git_repo())
437}
438
439fn resolve_git_ref_to_commit_id(
444 git_ref: &gix::Reference,
445 known_commit_oid: Option<&gix::oid>,
446) -> Option<gix::ObjectId> {
447 let mut peeling_ref = Cow::Borrowed(git_ref);
448
449 if let Some(known_oid) = known_commit_oid {
451 let raw_ref = &git_ref.inner;
452 if let Some(oid) = raw_ref.target.try_id()
453 && oid == known_oid
454 {
455 return Some(oid.to_owned());
456 }
457 if let Some(oid) = raw_ref.peeled
458 && oid == known_oid
459 {
460 return Some(oid);
463 }
464 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
468 let maybe_tag = git_ref
469 .try_id()
470 .and_then(|id| id.object().ok())
471 .and_then(|object| object.try_into_tag().ok());
472 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
473 let oid = oid.detach();
474 if oid == known_oid {
475 return Some(oid);
477 }
478 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid);
481 }
482 }
483 }
484
485 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
489 let is_commit = peeled_id
490 .object()
491 .is_ok_and(|object| object.kind.is_commit());
492 is_commit.then_some(peeled_id.detach())
493}
494
495#[derive(Error, Debug)]
496pub enum GitImportError {
497 #[error("Failed to read Git HEAD target commit {id}")]
498 MissingHeadTarget {
499 id: CommitId,
500 #[source]
501 err: BackendError,
502 },
503 #[error("Ancestor of Git ref {symbol} is missing")]
504 MissingRefAncestor {
505 symbol: RemoteRefSymbolBuf,
506 #[source]
507 err: BackendError,
508 },
509 #[error(transparent)]
510 Backend(#[from] BackendError),
511 #[error(transparent)]
512 Index(#[from] IndexError),
513 #[error(transparent)]
514 RevsetEvaluation(#[from] RevsetEvaluationError),
515 #[error(transparent)]
516 Git(Box<dyn std::error::Error + Send + Sync>),
517 #[error(transparent)]
518 UnexpectedBackend(#[from] UnexpectedGitBackendError),
519}
520
521impl GitImportError {
522 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
523 Self::Git(source.into())
524 }
525}
526
527#[derive(Debug)]
529pub struct GitImportOptions {
530 pub abandon_unreachable_commits: bool,
532 pub record_synthetic_predecessors: bool,
534 pub remote_auto_track_bookmarks: HashMap<RemoteNameBuf, StringMatcher>,
536}
537
538#[derive(Clone, Debug, Eq, PartialEq)]
539pub struct GitImportRefUpdate {
540 pub symbol: RemoteRefSymbolBuf,
541 pub old_remote_ref: RemoteRef,
542 pub new_target: RefTarget,
543}
544
545impl GitImportRefUpdate {
546 pub fn new(
547 symbol: RemoteRefSymbolBuf,
548 old_remote_ref: RemoteRef,
549 new_target: RefTarget,
550 ) -> Self {
551 Self {
552 symbol,
553 old_remote_ref,
554 new_target,
555 }
556 }
557}
558
559#[derive(Clone, Debug, Eq, PartialEq, Default)]
561pub struct GitImportStats {
562 pub abandoned_commits: Vec<Commit>,
564 pub rewritten_commit_ids: HashSet<CommitId>,
566 pub changed_remote_bookmarks: Vec<GitImportRefUpdate>,
569 pub changed_remote_tags: Vec<GitImportRefUpdate>,
572 pub failed_ref_names: Vec<BString>,
577}
578
579#[derive(Debug)]
580struct RefsToImport {
581 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
584 changed_remote_bookmarks: Vec<GitImportRefUpdate>,
587 changed_remote_tags: Vec<GitImportRefUpdate>,
590 failed_ref_names: Vec<BString>,
592}
593
594pub async fn import_refs(
599 mut_repo: &mut MutableRepo,
600 options: &GitImportOptions,
601) -> Result<GitImportStats, GitImportError> {
602 import_some_refs(mut_repo, options, |_, _| true).await
603}
604
605pub async fn import_some_refs(
610 mut_repo: &mut MutableRepo,
611 options: &GitImportOptions,
612 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
613) -> Result<GitImportStats, GitImportError> {
614 let git_repo = get_git_repo(mut_repo.store())?;
615
616 for remote_name in iter_remote_names(&git_repo) {
620 mut_repo.ensure_remote(&remote_name);
621 }
622
623 let all_remote_tags = false;
625 let refs_to_import =
626 diff_refs_to_import(mut_repo.view(), &git_repo, all_remote_tags, git_ref_filter)?;
627 import_refs_inner(mut_repo, refs_to_import, options).await
628}
629
630async fn import_refs_inner(
631 mut_repo: &mut MutableRepo,
632 refs_to_import: RefsToImport,
633 options: &GitImportOptions,
634) -> Result<GitImportStats, GitImportError> {
635 let store = mut_repo.store();
636 let git_backend = get_git_backend(store).expect("backend type should have been tested");
637
638 let RefsToImport {
639 changed_git_refs,
640 changed_remote_bookmarks,
641 changed_remote_tags,
642 failed_ref_names,
643 } = refs_to_import;
644
645 let iter_changed_refs = || itertools::chain(&changed_remote_bookmarks, &changed_remote_tags);
646 let (old_referenced_heads, new_referenced_heads) = {
648 let mut old_heads = Vec::new();
649 let mut new_heads = Vec::new();
650 for update in iter_changed_refs() {
651 old_heads.extend(update.old_remote_ref.target.present_adds().cloned());
652 new_heads.extend(update.new_target.present_adds().cloned());
653 }
654 (old_heads, new_heads)
655 };
656 let old_visible_heads = mut_repo.view().heads().iter().cloned().collect_vec();
657
658 let index = mut_repo.index();
664 let missing_head_ids: Vec<&CommitId> = stream::iter(&new_referenced_heads)
665 .map(async move |id| (id, index.has_id(id).await))
666 .buffered(mut_repo.store().concurrency())
667 .filter_map(async move |m| match m {
668 (id, Ok(false)) => Some(Ok(id)),
669 (_, Ok(true)) => None,
670 (_, Err(err)) => Some(Err(GitImportError::Index(err))),
671 })
672 .try_collect()
673 .await?;
674 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
675
676 let mut head_commits = Vec::new();
678 let get_commit = async |id: &CommitId, symbol: &RemoteRefSymbolBuf| {
679 let missing_ref_err = |err| GitImportError::MissingRefAncestor {
680 symbol: symbol.clone(),
681 err,
682 };
683 if !heads_imported && !index.has_id(id).await? {
685 git_backend
686 .import_head_commits([id])
687 .map_err(missing_ref_err)?;
688 }
689 store.get_commit_async(id).await.map_err(missing_ref_err)
690 };
691 for update in iter_changed_refs() {
694 for id in update.new_target.present_adds() {
695 let commit = get_commit(id, &update.symbol).await?;
696 head_commits.push(commit);
697 }
698 }
699 let imported_commits = mut_repo.index_commits(&head_commits).await?;
702 mut_repo.add_heads(&head_commits).await?;
703
704 for (full_name, new_target) in changed_git_refs {
706 mut_repo.set_git_ref_target(&full_name, new_target);
707 }
708 for update in &changed_remote_bookmarks {
709 let symbol = update.symbol.as_ref();
710 let base_target = update.old_remote_ref.tracked_target();
711 let new_remote_ref = RemoteRef {
712 target: update.new_target.clone(),
713 state: if update.old_remote_ref != ABSENT_REMOTE_REF {
714 update.old_remote_ref.state
715 } else {
716 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, options)
717 },
718 };
719 if new_remote_ref.is_tracked() {
720 mut_repo
721 .merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target)
722 .await?;
723 }
724 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
727 }
728 for update in &changed_remote_tags {
729 let symbol = update.symbol.as_ref();
730 let base_target = update.old_remote_ref.tracked_target();
731 let new_remote_ref = RemoteRef {
732 target: update.new_target.clone(),
733 state: if update.old_remote_ref != ABSENT_REMOTE_REF {
734 update.old_remote_ref.state
735 } else {
736 default_remote_ref_state_for(GitRefKind::Tag, symbol, options)
737 },
738 };
739 if new_remote_ref.is_tracked() {
740 mut_repo
741 .merge_local_tag(symbol.name, base_target, &new_remote_ref.target)
742 .await?;
743 }
744 mut_repo.set_remote_tag(symbol, new_remote_ref);
747 }
748
749 let any_old_referenced = !old_referenced_heads.is_empty();
750 let any_new_referenced = !new_referenced_heads.is_empty();
751 let old_visible_heads = RevsetExpression::commits(old_visible_heads);
752 let old_referenced_heads = RevsetExpression::commits(old_referenced_heads);
753 let new_referenced_heads = RevsetExpression::commits(new_referenced_heads);
754 let mut abandoned_commits = if options.abandon_unreachable_commits && any_old_referenced {
755 abandon_unreachable_commits(mut_repo, &old_referenced_heads).await?
756 } else {
757 vec![]
758 };
759 let rewritten_commit_ids = if options.record_synthetic_predecessors && any_new_referenced {
760 record_synthetic_predecessors(
761 mut_repo,
762 &old_visible_heads,
763 Diff::new(&old_referenced_heads, &new_referenced_heads),
764 &imported_commits,
765 options.abandon_unreachable_commits,
768 )
769 .await?
770 } else {
771 HashSet::new()
772 };
773 abandoned_commits.retain(|commit| !rewritten_commit_ids.contains(commit.id()));
774 let stats = GitImportStats {
775 abandoned_commits,
776 rewritten_commit_ids,
777 changed_remote_bookmarks,
778 changed_remote_tags,
779 failed_ref_names,
780 };
781 Ok(stats)
782}
783
784async fn abandon_unreachable_commits(
787 mut_repo: &mut MutableRepo,
788 hidable_git_heads: &Arc<ResolvedRevsetExpression>,
789) -> Result<Vec<Commit>, GitImportError> {
790 let pinned_expression = RevsetExpression::union_all(&[
791 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
793 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
794 .intersection(&RevsetExpression::visible_heads().ancestors()),
796 RevsetExpression::root(),
797 ]);
798 let abandoned_expression = pinned_expression
799 .range(hidable_git_heads)
800 .intersection(&RevsetExpression::visible_heads().ancestors());
802 let abandoned_commits: Vec<_> = abandoned_expression
803 .evaluate(mut_repo)?
804 .stream()
805 .commits(mut_repo.store())
806 .try_collect()
807 .await?;
808 for commit in &abandoned_commits {
809 mut_repo.record_abandoned_commit(commit);
810 }
811 Ok(abandoned_commits)
812}
813
814async fn record_synthetic_predecessors(
822 mut_repo: &mut MutableRepo,
823 old_visible_heads: &Arc<ResolvedRevsetExpression>,
824 Diff {
825 before: old_referenced_heads,
826 after: new_referenced_heads,
827 }: Diff<&Arc<ResolvedRevsetExpression>>,
828 imported_commits: &[Commit],
829 rewrite_commits: bool,
830) -> Result<HashSet<CommitId>, GitImportError> {
831 let build_change_to_commit_ids_map = async |expr: Arc<ResolvedRevsetExpression>| {
832 let mut change_to_commit_ids: HashMap<ChangeId, Vec<CommitId>> = HashMap::new();
833 let mut stream = expr.evaluate(mut_repo)?.commit_change_ids();
834 while let Some((commit_id, change_id)) = stream.try_next().await? {
835 let commit_ids = change_to_commit_ids.entry(change_id).or_default();
836 commit_ids.push(commit_id);
837 }
838 Ok::<_, GitImportError>(change_to_commit_ids)
839 };
840 let old_referenced_change_to_commit_ids =
841 build_change_to_commit_ids_map(new_referenced_heads.range(old_referenced_heads)).await?;
842 let new_referenced_change_to_commit_ids =
843 build_change_to_commit_ids_map(old_visible_heads.range(new_referenced_heads)).await?;
844 let imported_commit_ids: HashSet<_> = imported_commits.iter().map(Commit::id).collect();
845 let rewritable_commit_ids: HashSet<_> = if rewrite_commits {
846 new_referenced_heads
849 .range(old_referenced_heads)
850 .intersection(&old_visible_heads.ancestors())
851 .evaluate(mut_repo)?
852 .stream()
853 .try_collect()
854 .await?
855 } else {
856 HashSet::new()
857 };
858
859 let mut rewritten_commit_ids = HashSet::new();
860 for (change_id, new_commit_ids) in &new_referenced_change_to_commit_ids {
861 let predecessor_id: Option<CommitId>;
862 let rewrite_source_ids: &[CommitId];
863 if let Some(old_commit_ids) = old_referenced_change_to_commit_ids.get(change_id) {
864 predecessor_id = Some(old_commit_ids[0].clone());
867 rewrite_source_ids = old_commit_ids;
868 } else {
869 predecessor_id = None;
871 rewrite_source_ids = &[];
872 }
873 for new_commit_id in new_commit_ids
878 .iter()
879 .filter(|&id| imported_commit_ids.contains(id))
880 {
881 mut_repo.set_predecessors(new_commit_id.clone(), predecessor_id.as_slice().to_vec());
882 }
883 let rewrite_source_ids = rewrite_source_ids
884 .iter()
885 .filter(|id| rewritable_commit_ids.contains(id));
886 if let [new_commit_id] = &**new_commit_ids {
887 for old_commit_id in rewrite_source_ids {
888 mut_repo.set_rewritten_commit(old_commit_id.clone(), new_commit_id.clone());
889 rewritten_commit_ids.insert(old_commit_id.clone());
890 }
891 } else {
892 for old_commit_id in rewrite_source_ids {
893 mut_repo
894 .set_divergent_rewrite(old_commit_id.clone(), new_commit_ids.iter().cloned());
895 rewritten_commit_ids.insert(old_commit_id.clone());
896 }
897 }
898 }
899
900 Ok(rewritten_commit_ids)
901}
902
903fn diff_refs_to_import(
905 view: &View,
906 git_repo: &gix::Repository,
907 all_remote_tags: bool,
908 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
909) -> Result<RefsToImport, GitImportError> {
910 let mut known_git_refs = view
911 .git_refs()
912 .iter()
913 .filter_map(|(full_name, target)| {
914 let (kind, symbol) =
916 parse_git_ref(full_name).expect("stored git ref should be parsable");
917 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
918 })
919 .collect();
920 let mut known_remote_bookmarks = view
921 .all_remote_bookmarks()
922 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
923 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
924 .collect();
925 let mut known_remote_tags = if all_remote_tags {
926 view.all_remote_tags()
927 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
928 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
929 .collect()
930 } else {
931 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
932 view.remote_tags(remote)
933 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
934 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
935 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
936 .collect()
937 };
938
939 let mut changed_git_refs = Vec::new();
944 let mut changed_remote_bookmarks = Vec::new();
945 let mut changed_remote_tags = Vec::new();
946 let mut failed_ref_names = Vec::new();
947 let actual = git_repo.references().map_err(GitImportError::from_git)?;
948 collect_changed_refs_to_import(
949 actual.local_branches().map_err(GitImportError::from_git)?,
950 &mut known_git_refs,
951 &mut known_remote_bookmarks,
952 &mut changed_git_refs,
953 &mut changed_remote_bookmarks,
954 &mut failed_ref_names,
955 &git_ref_filter,
956 )?;
957 collect_changed_refs_to_import(
958 actual.remote_branches().map_err(GitImportError::from_git)?,
959 &mut known_git_refs,
960 &mut known_remote_bookmarks,
961 &mut changed_git_refs,
962 &mut changed_remote_bookmarks,
963 &mut failed_ref_names,
964 &git_ref_filter,
965 )?;
966 collect_changed_refs_to_import(
967 actual.tags().map_err(GitImportError::from_git)?,
968 &mut known_git_refs,
969 &mut known_remote_tags,
970 &mut changed_git_refs,
971 &mut changed_remote_tags,
972 &mut failed_ref_names,
973 &git_ref_filter,
974 )?;
975 if all_remote_tags {
976 collect_changed_remote_tags_to_import(
977 actual
978 .prefixed(REMOTE_TAG_REF_NAMESPACE)
979 .map_err(GitImportError::from_git)?,
980 &mut known_remote_tags,
981 &mut changed_remote_tags,
982 &mut failed_ref_names,
983 &git_ref_filter,
984 )?;
985 }
986 for full_name in known_git_refs.into_keys() {
987 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
988 }
989 for (RemoteRefKey(symbol), old) in known_remote_bookmarks {
990 if old.is_present() {
991 changed_remote_bookmarks.push(GitImportRefUpdate::new(
992 symbol.to_owned(),
993 old.clone(),
994 RefTarget::absent(),
995 ));
996 }
997 }
998 for (RemoteRefKey(symbol), old) in known_remote_tags {
999 if old.is_present() {
1000 changed_remote_tags.push(GitImportRefUpdate::new(
1001 symbol.to_owned(),
1002 old.clone(),
1003 RefTarget::absent(),
1004 ));
1005 }
1006 }
1007
1008 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1010 changed_remote_bookmarks
1011 .sort_unstable_by(|update1, update2| update1.symbol.cmp(&update2.symbol));
1012 changed_remote_tags.sort_unstable_by(|update1, update2| update1.symbol.cmp(&update2.symbol));
1013 failed_ref_names.sort_unstable();
1014 Ok(RefsToImport {
1015 changed_git_refs,
1016 changed_remote_bookmarks,
1017 changed_remote_tags,
1018 failed_ref_names,
1019 })
1020}
1021
1022fn collect_changed_refs_to_import(
1023 actual_git_refs: gix::reference::iter::Iter,
1024 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
1025 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
1026 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
1027 changed_remote_refs: &mut Vec<GitImportRefUpdate>,
1028 failed_ref_names: &mut Vec<BString>,
1029 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1030) -> Result<(), GitImportError> {
1031 for git_ref in actual_git_refs {
1032 let git_ref = git_ref.map_err(GitImportError::from_git)?;
1033 let full_name_bytes = git_ref.name().as_bstr();
1034 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
1035 failed_ref_names.push(full_name_bytes.to_owned());
1037 continue;
1038 };
1039 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
1040 failed_ref_names.push(full_name_bytes.to_owned());
1041 continue;
1042 }
1043 let full_name = GitRefName::new(full_name);
1044 let Some((kind, symbol)) = parse_git_ref(full_name) else {
1045 continue;
1047 };
1048 if !git_ref_filter(kind, symbol) {
1049 continue;
1050 }
1051 let old_git_target = known_git_refs.get(full_name).copied().flatten();
1052 let old_git_oid = old_git_target.as_normal().map(oid_from_commit_id);
1053 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
1054 continue;
1056 };
1057 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
1058 known_git_refs.remove(full_name);
1059 if new_target != *old_git_target {
1060 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
1061 }
1062 let old_remote_ref = known_remote_refs
1065 .remove(&symbol)
1066 .unwrap_or(&ABSENT_REMOTE_REF);
1067 if new_target != old_remote_ref.target {
1068 changed_remote_refs.push(GitImportRefUpdate::new(
1069 symbol.to_owned(),
1070 old_remote_ref.clone(),
1071 new_target,
1072 ));
1073 }
1074 }
1075 Ok(())
1076}
1077
1078fn collect_changed_remote_tags_to_import(
1081 actual_git_refs: gix::reference::iter::Iter,
1082 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, &RemoteRef>,
1083 changed_remote_refs: &mut Vec<GitImportRefUpdate>,
1084 failed_ref_names: &mut Vec<BString>,
1085 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1086) -> Result<(), GitImportError> {
1087 for git_ref in actual_git_refs {
1088 let git_ref = git_ref.map_err(GitImportError::from_git)?;
1089 let full_name_bytes = git_ref.name().as_bstr();
1090 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
1091 failed_ref_names.push(full_name_bytes.to_owned());
1093 continue;
1094 };
1095 let full_name = GitRefName::new(full_name);
1096 let Some((kind, symbol)) = parse_remote_tag_ref(full_name) else {
1097 continue;
1099 };
1100 if !git_ref_filter(kind, symbol) {
1101 continue;
1102 }
1103 let old_remote_ref = known_remote_refs
1104 .get(&symbol)
1105 .copied()
1106 .unwrap_or(&ABSENT_REMOTE_REF);
1107 let old_git_oid = old_remote_ref.target.as_normal().map(oid_from_commit_id);
1108 let Some(oid) = resolve_git_ref_to_commit_id(&git_ref, old_git_oid) else {
1109 continue;
1111 };
1112 let new_target = RefTarget::normal(CommitId::from_bytes(oid.as_bytes()));
1113 known_remote_refs.remove(&symbol);
1114 if new_target != old_remote_ref.target {
1115 changed_remote_refs.push(GitImportRefUpdate::new(
1116 symbol.to_owned(),
1117 old_remote_ref.clone(),
1118 new_target,
1119 ));
1120 }
1121 }
1122 Ok(())
1123}
1124
1125fn default_remote_ref_state_for(
1126 kind: GitRefKind,
1127 symbol: RemoteRefSymbol<'_>,
1128 options: &GitImportOptions,
1129) -> RemoteRefState {
1130 match kind {
1131 GitRefKind::Bookmark => {
1132 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1133 || options
1134 .remote_auto_track_bookmarks
1135 .get(symbol.remote)
1136 .is_some_and(|matcher| matcher.is_match(symbol.name.as_str()))
1137 {
1138 RemoteRefState::Tracked
1139 } else {
1140 RemoteRefState::New
1141 }
1142 }
1143 GitRefKind::Tag => RemoteRefState::Tracked,
1145 }
1146}
1147
1148fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
1154 itertools::chain(view.local_bookmarks(), view.local_tags())
1155 .flat_map(|(_, target)| target.present_adds())
1156 .cloned()
1157 .collect()
1158}
1159
1160fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
1167 itertools::chain(view.all_remote_bookmarks(), view.all_remote_tags())
1168 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
1169 .map(|(_, remote_ref)| &remote_ref.target)
1170 .flat_map(|target| target.present_adds())
1171 .cloned()
1172 .collect()
1173}
1174
1175pub async fn import_head(
1183 mut_repo: &mut MutableRepo,
1184 workspace_name: &WorkspaceName,
1185 workspace_root: &Path,
1186) -> Result<(), GitImportError> {
1187 let store = mut_repo.store();
1188 let git_backend = get_git_backend(store)?;
1189 let git_repo = git_backend
1190 .open_git_repo_at_workdir(workspace_root)
1191 .map_err(GitImportError::from_git)?;
1192
1193 let old_git_head = mut_repo.view().git_head(workspace_name);
1194 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
1195 Some(CommitId::from_bytes(oid.as_bytes()))
1196 } else {
1197 None
1198 };
1199 if old_git_head.as_resolved() == Some(&new_git_head_id) {
1200 return Ok(());
1201 }
1202
1203 if let Some(head_id) = &new_git_head_id {
1205 let index = mut_repo.index();
1206 if !index.has_id(head_id).await? {
1207 git_backend.import_head_commits([head_id]).map_err(|err| {
1208 GitImportError::MissingHeadTarget {
1209 id: head_id.clone(),
1210 err,
1211 }
1212 })?;
1213 }
1214 let commit = store.get_commit_async(head_id).await?;
1217 mut_repo.add_head(&commit).await?;
1218 }
1219
1220 mut_repo.set_git_head_target(workspace_name, RefTarget::resolved(new_git_head_id));
1221 Ok(())
1222}
1223
1224pub async fn import_head_commit(
1226 mut_repo: &mut MutableRepo,
1227) -> Result<Option<Commit>, GitImportError> {
1228 let store = mut_repo.store();
1229 let git_backend = get_git_backend(store)?;
1230 let git_repo = git_backend.git_repo();
1231
1232 let Ok(oid) = git_repo.head_id() else {
1233 return Ok(None);
1234 };
1235 let head_id = CommitId::from_bytes(oid.as_bytes());
1236
1237 let index = mut_repo.index();
1238 if !index.has_id(&head_id).await? {
1239 git_backend.import_head_commits([&head_id]).map_err(|err| {
1240 GitImportError::MissingHeadTarget {
1241 id: head_id.clone(),
1242 err,
1243 }
1244 })?;
1245 }
1246 let commit = store.get_commit_async(&head_id).await?;
1249 mut_repo.add_head(&commit).await?;
1250 Ok(Some(commit))
1251}
1252
1253#[derive(Error, Debug)]
1254pub enum GitExportError {
1255 #[error(transparent)]
1256 Git(Box<dyn std::error::Error + Send + Sync>),
1257 #[error(transparent)]
1258 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1259}
1260
1261impl GitExportError {
1262 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1263 Self::Git(source.into())
1264 }
1265}
1266
1267#[derive(Debug, Error)]
1269pub enum FailedRefExportReason {
1270 #[error("Name is not allowed in Git")]
1272 InvalidGitName,
1273 #[error("Ref was in a conflicted state from the last import")]
1276 ConflictedOldState,
1277 #[error("Ref cannot point to the root commit in Git")]
1279 OnRootCommit,
1280 #[error("Deleted ref had been modified in Git")]
1282 DeletedInJjModifiedInGit,
1283 #[error("Added ref had been added with a different target in Git")]
1285 AddedInJjAddedInGit,
1286 #[error("Modified ref had been deleted in Git")]
1288 ModifiedInJjDeletedInGit,
1289 #[error("Failed to delete")]
1291 FailedToDelete(#[source] Box<dyn std::error::Error + Send + Sync>),
1292 #[error("Failed to set")]
1294 FailedToSet(#[source] Box<dyn std::error::Error + Send + Sync>),
1295}
1296
1297#[derive(Debug)]
1299pub struct GitExportStats {
1300 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1302 pub failed_tags: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1306}
1307
1308#[derive(Debug)]
1309struct AllRefsToExport {
1310 bookmarks: RefsToExport,
1311 tags: RefsToExport,
1312}
1313
1314#[derive(Debug)]
1315struct RefsToExport {
1316 to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
1318 to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
1323 failed: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
1325}
1326
1327pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
1336 export_some_refs(mut_repo, |_, _| true)
1337}
1338
1339pub fn export_some_refs(
1340 mut_repo: &mut MutableRepo,
1341 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1342) -> Result<GitExportStats, GitExportError> {
1343 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
1344 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
1345 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
1346 let (_, value) = &map[index];
1347 Some(value)
1348 }
1349
1350 let AllRefsToExport { bookmarks, tags } = diff_refs_to_export(
1351 mut_repo.view(),
1352 mut_repo.store().root_commit_id(),
1353 &git_ref_filter,
1354 );
1355
1356 let check_and_detach_head = |git_repo: &gix::Repository| -> Result<(), GitExportError> {
1357 let Ok(head_ref) = git_repo.find_reference("HEAD") else {
1358 return Ok(());
1359 };
1360 let old_target = head_ref.inner.target;
1361 if let Some(target_name) = old_target.try_name()
1362 && let Ok(target_name_str) = str::from_utf8(target_name.as_bstr())
1363 && let Some((kind, symbol)) = parse_git_ref(target_name_str.as_ref())
1364 {
1365 let maybe_target_ref = git_repo
1367 .try_find_reference(target_name)
1368 .map_err(GitExportError::from_git)?;
1369 let current_oid = maybe_target_ref
1370 .map(|target_ref| target_ref.into_fully_peeled_id())
1371 .transpose()
1372 .map_err(GitExportError::from_git)?
1373 .map(|id| id.detach());
1374 let refs = match kind {
1375 GitRefKind::Bookmark => &bookmarks,
1376 GitRefKind::Tag => &tags,
1377 };
1378 let new_oid = if let Some((_old_oid, new_oid)) = get(&refs.to_update, symbol) {
1379 Some(new_oid)
1380 } else if get(&refs.to_delete, symbol).is_some() {
1381 None
1382 } else {
1383 current_oid.as_ref()
1384 };
1385 if new_oid != current_oid.as_ref() {
1386 update_git_head(
1387 git_repo,
1388 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
1389 current_oid,
1390 )
1391 .map_err(GitExportError::from_git)?;
1392 }
1393 }
1394 Ok(())
1395 };
1396
1397 let git_repo = get_git_repo(mut_repo.store())?;
1398
1399 check_and_detach_head(&git_repo)?;
1400 for worktree in git_repo.worktrees().map_err(GitExportError::from_git)? {
1401 if let Ok(worktree_repo) = worktree.into_repo_with_possibly_inaccessible_worktree() {
1402 check_and_detach_head(&worktree_repo)?;
1403 }
1404 }
1405
1406 let failed_bookmarks = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, bookmarks);
1407 let failed_tags = export_refs_to_git(mut_repo, &git_repo, GitRefKind::Tag, tags);
1408
1409 copy_exportable_local_bookmarks_to_remote_view(
1410 mut_repo,
1411 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
1412 |name| {
1413 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1414 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
1415 },
1416 );
1417 copy_exportable_local_tags_to_remote_view(mut_repo, REMOTE_NAME_FOR_LOCAL_GIT_REPO, |name| {
1418 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1419 git_ref_filter(GitRefKind::Tag, symbol) && get(&failed_tags, symbol).is_none()
1420 });
1421
1422 Ok(GitExportStats {
1423 failed_bookmarks,
1424 failed_tags,
1425 })
1426}
1427
1428fn export_refs_to_git(
1429 mut_repo: &mut MutableRepo,
1430 git_repo: &gix::Repository,
1431 kind: GitRefKind,
1432 refs: RefsToExport,
1433) -> Vec<(RemoteRefSymbolBuf, FailedRefExportReason)> {
1434 let mut failed = refs.failed;
1435 for (symbol, old_oid) in refs.to_delete {
1436 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1437 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1438 continue;
1439 };
1440 if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, &old_oid) {
1441 failed.push((symbol, reason));
1442 } else {
1443 let new_target = RefTarget::absent();
1444 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1445 }
1446 }
1447 for (symbol, (old_commit_oid, new_commit_oid)) in refs.to_update {
1448 let Some(git_ref_name) = to_git_ref_name(kind, symbol.as_ref()) else {
1449 failed.push((symbol, FailedRefExportReason::InvalidGitName));
1450 continue;
1451 };
1452 let new_ref_oid = match kind {
1453 GitRefKind::Bookmark => None,
1454 GitRefKind::Tag => {
1456 let remote_matcher = StringMatcher::all();
1457 find_git_tag_oid_to_copy(
1458 mut_repo.view(),
1459 git_repo,
1460 &symbol.name,
1461 &remote_matcher,
1462 &new_commit_oid,
1463 )
1464 }
1465 };
1466 if let Err(reason) = update_git_ref(
1467 git_repo,
1468 &git_ref_name,
1469 old_commit_oid,
1470 new_commit_oid,
1471 new_ref_oid,
1472 ) {
1473 failed.push((symbol, reason));
1474 } else {
1475 let new_target = RefTarget::normal(CommitId::from_bytes(new_commit_oid.as_bytes()));
1476 mut_repo.set_git_ref_target(&git_ref_name, new_target);
1477 }
1478 }
1479
1480 failed.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
1482 failed
1483}
1484
1485fn copy_exportable_local_bookmarks_to_remote_view(
1486 mut_repo: &mut MutableRepo,
1487 remote: &RemoteName,
1488 name_filter: impl Fn(&RefName) -> bool,
1489) {
1490 let new_local_bookmarks = mut_repo
1491 .view()
1492 .local_remote_bookmarks(remote)
1493 .filter_map(|(name, targets)| {
1494 let old_target = &targets.remote_ref.target;
1497 let new_target = targets.local_target;
1498 (new_target.is_resolved() && old_target != new_target).then_some((name, new_target))
1499 })
1500 .filter(|&(name, _)| name_filter(name))
1501 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1502 .collect_vec();
1503 for (name, new_target) in new_local_bookmarks {
1504 let new_remote_ref = RemoteRef {
1505 target: new_target,
1506 state: RemoteRefState::Tracked,
1507 };
1508 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1509 }
1510}
1511
1512fn copy_exportable_local_tags_to_remote_view(
1513 mut_repo: &mut MutableRepo,
1514 remote: &RemoteName,
1515 name_filter: impl Fn(&RefName) -> bool,
1516) {
1517 let new_local_tags = mut_repo
1518 .view()
1519 .local_remote_tags(remote)
1520 .filter_map(|(name, targets)| {
1521 let old_target = &targets.remote_ref.target;
1523 let new_target = targets.local_target;
1524 (new_target.is_resolved() && old_target != new_target).then_some((name, new_target))
1525 })
1526 .filter(|&(name, _)| name_filter(name))
1527 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1528 .collect_vec();
1529 for (name, new_target) in new_local_tags {
1530 let new_remote_ref = RemoteRef {
1531 target: new_target,
1532 state: RemoteRefState::Tracked,
1533 };
1534 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
1535 }
1536}
1537
1538fn diff_refs_to_export(
1540 view: &View,
1541 root_commit_id: &CommitId,
1542 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1543) -> AllRefsToExport {
1544 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1547 itertools::chain(
1548 view.local_bookmarks().map(|(name, target)| {
1549 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1550 (symbol, target)
1551 }),
1552 view.all_remote_bookmarks()
1553 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1554 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1555 )
1556 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1557 .map(|(symbol, new_target)| (symbol, (&ABSENT_REF_TARGET, new_target)))
1558 .collect();
1559 let mut all_tag_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> = view
1561 .local_tags()
1562 .map(|(name, target)| {
1563 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1564 (symbol, target)
1565 })
1566 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
1567 .map(|(symbol, new_target)| (symbol, (&ABSENT_REF_TARGET, new_target)))
1568 .collect();
1569 let known_git_refs = view
1570 .git_refs()
1571 .iter()
1572 .map(|(full_name, target)| {
1573 let (kind, symbol) =
1574 parse_git_ref(full_name).expect("stored git ref should be parsable");
1575 ((kind, symbol), target)
1576 })
1577 .filter(|&((kind, symbol), _)| git_ref_filter(kind, symbol));
1581 for ((kind, symbol), target) in known_git_refs {
1582 let ref_targets = match kind {
1583 GitRefKind::Bookmark => &mut all_bookmark_targets,
1584 GitRefKind::Tag => &mut all_tag_targets,
1585 };
1586 ref_targets
1587 .entry(symbol)
1588 .and_modify(|(old_target, _)| *old_target = target)
1589 .or_insert((target, &ABSENT_REF_TARGET));
1590 }
1591
1592 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1593 let bookmarks = collect_changed_refs_to_export(&all_bookmark_targets, &root_commit_target);
1594 let tags = collect_changed_refs_to_export(&all_tag_targets, &root_commit_target);
1595 AllRefsToExport { bookmarks, tags }
1596}
1597
1598fn collect_changed_refs_to_export(
1599 old_new_ref_targets: &HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)>,
1600 root_commit_target: &RefTarget,
1601) -> RefsToExport {
1602 let mut to_update = Vec::new();
1603 let mut to_delete = Vec::new();
1604 let mut failed = Vec::new();
1605 for (&symbol, &(old_target, new_target)) in old_new_ref_targets {
1606 if new_target == old_target {
1607 continue;
1608 }
1609 if new_target == root_commit_target {
1610 failed.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1612 continue;
1613 }
1614 let old_oid = if let Some(id) = old_target.as_normal() {
1615 Some(owned_oid_from_commit_id(id))
1616 } else if !old_target.is_resolved() {
1617 failed.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1620 continue;
1621 } else {
1622 assert!(old_target.is_absent());
1623 None
1624 };
1625 if let Some(id) = new_target.as_normal() {
1626 let new_oid = owned_oid_from_commit_id(id);
1627 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1628 } else if !new_target.is_resolved() {
1629 continue;
1631 } else {
1632 assert!(new_target.is_absent());
1633 to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1634 }
1635 }
1636
1637 to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1639 to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1640 failed.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1641 RefsToExport {
1642 to_update,
1643 to_delete,
1644 failed,
1645 }
1646}
1647
1648fn find_git_tag_oid_to_copy(
1651 view: &View,
1652 git_repo: &gix::Repository,
1653 name: &RefName,
1654 remote_matcher: &StringMatcher,
1655 commit_oid: &gix::oid,
1656) -> Option<gix::ObjectId> {
1657 view.remote_tags_matching(&StringMatcher::exact(name), remote_matcher)
1659 .filter(|(_, remote_ref)| {
1660 let maybe_id = remote_ref.tracked_target().as_normal();
1661 maybe_id.is_some_and(|id| id.as_bytes() == commit_oid.as_bytes())
1662 })
1663 .filter_map(|(symbol, _)| {
1665 let git_ref_name = to_git_or_remote_tag_ref_name(symbol);
1666 git_repo.find_reference(git_ref_name.as_str()).ok()
1667 })
1668 .filter(|git_ref| {
1671 resolve_git_ref_to_commit_id(git_ref, Some(commit_oid)).as_deref() == Some(commit_oid)
1672 })
1673 .find_map(|git_ref| git_ref.inner.target.try_into_id().ok())
1674}
1675
1676fn delete_git_ref(
1677 git_repo: &gix::Repository,
1678 git_ref_name: &GitRefName,
1679 old_oid: &gix::oid,
1680) -> Result<(), FailedRefExportReason> {
1681 let Some(git_ref) = git_repo
1682 .try_find_reference(git_ref_name.as_str())
1683 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?
1684 else {
1685 return Ok(());
1687 };
1688 if resolve_git_ref_to_commit_id(&git_ref, Some(old_oid)).as_deref() == Some(old_oid) {
1689 git_ref
1691 .delete()
1692 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))
1693 } else {
1694 Err(FailedRefExportReason::DeletedInJjModifiedInGit)
1696 }
1697}
1698
1699fn create_git_ref(
1701 git_repo: &gix::Repository,
1702 git_ref_name: &GitRefName,
1703 new_commit_oid: gix::ObjectId,
1704 new_ref_oid: Option<gix::ObjectId>,
1705) -> Result<(), FailedRefExportReason> {
1706 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1707 let constraint = gix::refs::transaction::PreviousValue::MustNotExist;
1708 let Err(set_err) =
1709 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1710 else {
1711 return Ok(());
1713 };
1714 let Some(git_ref) = git_repo
1715 .try_find_reference(git_ref_name.as_str())
1716 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1717 else {
1718 return Err(FailedRefExportReason::FailedToSet(set_err.into()));
1719 };
1720 if resolve_git_ref_to_commit_id(&git_ref, None) == Some(new_commit_oid) {
1723 Ok(())
1724 } else {
1725 Err(FailedRefExportReason::AddedInJjAddedInGit)
1726 }
1727}
1728
1729fn move_git_ref(
1731 git_repo: &gix::Repository,
1732 git_ref_name: &GitRefName,
1733 old_commit_oid: gix::ObjectId,
1734 new_commit_oid: gix::ObjectId,
1735 new_ref_oid: Option<gix::ObjectId>,
1736) -> Result<(), FailedRefExportReason> {
1737 let new_oid = new_ref_oid.unwrap_or(new_commit_oid);
1738 let constraint =
1739 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_commit_oid.into());
1740 let Err(set_err) =
1741 git_repo.reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1742 else {
1743 return Ok(());
1745 };
1746 let Some(git_ref) = git_repo
1748 .try_find_reference(git_ref_name.as_str())
1749 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?
1750 else {
1751 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1753 };
1754 let git_commit_oid = resolve_git_ref_to_commit_id(&git_ref, Some(&old_commit_oid));
1756 if git_commit_oid == Some(new_commit_oid) {
1757 Ok(())
1758 } else if git_commit_oid == Some(old_commit_oid) {
1759 let constraint =
1761 gix::refs::transaction::PreviousValue::MustExistAndMatch(git_ref.inner.target);
1762 git_repo
1763 .reference(git_ref_name.as_str(), new_oid, constraint, "export from jj")
1764 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1765 Ok(())
1766 } else {
1767 Err(FailedRefExportReason::FailedToSet(set_err.into()))
1768 }
1769}
1770
1771fn update_git_ref(
1772 git_repo: &gix::Repository,
1773 git_ref_name: &GitRefName,
1774 old_commit_oid: Option<gix::ObjectId>,
1775 new_commit_oid: gix::ObjectId,
1776 new_ref_oid: Option<gix::ObjectId>,
1777) -> Result<(), FailedRefExportReason> {
1778 match old_commit_oid {
1779 None => create_git_ref(git_repo, git_ref_name, new_commit_oid, new_ref_oid),
1780 Some(old_oid) => move_git_ref(git_repo, git_ref_name, old_oid, new_commit_oid, new_ref_oid),
1781 }
1782}
1783
1784fn update_git_head(
1787 git_repo: &gix::Repository,
1788 expected_ref: gix::refs::transaction::PreviousValue,
1789 new_oid: Option<gix::ObjectId>,
1790) -> gix::Result<()> {
1791 let mut ref_edits = Vec::new();
1792 let new_target = if let Some(oid) = new_oid {
1793 gix::refs::Target::Object(oid)
1794 } else {
1795 ref_edits.push(gix::refs::transaction::RefEdit {
1800 change: gix::refs::transaction::Change::Delete {
1801 expected: gix::refs::transaction::PreviousValue::Any,
1802 log: gix::refs::transaction::RefLog::AndReference,
1803 },
1804 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1805 deref: false,
1806 });
1807 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1808 };
1809 ref_edits.push(gix::refs::transaction::RefEdit {
1810 change: gix::refs::transaction::Change::Update {
1811 log: gix::refs::transaction::LogChange {
1812 message: "export from jj".into(),
1813 ..Default::default()
1814 },
1815 expected: expected_ref,
1816 new: new_target,
1817 },
1818 name: "HEAD".try_into().unwrap(),
1819 deref: false,
1820 });
1821 git_repo.edit_references(ref_edits)?;
1822 Ok(())
1823}
1824
1825#[derive(Debug, Error)]
1826pub enum GitCreateWorktreeError {
1827 #[error(transparent)]
1828 Subprocess(#[from] GitSubprocessError),
1829 #[error("Failed to inspect the Git worktree destination")]
1830 Destination(#[source] PathError),
1831 #[error("Failed to create temporary directory for Git worktree")]
1832 TempDir(#[source] std::io::Error),
1833 #[error("A .git file or directory already exists in the Git worktree destination")]
1834 ExistingGitLink,
1835 #[error("Failed to move .git gitlink file into place")]
1836 MoveGitLink(#[source] PathError),
1837 #[error(transparent)]
1838 Git(Box<dyn std::error::Error + Send + Sync>),
1839 #[error(transparent)]
1840 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1841}
1842
1843impl GitCreateWorktreeError {
1844 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1845 Self::Git(source.into())
1846 }
1847}
1848
1849pub fn create_worktree(
1861 store: &Store,
1862 subprocess_options: GitSubprocessOptions,
1863 destination: &Path,
1864) -> Result<(), GitCreateWorktreeError> {
1865 let git_backend = get_git_backend(store)?;
1866
1867 let branch_name = format!("jj-worktree-{:016x}", rand::random::<u64>());
1871 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
1872
1873 let destination_is_populated = destination.exists()
1874 && !is_empty_dir(destination).map_err(GitCreateWorktreeError::Destination)?;
1875 if destination_is_populated {
1876 add_worktree_to_populated_dir(&git_ctx, destination, &branch_name)?;
1877 } else {
1878 git_ctx.spawn_worktree_add(destination, &branch_name)?;
1879 }
1880
1881 let git_repo = git_backend
1890 .open_git_repo_at_workdir(destination)
1891 .map_err(GitCreateWorktreeError::from_git)?;
1892 let unborn_branch = gix::refs::Target::Symbolic(
1893 format!("refs/heads/{branch_name}")
1894 .try_into()
1895 .expect("valid ref name"),
1896 );
1897 update_git_head(
1898 &git_repo,
1899 gix::refs::transaction::PreviousValue::MustExistAndMatch(unborn_branch),
1900 None,
1901 )
1902 .map_err(GitCreateWorktreeError::from_git)
1903}
1904
1905fn add_worktree_to_populated_dir(
1911 git_ctx: &GitSubprocessContext,
1912 destination: &Path,
1913 branch_name: &str,
1914) -> Result<(), GitCreateWorktreeError> {
1915 let dot_git = destination.join(".git");
1916 if dot_git.exists() {
1917 return Err(GitCreateWorktreeError::ExistingGitLink);
1918 }
1919 let tmp_dir = TempDir::new_in(destination).map_err(GitCreateWorktreeError::TempDir)?;
1920 let tmp_path = tmp_dir.path();
1921 git_ctx.spawn_worktree_add(tmp_path, branch_name)?;
1922
1923 std::fs::rename(tmp_path.join(".git"), &dot_git)
1924 .context(&dot_git)
1925 .map_err(GitCreateWorktreeError::MoveGitLink)?;
1926 if let Err(err) = git_ctx.spawn_worktree_repair(destination) {
1927 std::fs::remove_file(&dot_git).ok();
1928 return Err(err.into());
1929 }
1930 Ok(())
1931}
1932
1933#[derive(Debug, Error)]
1934pub enum GitUnlinkWorktreeError {
1935 #[error("Failed to remove .git gitlink file")]
1936 RemoveGitLink(#[source] PathError),
1937 #[error(transparent)]
1938 Subprocess(#[from] GitSubprocessError),
1939 #[error(transparent)]
1940 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1941}
1942
1943pub fn unlink_worktree(
1956 store: &Store,
1957 subprocess_options: GitSubprocessOptions,
1958 worktree_path: &Path,
1959) -> Result<bool, GitUnlinkWorktreeError> {
1960 let dot_git = worktree_path.join(".git");
1961 if !dot_git.is_file() {
1962 return Ok(false);
1963 }
1964 let git_backend = get_git_backend(store)?;
1965 std::fs::remove_file(&dot_git)
1968 .context(&dot_git)
1969 .map_err(GitUnlinkWorktreeError::RemoveGitLink)?;
1970 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
1974 git_ctx.spawn_worktree_prune()?;
1975 Ok(true)
1976}
1977
1978#[derive(Debug, Error)]
1979pub enum GitResetHeadError {
1980 #[error(transparent)]
1981 Backend(#[from] BackendError),
1982 #[error(transparent)]
1983 Git(Box<dyn std::error::Error + Send + Sync>),
1984 #[error("Failed to update Git HEAD ref")]
1985 UpdateHeadRef(#[source] gix::Error),
1986 #[error(transparent)]
1987 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1988}
1989
1990impl GitResetHeadError {
1991 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1992 Self::Git(source.into())
1993 }
1994}
1995
1996pub async fn reset_head(
1999 mut_repo: &mut MutableRepo,
2000 workspace_name: &WorkspaceName,
2001 workspace_root: &Path,
2002 wc_commit: &Commit,
2003) -> Result<(), GitResetHeadError> {
2004 let git_backend = get_git_backend(mut_repo.store())?;
2005 let git_repo = git_backend
2006 .open_git_repo_at_workdir(workspace_root)
2007 .map_err(GitResetHeadError::from_git)?;
2008
2009 let first_parent_id = &wc_commit.parent_ids()[0];
2010 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
2011 RefTarget::normal(first_parent_id.clone())
2012 } else {
2013 RefTarget::absent()
2014 };
2015
2016 let old_head_target = mut_repo.git_head(workspace_name);
2018 if *old_head_target != new_head_target {
2019 let expected_ref = if let Some(id) = old_head_target.as_normal() {
2020 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
2023 if actual_head.is_detached() {
2024 let id = owned_oid_from_commit_id(id);
2025 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
2026 } else {
2027 gix::refs::transaction::PreviousValue::MustExist
2030 }
2031 } else {
2032 gix::refs::transaction::PreviousValue::MustExist
2034 };
2035 let new_oid = new_head_target.as_normal().map(owned_oid_from_commit_id);
2036 update_git_head(&git_repo, expected_ref, new_oid)
2037 .map_err(GitResetHeadError::UpdateHeadRef)?;
2038 mut_repo.set_git_head_target(workspace_name, new_head_target);
2039 }
2040
2041 if git_repo.state().is_some() {
2044 clear_operation_state(&git_repo)?;
2045 }
2046
2047 reset_index(mut_repo, &git_repo, wc_commit).await
2048}
2049
2050fn clear_operation_state(git_repo: &gix::Repository) -> Result<(), GitResetHeadError> {
2052 const STATE_FILE_NAMES: &[&str] = &[
2056 "MERGE_HEAD",
2057 "MERGE_MODE",
2058 "MERGE_MSG",
2059 "REVERT_HEAD",
2060 "CHERRY_PICK_HEAD",
2061 "BISECT_LOG",
2062 ];
2063 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
2064 let handle_err = |err: PathError| match err.source.kind() {
2065 std::io::ErrorKind::NotFound => Ok(()),
2066 _ => Err(GitResetHeadError::from_git(err)),
2067 };
2068 for file_name in STATE_FILE_NAMES {
2069 let path = git_repo.path().join(file_name);
2070 std::fs::remove_file(&path)
2071 .context(&path)
2072 .or_else(handle_err)?;
2073 }
2074 for dir_name in STATE_DIR_NAMES {
2075 let path = git_repo.path().join(dir_name);
2076 std::fs::remove_dir_all(&path)
2077 .context(&path)
2078 .or_else(handle_err)?;
2079 }
2080 Ok(())
2081}
2082
2083async fn reset_index(
2084 repo: &dyn Repo,
2085 git_repo: &gix::Repository,
2086 wc_commit: &Commit,
2087) -> Result<(), GitResetHeadError> {
2088 let parent_tree = wc_commit.parent_tree(repo).await?;
2089 let mut index = if let Some(tree_id) = parent_tree.tree_ids().as_resolved() {
2093 if tree_id == repo.store().empty_tree_id() {
2094 gix::index::File::from_state(
2098 gix::index::State::new(git_repo.object_hash()),
2099 git_repo.index_path(),
2100 )
2101 } else {
2102 git_repo
2105 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree_id.as_bytes()))
2106 .map_err(GitResetHeadError::from_git)?
2107 }
2108 } else {
2109 build_index_from_merged_tree(git_repo, &parent_tree)?
2110 };
2111
2112 let wc_tree = wc_commit.tree();
2113 update_intent_to_add_impl(git_repo, &mut index, &parent_tree, &wc_tree).await?;
2114
2115 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
2118 index
2119 .entries_mut_with_paths()
2120 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
2121 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
2122 .then_with(|| entry.stage().cmp(&old_entry.stage()))
2123 })
2124 .filter_map(|merged| merged.both())
2125 .map(|((entry, _), old_entry)| (entry, old_entry))
2126 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
2127 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
2128 }
2129
2130 debug_assert!(index.verify_entries().is_ok());
2131
2132 index
2133 .write(gix::index::write::Options::default())
2134 .map_err(GitResetHeadError::from_git)
2135}
2136
2137fn build_index_from_merged_tree(
2138 git_repo: &gix::Repository,
2139 merged_tree: &MergedTree,
2140) -> Result<gix::index::File, GitResetHeadError> {
2141 let mut index = gix::index::File::from_state(
2142 gix::index::State::new(git_repo.object_hash()),
2143 git_repo.index_path(),
2144 );
2145
2146 let mut push_index_entry =
2147 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
2148 let Some(entry) = maybe_entry else {
2149 return;
2150 };
2151
2152 let (id, mode) = match entry {
2153 TreeValue::File {
2154 id,
2155 executable,
2156 copy_id: _,
2157 } => {
2158 if *executable {
2159 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
2160 } else {
2161 (id.as_bytes(), gix::index::entry::Mode::FILE)
2162 }
2163 }
2164 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
2165 TreeValue::Tree(_) => {
2166 return;
2171 }
2172 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
2173 };
2174
2175 let path = BStr::new(path.as_internal_file_string());
2176
2177 index.dangerously_push_entry(
2180 gix::index::entry::Stat::default(),
2181 gix::ObjectId::from_bytes_or_panic(id),
2182 gix::index::entry::Flags::from_stage(stage),
2183 mode,
2184 path,
2185 );
2186 };
2187
2188 let mut has_many_sided_conflict = false;
2189
2190 for (path, entry) in merged_tree.entries() {
2191 let entry = entry?;
2192 if let Some(resolved) = entry.as_resolved() {
2193 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
2194 continue;
2195 }
2196
2197 let conflict = entry.simplify();
2198 if let [left, base, right] = conflict.as_slice() {
2199 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
2201 push_index_entry(&path, base, gix::index::entry::Stage::Base);
2202 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
2203 } else {
2204 has_many_sided_conflict = true;
2212 push_index_entry(
2213 &path,
2214 conflict.first(),
2215 gix::index::entry::Stage::Unconflicted,
2216 );
2217 }
2218 }
2219
2220 index.sort_entries();
2223
2224 if has_many_sided_conflict
2227 && index
2228 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
2229 .is_err()
2230 {
2231 let file_blob = git_repo
2232 .write_blob(
2233 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
2234 )
2235 .map_err(GitResetHeadError::from_git)?;
2236 index.dangerously_push_entry(
2237 gix::index::entry::Stat::default(),
2238 file_blob.detach(),
2239 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
2240 gix::index::entry::Mode::FILE,
2241 INDEX_DUMMY_CONFLICT_FILE.into(),
2242 );
2243 index.sort_entries();
2246 }
2247
2248 Ok(index)
2249}
2250
2251pub async fn update_intent_to_add(
2258 repo: &dyn Repo,
2259 workspace_root: &Path,
2260 old_tree: &MergedTree,
2261 new_tree: &MergedTree,
2262) -> Result<(), GitResetHeadError> {
2263 let git_backend = get_git_backend(repo.store())?;
2264 let git_repo = git_backend
2265 .open_git_repo_at_workdir(workspace_root)
2266 .map_err(GitResetHeadError::from_git)?;
2267 let mut index = git_repo
2268 .index_or_empty()
2269 .map_err(GitResetHeadError::from_git)?;
2270 let mut_index = Arc::make_mut(&mut index);
2271 update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).await?;
2272 debug_assert!(mut_index.verify_entries().is_ok());
2273 mut_index
2274 .write(gix::index::write::Options::default())
2275 .map_err(GitResetHeadError::from_git)?;
2276
2277 Ok(())
2278}
2279
2280async fn update_intent_to_add_impl(
2281 git_repo: &gix::Repository,
2282 index: &mut gix::index::File,
2283 old_tree: &MergedTree,
2284 new_tree: &MergedTree,
2285) -> Result<(), GitResetHeadError> {
2286 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
2287 let mut added_paths = vec![];
2288 let mut removed_paths = HashSet::new();
2289 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2290 let values = values?;
2291 if values.before.is_absent() {
2292 let executable = match values.after.as_normal() {
2293 Some(TreeValue::File {
2294 id: _,
2295 executable,
2296 copy_id: _,
2297 }) => *executable,
2298 Some(TreeValue::Symlink(_)) => false,
2299 _ => {
2300 continue;
2301 }
2302 };
2303 if index
2304 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
2305 .is_err()
2306 {
2307 added_paths.push((BString::from(path.into_internal_string()), executable));
2308 }
2309 } else if values.after.is_absent() {
2310 removed_paths.insert(BString::from(path.into_internal_string()));
2311 }
2312 }
2313
2314 if added_paths.is_empty() && removed_paths.is_empty() {
2315 return Ok(());
2316 }
2317
2318 if !added_paths.is_empty() {
2319 let empty_blob = git_repo
2321 .write_blob(b"")
2322 .map_err(GitResetHeadError::from_git)?
2323 .detach();
2324 for (path, executable) in added_paths {
2325 index.dangerously_push_entry(
2327 gix::index::entry::Stat::default(),
2328 empty_blob,
2329 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
2330 if executable {
2331 gix::index::entry::Mode::FILE_EXECUTABLE
2332 } else {
2333 gix::index::entry::Mode::FILE
2334 },
2335 path.as_ref(),
2336 );
2337 }
2338 }
2339 if !removed_paths.is_empty() {
2340 index.remove_entries(|_size, path, entry| {
2341 entry
2342 .flags
2343 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
2344 && removed_paths.contains(path)
2345 });
2346 }
2347
2348 index.sort_entries();
2349
2350 index.remove_tree();
2360
2361 Ok(())
2362}
2363
2364#[derive(Debug, Error)]
2365pub enum GitRemoteManagementError {
2366 #[error("No git remote named '{}'", .0.as_symbol())]
2367 NoSuchRemote(RemoteNameBuf),
2368 #[error("Git remote named '{}' already exists", .0.as_symbol())]
2369 RemoteAlreadyExists(RemoteNameBuf),
2370 #[error(transparent)]
2371 RemoteName(#[from] GitRemoteNameError),
2372 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
2373 NonstandardConfiguration(RemoteNameBuf),
2374 #[error("Error saving Git configuration")]
2375 GitConfigSaveError(#[source] std::io::Error),
2376 #[error("Unexpected Git error when managing remotes")]
2377 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
2378 #[error(transparent)]
2379 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2380 #[error(transparent)]
2381 RefExpansionError(#[from] GitRefExpansionError),
2382}
2383
2384impl GitRemoteManagementError {
2385 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
2386 Self::InternalGitError(source.into())
2387 }
2388}
2389
2390fn default_fetch_refspec(remote: &RemoteName) -> String {
2391 format!(
2392 "+refs/heads/*:{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/*",
2393 remote = remote.as_str()
2394 )
2395}
2396
2397fn add_ref(
2398 name: gix::refs::FullName,
2399 target: gix::refs::Target,
2400 message: BString,
2401) -> gix::refs::transaction::RefEdit {
2402 gix::refs::transaction::RefEdit {
2403 change: gix::refs::transaction::Change::Update {
2404 log: gix::refs::transaction::LogChange {
2405 mode: gix::refs::transaction::RefLog::AndReference,
2406 force_create_reflog: false,
2407 message,
2408 },
2409 expected: gix::refs::transaction::PreviousValue::MustNotExist,
2410 new: target,
2411 },
2412 name,
2413 deref: false,
2414 }
2415}
2416
2417fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
2418 gix::refs::transaction::RefEdit {
2419 change: gix::refs::transaction::Change::Delete {
2420 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
2421 reference.target().into_owned(),
2422 ),
2423 log: gix::refs::transaction::RefLog::AndReference,
2424 },
2425 name: reference.name().to_owned(),
2426 deref: false,
2427 }
2428}
2429
2430pub fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
2436 let mut config_file = File::create(
2437 config
2438 .meta()
2439 .path
2440 .as_ref()
2441 .expect("Git repository to have a config file"),
2442 )?;
2443 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
2444}
2445
2446fn save_remote(
2447 config: &mut gix::config::File,
2448 remote_name: &RemoteName,
2449 remote: &mut gix::Remote,
2450) -> Result<(), GitRemoteManagementError> {
2451 config
2458 .new_section("remote", remote_name.as_str())
2459 .map_err(GitRemoteManagementError::from_git)?;
2460 remote
2461 .save_as_to(remote_name.as_str(), config)
2462 .map_err(GitRemoteManagementError::from_git)?;
2463 Ok(())
2464}
2465
2466fn git_config_branch_section_ids_by_remote(
2467 config: &gix::config::File,
2468 remote_name: &RemoteName,
2469) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
2470 config
2471 .sections_by_name("branch")
2472 .into_iter()
2473 .flatten()
2474 .filter_map(|section| {
2475 let remote_values = section.values("remote");
2476 let push_remote_values = section.values("pushRemote");
2477 if !remote_values
2478 .iter()
2479 .chain(push_remote_values.iter())
2480 .any(|branch_remote_name| branch_remote_name == remote_name.as_str())
2481 {
2482 return None;
2483 }
2484 let is_supported_key = |name: &str| -> bool {
2486 name.eq_ignore_ascii_case("remote")
2487 || name.eq_ignore_ascii_case("merge")
2488 || name.eq_ignore_ascii_case("rebase")
2489 };
2490 if remote_values.len() > 1
2491 || push_remote_values.len() > 1
2492 || !section.value_names().all(|name| is_supported_key(&name))
2493 {
2494 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
2495 remote_name.to_owned(),
2496 )));
2497 }
2498 Some(Ok(section.id()))
2499 })
2500 .collect()
2501}
2502
2503fn rename_remote_in_git_branch_config_sections(
2504 config: &mut gix::config::File,
2505 old_remote_name: &RemoteName,
2506 new_remote_name: &RemoteName,
2507) -> Result<(), GitRemoteManagementError> {
2508 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
2509 config
2510 .section_mut_by_id(id)
2511 .expect("found section to exist")
2512 .set("remote", new_remote_name.as_str())
2513 .expect("'remote' to be a valid value name");
2514 }
2515 Ok(())
2516}
2517
2518fn remove_remote_git_branch_config_sections(
2519 config: &mut gix::config::File,
2520 remote_name: &RemoteName,
2521) -> Result<(), GitRemoteManagementError> {
2522 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
2523 config
2524 .remove_section_by_id(id)
2525 .expect("removed section to exist");
2526 }
2527 Ok(())
2528}
2529
2530fn remove_remote_git_config_sections(
2531 config: &mut gix::config::File,
2532 remote_name: &RemoteName,
2533) -> Result<(), GitRemoteManagementError> {
2534 let section_ids_to_remove: Vec<_> = config
2535 .sections_by_name("remote")
2536 .into_iter()
2537 .flatten()
2538 .filter(|section| {
2539 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
2540 })
2541 .map(|section| {
2542 if section.value_names().any(|name| {
2543 !name.eq_ignore_ascii_case("url")
2544 && !name.eq_ignore_ascii_case("fetch")
2545 && !name.eq_ignore_ascii_case("tagOpt")
2546 }) {
2547 return Err(GitRemoteManagementError::NonstandardConfiguration(
2548 remote_name.to_owned(),
2549 ));
2550 }
2551 Ok(section.id())
2552 })
2553 .try_collect()?;
2554 for id in section_ids_to_remove {
2555 config
2556 .remove_section_by_id(id)
2557 .expect("removed section to exist");
2558 }
2559 Ok(())
2560}
2561
2562pub fn get_all_remote_names(
2564 store: &Store,
2565) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
2566 let git_repo = get_git_repo(store)?;
2567 Ok(iter_remote_names(&git_repo).collect())
2568}
2569
2570fn iter_remote_names(git_repo: &gix::Repository) -> impl Iterator<Item = RemoteNameBuf> {
2571 git_repo
2572 .remote_names()
2573 .into_iter()
2574 .filter_map(|name| String::from_utf8(name.into()).ok())
2576 .map(RemoteNameBuf::from)
2577 .filter(|name| try_find_active_remote_inner(git_repo, name).is_some())
2579}
2580
2581pub fn try_find_active_remote<'a>(
2584 git_repo: &'a gix::Repository,
2585 name: &RemoteName,
2586) -> Result<Option<gix::Remote<'a>>, GitRemoteManagementError> {
2587 try_find_active_remote_inner(git_repo, name)
2588 .transpose()
2589 .map_err(GitRemoteManagementError::from_git)
2590}
2591
2592fn try_find_active_remote_inner<'a>(
2593 git_repo: &'a gix::Repository,
2594 name: &RemoteName,
2595) -> Option<gix::Result<gix::Remote<'a>>> {
2596 git_repo
2599 .try_find_remote(name.as_str())
2600 .filter(|result| match result {
2601 Ok(remote) => {
2602 remote.url(gix::remote::Direction::Fetch).is_some()
2603 || remote.url(gix::remote::Direction::Push).is_some()
2604 }
2605 Err(_) => true,
2606 })
2607}
2608
2609pub fn add_remote(
2610 mut_repo: &mut MutableRepo,
2611 remote_name: &RemoteName,
2612 url: &str,
2613 push_url: Option<&str>,
2614) -> Result<(), GitRemoteManagementError> {
2615 let git_repo = get_git_repo(mut_repo.store())?;
2616
2617 validate_remote_name(remote_name)?;
2618
2619 if try_find_active_remote_inner(&git_repo, remote_name).is_some() {
2620 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2621 remote_name.to_owned(),
2622 ));
2623 }
2624
2625 let mut remote = git_repo
2626 .remote_at(url)
2627 .map_err(GitRemoteManagementError::from_git)?
2628 .with_refspecs(
2629 [default_fetch_refspec(remote_name).as_bytes()],
2630 gix::remote::Direction::Fetch,
2631 )
2632 .expect("default refspec to be valid");
2633
2634 if let Some(push_url) = push_url {
2635 remote = remote
2636 .with_push_url(push_url)
2637 .map_err(GitRemoteManagementError::from_git)?;
2638 }
2639
2640 let mut config = git_repo.config_snapshot().clone();
2641 save_remote(&mut config, remote_name, &mut remote)?;
2642 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2643
2644 mut_repo.ensure_remote(remote_name);
2645
2646 Ok(())
2647}
2648
2649pub fn remove_remote(
2650 mut_repo: &mut MutableRepo,
2651 remote_name: &RemoteName,
2652) -> Result<(), GitRemoteManagementError> {
2653 let mut git_repo = get_git_repo(mut_repo.store())?;
2654
2655 if try_find_active_remote_inner(&git_repo, remote_name).is_none() {
2656 return Err(GitRemoteManagementError::NoSuchRemote(
2657 remote_name.to_owned(),
2658 ));
2659 }
2660
2661 let mut config = git_repo.config_snapshot().clone();
2662 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
2663 remove_remote_git_config_sections(&mut config, remote_name)?;
2664 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2665
2666 remove_remote_git_refs(&mut git_repo, remote_name)
2667 .map_err(GitRemoteManagementError::from_git)?;
2668
2669 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2670 remove_remote_refs(mut_repo, remote_name);
2671 }
2672
2673 Ok(())
2674}
2675
2676fn remove_remote_git_refs(
2677 git_repo: &mut gix::Repository,
2678 remote: &RemoteName,
2679) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2680 let bookmark_prefix = format!(
2681 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2682 remote = remote.as_str()
2683 );
2684 let tag_prefix = format!(
2685 "{REMOTE_TAG_REF_NAMESPACE}{remote}/",
2686 remote = remote.as_str()
2687 );
2688 let edits: Vec<_> = itertools::chain(
2689 git_repo
2690 .references()?
2691 .prefixed(bookmark_prefix.as_str())?
2692 .map_ok(remove_ref),
2693 git_repo
2694 .references()?
2695 .prefixed(tag_prefix.as_str())?
2696 .map_ok(remove_ref),
2697 )
2698 .try_collect()?;
2699 git_repo.edit_references(edits)?;
2700 Ok(())
2701}
2702
2703fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
2704 mut_repo.remove_remote(remote);
2705 let prefix = format!(
2706 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2707 remote = remote.as_str()
2708 );
2709 let git_refs_to_delete = mut_repo
2710 .view()
2711 .git_refs()
2712 .keys()
2713 .filter(|&r| r.as_str().starts_with(&prefix))
2714 .cloned()
2715 .collect_vec();
2716 for git_ref in git_refs_to_delete {
2717 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
2718 }
2719}
2720
2721pub fn rename_remote(
2722 mut_repo: &mut MutableRepo,
2723 old_remote_name: &RemoteName,
2724 new_remote_name: &RemoteName,
2725) -> Result<(), GitRemoteManagementError> {
2726 let mut git_repo = get_git_repo(mut_repo.store())?;
2727
2728 validate_remote_name(new_remote_name)?;
2729
2730 let mut remote = try_find_active_remote(&git_repo, old_remote_name)?
2731 .ok_or_else(|| GitRemoteManagementError::NoSuchRemote(old_remote_name.to_owned()))?;
2732
2733 if try_find_active_remote_inner(&git_repo, new_remote_name).is_some() {
2734 return Err(GitRemoteManagementError::RemoteAlreadyExists(
2735 new_remote_name.to_owned(),
2736 ));
2737 }
2738
2739 match (
2740 remote.refspecs(gix::remote::Direction::Fetch),
2741 remote.refspecs(gix::remote::Direction::Push),
2742 ) {
2743 ([refspec], [])
2744 if refspec.to_ref().to_bstring()
2745 == default_fetch_refspec(old_remote_name).as_bytes() => {}
2746 _ => {
2747 return Err(GitRemoteManagementError::NonstandardConfiguration(
2748 old_remote_name.to_owned(),
2749 ));
2750 }
2751 }
2752
2753 remote
2754 .replace_refspecs(
2755 [default_fetch_refspec(new_remote_name).as_bytes()],
2756 gix::remote::Direction::Fetch,
2757 )
2758 .expect("default refspec to be valid");
2759
2760 let mut config = git_repo.config_snapshot().clone();
2761 save_remote(&mut config, new_remote_name, &mut remote)?;
2762 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
2763 remove_remote_git_config_sections(&mut config, old_remote_name)?;
2764 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2765
2766 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
2767 .map_err(GitRemoteManagementError::from_git)?;
2768
2769 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
2770 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
2771 }
2772
2773 Ok(())
2774}
2775
2776fn rename_remote_git_refs(
2777 git_repo: &mut gix::Repository,
2778 old_remote_name: &RemoteName,
2779 new_remote_name: &RemoteName,
2780) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
2781 let to_prefixes = |namespace: &str| {
2782 (
2783 format!("{namespace}{remote}/", remote = old_remote_name.as_str()),
2784 format!("{namespace}{remote}/", remote = new_remote_name.as_str()),
2785 )
2786 };
2787 let to_rename_edits = {
2788 let ref_log_message = BString::from(format!(
2789 "renamed remote {old_remote_name} to {new_remote_name}",
2790 old_remote_name = old_remote_name.as_symbol(),
2791 new_remote_name = new_remote_name.as_symbol(),
2792 ));
2793 move |old_prefix: &str, new_prefix: &str, old_ref: gix::Reference| {
2794 let new_name = BString::new(
2795 [
2796 new_prefix.as_bytes(),
2797 &old_ref.name().as_bstr()[old_prefix.len()..],
2798 ]
2799 .concat(),
2800 );
2801 [
2802 add_ref(
2803 new_name.try_into().expect("new ref name to be valid"),
2804 old_ref.target().into_owned(),
2805 ref_log_message.clone(),
2806 ),
2807 remove_ref(old_ref),
2808 ]
2809 }
2810 };
2811
2812 let (old_bookmark_prefix, new_bookmark_prefix) = to_prefixes(REMOTE_BOOKMARK_REF_NAMESPACE);
2813 let (old_tag_prefix, new_tag_prefix) = to_prefixes(REMOTE_TAG_REF_NAMESPACE);
2814 let edits: Vec<_> = itertools::chain(
2815 git_repo
2816 .references()?
2817 .prefixed(old_bookmark_prefix.as_str())?
2818 .map_ok(|old_ref| to_rename_edits(&old_bookmark_prefix, &new_bookmark_prefix, old_ref)),
2819 git_repo
2820 .references()?
2821 .prefixed(old_tag_prefix.as_str())?
2822 .map_ok(|old_ref| to_rename_edits(&old_tag_prefix, &new_tag_prefix, old_ref)),
2823 )
2824 .flatten_ok()
2825 .try_collect()?;
2826 git_repo.edit_references(edits)?;
2827 Ok(())
2828}
2829
2830pub fn set_remote_urls(
2834 store: &Store,
2835 remote_name: &RemoteName,
2836 new_url: Option<&str>,
2837 new_push_url: Option<&str>,
2838) -> Result<(), GitRemoteManagementError> {
2839 if new_url.is_none() && new_push_url.is_none() {
2841 return Ok(());
2842 }
2843
2844 let git_repo = get_git_repo(store)?;
2845
2846 validate_remote_name(remote_name)?;
2847
2848 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2849 return Err(GitRemoteManagementError::NoSuchRemote(
2850 remote_name.to_owned(),
2851 ));
2852 };
2853 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2854
2855 if let Some(url) = new_url {
2856 remote = remote
2857 .with_url(url)
2858 .map_err(GitRemoteManagementError::from_git)?;
2859 }
2860
2861 if let Some(url) = new_push_url {
2862 remote = remote
2863 .with_push_url(url)
2864 .map_err(GitRemoteManagementError::from_git)?;
2865 }
2866
2867 let mut config = git_repo.config_snapshot().clone();
2868 save_remote(&mut config, remote_name, &mut remote)?;
2869 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2870
2871 Ok(())
2872}
2873
2874fn rename_remote_refs(
2875 mut_repo: &mut MutableRepo,
2876 old_remote_name: &RemoteName,
2877 new_remote_name: &RemoteName,
2878) {
2879 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2880 let prefix = format!(
2881 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/",
2882 remote = old_remote_name.as_str()
2883 );
2884 let git_refs = mut_repo
2885 .view()
2886 .git_refs()
2887 .iter()
2888 .filter_map(|(old, target)| {
2889 old.as_str().strip_prefix(&prefix).map(|p| {
2890 let new: GitRefNameBuf = format!(
2891 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{p}",
2892 remote = new_remote_name.as_str()
2893 )
2894 .into();
2895 (old.clone(), new, target.clone())
2896 })
2897 })
2898 .collect_vec();
2899 for (old, new, target) in git_refs {
2900 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2901 mut_repo.set_git_ref_target(&new, target);
2902 }
2903}
2904
2905const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2906
2907#[derive(Error, Debug)]
2908pub enum GitFetchError {
2909 #[error("No git remote named '{}'", .0.as_symbol())]
2910 NoSuchRemote(RemoteNameBuf),
2911 #[error(transparent)]
2912 RemoteName(#[from] GitRemoteNameError),
2913 #[error("Failed to update refs: {}", .0.iter().map(|n| n.as_symbol()).join(", "))]
2914 RejectedUpdates(Vec<GitRefNameBuf>),
2915 #[error(transparent)]
2916 Subprocess(#[from] GitSubprocessError),
2917}
2918
2919#[derive(Error, Debug)]
2920pub enum GitDefaultRefspecError {
2921 #[error("No git remote named '{}'", .0.as_symbol())]
2922 NoSuchRemote(RemoteNameBuf),
2923 #[error("Invalid configuration for remote `{}`", .0.as_symbol())]
2924 InvalidRemoteConfiguration(RemoteNameBuf, #[source] gix::Error),
2925}
2926
2927struct FetchedRefs {
2928 remote: RemoteNameBuf,
2929 bookmark_matcher: StringMatcher,
2930 tag_matcher: StringMatcher,
2931}
2932
2933#[derive(Clone, Debug)]
2935pub struct GitFetchRefExpression {
2936 pub bookmark: StringExpression,
2938 pub tag: StringExpression,
2944}
2945
2946#[derive(Debug)]
2948pub struct ExpandedFetchRefSpecs {
2949 expr: GitFetchRefExpression,
2951 refspecs: Vec<RefSpec>,
2952 negative_refspecs: Vec<NegativeRefSpec>,
2953}
2954
2955#[derive(Error, Debug)]
2956pub enum GitRefExpansionError {
2957 #[error(transparent)]
2958 Expression(#[from] GitRefExpressionError),
2959 #[error(
2960 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2961 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2962 )]
2963 InvalidBranchPattern(StringPattern),
2964}
2965
2966pub fn expand_fetch_refspecs(
2968 remote: &RemoteName,
2969 expr: GitFetchRefExpression,
2970) -> Result<ExpandedFetchRefSpecs, GitRefExpansionError> {
2971 let (positive_bookmarks, negative_bookmarks) =
2972 split_into_positive_negative_patterns(&expr.bookmark)?;
2973 let (positive_tags, negative_tags) = split_into_positive_negative_patterns(&expr.tag)?;
2974
2975 let refspecs = itertools::chain(
2976 positive_bookmarks
2977 .iter()
2978 .map(|&pattern| pattern_to_refspec_glob(pattern))
2979 .map_ok(|glob| {
2980 RefSpec::forced(
2981 format!("refs/heads/{glob}"),
2982 format!(
2983 "{REMOTE_BOOKMARK_REF_NAMESPACE}{remote}/{glob}",
2984 remote = remote.as_str()
2985 ),
2986 )
2987 }),
2988 positive_tags
2989 .iter()
2990 .map(|&pattern| pattern_to_refspec_glob(pattern))
2991 .map_ok(|glob| {
2992 RefSpec::forced(
2993 format!("refs/tags/{glob}"),
2994 format!(
2995 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{glob}",
2996 remote = remote.as_str()
2997 ),
2998 )
2999 }),
3000 )
3001 .try_collect()?;
3002
3003 let negative_refspecs = itertools::chain(
3004 negative_bookmarks
3005 .iter()
3006 .map(|&pattern| pattern_to_refspec_glob(pattern))
3007 .map_ok(|glob| NegativeRefSpec::new(format!("refs/heads/{glob}"))),
3008 negative_tags
3009 .iter()
3010 .map(|&pattern| pattern_to_refspec_glob(pattern))
3011 .map_ok(|glob| NegativeRefSpec::new(format!("refs/tags/{glob}"))),
3012 )
3013 .try_collect()?;
3014
3015 Ok(ExpandedFetchRefSpecs {
3016 expr,
3017 refspecs,
3018 negative_refspecs,
3019 })
3020}
3021
3022fn pattern_to_refspec_glob(pattern: &StringPattern) -> Result<Cow<'_, str>, GitRefExpansionError> {
3023 pattern
3024 .to_glob()
3025 .filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
3028 .ok_or_else(|| GitRefExpansionError::InvalidBranchPattern(pattern.clone()))
3029}
3030
3031#[derive(Debug, Error)]
3032pub enum GitRefExpressionError {
3033 #[error("Cannot use `~` in sub expression")]
3034 NestedNotIn,
3035 #[error("Cannot use `&` in sub expression")]
3036 NestedIntersection,
3037 #[error("Cannot use `&` for positive expressions")]
3038 PositiveIntersection,
3039}
3040
3041fn split_into_positive_negative_patterns(
3044 expr: &StringExpression,
3045) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
3046 static ALL: StringPattern = StringPattern::all();
3047
3048 fn visit_positive<'a>(
3062 expr: &'a StringExpression,
3063 positives: &mut Vec<&'a StringPattern>,
3064 negatives: &mut Vec<&'a StringPattern>,
3065 ) -> Result<(), GitRefExpressionError> {
3066 match expr {
3067 StringExpression::Pattern(pattern) => {
3068 positives.push(pattern);
3069 Ok(())
3070 }
3071 StringExpression::NotIn(complement) => {
3072 positives.push(&ALL);
3073 visit_negative(complement, negatives)
3074 }
3075 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
3076 StringExpression::Intersection(expr1, expr2) => {
3077 match (expr1.as_ref(), expr2.as_ref()) {
3078 (other, StringExpression::NotIn(complement))
3079 | (StringExpression::NotIn(complement), other) => {
3080 visit_positive(other, positives, negatives)?;
3081 visit_negative(complement, negatives)
3082 }
3083 _ => Err(GitRefExpressionError::PositiveIntersection),
3084 }
3085 }
3086 }
3087 }
3088
3089 fn visit_negative<'a>(
3090 expr: &'a StringExpression,
3091 negatives: &mut Vec<&'a StringPattern>,
3092 ) -> Result<(), GitRefExpressionError> {
3093 match expr {
3094 StringExpression::Pattern(pattern) => {
3095 negatives.push(pattern);
3096 Ok(())
3097 }
3098 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
3099 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
3100 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
3101 }
3102 }
3103
3104 fn visit_union<'a>(
3105 expr1: &'a StringExpression,
3106 expr2: &'a StringExpression,
3107 patterns: &mut Vec<&'a StringPattern>,
3108 ) -> Result<(), GitRefExpressionError> {
3109 visit_union_sub(expr1, patterns)?;
3110 visit_union_sub(expr2, patterns)
3111 }
3112
3113 fn visit_union_sub<'a>(
3114 expr: &'a StringExpression,
3115 patterns: &mut Vec<&'a StringPattern>,
3116 ) -> Result<(), GitRefExpressionError> {
3117 match expr {
3118 StringExpression::Pattern(pattern) => {
3119 patterns.push(pattern);
3120 Ok(())
3121 }
3122 StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
3123 StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
3124 StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
3125 }
3126 }
3127
3128 let mut positives = Vec::new();
3129 let mut negatives = Vec::new();
3130 visit_positive(expr, &mut positives, &mut negatives)?;
3131 if positives.iter().all(|pattern| pattern.is_all())
3134 && !negatives.is_empty()
3135 && negatives.iter().all(|pattern| pattern.is_all())
3136 {
3137 Ok((vec![], vec![]))
3138 } else {
3139 Ok((positives, negatives))
3140 }
3141}
3142
3143#[derive(Debug)]
3147#[must_use = "warnings should be surfaced in the UI"]
3148pub struct IgnoredRefspecs(pub Vec<IgnoredRefspec>);
3149
3150#[derive(Debug)]
3153pub struct IgnoredRefspec {
3154 pub refspec: BString,
3156 pub reason: &'static str,
3158}
3159
3160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3161enum FetchRefSpecKind {
3162 Positive,
3163 Negative,
3164}
3165
3166pub fn load_default_fetch_bookmarks(
3168 remote_name: &RemoteName,
3169 git_repo: &gix::Repository,
3170) -> Result<(IgnoredRefspecs, StringExpression), GitDefaultRefspecError> {
3171 let remote = try_find_active_remote_inner(git_repo, remote_name)
3172 .ok_or_else(|| GitDefaultRefspecError::NoSuchRemote(remote_name.to_owned()))?
3173 .map_err(|e| {
3174 GitDefaultRefspecError::InvalidRemoteConfiguration(remote_name.to_owned(), e)
3175 })?;
3176
3177 let remote_refspecs = remote.refspecs(gix::remote::Direction::Fetch);
3178 let mut ignored_refspecs = Vec::with_capacity(remote_refspecs.len());
3179 let mut positive_bookmarks = Vec::with_capacity(remote_refspecs.len());
3180 let mut negative_bookmarks = Vec::new();
3181 for refspec in remote_refspecs {
3182 let refspec = refspec.to_ref();
3183 match parse_fetch_refspec(remote_name, refspec) {
3184 Ok((FetchRefSpecKind::Positive, bookmark)) => {
3185 positive_bookmarks.push(StringExpression::pattern(bookmark));
3186 }
3187 Ok((FetchRefSpecKind::Negative, bookmark)) => {
3188 negative_bookmarks.push(StringExpression::pattern(bookmark));
3189 }
3190 Err(reason) => {
3191 let refspec = refspec.to_bstring();
3192 ignored_refspecs.push(IgnoredRefspec { refspec, reason });
3193 }
3194 }
3195 }
3196
3197 let mut bookmark_expr = StringExpression::union_all(positive_bookmarks);
3198 if !negative_bookmarks.is_empty() {
3200 bookmark_expr =
3201 bookmark_expr.intersection(StringExpression::union_all(negative_bookmarks).negated());
3202 }
3203
3204 Ok((IgnoredRefspecs(ignored_refspecs), bookmark_expr))
3205}
3206
3207fn parse_fetch_refspec(
3208 remote_name: &RemoteName,
3209 refspec: gix::refspec::RefSpecRef<'_>,
3210) -> Result<(FetchRefSpecKind, StringPattern), &'static str> {
3211 let ensure_utf8 = |s| str::from_utf8(s).map_err(|_| "invalid UTF-8");
3212
3213 let (src, positive_dst) = match refspec.instruction() {
3214 Instruction::Push(_) => panic!("push refspec should be filtered out by caller"),
3215 Instruction::Fetch(fetch) => match fetch {
3216 gix::refspec::instruction::Fetch::Only { src: _ } => {
3217 return Err("fetch-only refspecs are not supported");
3218 }
3219 gix::refspec::instruction::Fetch::AndUpdate {
3220 src,
3221 dst,
3222 allow_non_fast_forward,
3223 } => {
3224 if !allow_non_fast_forward {
3225 return Err("non-forced refspecs are not supported");
3226 }
3227 (ensure_utf8(src)?, Some(ensure_utf8(dst)?))
3228 }
3229 gix::refspec::instruction::Fetch::Exclude { src } => (ensure_utf8(src)?, None),
3230 },
3231 };
3232
3233 let src_branch = src
3234 .strip_prefix("refs/heads/")
3235 .ok_or("only refs/heads/ is supported for refspec sources")?;
3236 let branch = StringPattern::glob(src_branch).map_err(|_| "invalid pattern")?;
3237
3238 if let Some(dst) = positive_dst {
3239 let dst_without_prefix = dst
3240 .strip_prefix(REMOTE_BOOKMARK_REF_NAMESPACE)
3241 .ok_or("only refs/remotes/ is supported for fetch destinations")?;
3242 let dst_branch = dst_without_prefix
3243 .strip_prefix(remote_name.as_str())
3244 .and_then(|d| d.strip_prefix("/"))
3245 .ok_or("remote renaming not supported")?;
3246 if src_branch != dst_branch {
3247 return Err("renaming is not supported");
3248 }
3249 Ok((FetchRefSpecKind::Positive, branch))
3250 } else {
3251 Ok((FetchRefSpecKind::Negative, branch))
3252 }
3253}
3254
3255pub struct GitFetch<'a> {
3257 mut_repo: &'a mut MutableRepo,
3258 git_repo: Box<gix::Repository>,
3259 git_ctx: GitSubprocessContext,
3260 import_options: &'a GitImportOptions,
3261 fetched: Vec<FetchedRefs>,
3262}
3263
3264impl<'a> GitFetch<'a> {
3265 pub fn new(
3266 mut_repo: &'a mut MutableRepo,
3267 subprocess_options: GitSubprocessOptions,
3268 import_options: &'a GitImportOptions,
3269 ) -> Result<Self, UnexpectedGitBackendError> {
3270 let git_backend = get_git_backend(mut_repo.store())?;
3271 let git_repo = Box::new(git_backend.git_repo());
3272 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3273 Ok(GitFetch {
3274 mut_repo,
3275 git_repo,
3276 git_ctx,
3277 import_options,
3278 fetched: vec![],
3279 })
3280 }
3281
3282 #[tracing::instrument(skip(self, callback))]
3288 pub fn fetch(
3289 &mut self,
3290 remote_name: &RemoteName,
3291 ExpandedFetchRefSpecs {
3292 expr,
3293 refspecs: mut remaining_refspecs,
3294 negative_refspecs,
3295 }: ExpandedFetchRefSpecs,
3296 callback: &mut dyn GitSubprocessCallback,
3297 depth: Option<NonZeroU32>,
3298 ) -> Result<(), GitFetchError> {
3299 validate_remote_name(remote_name)?;
3300
3301 if try_find_active_remote_inner(&self.git_repo, remote_name).is_none() {
3303 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3304 }
3305
3306 if remaining_refspecs.is_empty() {
3307 return Ok(());
3309 }
3310
3311 let mut branches_to_prune = Vec::new();
3312 let updates = loop {
3320 let status = self.git_ctx.spawn_fetch(
3321 remote_name,
3322 &remaining_refspecs,
3323 &negative_refspecs,
3324 callback,
3325 depth,
3326 )?;
3327 let failing_refspec = match status {
3328 GitFetchStatus::Updates(updates) => break updates,
3329 GitFetchStatus::NoRemoteRef(failing_refspec) => failing_refspec,
3330 };
3331 tracing::debug!(failing_refspec, "failed to fetch ref");
3332 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
3333
3334 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
3335 branches_to_prune.push(format!(
3336 "{remote_name}/{branch_name}",
3337 remote_name = remote_name.as_str()
3338 ));
3339 }
3340 };
3341
3342 if !updates.rejected.is_empty() {
3345 let names = updates.rejected.into_iter().map(|(name, _)| name).collect();
3346 return Err(GitFetchError::RejectedUpdates(names));
3347 }
3348
3349 self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
3352
3353 self.fetched.push(FetchedRefs {
3354 remote: remote_name.to_owned(),
3355 bookmark_matcher: expr.bookmark.to_matcher(),
3356 tag_matcher: expr.tag.to_matcher(),
3357 });
3358 Ok(())
3359 }
3360
3361 #[tracing::instrument(skip(self))]
3363 pub fn get_default_branch(
3364 &self,
3365 remote_name: &RemoteName,
3366 ) -> Result<Option<RefNameBuf>, GitFetchError> {
3367 if try_find_active_remote_inner(&self.git_repo, remote_name).is_none() {
3368 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
3369 }
3370 let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
3371 tracing::debug!(?default_branch);
3372 Ok(default_branch)
3373 }
3374
3375 #[tracing::instrument(skip(self))]
3382 pub async fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
3383 tracing::debug!("import_refs");
3384 let all_remote_tags = true;
3385 let refs_to_import = diff_refs_to_import(
3386 self.mut_repo.view(),
3387 &self.git_repo,
3388 all_remote_tags,
3389 |kind, symbol| match kind {
3390 GitRefKind::Bookmark => self
3391 .fetched
3392 .iter()
3393 .filter(|fetched| fetched.remote == symbol.remote)
3394 .any(|fetched| fetched.bookmark_matcher.is_match(symbol.name.as_str())),
3395 GitRefKind::Tag => self
3396 .fetched
3397 .iter()
3398 .filter(|fetched| fetched.remote == symbol.remote)
3399 .any(|fetched| fetched.tag_matcher.is_match(symbol.name.as_str())),
3400 },
3401 )?;
3402 let import_stats =
3403 import_refs_inner(self.mut_repo, refs_to_import, self.import_options).await?;
3404
3405 self.fetched.clear();
3406
3407 Ok(import_stats)
3408 }
3409}
3410
3411#[derive(Error, Debug)]
3412pub enum GitPushError {
3413 #[error("No git remote named '{}'", .0.as_symbol())]
3414 NoSuchRemote(RemoteNameBuf),
3415 #[error(transparent)]
3416 RemoteName(#[from] GitRemoteNameError),
3417 #[error(transparent)]
3418 Subprocess(#[from] GitSubprocessError),
3419 #[error(transparent)]
3420 UnexpectedBackend(#[from] UnexpectedGitBackendError),
3421}
3422
3423#[derive(Clone, Debug, Default)]
3424pub struct GitPushRefTargets {
3425 pub bookmarks: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3427 pub tags: Vec<(RefNameBuf, Diff<Option<CommitId>>)>,
3429}
3430
3431pub struct GitRefUpdate {
3432 pub qualified_name: GitRefNameBuf,
3433 pub targets: Diff<Option<gix::ObjectId>>,
3438}
3439
3440#[derive(Clone, Debug, Default)]
3442pub struct GitPushOptions {
3443 pub remote_push_options: Vec<String>,
3445}
3446
3447pub fn push_refs(
3449 mut_repo: &mut MutableRepo,
3450 subprocess_options: GitSubprocessOptions,
3451 remote: &RemoteName,
3452 targets: &GitPushRefTargets,
3453 callback: &mut dyn GitSubprocessCallback,
3454 options: &GitPushOptions,
3455) -> Result<GitPushStats, GitPushError> {
3456 validate_remote_name(remote)?;
3457
3458 let git_repo = get_git_repo(mut_repo.store())?;
3459 let to_tag_target = |name: &RefName, remote: &RemoteName, id: &CommitId| {
3460 let remote_matcher = StringMatcher::exact(remote);
3461 let oid = owned_oid_from_commit_id(id);
3462 find_git_tag_oid_to_copy(mut_repo.view(), &git_repo, name, &remote_matcher, &oid)
3463 .unwrap_or(oid)
3464 };
3465 let ref_updates = itertools::chain(
3466 targets.bookmarks.iter().map(|(name, update)| GitRefUpdate {
3467 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
3468 targets: update
3469 .as_ref()
3470 .map(|id| id.as_ref().map(owned_oid_from_commit_id)),
3471 }),
3472 targets.tags.iter().map(|(name, update)| GitRefUpdate {
3473 qualified_name: format!("refs/tags/{name}", name = name.as_str()).into(),
3474 targets: Diff {
3475 before: update
3476 .before
3477 .as_ref()
3478 .map(|id| to_tag_target(name, remote, id)),
3479 after: update
3480 .after
3481 .as_ref()
3482 .map(|id| to_tag_target(name, REMOTE_NAME_FOR_LOCAL_GIT_REPO, id)),
3483 },
3484 }),
3485 )
3486 .collect_vec();
3487
3488 let push_stats = push_updates(
3489 mut_repo,
3490 subprocess_options,
3491 remote,
3492 &ref_updates,
3493 callback,
3494 options,
3495 )?;
3496 tracing::debug!(?push_stats);
3497
3498 let pushed: HashSet<&GitRefName> = push_stats.pushed.iter().map(AsRef::as_ref).collect();
3499 let pushed_bookmark_updates = || {
3500 iter::zip(&targets.bookmarks, &ref_updates[..targets.bookmarks.len()])
3501 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3502 .map(|((name, update), _)| (&**name, update))
3503 };
3504 let pushed_tag_updates = || {
3505 iter::zip(&targets.tags, &ref_updates[targets.bookmarks.len()..])
3506 .filter(|(_, ref_update)| pushed.contains(&*ref_update.qualified_name))
3507 .map(|((name, update), ref_update)| (&**name, update, ref_update))
3508 };
3509
3510 let unexported_bookmarks = {
3513 let refs = build_pushed_bookmarks_to_export(remote, pushed_bookmark_updates());
3514 export_refs_to_git(mut_repo, &git_repo, GitRefKind::Bookmark, refs)
3515 };
3516 for (name, _, ref_update) in pushed_tag_updates() {
3520 let symbol = name.to_remote_symbol(remote);
3521 let edit = to_remote_tag_ref_update(symbol, ref_update.targets.after);
3522 if let Err(err) = git_repo.edit_reference(edit) {
3523 tracing::warn!(?symbol, ?err, "failed to update remote tag ref");
3524 }
3525 }
3526
3527 debug_assert!(unexported_bookmarks.is_sorted_by_key(|(symbol, _)| symbol));
3528 let is_exported_bookmark = |name: &RefName| {
3529 unexported_bookmarks
3530 .binary_search_by_key(&name, |(symbol, _)| &symbol.name)
3531 .is_err()
3532 };
3533 for (name, update) in pushed_bookmark_updates().filter(|(name, _)| is_exported_bookmark(name)) {
3534 let new_remote_ref = RemoteRef {
3535 target: RefTarget::resolved(update.after.clone()),
3536 state: RemoteRefState::Tracked,
3537 };
3538 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
3539 }
3540 for (name, update, _) in pushed_tag_updates() {
3541 let new_remote_ref = RemoteRef {
3542 target: RefTarget::resolved(update.after.clone()),
3543 state: RemoteRefState::Tracked,
3544 };
3545 mut_repo.set_remote_tag(name.to_remote_symbol(remote), new_remote_ref);
3546 }
3547
3548 assert!(push_stats.unexported_bookmarks.is_empty());
3552 let push_stats = GitPushStats {
3553 pushed: push_stats.pushed,
3554 rejected: push_stats.rejected,
3555 remote_rejected: push_stats.remote_rejected,
3556 unexported_bookmarks,
3557 };
3558 Ok(push_stats)
3559}
3560
3561pub fn push_updates(
3563 repo: &dyn Repo,
3564 subprocess_options: GitSubprocessOptions,
3565 remote_name: &RemoteName,
3566 updates: &[GitRefUpdate],
3567 callback: &mut dyn GitSubprocessCallback,
3568 options: &GitPushOptions,
3569) -> Result<GitPushStats, GitPushError> {
3570 let mut qualified_remote_refs_expected_locations = HashMap::new();
3571 let mut refspecs = vec![];
3572 for update in updates {
3573 qualified_remote_refs_expected_locations.insert(
3574 update.qualified_name.as_ref(),
3575 update.targets.before.as_deref(),
3576 );
3577 if let Some(new_target) = &update.targets.after {
3578 refspecs.push(RefSpec::forced(
3582 new_target.to_string(),
3583 &update.qualified_name,
3584 ));
3585 } else {
3586 refspecs.push(RefSpec::delete(&update.qualified_name));
3590 }
3591 }
3592
3593 let git_backend = get_git_backend(repo.store())?;
3594 let git_repo = git_backend.git_repo();
3595 let git_ctx = GitSubprocessContext::from_git_backend(git_backend, subprocess_options);
3596
3597 if try_find_active_remote_inner(&git_repo, remote_name).is_none() {
3599 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
3600 }
3601
3602 let refs_to_push: Vec<RefToPush> = refspecs
3603 .iter()
3604 .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
3605 .collect();
3606
3607 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, callback, options)?;
3608 push_stats.pushed.sort();
3609 push_stats.rejected.sort();
3610 push_stats.remote_rejected.sort();
3611 Ok(push_stats)
3612}
3613
3614fn build_pushed_bookmarks_to_export<'a>(
3616 remote: &RemoteName,
3617 pushed_updates: impl IntoIterator<Item = (&'a RefName, &'a Diff<Option<CommitId>>)>,
3618) -> RefsToExport {
3619 let mut to_update = Vec::new();
3620 let mut to_delete = Vec::new();
3621 for (name, update) in pushed_updates {
3622 let symbol = name.to_remote_symbol(remote);
3623 match (update.before.as_ref(), update.after.as_ref()) {
3624 (old, Some(new)) => {
3625 let old_oid = old.map(owned_oid_from_commit_id);
3626 let new_oid = owned_oid_from_commit_id(new);
3627 to_update.push((symbol.to_owned(), (old_oid, new_oid)));
3628 }
3629 (Some(old), None) => {
3630 let old_oid = owned_oid_from_commit_id(old);
3631 to_delete.push((symbol.to_owned(), old_oid));
3632 }
3633 (None, None) => panic!("old/new targets should differ"),
3634 }
3635 }
3636
3637 RefsToExport {
3638 to_update,
3639 to_delete,
3640 failed: vec![],
3641 }
3642}
3643
3644fn to_remote_tag_ref_update(
3646 symbol: RemoteRefSymbol<'_>,
3647 new_oid: Option<gix::ObjectId>,
3648) -> gix::refs::transaction::RefEdit {
3649 let expected = gix::refs::transaction::PreviousValue::Any;
3652 let change = match new_oid {
3653 Some(oid) => gix::refs::transaction::Change::Update {
3654 log: gix::refs::transaction::LogChange::default(),
3655 expected,
3656 new: oid.into(),
3657 },
3658 None => gix::refs::transaction::Change::Delete {
3659 expected,
3660 log: gix::refs::transaction::RefLog::AndReference,
3661 },
3662 };
3663 let name = format!(
3664 "{REMOTE_TAG_REF_NAMESPACE}{remote}/{name}",
3665 remote = symbol.remote.as_str(),
3666 name = symbol.name.as_str()
3667 );
3668 gix::refs::transaction::RefEdit {
3669 change,
3670 name: name.try_into().expect("pushed ref name should be valid"),
3671 deref: false,
3672 }
3673}
3674
3675#[cfg(test)]
3676mod tests {
3677 use std::assert_matches;
3678
3679 use super::*;
3680 use crate::revset;
3681 use crate::revset::RevsetDiagnostics;
3682
3683 #[test]
3684 fn test_split_positive_negative_patterns() {
3685 fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
3686 try_split(text).unwrap()
3687 }
3688
3689 fn try_split(
3690 text: &str,
3691 ) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
3692 let mut diagnostics = RevsetDiagnostics::new();
3693 let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
3694 let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
3695 Ok((
3696 positives.into_iter().cloned().collect(),
3697 negatives.into_iter().cloned().collect(),
3698 ))
3699 }
3700
3701 insta::assert_compact_debug_snapshot!(
3702 split("a"),
3703 @r#"([Exact("a")], [])"#);
3704 insta::assert_compact_debug_snapshot!(
3705 split("~a"),
3706 @r#"([Substring("")], [Exact("a")])"#);
3707 insta::assert_compact_debug_snapshot!(
3708 split("~a~b"),
3709 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3710 insta::assert_compact_debug_snapshot!(
3711 split("~(a|b)"),
3712 @r#"([Substring("")], [Exact("a"), Exact("b")])"#);
3713 insta::assert_compact_debug_snapshot!(
3714 split("a|b"),
3715 @r#"([Exact("a"), Exact("b")], [])"#);
3716 insta::assert_compact_debug_snapshot!(
3717 split("(a|b)&~c"),
3718 @r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
3719 insta::assert_compact_debug_snapshot!(
3720 split("~a&b"),
3721 @r#"([Exact("b")], [Exact("a")])"#);
3722 insta::assert_compact_debug_snapshot!(
3723 split("a&~b&~c"),
3724 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3725 insta::assert_compact_debug_snapshot!(
3726 split("~a&b&~c"),
3727 @r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
3728 insta::assert_compact_debug_snapshot!(
3729 split("a&~(b|c)"),
3730 @r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
3731 insta::assert_compact_debug_snapshot!(
3732 split("((a|b)|c)&~(d|(e|f))"),
3733 @r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
3734 assert_matches!(
3735 try_split("a&b"),
3736 Err(GitRefExpressionError::PositiveIntersection)
3737 );
3738 assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
3739 assert_matches!(
3740 try_split("a&~(b&~c)"),
3741 Err(GitRefExpressionError::NestedIntersection)
3742 );
3743 assert_matches!(
3744 try_split("(a|b)&c"),
3745 Err(GitRefExpressionError::PositiveIntersection)
3746 );
3747 assert_matches!(
3748 try_split("(a&~b)&(~c&~d)"),
3749 Err(GitRefExpressionError::PositiveIntersection)
3750 );
3751 assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
3752 assert_matches!(
3753 try_split("a&~b|c&~d"),
3754 Err(GitRefExpressionError::NestedIntersection)
3755 );
3756
3757 insta::assert_compact_debug_snapshot!(
3760 split("*"),
3761 @r#"([Glob(GlobPattern("*"))], [])"#);
3762 insta::assert_compact_debug_snapshot!(
3763 split("~*"),
3764 @"([], [])");
3765 insta::assert_compact_debug_snapshot!(
3766 split("a~*"),
3767 @r#"([Exact("a")], [Glob(GlobPattern("*"))])"#);
3768 insta::assert_compact_debug_snapshot!(
3769 split("~(a|*)"),
3770 @r#"([Substring("")], [Exact("a"), Glob(GlobPattern("*"))])"#);
3771 }
3772}