1#![allow(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::fs::File;
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::str;
26use std::sync::Arc;
27
28use bstr::BStr;
29use bstr::BString;
30use futures::StreamExt as _;
31use itertools::Itertools as _;
32use pollster::FutureExt as _;
33use thiserror::Error;
34
35use crate::backend::BackendError;
36use crate::backend::BackendResult;
37use crate::backend::CommitId;
38use crate::backend::TreeValue;
39use crate::commit::Commit;
40use crate::file_util::IoResultExt as _;
41use crate::file_util::PathError;
42use crate::git_backend::GitBackend;
43use crate::git_subprocess::GitSubprocessContext;
44use crate::git_subprocess::GitSubprocessError;
45#[cfg(feature = "git2")]
46use crate::index::Index;
47use crate::matchers::EverythingMatcher;
48use crate::merged_tree::MergedTree;
49use crate::merged_tree::TreeDiffEntry;
50use crate::object_id::ObjectId as _;
51use crate::op_store::RefTarget;
52use crate::op_store::RefTargetOptionExt as _;
53use crate::op_store::RemoteRef;
54use crate::op_store::RemoteRefState;
55use crate::ref_name::GitRefName;
56use crate::ref_name::GitRefNameBuf;
57use crate::ref_name::RefName;
58use crate::ref_name::RefNameBuf;
59use crate::ref_name::RemoteName;
60use crate::ref_name::RemoteNameBuf;
61use crate::ref_name::RemoteRefSymbol;
62use crate::ref_name::RemoteRefSymbolBuf;
63#[cfg(feature = "git2")]
64use crate::refs;
65use crate::refs::BookmarkPushUpdate;
66use crate::repo::MutableRepo;
67use crate::repo::Repo;
68use crate::repo_path::RepoPath;
69use crate::revset::RevsetExpression;
70use crate::settings::GitSettings;
71use crate::store::Store;
72use crate::str_util::StringPattern;
73use crate::view::View;
74
75pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
77pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
79const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
81const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
84
85#[derive(Debug, Error)]
86pub enum GitRemoteNameError {
87 #[error(
88 "Git remote named '{name}' is reserved for local Git repository",
89 name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
90 )]
91 ReservedForLocalGitRepo,
92 #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
93 WithSlash(RemoteNameBuf),
94}
95
96fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
97 if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
98 Err(GitRemoteNameError::ReservedForLocalGitRepo)
99 } else if name.as_str().contains("/") {
100 Err(GitRemoteNameError::WithSlash(name.to_owned()))
101 } else {
102 Ok(())
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum GitRefKind {
109 Bookmark,
110 Tag,
111}
112
113#[derive(Clone, Debug, Default, Eq, PartialEq)]
115pub struct GitPushStats {
116 pub pushed: Vec<GitRefNameBuf>,
118 pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
120 pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
122}
123
124impl GitPushStats {
125 pub fn all_ok(&self) -> bool {
126 self.rejected.is_empty() && self.remote_rejected.is_empty()
127 }
128}
129
130#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
135
136impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
137 fn borrow(&self) -> &RemoteRefSymbol<'b> {
138 &self.0
139 }
140}
141
142#[derive(Debug, Hash, PartialEq, Eq)]
148pub(crate) struct RefSpec {
149 forced: bool,
150 source: Option<String>,
153 destination: String,
154}
155
156impl RefSpec {
157 fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
158 RefSpec {
159 forced: true,
160 source: Some(source.into()),
161 destination: destination.into(),
162 }
163 }
164
165 fn delete(destination: impl Into<String>) -> Self {
166 RefSpec {
168 forced: false,
169 source: None,
170 destination: destination.into(),
171 }
172 }
173
174 pub(crate) fn to_git_format(&self) -> String {
175 format!(
176 "{}{}",
177 if self.forced { "+" } else { "" },
178 self.to_git_format_not_forced()
179 )
180 }
181
182 pub(crate) fn to_git_format_not_forced(&self) -> String {
188 if let Some(s) = &self.source {
189 format!("{}:{}", s, self.destination)
190 } else {
191 format!(":{}", self.destination)
192 }
193 }
194}
195
196pub(crate) struct RefToPush<'a> {
199 pub(crate) refspec: &'a RefSpec,
200 pub(crate) expected_location: Option<&'a CommitId>,
201}
202
203impl<'a> RefToPush<'a> {
204 fn new(
205 refspec: &'a RefSpec,
206 expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
207 ) -> Self {
208 let expected_location = *expected_locations
209 .get(GitRefName::new(&refspec.destination))
210 .expect(
211 "The refspecs and the expected locations were both constructed from the same \
212 source of truth. This means the lookup should always work.",
213 );
214
215 RefToPush {
216 refspec,
217 expected_location,
218 }
219 }
220
221 pub(crate) fn to_git_lease(&self) -> String {
222 format!(
223 "{}:{}",
224 self.refspec.destination,
225 self.expected_location
226 .map(|x| x.to_string())
227 .as_deref()
228 .unwrap_or("")
229 )
230 }
231}
232
233pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
236 if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
237 if name == "HEAD" {
239 return None;
240 }
241 let name = RefName::new(name);
242 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
243 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
244 } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
245 let (remote, name) = remote_and_name.split_once('/')?;
246 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
248 return None;
249 }
250 let name = RefName::new(name);
251 let remote = RemoteName::new(remote);
252 Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
253 } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
254 let name = RefName::new(name);
255 let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
256 Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
257 } else {
258 None
259 }
260}
261
262fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
263 let RemoteRefSymbol { name, remote } = symbol;
264 let name = name.as_str();
265 let remote = remote.as_str();
266 if name.is_empty() || remote.is_empty() {
267 return None;
268 }
269 match kind {
270 GitRefKind::Bookmark => {
271 if name == "HEAD" {
272 return None;
273 }
274 if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
275 Some(format!("refs/heads/{name}").into())
276 } else {
277 Some(format!("refs/remotes/{remote}/{name}").into())
278 }
279 }
280 GitRefKind::Tag => {
281 (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
282 }
283 }
284}
285
286#[derive(Debug, Error)]
287#[error("The repo is not backed by a Git repo")]
288pub struct UnexpectedGitBackendError;
289
290pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
292 store
293 .backend_impl()
294 .downcast_ref()
295 .ok_or(UnexpectedGitBackendError)
296}
297
298pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
300 get_git_backend(store).map(|backend| backend.git_repo())
301}
302
303fn resolve_git_ref_to_commit_id(
308 git_ref: &gix::Reference,
309 known_target: &RefTarget,
310) -> Option<CommitId> {
311 let mut peeling_ref = Cow::Borrowed(git_ref);
312
313 if let Some(id) = known_target.as_normal() {
315 let raw_ref = &git_ref.inner;
316 if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
317 return Some(id.clone());
318 }
319 if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {
320 return Some(id.clone());
323 }
324 if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
328 let maybe_tag = git_ref
329 .try_id()
330 .and_then(|id| id.object().ok())
331 .and_then(|object| object.try_into_tag().ok());
332 if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
333 if oid.as_bytes() == id.as_bytes() {
334 return Some(id.clone());
336 }
337 peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());
340 }
341 }
342 }
343
344 let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
348 let is_commit = peeled_id
349 .object()
350 .is_ok_and(|object| object.kind.is_commit());
351 is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
352}
353
354#[derive(Error, Debug)]
355pub enum GitImportError {
356 #[error("Failed to read Git HEAD target commit {id}")]
357 MissingHeadTarget {
358 id: CommitId,
359 #[source]
360 err: BackendError,
361 },
362 #[error("Ancestor of Git ref {symbol} is missing")]
363 MissingRefAncestor {
364 symbol: RemoteRefSymbolBuf,
365 #[source]
366 err: BackendError,
367 },
368 #[error(transparent)]
369 Backend(BackendError),
370 #[error(transparent)]
371 Git(Box<dyn std::error::Error + Send + Sync>),
372 #[error(transparent)]
373 UnexpectedBackend(#[from] UnexpectedGitBackendError),
374}
375
376impl GitImportError {
377 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
378 GitImportError::Git(source.into())
379 }
380}
381
382#[derive(Clone, Debug, Eq, PartialEq, Default)]
384pub struct GitImportStats {
385 pub abandoned_commits: Vec<CommitId>,
387 pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
390 pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
393 pub failed_ref_names: Vec<BString>,
398}
399
400#[derive(Debug)]
401struct RefsToImport {
402 changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
405 changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
408 changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
411 failed_ref_names: Vec<BString>,
413}
414
415pub fn import_refs(
420 mut_repo: &mut MutableRepo,
421 git_settings: &GitSettings,
422) -> Result<GitImportStats, GitImportError> {
423 import_some_refs(mut_repo, git_settings, |_, _| true)
424}
425
426pub fn import_some_refs(
431 mut_repo: &mut MutableRepo,
432 git_settings: &GitSettings,
433 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
434) -> Result<GitImportStats, GitImportError> {
435 let store = mut_repo.store();
436 let git_backend = get_git_backend(store)?;
437 let git_repo = git_backend.git_repo();
438
439 let RefsToImport {
440 changed_git_refs,
441 changed_remote_bookmarks,
442 changed_remote_tags,
443 failed_ref_names,
444 } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;
445
446 let index = mut_repo.index();
453 let missing_head_ids = changed_git_refs
454 .iter()
455 .flat_map(|(_, new_target)| new_target.added_ids())
456 .filter(|&id| !index.has_id(id));
457 let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
458
459 let mut head_commits = Vec::new();
461 let get_commit = |id| {
462 if !heads_imported && !index.has_id(id) {
464 git_backend.import_head_commits([id])?;
465 }
466 store.get_commit(id)
467 };
468 for (symbol, (_, new_target)) in
469 itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)
470 {
471 for id in new_target.added_ids() {
472 let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {
473 symbol: symbol.clone(),
474 err,
475 })?;
476 head_commits.push(commit);
477 }
478 }
479 mut_repo
482 .add_heads(&head_commits)
483 .map_err(GitImportError::Backend)?;
484
485 for (full_name, new_target) in changed_git_refs {
487 mut_repo.set_git_ref_target(&full_name, new_target);
488 }
489 for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
490 let symbol = symbol.as_ref();
491 let base_target = old_remote_ref.tracked_target();
492 let new_remote_ref = RemoteRef {
493 target: new_target.clone(),
494 state: if old_remote_ref.is_present() {
495 old_remote_ref.state
496 } else {
497 default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)
498 },
499 };
500 if new_remote_ref.is_tracked() {
501 mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target);
502 }
503 mut_repo.set_remote_bookmark(symbol, new_remote_ref);
506 }
507 for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
508 let symbol = symbol.as_ref();
509 let base_target = old_remote_ref.tracked_target();
510 let new_remote_ref = RemoteRef {
511 target: new_target.clone(),
512 state: if old_remote_ref.is_present() {
513 old_remote_ref.state
514 } else {
515 default_remote_ref_state_for(GitRefKind::Tag, symbol, git_settings)
516 },
517 };
518 if new_remote_ref.is_tracked() {
519 mut_repo.merge_tag(symbol.name, base_target, &new_remote_ref.target);
520 }
521 }
523
524 let abandoned_commits = if git_settings.abandon_unreachable_commits {
525 abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
526 .map_err(GitImportError::Backend)?
527 } else {
528 vec![]
529 };
530 let stats = GitImportStats {
531 abandoned_commits,
532 changed_remote_bookmarks,
533 changed_remote_tags,
534 failed_ref_names,
535 };
536 Ok(stats)
537}
538
539fn abandon_unreachable_commits(
542 mut_repo: &mut MutableRepo,
543 changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
544 changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
545) -> BackendResult<Vec<CommitId>> {
546 let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
547 .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
548 .cloned()
549 .collect_vec();
550 if hidable_git_heads.is_empty() {
551 return Ok(vec![]);
552 }
553 let pinned_expression = RevsetExpression::union_all(&[
554 RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
556 RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
557 .intersection(&RevsetExpression::visible_heads().ancestors()),
559 RevsetExpression::root(),
560 ]);
561 let abandoned_expression = pinned_expression
562 .range(&RevsetExpression::commits(hidable_git_heads))
563 .intersection(&RevsetExpression::visible_heads().ancestors());
565 let abandoned_commit_ids: Vec<_> = abandoned_expression
566 .evaluate(mut_repo)
567 .map_err(|err| err.into_backend_error())?
568 .iter()
569 .try_collect()
570 .map_err(|err| err.into_backend_error())?;
571 for id in &abandoned_commit_ids {
572 let commit = mut_repo.store().get_commit(id)?;
573 mut_repo.record_abandoned_commit(&commit);
574 }
575 Ok(abandoned_commit_ids)
576}
577
578fn diff_refs_to_import(
580 view: &View,
581 git_repo: &gix::Repository,
582 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
583) -> Result<RefsToImport, GitImportError> {
584 let mut known_git_refs = view
585 .git_refs()
586 .iter()
587 .filter_map(|(full_name, target)| {
588 let (kind, symbol) =
590 parse_git_ref(full_name).expect("stored git ref should be parsable");
591 git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
592 })
593 .collect();
594 let mut known_remote_bookmarks = view
596 .all_remote_bookmarks()
597 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
598 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), (&remote_ref.target, remote_ref.state)))
599 .collect();
600 let mut known_remote_tags = view
603 .tags()
604 .iter()
605 .map(|(name, target)| {
606 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
607 let state = RemoteRefState::Tracked;
608 (symbol, (target, state))
609 })
610 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
611 .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
612 .collect();
613
614 let mut changed_git_refs = Vec::new();
615 let mut changed_remote_bookmarks = Vec::new();
616 let mut changed_remote_tags = Vec::new();
617 let mut failed_ref_names = Vec::new();
618 let actual = git_repo.references().map_err(GitImportError::from_git)?;
619 collect_changed_refs_to_import(
620 actual.local_branches().map_err(GitImportError::from_git)?,
621 &mut known_git_refs,
622 &mut known_remote_bookmarks,
623 &mut changed_git_refs,
624 &mut changed_remote_bookmarks,
625 &mut failed_ref_names,
626 &git_ref_filter,
627 )?;
628 collect_changed_refs_to_import(
629 actual.remote_branches().map_err(GitImportError::from_git)?,
630 &mut known_git_refs,
631 &mut known_remote_bookmarks,
632 &mut changed_git_refs,
633 &mut changed_remote_bookmarks,
634 &mut failed_ref_names,
635 &git_ref_filter,
636 )?;
637 collect_changed_refs_to_import(
638 actual.tags().map_err(GitImportError::from_git)?,
639 &mut known_git_refs,
640 &mut known_remote_tags,
641 &mut changed_git_refs,
642 &mut changed_remote_tags,
643 &mut failed_ref_names,
644 &git_ref_filter,
645 )?;
646 for full_name in known_git_refs.into_keys() {
647 changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
648 }
649 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_bookmarks {
650 let old_remote_ref = RemoteRef {
651 target: old_target.clone(),
652 state: old_state,
653 };
654 changed_remote_bookmarks.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
655 }
656 for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_tags {
657 let old_remote_ref = RemoteRef {
658 target: old_target.clone(),
659 state: old_state,
660 };
661 changed_remote_tags.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
662 }
663
664 changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
666 changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
667 changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
668 failed_ref_names.sort_unstable();
669 Ok(RefsToImport {
670 changed_git_refs,
671 changed_remote_bookmarks,
672 changed_remote_tags,
673 failed_ref_names,
674 })
675}
676
677fn collect_changed_refs_to_import(
678 actual_git_refs: gix::reference::iter::Iter<'_>,
679 known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
680 known_remote_refs: &mut HashMap<RemoteRefKey<'_>, (&RefTarget, RemoteRefState)>,
681 changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
682 changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
683 failed_ref_names: &mut Vec<BString>,
684 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
685) -> Result<(), GitImportError> {
686 for git_ref in actual_git_refs {
687 let git_ref = git_ref.map_err(GitImportError::from_git)?;
688 let full_name_bytes = git_ref.name().as_bstr();
689 let Ok(full_name) = str::from_utf8(full_name_bytes) else {
690 failed_ref_names.push(full_name_bytes.to_owned());
692 continue;
693 };
694 if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
695 failed_ref_names.push(full_name_bytes.to_owned());
696 continue;
697 }
698 let full_name = GitRefName::new(full_name);
699 let Some((kind, symbol)) = parse_git_ref(full_name) else {
700 continue;
702 };
703 if !git_ref_filter(kind, symbol) {
704 continue;
705 }
706 let old_git_target = known_git_refs.get(full_name).copied().flatten();
707 let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
708 continue;
710 };
711 let new_target = RefTarget::normal(id);
712 known_git_refs.remove(full_name);
713 if new_target != *old_git_target {
714 changed_git_refs.push((full_name.to_owned(), new_target.clone()));
715 }
716 let (old_remote_target, old_remote_state) = known_remote_refs
719 .remove(&symbol)
720 .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));
721 if new_target != *old_remote_target {
722 let old_remote_ref = RemoteRef {
723 target: old_remote_target.clone(),
724 state: old_remote_state,
725 };
726 changed_remote_refs.push((symbol.to_owned(), (old_remote_ref, new_target)));
727 }
728 }
729 Ok(())
730}
731
732fn default_remote_ref_state_for(
733 kind: GitRefKind,
734 symbol: RemoteRefSymbol<'_>,
735 git_settings: &GitSettings,
736) -> RemoteRefState {
737 match kind {
738 GitRefKind::Bookmark => {
739 if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
740 RemoteRefState::Tracked
741 } else {
742 RemoteRefState::New
743 }
744 }
745 GitRefKind::Tag => RemoteRefState::Tracked,
746 }
747}
748
749fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
755 itertools::chain(
756 view.local_bookmarks().map(|(_, target)| target),
757 view.tags().values(),
758 )
759 .flat_map(|target| target.added_ids())
760 .cloned()
761 .collect()
762}
763
764fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
771 view.all_remote_bookmarks()
772 .filter(|(_, remote_ref)| !remote_ref.is_tracked())
773 .map(|(_, remote_ref)| &remote_ref.target)
774 .flat_map(|target| target.added_ids())
775 .cloned()
776 .collect()
777}
778
779pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
787 let store = mut_repo.store();
788 let git_backend = get_git_backend(store)?;
789 let git_repo = git_backend.git_repo();
790
791 let old_git_head = mut_repo.view().git_head();
792 let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
793 Some(CommitId::from_bytes(oid.as_bytes()))
794 } else {
795 None
796 };
797 if old_git_head.as_resolved() == Some(&new_git_head_id) {
798 return Ok(());
799 }
800
801 if let Some(head_id) = &new_git_head_id {
803 let index = mut_repo.index();
804 if !index.has_id(head_id) {
805 git_backend.import_head_commits([head_id]).map_err(|err| {
806 GitImportError::MissingHeadTarget {
807 id: head_id.clone(),
808 err,
809 }
810 })?;
811 }
812 store
815 .get_commit(head_id)
816 .and_then(|commit| mut_repo.add_head(&commit))
817 .map_err(GitImportError::Backend)?;
818 }
819
820 mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
821 Ok(())
822}
823
824#[derive(Error, Debug)]
825pub enum GitExportError {
826 #[error(transparent)]
827 Git(Box<dyn std::error::Error + Send + Sync>),
828 #[error(transparent)]
829 UnexpectedBackend(#[from] UnexpectedGitBackendError),
830}
831
832impl GitExportError {
833 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
834 GitExportError::Git(source.into())
835 }
836}
837
838#[derive(Debug, Error)]
840pub enum FailedRefExportReason {
841 #[error("Name is not allowed in Git")]
843 InvalidGitName,
844 #[error("Ref was in a conflicted state from the last import")]
847 ConflictedOldState,
848 #[error("Ref cannot point to the root commit in Git")]
850 OnRootCommit,
851 #[error("Deleted ref had been modified in Git")]
853 DeletedInJjModifiedInGit,
854 #[error("Added ref had been added with a different target in Git")]
856 AddedInJjAddedInGit,
857 #[error("Modified ref had been deleted in Git")]
859 ModifiedInJjDeletedInGit,
860 #[error("Failed to delete")]
862 FailedToDelete(#[source] Box<gix::reference::edit::Error>),
863 #[error("Failed to set")]
865 FailedToSet(#[source] Box<gix::reference::edit::Error>),
866}
867
868#[derive(Debug)]
870pub struct GitExportStats {
871 pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
873}
874
875#[derive(Debug)]
876struct RefsToExport {
877 bookmarks_to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
880 bookmarks_to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
885 failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
887}
888
889pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
900 export_some_refs(mut_repo, |_, _| true)
901}
902
903pub fn export_some_refs(
904 mut_repo: &mut MutableRepo,
905 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
906) -> Result<GitExportStats, GitExportError> {
907 fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
908 debug_assert!(map.is_sorted_by_key(|(k, _)| k));
909 let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
910 let (_, value) = &map[index];
911 Some(value)
912 }
913
914 let git_repo = get_git_repo(mut_repo.store())?;
915
916 let RefsToExport {
917 bookmarks_to_update,
918 bookmarks_to_delete,
919 mut failed_bookmarks,
920 } = diff_refs_to_export(
921 mut_repo.view(),
922 mut_repo.store().root_commit_id(),
923 &git_ref_filter,
924 );
925
926 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
928 let target_name = head_ref.target().try_name().map(|name| name.to_owned());
929 if let Some((GitRefKind::Bookmark, symbol)) = target_name
930 .as_ref()
931 .and_then(|name| str::from_utf8(name.as_bstr()).ok())
932 .and_then(|name| parse_git_ref(name.as_ref()))
933 {
934 let old_target = head_ref.inner.target.clone();
935 let current_oid = match head_ref.into_fully_peeled_id() {
936 Ok(id) => Some(id.detach()),
937 Err(gix::reference::peel::Error::ToId(
938 gix::refs::peel::to_id::Error::FollowToObject(
939 gix::refs::peel::to_object::Error::Follow(
940 gix::refs::file::find::existing::Error::NotFound { .. },
941 ),
942 ),
943 )) => None, Err(err) => return Err(GitExportError::from_git(err)),
945 };
946 let new_oid = if let Some((_old_oid, new_oid)) = get(&bookmarks_to_update, symbol) {
947 Some(new_oid)
948 } else if get(&bookmarks_to_delete, symbol).is_some() {
949 None
950 } else {
951 current_oid.as_ref()
952 };
953 if new_oid != current_oid.as_ref() {
954 update_git_head(
955 &git_repo,
956 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
957 current_oid,
958 )
959 .map_err(GitExportError::from_git)?;
960 }
961 }
962 }
963 for (symbol, old_oid) in bookmarks_to_delete {
964 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
965 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
966 continue;
967 };
968 if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) {
969 failed_bookmarks.push((symbol, reason));
970 } else {
971 let new_target = RefTarget::absent();
972 mut_repo.set_git_ref_target(&git_ref_name, new_target);
973 }
974 }
975 for (symbol, (old_oid, new_oid)) in bookmarks_to_update {
976 let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
977 failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
978 continue;
979 };
980 if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) {
981 failed_bookmarks.push((symbol, reason));
982 } else {
983 let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
984 mut_repo.set_git_ref_target(&git_ref_name, new_target);
985 }
986 }
987
988 failed_bookmarks.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
990
991 copy_exportable_local_bookmarks_to_remote_view(
992 mut_repo,
993 REMOTE_NAME_FOR_LOCAL_GIT_REPO,
994 |name| {
995 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
996 git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
997 },
998 );
999
1000 Ok(GitExportStats { failed_bookmarks })
1001}
1002
1003fn copy_exportable_local_bookmarks_to_remote_view(
1004 mut_repo: &mut MutableRepo,
1005 remote: &RemoteName,
1006 name_filter: impl Fn(&RefName) -> bool,
1007) {
1008 let new_local_bookmarks = mut_repo
1009 .view()
1010 .local_remote_bookmarks(remote)
1011 .filter_map(|(name, targets)| {
1012 let old_target = &targets.remote_ref.target;
1015 let new_target = targets.local_target;
1016 (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1017 })
1018 .filter(|&(name, _)| name_filter(name))
1019 .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1020 .collect_vec();
1021 for (name, new_target) in new_local_bookmarks {
1022 let new_remote_ref = RemoteRef {
1023 target: new_target,
1024 state: RemoteRefState::Tracked,
1025 };
1026 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1027 }
1028}
1029
1030fn diff_refs_to_export(
1032 view: &View,
1033 root_commit_id: &CommitId,
1034 git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1035) -> RefsToExport {
1036 let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1039 itertools::chain(
1040 view.local_bookmarks().map(|(name, target)| {
1041 let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1042 (symbol, target)
1043 }),
1044 view.all_remote_bookmarks()
1045 .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1046 .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1047 )
1048 .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1049 .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1050 .collect();
1051 let known_git_refs = view
1052 .git_refs()
1053 .iter()
1054 .map(|(full_name, target)| {
1055 let (kind, symbol) =
1056 parse_git_ref(full_name).expect("stored git ref should be parsable");
1057 ((kind, symbol), target)
1058 })
1059 .filter(|&((kind, symbol), _)| {
1060 kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol)
1064 });
1065 for ((_kind, symbol), target) in known_git_refs {
1066 all_bookmark_targets
1067 .entry(symbol)
1068 .and_modify(|(old_target, _)| *old_target = target)
1069 .or_insert((target, RefTarget::absent_ref()));
1070 }
1071
1072 let mut bookmarks_to_update = Vec::new();
1073 let mut bookmarks_to_delete = Vec::new();
1074 let mut failed_bookmarks = Vec::new();
1075 let root_commit_target = RefTarget::normal(root_commit_id.clone());
1076 for (symbol, (old_target, new_target)) in all_bookmark_targets {
1077 if new_target == old_target {
1078 continue;
1079 }
1080 if *new_target == root_commit_target {
1081 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1083 continue;
1084 }
1085 let old_oid = if let Some(id) = old_target.as_normal() {
1086 Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1087 } else if old_target.has_conflict() {
1088 failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1091 continue;
1092 } else {
1093 assert!(old_target.is_absent());
1094 None
1095 };
1096 if let Some(id) = new_target.as_normal() {
1097 let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1098 bookmarks_to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1099 } else if new_target.has_conflict() {
1100 continue;
1102 } else {
1103 assert!(new_target.is_absent());
1104 bookmarks_to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1105 }
1106 }
1107
1108 bookmarks_to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1110 bookmarks_to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1111 failed_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1112 RefsToExport {
1113 bookmarks_to_update,
1114 bookmarks_to_delete,
1115 failed_bookmarks,
1116 }
1117}
1118
1119fn delete_git_ref(
1120 git_repo: &gix::Repository,
1121 git_ref_name: &GitRefName,
1122 old_oid: &gix::oid,
1123) -> Result<(), FailedRefExportReason> {
1124 if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1125 if git_ref.inner.target.try_id() == Some(old_oid) {
1126 git_ref
1128 .delete()
1129 .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1130 } else {
1131 return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1133 }
1134 } else {
1135 }
1137 Ok(())
1138}
1139
1140fn update_git_ref(
1141 git_repo: &gix::Repository,
1142 git_ref_name: &GitRefName,
1143 old_oid: Option<gix::ObjectId>,
1144 new_oid: gix::ObjectId,
1145) -> Result<(), FailedRefExportReason> {
1146 match old_oid {
1147 None => {
1148 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1149 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1152 return Err(FailedRefExportReason::AddedInJjAddedInGit);
1153 }
1154 } else {
1155 git_repo
1157 .reference(
1158 git_ref_name.as_str(),
1159 new_oid,
1160 gix::refs::transaction::PreviousValue::MustNotExist,
1161 "export from jj",
1162 )
1163 .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1164 }
1165 }
1166 Some(old_oid) => {
1167 if let Err(err) = git_repo.reference(
1169 git_ref_name.as_str(),
1170 new_oid,
1171 gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1172 "export from jj",
1173 ) {
1174 if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1176 if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1178 return Err(FailedRefExportReason::FailedToSet(err.into()));
1179 }
1180 } else {
1181 return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1183 }
1184 } else {
1185 }
1188 }
1189 }
1190 Ok(())
1191}
1192
1193fn update_git_head(
1196 git_repo: &gix::Repository,
1197 expected_ref: gix::refs::transaction::PreviousValue,
1198 new_oid: Option<gix::ObjectId>,
1199) -> Result<(), gix::reference::edit::Error> {
1200 let mut ref_edits = Vec::new();
1201 let new_target = if let Some(oid) = new_oid {
1202 gix::refs::Target::Object(oid)
1203 } else {
1204 ref_edits.push(gix::refs::transaction::RefEdit {
1209 change: gix::refs::transaction::Change::Delete {
1210 expected: gix::refs::transaction::PreviousValue::Any,
1211 log: gix::refs::transaction::RefLog::AndReference,
1212 },
1213 name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1214 deref: false,
1215 });
1216 gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1217 };
1218 ref_edits.push(gix::refs::transaction::RefEdit {
1219 change: gix::refs::transaction::Change::Update {
1220 log: gix::refs::transaction::LogChange {
1221 message: "export from jj".into(),
1222 ..Default::default()
1223 },
1224 expected: expected_ref,
1225 new: new_target,
1226 },
1227 name: "HEAD".try_into().unwrap(),
1228 deref: false,
1229 });
1230 git_repo.edit_references(ref_edits)?;
1231 Ok(())
1232}
1233
1234#[derive(Debug, Error)]
1235pub enum GitResetHeadError {
1236 #[error(transparent)]
1237 Backend(#[from] BackendError),
1238 #[error(transparent)]
1239 Git(Box<dyn std::error::Error + Send + Sync>),
1240 #[error("Failed to update Git HEAD ref")]
1241 UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1242 #[error(transparent)]
1243 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1244}
1245
1246impl GitResetHeadError {
1247 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1248 GitResetHeadError::Git(source.into())
1249 }
1250}
1251
1252pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1255 let git_repo = get_git_repo(mut_repo.store())?;
1256
1257 let first_parent_id = &wc_commit.parent_ids()[0];
1258 let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1259 RefTarget::normal(first_parent_id.clone())
1260 } else {
1261 RefTarget::absent()
1262 };
1263
1264 let old_head_target = mut_repo.git_head();
1266 if old_head_target != new_head_target {
1267 let expected_ref = if let Some(id) = old_head_target.as_normal() {
1268 let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1271 if actual_head.is_detached() {
1272 let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1273 gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1274 } else {
1275 gix::refs::transaction::PreviousValue::MustExist
1278 }
1279 } else {
1280 gix::refs::transaction::PreviousValue::MustExist
1282 };
1283 let new_oid = new_head_target
1284 .as_normal()
1285 .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1286 update_git_head(&git_repo, expected_ref, new_oid)
1287 .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1288 mut_repo.set_git_head_target(new_head_target);
1289 }
1290
1291 if git_repo.state().is_some() {
1296 const STATE_FILE_NAMES: &[&str] = &[
1300 "MERGE_HEAD",
1301 "MERGE_MODE",
1302 "MERGE_MSG",
1303 "REVERT_HEAD",
1304 "CHERRY_PICK_HEAD",
1305 "BISECT_LOG",
1306 ];
1307 const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1308 let handle_err = |err: PathError| match err.error.kind() {
1309 std::io::ErrorKind::NotFound => Ok(()),
1310 _ => Err(GitResetHeadError::from_git(err)),
1311 };
1312 for file_name in STATE_FILE_NAMES {
1313 let path = git_repo.path().join(file_name);
1314 std::fs::remove_file(&path)
1315 .context(&path)
1316 .or_else(handle_err)?;
1317 }
1318 for dir_name in STATE_DIR_NAMES {
1319 let path = git_repo.path().join(dir_name);
1320 std::fs::remove_dir_all(&path)
1321 .context(&path)
1322 .or_else(handle_err)?;
1323 }
1324 }
1325
1326 let parent_tree = wc_commit.parent_tree(mut_repo)?;
1327
1328 let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
1332 if tree.id() == mut_repo.store().empty_tree_id() {
1333 gix::index::File::from_state(
1337 gix::index::State::new(git_repo.object_hash()),
1338 git_repo.index_path(),
1339 )
1340 } else {
1341 git_repo
1344 .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes()))
1345 .map_err(GitResetHeadError::from_git)?
1346 }
1347 } else {
1348 build_index_from_merged_tree(&git_repo, parent_tree.clone())?
1349 };
1350
1351 let wc_tree = wc_commit.tree()?;
1352 update_intent_to_add_impl(&mut index, &parent_tree, &wc_tree, git_repo.object_hash())
1353 .block_on()?;
1354
1355 if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1358 index
1359 .entries_mut_with_paths()
1360 .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1361 gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1362 .then_with(|| entry.stage().cmp(&old_entry.stage()))
1363 })
1364 .filter_map(|merged| merged.both())
1365 .map(|((entry, _), old_entry)| (entry, old_entry))
1366 .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1367 .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1368 }
1369
1370 debug_assert!(index.verify_entries().is_ok());
1371
1372 index
1373 .write(gix::index::write::Options::default())
1374 .map_err(GitResetHeadError::from_git)?;
1375
1376 Ok(())
1377}
1378
1379fn build_index_from_merged_tree(
1380 git_repo: &gix::Repository,
1381 merged_tree: MergedTree,
1382) -> Result<gix::index::File, GitResetHeadError> {
1383 let mut index = gix::index::File::from_state(
1384 gix::index::State::new(git_repo.object_hash()),
1385 git_repo.index_path(),
1386 );
1387
1388 let mut push_index_entry =
1389 |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1390 let Some(entry) = maybe_entry else {
1391 return;
1392 };
1393
1394 let (id, mode) = match entry {
1395 TreeValue::File { id, executable } => {
1396 if *executable {
1397 (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1398 } else {
1399 (id.as_bytes(), gix::index::entry::Mode::FILE)
1400 }
1401 }
1402 TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1403 TreeValue::Tree(_) => {
1404 return;
1409 }
1410 TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1411 TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"),
1412 };
1413
1414 let path = BStr::new(path.as_internal_file_string());
1415
1416 index.dangerously_push_entry(
1419 gix::index::entry::Stat::default(),
1420 gix::ObjectId::from_bytes_or_panic(id),
1421 gix::index::entry::Flags::from_stage(stage),
1422 mode,
1423 path,
1424 );
1425 };
1426
1427 let mut has_many_sided_conflict = false;
1428
1429 for (path, entry) in merged_tree.entries() {
1430 let entry = entry?;
1431 if let Some(resolved) = entry.as_resolved() {
1432 push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1433 continue;
1434 }
1435
1436 let conflict = entry.simplify();
1437 if let [left, base, right] = conflict.as_slice() {
1438 push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1440 push_index_entry(&path, base, gix::index::entry::Stage::Base);
1441 push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1442 } else {
1443 has_many_sided_conflict = true;
1451 push_index_entry(
1452 &path,
1453 conflict.first(),
1454 gix::index::entry::Stage::Unconflicted,
1455 );
1456 }
1457 }
1458
1459 index.sort_entries();
1462
1463 if has_many_sided_conflict
1466 && index
1467 .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1468 .is_err()
1469 {
1470 let file_blob = git_repo
1471 .write_blob(
1472 b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1473 )
1474 .map_err(GitResetHeadError::from_git)?;
1475 index.dangerously_push_entry(
1476 gix::index::entry::Stat::default(),
1477 file_blob.detach(),
1478 gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1479 gix::index::entry::Mode::FILE,
1480 INDEX_DUMMY_CONFLICT_FILE.into(),
1481 );
1482 index.sort_entries();
1485 }
1486
1487 Ok(index)
1488}
1489
1490pub fn update_intent_to_add(
1497 repo: &dyn Repo,
1498 old_tree: &MergedTree,
1499 new_tree: &MergedTree,
1500) -> Result<(), GitResetHeadError> {
1501 let git_repo = get_git_repo(repo.store())?;
1502 let mut index = git_repo
1503 .index_or_empty()
1504 .map_err(GitResetHeadError::from_git)?;
1505 let mut_index = Arc::make_mut(&mut index);
1506 update_intent_to_add_impl(mut_index, old_tree, new_tree, git_repo.object_hash()).block_on()?;
1507 debug_assert!(mut_index.verify_entries().is_ok());
1508 mut_index
1509 .write(gix::index::write::Options::default())
1510 .map_err(GitResetHeadError::from_git)?;
1511
1512 Ok(())
1513}
1514
1515async fn update_intent_to_add_impl(
1516 index: &mut gix::index::File,
1517 old_tree: &MergedTree,
1518 new_tree: &MergedTree,
1519 hash_kind: gix::hash::Kind,
1520) -> BackendResult<()> {
1521 let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1522 let mut added_paths = vec![];
1523 let mut removed_paths = HashSet::new();
1524 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1525 let (before, after) = values?;
1526 if before.is_absent() {
1527 let executable = match after.as_normal() {
1528 Some(TreeValue::File { id: _, executable }) => *executable,
1529 Some(TreeValue::Symlink(_)) => false,
1530 _ => {
1531 continue;
1532 }
1533 };
1534 if index
1535 .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1536 .is_err()
1537 {
1538 added_paths.push((BString::from(path.into_internal_string()), executable));
1539 }
1540 } else if after.is_absent() {
1541 removed_paths.insert(BString::from(path.into_internal_string()));
1542 }
1543 }
1544
1545 if added_paths.is_empty() && removed_paths.is_empty() {
1546 return Ok(());
1547 }
1548
1549 for (path, executable) in added_paths {
1550 index.dangerously_push_entry(
1552 gix::index::entry::Stat::default(),
1553 gix::ObjectId::empty_blob(hash_kind),
1554 gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1555 if executable {
1556 gix::index::entry::Mode::FILE_EXECUTABLE
1557 } else {
1558 gix::index::entry::Mode::FILE
1559 },
1560 path.as_ref(),
1561 );
1562 }
1563 if !removed_paths.is_empty() {
1564 index.remove_entries(|_size, path, entry| {
1565 entry
1566 .flags
1567 .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1568 && removed_paths.contains(path)
1569 });
1570 }
1571
1572 index.sort_entries();
1573
1574 Ok(())
1575}
1576
1577#[derive(Debug, Error)]
1578pub enum GitRemoteManagementError {
1579 #[error("No git remote named '{}'", .0.as_symbol())]
1580 NoSuchRemote(RemoteNameBuf),
1581 #[error("Git remote named '{}' already exists", .0.as_symbol())]
1582 RemoteAlreadyExists(RemoteNameBuf),
1583 #[error(transparent)]
1584 RemoteName(#[from] GitRemoteNameError),
1585 #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1586 NonstandardConfiguration(RemoteNameBuf),
1587 #[error("Error saving Git configuration")]
1588 GitConfigSaveError(#[source] std::io::Error),
1589 #[error("Unexpected Git error when managing remotes")]
1590 InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1591 #[error(transparent)]
1592 UnexpectedBackend(#[from] UnexpectedGitBackendError),
1593}
1594
1595impl GitRemoteManagementError {
1596 fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1597 GitRemoteManagementError::InternalGitError(source.into())
1598 }
1599}
1600
1601#[cfg(feature = "git2")]
1602fn is_remote_not_found_err(err: &git2::Error) -> bool {
1603 matches!(
1604 (err.class(), err.code()),
1605 (
1606 git2::ErrorClass::Config,
1607 git2::ErrorCode::NotFound | git2::ErrorCode::InvalidSpec
1608 )
1609 )
1610}
1611
1612pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1617 remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1618}
1619
1620fn default_fetch_refspec(remote: &RemoteName) -> String {
1621 format!(
1622 "+refs/heads/*:refs/remotes/{remote}/*",
1623 remote = remote.as_str()
1624 )
1625}
1626
1627fn add_ref(
1628 name: gix::refs::FullName,
1629 target: gix::refs::Target,
1630 message: BString,
1631) -> gix::refs::transaction::RefEdit {
1632 gix::refs::transaction::RefEdit {
1633 change: gix::refs::transaction::Change::Update {
1634 log: gix::refs::transaction::LogChange {
1635 mode: gix::refs::transaction::RefLog::AndReference,
1636 force_create_reflog: false,
1637 message,
1638 },
1639 expected: gix::refs::transaction::PreviousValue::MustNotExist,
1640 new: target,
1641 },
1642 name,
1643 deref: false,
1644 }
1645}
1646
1647fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1648 gix::refs::transaction::RefEdit {
1649 change: gix::refs::transaction::Change::Delete {
1650 expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1651 reference.target().into_owned(),
1652 ),
1653 log: gix::refs::transaction::RefLog::AndReference,
1654 },
1655 name: reference.name().to_owned(),
1656 deref: false,
1657 }
1658}
1659
1660fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1666 let mut config_file = File::create(
1667 config
1668 .meta()
1669 .path
1670 .as_ref()
1671 .expect("Git repository to have a config file"),
1672 )?;
1673 config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1674}
1675
1676fn save_remote(
1677 config: &mut gix::config::File<'static>,
1678 remote_name: &RemoteName,
1679 remote: &mut gix::Remote,
1680) -> Result<(), GitRemoteManagementError> {
1681 config
1688 .new_section(
1689 "remote",
1690 Some(Cow::Owned(BString::from(remote_name.as_str()))),
1691 )
1692 .map_err(GitRemoteManagementError::from_git)?;
1693 remote
1694 .save_as_to(remote_name.as_str(), config)
1695 .map_err(GitRemoteManagementError::from_git)?;
1696 Ok(())
1697}
1698
1699fn git_config_branch_section_ids_by_remote(
1700 config: &gix::config::File,
1701 remote_name: &RemoteName,
1702) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1703 config
1704 .sections_by_name("branch")
1705 .into_iter()
1706 .flatten()
1707 .filter_map(|section| {
1708 let remote_values = section.values("remote");
1709 let push_remote_values = section.values("pushRemote");
1710 if !remote_values
1711 .iter()
1712 .chain(push_remote_values.iter())
1713 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1714 {
1715 return None;
1716 }
1717 if remote_values.len() > 1
1718 || push_remote_values.len() > 1
1719 || section.value_names().any(|name| {
1720 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1721 })
1722 {
1723 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1724 remote_name.to_owned(),
1725 )));
1726 }
1727 Some(Ok(section.id()))
1728 })
1729 .collect()
1730}
1731
1732fn rename_remote_in_git_branch_config_sections(
1733 config: &mut gix::config::File,
1734 old_remote_name: &RemoteName,
1735 new_remote_name: &RemoteName,
1736) -> Result<(), GitRemoteManagementError> {
1737 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1738 config
1739 .section_mut_by_id(id)
1740 .expect("found section to exist")
1741 .set(
1742 "remote"
1743 .try_into()
1744 .expect("'remote' to be a valid value name"),
1745 BStr::new(new_remote_name.as_str()),
1746 );
1747 }
1748 Ok(())
1749}
1750
1751fn remove_remote_git_branch_config_sections(
1752 config: &mut gix::config::File,
1753 remote_name: &RemoteName,
1754) -> Result<(), GitRemoteManagementError> {
1755 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1756 config
1757 .remove_section_by_id(id)
1758 .expect("removed section to exist");
1759 }
1760 Ok(())
1761}
1762
1763fn remove_remote_git_config_sections(
1764 config: &mut gix::config::File,
1765 remote_name: &RemoteName,
1766) -> Result<(), GitRemoteManagementError> {
1767 let section_ids_to_remove: Vec<_> = config
1768 .sections_by_name("remote")
1769 .into_iter()
1770 .flatten()
1771 .filter(|section| {
1772 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1773 })
1774 .map(|section| {
1775 if section.value_names().any(|name| {
1776 !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1777 }) {
1778 return Err(GitRemoteManagementError::NonstandardConfiguration(
1779 remote_name.to_owned(),
1780 ));
1781 }
1782 Ok(section.id())
1783 })
1784 .try_collect()?;
1785 for id in section_ids_to_remove {
1786 config
1787 .remove_section_by_id(id)
1788 .expect("removed section to exist");
1789 }
1790 Ok(())
1791}
1792
1793pub fn get_all_remote_names(
1795 store: &Store,
1796) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1797 let git_repo = get_git_repo(store)?;
1798 let names = git_repo
1799 .remote_names()
1800 .into_iter()
1801 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1803 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1805 .map(RemoteNameBuf::from)
1806 .collect();
1807 Ok(names)
1808}
1809
1810pub fn add_remote(
1811 store: &Store,
1812 remote_name: &RemoteName,
1813 url: &str,
1814) -> Result<(), GitRemoteManagementError> {
1815 let git_repo = get_git_repo(store)?;
1816
1817 validate_remote_name(remote_name)?;
1818
1819 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1820 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1821 remote_name.to_owned(),
1822 ));
1823 }
1824
1825 let mut remote = git_repo
1826 .remote_at(url)
1827 .map_err(GitRemoteManagementError::from_git)?
1828 .with_refspecs(
1829 [default_fetch_refspec(remote_name).as_bytes()],
1830 gix::remote::Direction::Fetch,
1831 )
1832 .expect("default refspec to be valid");
1833
1834 let mut config = git_repo.config_snapshot().clone();
1835 save_remote(&mut config, remote_name, &mut remote)?;
1836 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1837
1838 Ok(())
1839}
1840
1841pub fn remove_remote(
1842 mut_repo: &mut MutableRepo,
1843 remote_name: &RemoteName,
1844) -> Result<(), GitRemoteManagementError> {
1845 let mut git_repo = get_git_repo(mut_repo.store())?;
1846
1847 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1848 return Err(GitRemoteManagementError::NoSuchRemote(
1849 remote_name.to_owned(),
1850 ));
1851 };
1852
1853 let mut config = git_repo.config_snapshot().clone();
1854 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1855 remove_remote_git_config_sections(&mut config, remote_name)?;
1856 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1857
1858 remove_remote_git_refs(&mut git_repo, remote_name)
1859 .map_err(GitRemoteManagementError::from_git)?;
1860
1861 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1862 remove_remote_refs(mut_repo, remote_name);
1863 }
1864
1865 Ok(())
1866}
1867
1868fn remove_remote_git_refs(
1869 git_repo: &mut gix::Repository,
1870 remote: &RemoteName,
1871) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1872 let edits: Vec<_> = git_repo
1873 .references()?
1874 .prefixed(format!("refs/remotes/{remote}/", remote = remote.as_str()))?
1875 .map_ok(remove_ref)
1876 .try_collect()?;
1877 git_repo.edit_references(edits)?;
1878 Ok(())
1879}
1880
1881fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1882 mut_repo.remove_remote(remote);
1883 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1884 let git_refs_to_delete = mut_repo
1885 .view()
1886 .git_refs()
1887 .keys()
1888 .filter(|&r| r.as_str().starts_with(&prefix))
1889 .cloned()
1890 .collect_vec();
1891 for git_ref in git_refs_to_delete {
1892 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1893 }
1894}
1895
1896pub fn rename_remote(
1897 mut_repo: &mut MutableRepo,
1898 old_remote_name: &RemoteName,
1899 new_remote_name: &RemoteName,
1900) -> Result<(), GitRemoteManagementError> {
1901 let mut git_repo = get_git_repo(mut_repo.store())?;
1902
1903 validate_remote_name(new_remote_name)?;
1904
1905 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1906 return Err(GitRemoteManagementError::NoSuchRemote(
1907 old_remote_name.to_owned(),
1908 ));
1909 };
1910 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1911
1912 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1913 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1914 new_remote_name.to_owned(),
1915 ));
1916 }
1917
1918 match (
1919 remote.refspecs(gix::remote::Direction::Fetch),
1920 remote.refspecs(gix::remote::Direction::Push),
1921 ) {
1922 ([refspec], [])
1923 if refspec.to_ref().to_bstring()
1924 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1925 _ => {
1926 return Err(GitRemoteManagementError::NonstandardConfiguration(
1927 old_remote_name.to_owned(),
1928 ))
1929 }
1930 }
1931
1932 remote
1933 .replace_refspecs(
1934 [default_fetch_refspec(new_remote_name).as_bytes()],
1935 gix::remote::Direction::Fetch,
1936 )
1937 .expect("default refspec to be valid");
1938
1939 let mut config = git_repo.config_snapshot().clone();
1940 save_remote(&mut config, new_remote_name, &mut remote)?;
1941 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1942 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1943 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1944
1945 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1946 .map_err(GitRemoteManagementError::from_git)?;
1947
1948 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1949 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1950 }
1951
1952 Ok(())
1953}
1954
1955fn rename_remote_git_refs(
1956 git_repo: &mut gix::Repository,
1957 old_remote_name: &RemoteName,
1958 new_remote_name: &RemoteName,
1959) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1960 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1961 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1962 let ref_log_message = BString::from(format!(
1963 "renamed remote {old_remote_name} to {new_remote_name}",
1964 old_remote_name = old_remote_name.as_symbol(),
1965 new_remote_name = new_remote_name.as_symbol(),
1966 ));
1967
1968 let edits: Vec<_> = git_repo
1969 .references()?
1970 .prefixed(old_prefix.clone())?
1971 .map_ok(|old_ref| {
1972 let new_name = BString::new(
1973 [
1974 new_prefix.as_bytes(),
1975 &old_ref.name().as_bstr()[old_prefix.len()..],
1976 ]
1977 .concat(),
1978 );
1979 [
1980 add_ref(
1981 new_name.try_into().expect("new ref name to be valid"),
1982 old_ref.target().into_owned(),
1983 ref_log_message.clone(),
1984 ),
1985 remove_ref(old_ref),
1986 ]
1987 })
1988 .flatten_ok()
1989 .try_collect()?;
1990 git_repo.edit_references(edits)?;
1991 Ok(())
1992}
1993
1994fn gix_remote_with_fetch_url<Url, E>(
2000 remote: gix::Remote,
2001 url: Url,
2002) -> Result<gix::Remote, gix::remote::init::Error>
2003where
2004 Url: TryInto<gix::Url, Error = E>,
2005 gix::url::parse::Error: From<E>,
2006{
2007 let mut new_remote = remote.repo().remote_at(url)?;
2008 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2014 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2015 new_remote
2016 .replace_refspecs(
2017 remote
2018 .refspecs(direction)
2019 .iter()
2020 .map(|refspec| refspec.to_ref().to_bstring()),
2021 direction,
2022 )
2023 .expect("existing refspecs to be valid");
2024 }
2025 Ok(new_remote)
2026}
2027
2028pub fn set_remote_url(
2029 store: &Store,
2030 remote_name: &RemoteName,
2031 new_remote_url: &str,
2032) -> Result<(), GitRemoteManagementError> {
2033 let git_repo = get_git_repo(store)?;
2034
2035 validate_remote_name(remote_name)?;
2036
2037 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2038 return Err(GitRemoteManagementError::NoSuchRemote(
2039 remote_name.to_owned(),
2040 ));
2041 };
2042 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2043
2044 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2045 return Err(GitRemoteManagementError::NonstandardConfiguration(
2046 remote_name.to_owned(),
2047 ));
2048 }
2049
2050 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2051 .map_err(GitRemoteManagementError::from_git)?;
2052
2053 let mut config = git_repo.config_snapshot().clone();
2054 save_remote(&mut config, remote_name, &mut remote)?;
2055 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2056
2057 Ok(())
2058}
2059
2060fn rename_remote_refs(
2061 mut_repo: &mut MutableRepo,
2062 old_remote_name: &RemoteName,
2063 new_remote_name: &RemoteName,
2064) {
2065 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2066 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2067 let git_refs = mut_repo
2068 .view()
2069 .git_refs()
2070 .iter()
2071 .filter_map(|(old, target)| {
2072 old.as_str().strip_prefix(&prefix).map(|p| {
2073 let new: GitRefNameBuf =
2074 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2075 (old.clone(), new, target.clone())
2076 })
2077 })
2078 .collect_vec();
2079 for (old, new, target) in git_refs {
2080 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2081 mut_repo.set_git_ref_target(&new, target);
2082 }
2083}
2084
2085#[cfg(feature = "git2")]
2086fn open_git2_repo(git_backend: &GitBackend) -> Result<git2::Repository, git2::Error> {
2087 let mut flags = git2::RepositoryOpenFlags::NO_SEARCH;
2088 if std::env::var("JJ_DEBUG_HERMETIC_GIT2").as_deref() == Ok("1") {
2089 flags.insert(git2::RepositoryOpenFlags::FROM_ENV);
2090 }
2091 git2::Repository::open_ext(
2092 git_backend.git_repo_path(),
2093 flags,
2094 &[] as &[&std::ffi::OsStr],
2095 )
2096}
2097
2098const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2099
2100#[derive(Error, Debug)]
2101pub enum GitFetchError {
2102 #[error("No git remote named '{}'", .0.as_symbol())]
2103 NoSuchRemote(RemoteNameBuf),
2104 #[error(
2105 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2106 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2107 )]
2108 InvalidBranchPattern(StringPattern),
2109 #[error(transparent)]
2110 RemoteName(#[from] GitRemoteNameError),
2111 #[cfg(feature = "git2")]
2112 #[error(transparent)]
2113 Git2(#[from] git2::Error),
2114 #[error(transparent)]
2115 Subprocess(#[from] GitSubprocessError),
2116}
2117
2118#[derive(Debug, Error)]
2121pub enum GitFetchPrepareError {
2122 #[cfg(feature = "git2")]
2123 #[error(transparent)]
2124 Git2(#[from] git2::Error),
2125 #[error(transparent)]
2126 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2127}
2128
2129#[cfg(feature = "git2")]
2130fn git2_fetch_options(
2131 mut callbacks: RemoteCallbacks<'_>,
2132 depth: Option<NonZeroU32>,
2133) -> git2::FetchOptions<'_> {
2134 let mut proxy_options = git2::ProxyOptions::new();
2135 proxy_options.auto();
2136
2137 let mut fetch_options = git2::FetchOptions::new();
2138 fetch_options.proxy_options(proxy_options);
2139 if callbacks.progress.is_none() {
2143 callbacks.sideband_progress = None;
2144 }
2145 fetch_options.remote_callbacks(callbacks.into_git());
2146 if let Some(depth) = depth {
2147 fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX));
2148 }
2149
2150 fetch_options
2151}
2152
2153struct FetchedBranches {
2154 remote: RemoteNameBuf,
2155 branches: Vec<StringPattern>,
2156}
2157
2158pub struct GitFetch<'a> {
2160 mut_repo: &'a mut MutableRepo,
2161 fetch_impl: GitFetchImpl<'a>,
2162 git_settings: &'a GitSettings,
2163 fetched: Vec<FetchedBranches>,
2164}
2165
2166impl<'a> GitFetch<'a> {
2167 pub fn new(
2168 mut_repo: &'a mut MutableRepo,
2169 git_settings: &'a GitSettings,
2170 ) -> Result<Self, GitFetchPrepareError> {
2171 let fetch_impl = GitFetchImpl::new(mut_repo.store(), git_settings)?;
2172 Ok(GitFetch {
2173 mut_repo,
2174 fetch_impl,
2175 git_settings,
2176 fetched: vec![],
2177 })
2178 }
2179
2180 #[tracing::instrument(skip(self, callbacks))]
2186 pub fn fetch(
2187 &mut self,
2188 remote_name: &RemoteName,
2189 branch_names: &[StringPattern],
2190 callbacks: RemoteCallbacks<'_>,
2191 depth: Option<NonZeroU32>,
2192 ) -> Result<(), GitFetchError> {
2193 validate_remote_name(remote_name)?;
2194 self.fetch_impl
2195 .fetch(remote_name, branch_names, callbacks, depth)?;
2196 self.fetched.push(FetchedBranches {
2197 remote: remote_name.to_owned(),
2198 branches: branch_names.to_vec(),
2199 });
2200 Ok(())
2201 }
2202
2203 #[tracing::instrument(skip(self, callbacks))]
2205 pub fn get_default_branch(
2206 &self,
2207 remote_name: &RemoteName,
2208 callbacks: RemoteCallbacks<'_>,
2209 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2210 self.fetch_impl.get_default_branch(remote_name, callbacks)
2211 }
2212
2213 #[tracing::instrument(skip(self))]
2221 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2222 tracing::debug!("import_refs");
2223 let import_stats =
2224 import_some_refs(
2225 self.mut_repo,
2226 self.git_settings,
2227 |kind, symbol| match kind {
2228 GitRefKind::Bookmark => self
2229 .fetched
2230 .iter()
2231 .filter(|fetched| fetched.remote == symbol.remote)
2232 .any(|fetched| {
2233 fetched
2234 .branches
2235 .iter()
2236 .any(|pattern| pattern.matches(symbol.name.as_str()))
2237 }),
2238 GitRefKind::Tag => true,
2239 },
2240 )?;
2241
2242 self.fetched.clear();
2243
2244 Ok(import_stats)
2245 }
2246}
2247
2248fn expand_fetch_refspecs(
2249 remote: &RemoteName,
2250 branch_names: &[StringPattern],
2251) -> Result<Vec<RefSpec>, GitFetchError> {
2252 branch_names
2253 .iter()
2254 .map(|pattern| {
2255 pattern
2256 .to_glob()
2257 .filter(
2258 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2261 )
2262 .map(|glob| {
2263 RefSpec::forced(
2264 format!("refs/heads/{glob}"),
2265 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2266 )
2267 })
2268 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2269 })
2270 .collect()
2271}
2272
2273enum GitFetchImpl<'a> {
2274 #[cfg(feature = "git2")]
2275 Git2 { git_repo: git2::Repository },
2276 Subprocess {
2277 git_repo: Box<gix::Repository>,
2278 git_ctx: GitSubprocessContext<'a>,
2279 },
2280}
2281
2282impl<'a> GitFetchImpl<'a> {
2283 fn new(store: &Store, git_settings: &'a GitSettings) -> Result<Self, GitFetchPrepareError> {
2284 let git_backend = get_git_backend(store)?;
2285 #[cfg(feature = "git2")]
2286 if !git_settings.subprocess {
2287 let git_repo = open_git2_repo(git_backend)?;
2288 return Ok(GitFetchImpl::Git2 { git_repo });
2289 }
2290 let git_repo = Box::new(git_backend.git_repo());
2291 let git_ctx =
2292 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2293 Ok(GitFetchImpl::Subprocess { git_repo, git_ctx })
2294 }
2295
2296 fn fetch(
2297 &self,
2298 remote_name: &RemoteName,
2299 branch_names: &[StringPattern],
2300 callbacks: RemoteCallbacks<'_>,
2301 depth: Option<NonZeroU32>,
2302 ) -> Result<(), GitFetchError> {
2303 match self {
2304 #[cfg(feature = "git2")]
2305 GitFetchImpl::Git2 { git_repo } => {
2306 git2_fetch(git_repo, remote_name, branch_names, callbacks, depth)
2307 }
2308 GitFetchImpl::Subprocess { git_repo, git_ctx } => subprocess_fetch(
2309 git_repo,
2310 git_ctx,
2311 remote_name,
2312 branch_names,
2313 callbacks,
2314 depth,
2315 ),
2316 }
2317 }
2318
2319 fn get_default_branch(
2320 &self,
2321 remote_name: &RemoteName,
2322 callbacks: RemoteCallbacks<'_>,
2323 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2324 match self {
2325 #[cfg(feature = "git2")]
2326 GitFetchImpl::Git2 { git_repo } => {
2327 git2_get_default_branch(git_repo, remote_name, callbacks)
2328 }
2329 GitFetchImpl::Subprocess { git_repo, git_ctx } => {
2330 subprocess_get_default_branch(git_repo, git_ctx, remote_name, callbacks)
2331 }
2332 }
2333 }
2334}
2335
2336#[cfg(feature = "git2")]
2337fn git2_fetch(
2338 git_repo: &git2::Repository,
2339 remote_name: &RemoteName,
2340 branch_names: &[StringPattern],
2341 callbacks: RemoteCallbacks<'_>,
2342 depth: Option<NonZeroU32>,
2343) -> Result<(), GitFetchError> {
2344 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2345 if is_remote_not_found_err(&err) {
2346 GitFetchError::NoSuchRemote(remote_name.to_owned())
2347 } else {
2348 GitFetchError::Git2(err)
2349 }
2350 })?;
2351 let refspecs: Vec<String> = expand_fetch_refspecs(remote_name, branch_names)?
2354 .iter()
2355 .map(|refspec| refspec.to_git_format())
2356 .collect();
2357
2358 if refspecs.is_empty() {
2359 return Ok(());
2361 }
2362
2363 tracing::debug!("remote.download");
2364 remote.download(&refspecs, Some(&mut git2_fetch_options(callbacks, depth)))?;
2365 tracing::debug!("remote.prune");
2366 remote.prune(None)?;
2367 tracing::debug!("remote.update_tips");
2368 remote.update_tips(
2369 None,
2370 git2::RemoteUpdateFlags::empty(),
2371 git2::AutotagOption::Unspecified,
2372 None,
2373 )?;
2374 tracing::debug!("remote.disconnect");
2375 remote.disconnect()?;
2376 Ok(())
2377}
2378
2379#[cfg(feature = "git2")]
2380fn git2_get_default_branch(
2381 git_repo: &git2::Repository,
2382 remote_name: &RemoteName,
2383 callbacks: RemoteCallbacks<'_>,
2384) -> Result<Option<RefNameBuf>, GitFetchError> {
2385 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2386 if is_remote_not_found_err(&err) {
2387 GitFetchError::NoSuchRemote(remote_name.to_owned())
2388 } else {
2389 GitFetchError::Git2(err)
2390 }
2391 })?;
2392 tracing::debug!("remote.connect");
2394 let connection = {
2395 let mut proxy_options = git2::ProxyOptions::new();
2396 proxy_options.auto();
2397 remote.connect_auth(
2398 git2::Direction::Fetch,
2399 Some(callbacks.into_git()),
2400 Some(proxy_options),
2401 )?
2402 };
2403 let mut default_branch = None;
2404 tracing::debug!("remote.default_branch");
2405 if let Ok(default_ref_buf) = connection.default_branch() {
2406 if let Some(default_ref) = default_ref_buf.as_str() {
2407 if let Some(branch_name) = default_ref
2409 .strip_prefix("refs/heads/")
2410 .filter(|&name| name != "HEAD")
2411 {
2412 tracing::debug!(default_branch = branch_name);
2413 default_branch = Some(branch_name.into());
2414 }
2415 }
2416 }
2417 Ok(default_branch)
2418}
2419
2420fn subprocess_fetch(
2421 git_repo: &gix::Repository,
2422 git_ctx: &GitSubprocessContext,
2423 remote_name: &RemoteName,
2424 branch_names: &[StringPattern],
2425 mut callbacks: RemoteCallbacks<'_>,
2426 depth: Option<NonZeroU32>,
2427) -> Result<(), GitFetchError> {
2428 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2430 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2431 }
2432 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2435 if remaining_refspecs.is_empty() {
2436 return Ok(());
2438 }
2439
2440 let mut branches_to_prune = Vec::new();
2441 while let Some(failing_refspec) =
2449 git_ctx.spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2450 {
2451 tracing::debug!(failing_refspec, "failed to fetch ref");
2452 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2453
2454 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2455 branches_to_prune.push(format!(
2456 "{remote_name}/{branch_name}",
2457 remote_name = remote_name.as_str()
2458 ));
2459 }
2460 }
2461
2462 git_ctx.spawn_branch_prune(&branches_to_prune)?;
2465 Ok(())
2466}
2467
2468fn subprocess_get_default_branch(
2469 git_repo: &gix::Repository,
2470 git_ctx: &GitSubprocessContext,
2471 remote_name: &RemoteName,
2472 _callbacks: RemoteCallbacks<'_>,
2473) -> Result<Option<RefNameBuf>, GitFetchError> {
2474 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2475 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2476 }
2477 let default_branch = git_ctx.spawn_remote_show(remote_name)?;
2478 tracing::debug!(?default_branch);
2479 Ok(default_branch)
2480}
2481
2482#[derive(Error, Debug)]
2483pub enum GitPushError {
2484 #[error("No git remote named '{}'", .0.as_symbol())]
2485 NoSuchRemote(RemoteNameBuf),
2486 #[error(transparent)]
2487 RemoteName(#[from] GitRemoteNameError),
2488 #[cfg(feature = "git2")]
2489 #[error(transparent)]
2490 Git2(#[from] git2::Error),
2491 #[error(transparent)]
2492 Subprocess(#[from] GitSubprocessError),
2493 #[error(transparent)]
2494 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2495}
2496
2497#[derive(Clone, Debug)]
2498pub struct GitBranchPushTargets {
2499 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2500}
2501
2502pub struct GitRefUpdate {
2503 pub qualified_name: GitRefNameBuf,
2504 pub expected_current_target: Option<CommitId>,
2509 pub new_target: Option<CommitId>,
2510}
2511
2512pub fn push_branches(
2514 mut_repo: &mut MutableRepo,
2515 git_settings: &GitSettings,
2516 remote: &RemoteName,
2517 targets: &GitBranchPushTargets,
2518 callbacks: RemoteCallbacks<'_>,
2519) -> Result<GitPushStats, GitPushError> {
2520 validate_remote_name(remote)?;
2521
2522 let ref_updates = targets
2523 .branch_updates
2524 .iter()
2525 .map(|(name, update)| GitRefUpdate {
2526 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2527 expected_current_target: update.old_target.clone(),
2528 new_target: update.new_target.clone(),
2529 })
2530 .collect_vec();
2531
2532 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2533 tracing::debug!(?push_stats);
2534
2535 if push_stats.all_ok() {
2539 for (name, update) in &targets.branch_updates {
2540 let git_ref_name: GitRefNameBuf = format!(
2541 "refs/remotes/{remote}/{name}",
2542 remote = remote.as_str(),
2543 name = name.as_str()
2544 )
2545 .into();
2546 let new_remote_ref = RemoteRef {
2547 target: RefTarget::resolved(update.new_target.clone()),
2548 state: RemoteRefState::Tracked,
2549 };
2550 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2551 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2552 }
2553 }
2554
2555 Ok(push_stats)
2556}
2557
2558pub fn push_updates(
2560 repo: &dyn Repo,
2561 git_settings: &GitSettings,
2562 remote_name: &RemoteName,
2563 updates: &[GitRefUpdate],
2564 callbacks: RemoteCallbacks<'_>,
2565) -> Result<GitPushStats, GitPushError> {
2566 let mut qualified_remote_refs_expected_locations = HashMap::new();
2567 let mut refspecs = vec![];
2568 for update in updates {
2569 qualified_remote_refs_expected_locations.insert(
2570 update.qualified_name.as_ref(),
2571 update.expected_current_target.as_ref(),
2572 );
2573 if let Some(new_target) = &update.new_target {
2574 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2578 } else {
2579 refspecs.push(RefSpec::delete(&update.qualified_name));
2583 }
2584 }
2585 let git_backend = get_git_backend(repo.store())?;
2589 #[cfg(feature = "git2")]
2590 if !git_settings.subprocess {
2591 let git_repo = open_git2_repo(git_backend)?;
2592 let refspecs: Vec<String> = refspecs.iter().map(RefSpec::to_git_format).collect();
2593 return git2_push_refs(
2594 repo,
2595 &git_repo,
2596 remote_name,
2597 &qualified_remote_refs_expected_locations,
2598 &refspecs,
2599 callbacks,
2600 );
2601 }
2602 let git_repo = git_backend.git_repo();
2603 let git_ctx =
2604 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2605 subprocess_push_refs(
2606 &git_repo,
2607 &git_ctx,
2608 remote_name,
2609 &qualified_remote_refs_expected_locations,
2610 &refspecs,
2611 callbacks,
2612 )
2613}
2614
2615#[cfg(feature = "git2")]
2616fn git2_push_refs(
2617 repo: &dyn Repo,
2618 git_repo: &git2::Repository,
2619 remote_name: &RemoteName,
2620 qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2621 refspecs: &[String],
2622 callbacks: RemoteCallbacks<'_>,
2623) -> Result<GitPushStats, GitPushError> {
2624 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2625 if is_remote_not_found_err(&err) {
2626 GitPushError::NoSuchRemote(remote_name.to_owned())
2627 } else {
2628 GitPushError::Git2(err)
2629 }
2630 })?;
2631
2632 let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs_expected_locations
2633 .keys()
2634 .copied()
2635 .collect();
2636 let mut failed_push_negotiations = vec![];
2637 let mut pushed_refs = vec![];
2638
2639 let push_result = {
2640 let mut push_options = git2::PushOptions::new();
2641 let mut proxy_options = git2::ProxyOptions::new();
2642 proxy_options.auto();
2643 push_options.proxy_options(proxy_options);
2644 let mut callbacks = callbacks.into_git();
2645 callbacks.push_negotiation(|updates| {
2646 for update in updates {
2647 let dst_refname: &GitRefName = update
2648 .dst_refname()
2649 .expect("Expect reference name to be valid UTF-8")
2650 .as_ref();
2651 let expected_remote_location = *qualified_remote_refs_expected_locations
2652 .get(dst_refname)
2653 .expect("Push is trying to move a ref it wasn't asked to move");
2654 let oid_to_maybe_commitid =
2655 |oid: git2::Oid| (!oid.is_zero()).then(|| CommitId::from_bytes(oid.as_bytes()));
2656 let actual_remote_location = oid_to_maybe_commitid(update.src());
2657 let local_location = oid_to_maybe_commitid(update.dst());
2658
2659 match allow_push(
2660 repo.index(),
2661 actual_remote_location.as_ref(),
2662 expected_remote_location,
2663 local_location.as_ref(),
2664 ) {
2665 Ok(PushAllowReason::NormalMatch) => {}
2666 Ok(PushAllowReason::UnexpectedNoop) => {
2667 tracing::info!(
2668 "The push of {dst_refname:?} is unexpectedly a no-op, the remote \
2669 branch is already at {actual_remote_location:?}. We expected it to \
2670 be at {expected_remote_location:?}. We don't consider this an error.",
2671 );
2672 }
2673 Ok(PushAllowReason::ExceptionalFastforward) => {
2674 tracing::info!(
2677 "We allow the push of {dst_refname:?} to {local_location:?}, even \
2678 though it is unexpectedly at {actual_remote_location:?} on the \
2679 server rather than the expected {expected_remote_location:?}. The \
2680 desired location is a descendant of the actual location, and the \
2681 actual location is a descendant of the expected location.",
2682 );
2683 }
2684 Err(()) => {
2685 tracing::info!(
2691 "Cannot push {dst_refname:?} to {local_location:?}; it is at \
2692 unexpectedly at {actual_remote_location:?} on the server as opposed \
2693 to the expected {expected_remote_location:?}",
2694 );
2695 failed_push_negotiations.push(dst_refname.to_owned());
2696 }
2697 }
2698 }
2699
2700 if failed_push_negotiations.is_empty() {
2701 Ok(())
2702 } else {
2703 Err(git2::Error::from_str("failed push negotiation"))
2704 }
2705 });
2706 callbacks.push_update_reference(|refname, status| {
2707 let refname = GitRefName::new(refname);
2708 if status.is_none() {
2710 remaining_remote_refs.remove(refname);
2711 pushed_refs.push(refname.to_owned());
2712 }
2713 Ok(())
2714 });
2715 push_options.remote_callbacks(callbacks);
2716 remote.push(refspecs, Some(&mut push_options))
2717 };
2718
2719 for failed_update in &failed_push_negotiations {
2720 remaining_remote_refs.remove(&**failed_update);
2721 }
2722 let rejected: Vec<_> = failed_push_negotiations
2723 .into_iter()
2724 .sorted()
2725 .map(|name| (name, None))
2726 .collect();
2727 let remote_rejected: Vec<_> = remaining_remote_refs
2728 .into_iter()
2729 .sorted()
2730 .map(|name| (name.to_owned(), None))
2731 .collect();
2732 pushed_refs.sort();
2733
2734 let push_stats = if !rejected.is_empty() {
2735 assert!(push_result.is_err());
2743 GitPushStats {
2744 rejected,
2745 remote_rejected,
2746 ..Default::default()
2747 }
2748 } else {
2749 push_result?;
2750 GitPushStats {
2751 pushed: pushed_refs,
2752 remote_rejected,
2753 ..Default::default()
2754 }
2755 };
2756
2757 Ok(push_stats)
2758}
2759
2760fn subprocess_push_refs(
2761 git_repo: &gix::Repository,
2762 git_ctx: &GitSubprocessContext,
2763 remote_name: &RemoteName,
2764 qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2765 refspecs: &[RefSpec],
2766 mut callbacks: RemoteCallbacks<'_>,
2767) -> Result<GitPushStats, GitPushError> {
2768 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2770 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2771 }
2772
2773 let refs_to_push: Vec<RefToPush> = refspecs
2774 .iter()
2775 .map(|full_refspec| RefToPush::new(full_refspec, qualified_remote_refs_expected_locations))
2776 .collect();
2777
2778 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2779 push_stats.pushed.sort();
2780 push_stats.rejected.sort();
2781 push_stats.remote_rejected.sort();
2782 Ok(push_stats)
2783}
2784
2785#[cfg(feature = "git2")]
2786#[derive(Debug, Clone, PartialEq, Eq)]
2787enum PushAllowReason {
2788 NormalMatch,
2789 ExceptionalFastforward,
2790 UnexpectedNoop,
2791}
2792
2793#[cfg(feature = "git2")]
2794fn allow_push(
2795 index: &dyn Index,
2796 actual_remote_location: Option<&CommitId>,
2797 expected_remote_location: Option<&CommitId>,
2798 destination_location: Option<&CommitId>,
2799) -> Result<PushAllowReason, ()> {
2800 if actual_remote_location == expected_remote_location {
2801 return Ok(PushAllowReason::NormalMatch);
2802 }
2803
2804 if !actual_remote_location.is_none_or(|id| index.has_id(id)) {
2816 return Err(());
2817 }
2818 let remote_target = RefTarget::resolved(actual_remote_location.cloned());
2819 let base_target = RefTarget::resolved(expected_remote_location.cloned());
2820 let local_target = RefTarget::resolved(destination_location.cloned());
2822 if refs::merge_ref_targets(index, &remote_target, &base_target, &local_target) == local_target {
2823 Ok(if actual_remote_location == destination_location {
2827 PushAllowReason::UnexpectedNoop
2831 } else {
2832 PushAllowReason::ExceptionalFastforward
2839 })
2840 } else {
2841 Err(())
2842 }
2843}
2844
2845#[non_exhaustive]
2846#[derive(Default)]
2847#[expect(clippy::type_complexity)]
2848pub struct RemoteCallbacks<'a> {
2849 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2850 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2851 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2852 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2853 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2854}
2855
2856#[cfg(feature = "git2")]
2857impl<'a> RemoteCallbacks<'a> {
2858 fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
2859 let mut callbacks = git2::RemoteCallbacks::new();
2860 if let Some(progress_cb) = self.progress {
2861 callbacks.transfer_progress(move |progress| {
2862 progress_cb(&Progress {
2863 bytes_downloaded: (progress.received_objects() < progress.total_objects())
2864 .then(|| progress.received_bytes() as u64),
2865 overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
2866 / (progress.total_objects() + progress.total_deltas()) as f32,
2867 });
2868 true
2869 });
2870 }
2871 if let Some(sideband_progress_cb) = self.sideband_progress {
2872 callbacks.sideband_progress(move |data| {
2873 sideband_progress_cb(data);
2874 true
2875 });
2876 }
2877 let mut tried_ssh_agent = false;
2880 let mut ssh_key_paths_to_try: Option<Vec<PathBuf>> = None;
2881 callbacks.credentials(move |url, username_from_url, allowed_types| {
2882 let span = tracing::debug_span!("RemoteCallbacks.credentials");
2883 let _ = span.enter();
2884
2885 let git_config = git2::Config::open_default();
2886 let credential_helper = git_config
2887 .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
2888 if let Ok(creds) = credential_helper {
2889 tracing::info!("using credential_helper");
2890 return Ok(creds);
2891 } else if let Some(username) = username_from_url {
2892 if allowed_types.contains(git2::CredentialType::SSH_KEY) {
2893 if !tried_ssh_agent {
2896 tracing::info!(username, "trying ssh_key_from_agent");
2897 tried_ssh_agent = true;
2898 return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
2899 tracing::error!(err = %err);
2900 err
2901 });
2902 }
2903
2904 let paths = ssh_key_paths_to_try.get_or_insert_with(|| {
2905 if let Some(ref mut cb) = self.get_ssh_keys {
2906 let mut paths = cb(username);
2907 paths.reverse();
2908 paths
2909 } else {
2910 vec![]
2911 }
2912 });
2913
2914 if let Some(path) = paths.pop() {
2915 tracing::info!(username, path = ?path, "trying ssh_key");
2916 return git2::Cred::ssh_key(username, None, &path, None).map_err(|err| {
2917 tracing::error!(err = %err);
2918 err
2919 });
2920 }
2921 }
2922 if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2923 if let Some(ref mut cb) = self.get_password {
2924 if let Some(pw) = cb(url, username) {
2925 tracing::info!(
2926 username,
2927 "using userpass_plaintext with username from url"
2928 );
2929 return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
2930 tracing::error!(err = %err);
2931 err
2932 });
2933 }
2934 }
2935 }
2936 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2937 if let Some(ref mut cb) = self.get_username_password {
2938 if let Some((username, pw)) = cb(url) {
2939 tracing::info!(username, "using userpass_plaintext");
2940 return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
2941 tracing::error!(err = %err);
2942 err
2943 });
2944 }
2945 }
2946 }
2947 tracing::info!("using default");
2948 git2::Cred::default()
2949 });
2950 callbacks
2951 }
2952}
2953
2954#[derive(Clone, Debug)]
2955pub struct Progress {
2956 pub bytes_downloaded: Option<u64>,
2958 pub overall: f32,
2959}