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.expect_backend_error())?
568 .iter()
569 .try_collect()
570 .map_err(|err| err.expect_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 git_config_branch_section_ids_by_remote(
1677 config: &gix::config::File,
1678 remote_name: &RemoteName,
1679) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1680 config
1681 .sections_by_name("branch")
1682 .into_iter()
1683 .flatten()
1684 .filter_map(|section| {
1685 let remote_values = section.values("remote");
1686 let push_remote_values = section.values("pushRemote");
1687 if !remote_values
1688 .iter()
1689 .chain(push_remote_values.iter())
1690 .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1691 {
1692 return None;
1693 }
1694 if remote_values.len() > 1
1695 || push_remote_values.len() > 1
1696 || section.value_names().any(|name| {
1697 !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1698 })
1699 {
1700 return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1701 remote_name.to_owned(),
1702 )));
1703 }
1704 Some(Ok(section.id()))
1705 })
1706 .collect()
1707}
1708
1709fn rename_remote_in_git_branch_config_sections(
1710 config: &mut gix::config::File,
1711 old_remote_name: &RemoteName,
1712 new_remote_name: &RemoteName,
1713) -> Result<(), GitRemoteManagementError> {
1714 for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1715 config
1716 .section_mut_by_id(id)
1717 .expect("found section to exist")
1718 .set(
1719 "remote"
1720 .try_into()
1721 .expect("'remote' to be a valid value name"),
1722 BStr::new(new_remote_name.as_str()),
1723 );
1724 }
1725 Ok(())
1726}
1727
1728fn remove_remote_git_branch_config_sections(
1729 config: &mut gix::config::File,
1730 remote_name: &RemoteName,
1731) -> Result<(), GitRemoteManagementError> {
1732 for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1733 config
1734 .remove_section_by_id(id)
1735 .expect("removed section to exist");
1736 }
1737 Ok(())
1738}
1739
1740fn remove_remote_git_config_sections(
1741 config: &mut gix::config::File,
1742 remote_name: &RemoteName,
1743) -> Result<(), GitRemoteManagementError> {
1744 let section_ids_to_remove: Vec<_> = config
1745 .sections_by_name("remote")
1746 .into_iter()
1747 .flatten()
1748 .filter(|section| {
1749 section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1750 })
1751 .map(|section| {
1752 if section.value_names().any(|name| {
1753 !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1754 }) {
1755 return Err(GitRemoteManagementError::NonstandardConfiguration(
1756 remote_name.to_owned(),
1757 ));
1758 }
1759 Ok(section.id())
1760 })
1761 .try_collect()?;
1762 for id in section_ids_to_remove {
1763 config
1764 .remove_section_by_id(id)
1765 .expect("removed section to exist");
1766 }
1767 Ok(())
1768}
1769
1770pub fn get_all_remote_names(
1772 store: &Store,
1773) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1774 let git_repo = get_git_repo(store)?;
1775 let names = git_repo
1776 .remote_names()
1777 .into_iter()
1778 .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1780 .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1782 .map(RemoteNameBuf::from)
1783 .collect();
1784 Ok(names)
1785}
1786
1787pub fn add_remote(
1788 store: &Store,
1789 remote_name: &RemoteName,
1790 url: &str,
1791) -> Result<(), GitRemoteManagementError> {
1792 let git_repo = get_git_repo(store)?;
1793
1794 validate_remote_name(remote_name)?;
1795
1796 if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1797 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1798 remote_name.to_owned(),
1799 ));
1800 }
1801
1802 let mut remote = git_repo
1803 .remote_at(url)
1804 .map_err(GitRemoteManagementError::from_git)?
1805 .with_refspecs(
1806 [default_fetch_refspec(remote_name).as_bytes()],
1807 gix::remote::Direction::Fetch,
1808 )
1809 .expect("default refspec to be valid");
1810
1811 let mut config = git_repo.config_snapshot().clone();
1812 remote
1813 .save_as_to(remote_name.as_str(), &mut config)
1814 .map_err(GitRemoteManagementError::from_git)?;
1815 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1816
1817 Ok(())
1818}
1819
1820pub fn remove_remote(
1821 mut_repo: &mut MutableRepo,
1822 remote_name: &RemoteName,
1823) -> Result<(), GitRemoteManagementError> {
1824 let mut git_repo = get_git_repo(mut_repo.store())?;
1825
1826 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1827 return Err(GitRemoteManagementError::NoSuchRemote(
1828 remote_name.to_owned(),
1829 ));
1830 };
1831
1832 let mut config = git_repo.config_snapshot().clone();
1833 remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1834 remove_remote_git_config_sections(&mut config, remote_name)?;
1835 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1836
1837 remove_remote_git_refs(&mut git_repo, remote_name)
1838 .map_err(GitRemoteManagementError::from_git)?;
1839
1840 if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1841 remove_remote_refs(mut_repo, remote_name);
1842 }
1843
1844 Ok(())
1845}
1846
1847fn remove_remote_git_refs(
1848 git_repo: &mut gix::Repository,
1849 remote: &RemoteName,
1850) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1851 let edits: Vec<_> = git_repo
1852 .references()?
1853 .prefixed(format!("refs/remotes/{remote}/", remote = remote.as_str()))?
1854 .map_ok(remove_ref)
1855 .try_collect()?;
1856 git_repo.edit_references(edits)?;
1857 Ok(())
1858}
1859
1860fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1861 mut_repo.remove_remote(remote);
1862 let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1863 let git_refs_to_delete = mut_repo
1864 .view()
1865 .git_refs()
1866 .keys()
1867 .filter(|&r| r.as_str().starts_with(&prefix))
1868 .cloned()
1869 .collect_vec();
1870 for git_ref in git_refs_to_delete {
1871 mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1872 }
1873}
1874
1875pub fn rename_remote(
1876 mut_repo: &mut MutableRepo,
1877 old_remote_name: &RemoteName,
1878 new_remote_name: &RemoteName,
1879) -> Result<(), GitRemoteManagementError> {
1880 let mut git_repo = get_git_repo(mut_repo.store())?;
1881
1882 validate_remote_name(new_remote_name)?;
1883
1884 let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1885 return Err(GitRemoteManagementError::NoSuchRemote(
1886 old_remote_name.to_owned(),
1887 ));
1888 };
1889 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1890
1891 if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1892 return Err(GitRemoteManagementError::RemoteAlreadyExists(
1893 new_remote_name.to_owned(),
1894 ));
1895 }
1896
1897 match (
1898 remote.refspecs(gix::remote::Direction::Fetch),
1899 remote.refspecs(gix::remote::Direction::Push),
1900 ) {
1901 ([refspec], [])
1902 if refspec.to_ref().to_bstring()
1903 == default_fetch_refspec(old_remote_name).as_bytes() => {}
1904 _ => {
1905 return Err(GitRemoteManagementError::NonstandardConfiguration(
1906 old_remote_name.to_owned(),
1907 ))
1908 }
1909 }
1910
1911 remote
1912 .replace_refspecs(
1913 [default_fetch_refspec(new_remote_name).as_bytes()],
1914 gix::remote::Direction::Fetch,
1915 )
1916 .expect("default refspec to be valid");
1917
1918 let mut config = git_repo.config_snapshot().clone();
1919 remote
1920 .save_as_to(new_remote_name.as_str(), &mut config)
1921 .map_err(GitRemoteManagementError::from_git)?;
1922 rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1923 remove_remote_git_config_sections(&mut config, old_remote_name)?;
1924 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1925
1926 rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1927 .map_err(GitRemoteManagementError::from_git)?;
1928
1929 if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1930 rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1931 }
1932
1933 Ok(())
1934}
1935
1936fn rename_remote_git_refs(
1937 git_repo: &mut gix::Repository,
1938 old_remote_name: &RemoteName,
1939 new_remote_name: &RemoteName,
1940) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1941 let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1942 let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1943 let ref_log_message = BString::from(format!(
1944 "renamed remote {old_remote_name} to {new_remote_name}",
1945 old_remote_name = old_remote_name.as_symbol(),
1946 new_remote_name = new_remote_name.as_symbol(),
1947 ));
1948
1949 let edits: Vec<_> = git_repo
1950 .references()?
1951 .prefixed(old_prefix.clone())?
1952 .map_ok(|old_ref| {
1953 let new_name = BString::new(
1954 [
1955 new_prefix.as_bytes(),
1956 &old_ref.name().as_bstr()[old_prefix.len()..],
1957 ]
1958 .concat(),
1959 );
1960 [
1961 add_ref(
1962 new_name.try_into().expect("new ref name to be valid"),
1963 old_ref.target().into_owned(),
1964 ref_log_message.clone(),
1965 ),
1966 remove_ref(old_ref),
1967 ]
1968 })
1969 .flatten_ok()
1970 .try_collect()?;
1971 git_repo.edit_references(edits)?;
1972 Ok(())
1973}
1974
1975fn gix_remote_with_fetch_url<Url, E>(
1981 remote: gix::Remote,
1982 url: Url,
1983) -> Result<gix::Remote, gix::remote::init::Error>
1984where
1985 Url: TryInto<gix::Url, Error = E>,
1986 gix::url::parse::Error: From<E>,
1987{
1988 let mut new_remote = remote.repo().remote_at(url)?;
1989 new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
1995 for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
1996 new_remote
1997 .replace_refspecs(
1998 remote
1999 .refspecs(direction)
2000 .iter()
2001 .map(|refspec| refspec.to_ref().to_bstring()),
2002 direction,
2003 )
2004 .expect("existing refspecs to be valid");
2005 }
2006 Ok(new_remote)
2007}
2008
2009pub fn set_remote_url(
2010 store: &Store,
2011 remote_name: &RemoteName,
2012 new_remote_url: &str,
2013) -> Result<(), GitRemoteManagementError> {
2014 let git_repo = get_git_repo(store)?;
2015
2016 validate_remote_name(remote_name)?;
2017
2018 let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2019 return Err(GitRemoteManagementError::NoSuchRemote(
2020 remote_name.to_owned(),
2021 ));
2022 };
2023 let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2024
2025 if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2026 return Err(GitRemoteManagementError::NonstandardConfiguration(
2027 remote_name.to_owned(),
2028 ));
2029 }
2030
2031 remote = gix_remote_with_fetch_url(remote, new_remote_url)
2032 .map_err(GitRemoteManagementError::from_git)?;
2033
2034 let mut config = git_repo.config_snapshot().clone();
2035 remote
2036 .save_as_to(remote_name.as_str(), &mut config)
2037 .map_err(GitRemoteManagementError::from_git)?;
2038 save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2039
2040 Ok(())
2041}
2042
2043fn rename_remote_refs(
2044 mut_repo: &mut MutableRepo,
2045 old_remote_name: &RemoteName,
2046 new_remote_name: &RemoteName,
2047) {
2048 mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2049 let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2050 let git_refs = mut_repo
2051 .view()
2052 .git_refs()
2053 .iter()
2054 .filter_map(|(old, target)| {
2055 old.as_str().strip_prefix(&prefix).map(|p| {
2056 let new: GitRefNameBuf =
2057 format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2058 (old.clone(), new, target.clone())
2059 })
2060 })
2061 .collect_vec();
2062 for (old, new, target) in git_refs {
2063 mut_repo.set_git_ref_target(&old, RefTarget::absent());
2064 mut_repo.set_git_ref_target(&new, target);
2065 }
2066}
2067
2068const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2069
2070#[derive(Error, Debug)]
2071pub enum GitFetchError {
2072 #[error("No git remote named '{}'", .0.as_symbol())]
2073 NoSuchRemote(RemoteNameBuf),
2074 #[error(
2075 "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2076 chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2077 )]
2078 InvalidBranchPattern(StringPattern),
2079 #[error(transparent)]
2080 RemoteName(#[from] GitRemoteNameError),
2081 #[cfg(feature = "git2")]
2082 #[error(transparent)]
2083 Git2(#[from] git2::Error),
2084 #[error(transparent)]
2085 Subprocess(#[from] GitSubprocessError),
2086}
2087
2088#[derive(Debug, Error)]
2091pub enum GitFetchPrepareError {
2092 #[cfg(feature = "git2")]
2093 #[error(transparent)]
2094 Git2(#[from] git2::Error),
2095 #[error(transparent)]
2096 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2097}
2098
2099#[cfg(feature = "git2")]
2100fn git2_fetch_options(
2101 mut callbacks: RemoteCallbacks<'_>,
2102 depth: Option<NonZeroU32>,
2103) -> git2::FetchOptions<'_> {
2104 let mut proxy_options = git2::ProxyOptions::new();
2105 proxy_options.auto();
2106
2107 let mut fetch_options = git2::FetchOptions::new();
2108 fetch_options.proxy_options(proxy_options);
2109 if callbacks.progress.is_none() {
2113 callbacks.sideband_progress = None;
2114 }
2115 fetch_options.remote_callbacks(callbacks.into_git());
2116 if let Some(depth) = depth {
2117 fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX));
2118 }
2119
2120 fetch_options
2121}
2122
2123struct FetchedBranches {
2124 remote: RemoteNameBuf,
2125 branches: Vec<StringPattern>,
2126}
2127
2128pub struct GitFetch<'a> {
2130 mut_repo: &'a mut MutableRepo,
2131 fetch_impl: GitFetchImpl<'a>,
2132 git_settings: &'a GitSettings,
2133 fetched: Vec<FetchedBranches>,
2134}
2135
2136impl<'a> GitFetch<'a> {
2137 pub fn new(
2138 mut_repo: &'a mut MutableRepo,
2139 git_settings: &'a GitSettings,
2140 ) -> Result<Self, GitFetchPrepareError> {
2141 let fetch_impl = GitFetchImpl::new(mut_repo.store(), git_settings)?;
2142 Ok(GitFetch {
2143 mut_repo,
2144 fetch_impl,
2145 git_settings,
2146 fetched: vec![],
2147 })
2148 }
2149
2150 #[tracing::instrument(skip(self, callbacks))]
2156 pub fn fetch(
2157 &mut self,
2158 remote_name: &RemoteName,
2159 branch_names: &[StringPattern],
2160 callbacks: RemoteCallbacks<'_>,
2161 depth: Option<NonZeroU32>,
2162 ) -> Result<(), GitFetchError> {
2163 validate_remote_name(remote_name)?;
2164 self.fetch_impl
2165 .fetch(remote_name, branch_names, callbacks, depth)?;
2166 self.fetched.push(FetchedBranches {
2167 remote: remote_name.to_owned(),
2168 branches: branch_names.to_vec(),
2169 });
2170 Ok(())
2171 }
2172
2173 #[tracing::instrument(skip(self, callbacks))]
2175 pub fn get_default_branch(
2176 &self,
2177 remote_name: &RemoteName,
2178 callbacks: RemoteCallbacks<'_>,
2179 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2180 self.fetch_impl.get_default_branch(remote_name, callbacks)
2181 }
2182
2183 #[tracing::instrument(skip(self))]
2191 pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2192 tracing::debug!("import_refs");
2193 let import_stats =
2194 import_some_refs(
2195 self.mut_repo,
2196 self.git_settings,
2197 |kind, symbol| match kind {
2198 GitRefKind::Bookmark => self
2199 .fetched
2200 .iter()
2201 .filter(|fetched| fetched.remote == symbol.remote)
2202 .any(|fetched| {
2203 fetched
2204 .branches
2205 .iter()
2206 .any(|pattern| pattern.matches(symbol.name.as_str()))
2207 }),
2208 GitRefKind::Tag => true,
2209 },
2210 )?;
2211
2212 self.fetched.clear();
2213
2214 Ok(import_stats)
2215 }
2216}
2217
2218fn expand_fetch_refspecs(
2219 remote: &RemoteName,
2220 branch_names: &[StringPattern],
2221) -> Result<Vec<RefSpec>, GitFetchError> {
2222 branch_names
2223 .iter()
2224 .map(|pattern| {
2225 pattern
2226 .to_glob()
2227 .filter(
2228 |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2231 )
2232 .map(|glob| {
2233 RefSpec::forced(
2234 format!("refs/heads/{glob}"),
2235 format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2236 )
2237 })
2238 .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2239 })
2240 .collect()
2241}
2242
2243enum GitFetchImpl<'a> {
2244 #[cfg(feature = "git2")]
2245 Git2 { git_repo: git2::Repository },
2246 Subprocess {
2247 git_repo: Box<gix::Repository>,
2248 git_ctx: GitSubprocessContext<'a>,
2249 },
2250}
2251
2252impl<'a> GitFetchImpl<'a> {
2253 fn new(store: &Store, git_settings: &'a GitSettings) -> Result<Self, GitFetchPrepareError> {
2254 let git_backend = get_git_backend(store)?;
2255 #[cfg(feature = "git2")]
2256 if !git_settings.subprocess {
2257 let git_repo = git2::Repository::open(git_backend.git_repo_path())?;
2258 return Ok(GitFetchImpl::Git2 { git_repo });
2259 }
2260 let git_repo = Box::new(git_backend.git_repo());
2261 let git_ctx =
2262 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2263 Ok(GitFetchImpl::Subprocess { git_repo, git_ctx })
2264 }
2265
2266 fn fetch(
2267 &self,
2268 remote_name: &RemoteName,
2269 branch_names: &[StringPattern],
2270 callbacks: RemoteCallbacks<'_>,
2271 depth: Option<NonZeroU32>,
2272 ) -> Result<(), GitFetchError> {
2273 match self {
2274 #[cfg(feature = "git2")]
2275 GitFetchImpl::Git2 { git_repo } => {
2276 git2_fetch(git_repo, remote_name, branch_names, callbacks, depth)
2277 }
2278 GitFetchImpl::Subprocess { git_repo, git_ctx } => subprocess_fetch(
2279 git_repo,
2280 git_ctx,
2281 remote_name,
2282 branch_names,
2283 callbacks,
2284 depth,
2285 ),
2286 }
2287 }
2288
2289 fn get_default_branch(
2290 &self,
2291 remote_name: &RemoteName,
2292 callbacks: RemoteCallbacks<'_>,
2293 ) -> Result<Option<RefNameBuf>, GitFetchError> {
2294 match self {
2295 #[cfg(feature = "git2")]
2296 GitFetchImpl::Git2 { git_repo } => {
2297 git2_get_default_branch(git_repo, remote_name, callbacks)
2298 }
2299 GitFetchImpl::Subprocess { git_repo, git_ctx } => {
2300 subprocess_get_default_branch(git_repo, git_ctx, remote_name, callbacks)
2301 }
2302 }
2303 }
2304}
2305
2306#[cfg(feature = "git2")]
2307fn git2_fetch(
2308 git_repo: &git2::Repository,
2309 remote_name: &RemoteName,
2310 branch_names: &[StringPattern],
2311 callbacks: RemoteCallbacks<'_>,
2312 depth: Option<NonZeroU32>,
2313) -> Result<(), GitFetchError> {
2314 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2315 if is_remote_not_found_err(&err) {
2316 GitFetchError::NoSuchRemote(remote_name.to_owned())
2317 } else {
2318 GitFetchError::Git2(err)
2319 }
2320 })?;
2321 let refspecs: Vec<String> = expand_fetch_refspecs(remote_name, branch_names)?
2324 .iter()
2325 .map(|refspec| refspec.to_git_format())
2326 .collect();
2327
2328 if refspecs.is_empty() {
2329 return Ok(());
2331 }
2332
2333 tracing::debug!("remote.download");
2334 remote.download(&refspecs, Some(&mut git2_fetch_options(callbacks, depth)))?;
2335 tracing::debug!("remote.prune");
2336 remote.prune(None)?;
2337 tracing::debug!("remote.update_tips");
2338 remote.update_tips(
2339 None,
2340 git2::RemoteUpdateFlags::empty(),
2341 git2::AutotagOption::Unspecified,
2342 None,
2343 )?;
2344 tracing::debug!("remote.disconnect");
2345 remote.disconnect()?;
2346 Ok(())
2347}
2348
2349#[cfg(feature = "git2")]
2350fn git2_get_default_branch(
2351 git_repo: &git2::Repository,
2352 remote_name: &RemoteName,
2353 callbacks: RemoteCallbacks<'_>,
2354) -> Result<Option<RefNameBuf>, GitFetchError> {
2355 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2356 if is_remote_not_found_err(&err) {
2357 GitFetchError::NoSuchRemote(remote_name.to_owned())
2358 } else {
2359 GitFetchError::Git2(err)
2360 }
2361 })?;
2362 tracing::debug!("remote.connect");
2364 let connection = {
2365 let mut proxy_options = git2::ProxyOptions::new();
2366 proxy_options.auto();
2367 remote.connect_auth(
2368 git2::Direction::Fetch,
2369 Some(callbacks.into_git()),
2370 Some(proxy_options),
2371 )?
2372 };
2373 let mut default_branch = None;
2374 tracing::debug!("remote.default_branch");
2375 if let Ok(default_ref_buf) = connection.default_branch() {
2376 if let Some(default_ref) = default_ref_buf.as_str() {
2377 if let Some(branch_name) = default_ref
2379 .strip_prefix("refs/heads/")
2380 .filter(|&name| name != "HEAD")
2381 {
2382 tracing::debug!(default_branch = branch_name);
2383 default_branch = Some(branch_name.into());
2384 }
2385 }
2386 }
2387 Ok(default_branch)
2388}
2389
2390fn subprocess_fetch(
2391 git_repo: &gix::Repository,
2392 git_ctx: &GitSubprocessContext,
2393 remote_name: &RemoteName,
2394 branch_names: &[StringPattern],
2395 mut callbacks: RemoteCallbacks<'_>,
2396 depth: Option<NonZeroU32>,
2397) -> Result<(), GitFetchError> {
2398 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2400 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2401 }
2402 let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2405 if remaining_refspecs.is_empty() {
2406 return Ok(());
2408 }
2409
2410 let mut branches_to_prune = Vec::new();
2411 while let Some(failing_refspec) =
2419 git_ctx.spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2420 {
2421 tracing::debug!(failing_refspec, "failed to fetch ref");
2422 remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2423
2424 if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2425 branches_to_prune.push(format!(
2426 "{remote_name}/{branch_name}",
2427 remote_name = remote_name.as_str()
2428 ));
2429 }
2430 }
2431
2432 git_ctx.spawn_branch_prune(&branches_to_prune)?;
2435 Ok(())
2436}
2437
2438fn subprocess_get_default_branch(
2439 git_repo: &gix::Repository,
2440 git_ctx: &GitSubprocessContext,
2441 remote_name: &RemoteName,
2442 _callbacks: RemoteCallbacks<'_>,
2443) -> Result<Option<RefNameBuf>, GitFetchError> {
2444 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2445 return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2446 }
2447 let default_branch = git_ctx.spawn_remote_show(remote_name)?;
2448 tracing::debug!(?default_branch);
2449 Ok(default_branch)
2450}
2451
2452#[derive(Error, Debug)]
2453pub enum GitPushError {
2454 #[error("No git remote named '{}'", .0.as_symbol())]
2455 NoSuchRemote(RemoteNameBuf),
2456 #[error(transparent)]
2457 RemoteName(#[from] GitRemoteNameError),
2458 #[cfg(feature = "git2")]
2459 #[error(transparent)]
2460 Git2(#[from] git2::Error),
2461 #[error(transparent)]
2462 Subprocess(#[from] GitSubprocessError),
2463 #[error(transparent)]
2464 UnexpectedBackend(#[from] UnexpectedGitBackendError),
2465}
2466
2467#[derive(Clone, Debug)]
2468pub struct GitBranchPushTargets {
2469 pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2470}
2471
2472pub struct GitRefUpdate {
2473 pub qualified_name: GitRefNameBuf,
2474 pub expected_current_target: Option<CommitId>,
2479 pub new_target: Option<CommitId>,
2480}
2481
2482pub fn push_branches(
2484 mut_repo: &mut MutableRepo,
2485 git_settings: &GitSettings,
2486 remote: &RemoteName,
2487 targets: &GitBranchPushTargets,
2488 callbacks: RemoteCallbacks<'_>,
2489) -> Result<GitPushStats, GitPushError> {
2490 validate_remote_name(remote)?;
2491
2492 let ref_updates = targets
2493 .branch_updates
2494 .iter()
2495 .map(|(name, update)| GitRefUpdate {
2496 qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2497 expected_current_target: update.old_target.clone(),
2498 new_target: update.new_target.clone(),
2499 })
2500 .collect_vec();
2501
2502 let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2503 tracing::debug!(?push_stats);
2504
2505 if push_stats.all_ok() {
2509 for (name, update) in &targets.branch_updates {
2510 let git_ref_name: GitRefNameBuf = format!(
2511 "refs/remotes/{remote}/{name}",
2512 remote = remote.as_str(),
2513 name = name.as_str()
2514 )
2515 .into();
2516 let new_remote_ref = RemoteRef {
2517 target: RefTarget::resolved(update.new_target.clone()),
2518 state: RemoteRefState::Tracked,
2519 };
2520 mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2521 mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2522 }
2523 }
2524
2525 Ok(push_stats)
2526}
2527
2528pub fn push_updates(
2530 repo: &dyn Repo,
2531 git_settings: &GitSettings,
2532 remote_name: &RemoteName,
2533 updates: &[GitRefUpdate],
2534 callbacks: RemoteCallbacks<'_>,
2535) -> Result<GitPushStats, GitPushError> {
2536 let mut qualified_remote_refs_expected_locations = HashMap::new();
2537 let mut refspecs = vec![];
2538 for update in updates {
2539 qualified_remote_refs_expected_locations.insert(
2540 update.qualified_name.as_ref(),
2541 update.expected_current_target.as_ref(),
2542 );
2543 if let Some(new_target) = &update.new_target {
2544 refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2548 } else {
2549 refspecs.push(RefSpec::delete(&update.qualified_name));
2553 }
2554 }
2555 let git_backend = get_git_backend(repo.store())?;
2559 #[cfg(feature = "git2")]
2560 if !git_settings.subprocess {
2561 let git_repo = git2::Repository::open(git_backend.git_repo_path())?;
2562 let refspecs: Vec<String> = refspecs.iter().map(RefSpec::to_git_format).collect();
2563 return git2_push_refs(
2564 repo,
2565 &git_repo,
2566 remote_name,
2567 &qualified_remote_refs_expected_locations,
2568 &refspecs,
2569 callbacks,
2570 );
2571 }
2572 let git_repo = git_backend.git_repo();
2573 let git_ctx =
2574 GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2575 subprocess_push_refs(
2576 &git_repo,
2577 &git_ctx,
2578 remote_name,
2579 &qualified_remote_refs_expected_locations,
2580 &refspecs,
2581 callbacks,
2582 )
2583}
2584
2585#[cfg(feature = "git2")]
2586fn git2_push_refs(
2587 repo: &dyn Repo,
2588 git_repo: &git2::Repository,
2589 remote_name: &RemoteName,
2590 qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2591 refspecs: &[String],
2592 callbacks: RemoteCallbacks<'_>,
2593) -> Result<GitPushStats, GitPushError> {
2594 let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2595 if is_remote_not_found_err(&err) {
2596 GitPushError::NoSuchRemote(remote_name.to_owned())
2597 } else {
2598 GitPushError::Git2(err)
2599 }
2600 })?;
2601
2602 let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs_expected_locations
2603 .keys()
2604 .copied()
2605 .collect();
2606 let mut failed_push_negotiations = vec![];
2607 let mut pushed_refs = vec![];
2608
2609 let push_result = {
2610 let mut push_options = git2::PushOptions::new();
2611 let mut proxy_options = git2::ProxyOptions::new();
2612 proxy_options.auto();
2613 push_options.proxy_options(proxy_options);
2614 let mut callbacks = callbacks.into_git();
2615 callbacks.push_negotiation(|updates| {
2616 for update in updates {
2617 let dst_refname: &GitRefName = update
2618 .dst_refname()
2619 .expect("Expect reference name to be valid UTF-8")
2620 .as_ref();
2621 let expected_remote_location = *qualified_remote_refs_expected_locations
2622 .get(dst_refname)
2623 .expect("Push is trying to move a ref it wasn't asked to move");
2624 let oid_to_maybe_commitid =
2625 |oid: git2::Oid| (!oid.is_zero()).then(|| CommitId::from_bytes(oid.as_bytes()));
2626 let actual_remote_location = oid_to_maybe_commitid(update.src());
2627 let local_location = oid_to_maybe_commitid(update.dst());
2628
2629 match allow_push(
2630 repo.index(),
2631 actual_remote_location.as_ref(),
2632 expected_remote_location,
2633 local_location.as_ref(),
2634 ) {
2635 Ok(PushAllowReason::NormalMatch) => {}
2636 Ok(PushAllowReason::UnexpectedNoop) => {
2637 tracing::info!(
2638 "The push of {dst_refname:?} is unexpectedly a no-op, the remote \
2639 branch is already at {actual_remote_location:?}. We expected it to \
2640 be at {expected_remote_location:?}. We don't consider this an error.",
2641 );
2642 }
2643 Ok(PushAllowReason::ExceptionalFastforward) => {
2644 tracing::info!(
2647 "We allow the push of {dst_refname:?} to {local_location:?}, even \
2648 though it is unexpectedly at {actual_remote_location:?} on the \
2649 server rather than the expected {expected_remote_location:?}. The \
2650 desired location is a descendant of the actual location, and the \
2651 actual location is a descendant of the expected location.",
2652 );
2653 }
2654 Err(()) => {
2655 tracing::info!(
2661 "Cannot push {dst_refname:?} to {local_location:?}; it is at \
2662 unexpectedly at {actual_remote_location:?} on the server as opposed \
2663 to the expected {expected_remote_location:?}",
2664 );
2665 failed_push_negotiations.push(dst_refname.to_owned());
2666 }
2667 }
2668 }
2669
2670 if failed_push_negotiations.is_empty() {
2671 Ok(())
2672 } else {
2673 Err(git2::Error::from_str("failed push negotiation"))
2674 }
2675 });
2676 callbacks.push_update_reference(|refname, status| {
2677 let refname = GitRefName::new(refname);
2678 if status.is_none() {
2680 remaining_remote_refs.remove(refname);
2681 pushed_refs.push(refname.to_owned());
2682 }
2683 Ok(())
2684 });
2685 push_options.remote_callbacks(callbacks);
2686 remote.push(refspecs, Some(&mut push_options))
2687 };
2688
2689 for failed_update in &failed_push_negotiations {
2690 remaining_remote_refs.remove(&**failed_update);
2691 }
2692 let rejected: Vec<_> = failed_push_negotiations
2693 .into_iter()
2694 .sorted()
2695 .map(|name| (name, None))
2696 .collect();
2697 let remote_rejected: Vec<_> = remaining_remote_refs
2698 .into_iter()
2699 .sorted()
2700 .map(|name| (name.to_owned(), None))
2701 .collect();
2702 pushed_refs.sort();
2703
2704 let push_stats = if !rejected.is_empty() {
2705 assert!(push_result.is_err());
2713 GitPushStats {
2714 rejected,
2715 remote_rejected,
2716 ..Default::default()
2717 }
2718 } else {
2719 push_result?;
2720 GitPushStats {
2721 pushed: pushed_refs,
2722 remote_rejected,
2723 ..Default::default()
2724 }
2725 };
2726
2727 Ok(push_stats)
2728}
2729
2730fn subprocess_push_refs(
2731 git_repo: &gix::Repository,
2732 git_ctx: &GitSubprocessContext,
2733 remote_name: &RemoteName,
2734 qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2735 refspecs: &[RefSpec],
2736 mut callbacks: RemoteCallbacks<'_>,
2737) -> Result<GitPushStats, GitPushError> {
2738 if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2740 return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2741 }
2742
2743 let refs_to_push: Vec<RefToPush> = refspecs
2744 .iter()
2745 .map(|full_refspec| RefToPush::new(full_refspec, qualified_remote_refs_expected_locations))
2746 .collect();
2747
2748 let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2749 push_stats.pushed.sort();
2750 push_stats.rejected.sort();
2751 push_stats.remote_rejected.sort();
2752 Ok(push_stats)
2753}
2754
2755#[cfg(feature = "git2")]
2756#[derive(Debug, Clone, PartialEq, Eq)]
2757enum PushAllowReason {
2758 NormalMatch,
2759 ExceptionalFastforward,
2760 UnexpectedNoop,
2761}
2762
2763#[cfg(feature = "git2")]
2764fn allow_push(
2765 index: &dyn Index,
2766 actual_remote_location: Option<&CommitId>,
2767 expected_remote_location: Option<&CommitId>,
2768 destination_location: Option<&CommitId>,
2769) -> Result<PushAllowReason, ()> {
2770 if actual_remote_location == expected_remote_location {
2771 return Ok(PushAllowReason::NormalMatch);
2772 }
2773
2774 if !actual_remote_location.is_none_or(|id| index.has_id(id)) {
2786 return Err(());
2787 }
2788 let remote_target = RefTarget::resolved(actual_remote_location.cloned());
2789 let base_target = RefTarget::resolved(expected_remote_location.cloned());
2790 let local_target = RefTarget::resolved(destination_location.cloned());
2792 if refs::merge_ref_targets(index, &remote_target, &base_target, &local_target) == local_target {
2793 Ok(if actual_remote_location == destination_location {
2797 PushAllowReason::UnexpectedNoop
2801 } else {
2802 PushAllowReason::ExceptionalFastforward
2809 })
2810 } else {
2811 Err(())
2812 }
2813}
2814
2815#[non_exhaustive]
2816#[derive(Default)]
2817#[expect(clippy::type_complexity)]
2818pub struct RemoteCallbacks<'a> {
2819 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2820 pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2821 pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2822 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2823 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2824}
2825
2826#[cfg(feature = "git2")]
2827impl<'a> RemoteCallbacks<'a> {
2828 fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
2829 let mut callbacks = git2::RemoteCallbacks::new();
2830 if let Some(progress_cb) = self.progress {
2831 callbacks.transfer_progress(move |progress| {
2832 progress_cb(&Progress {
2833 bytes_downloaded: (progress.received_objects() < progress.total_objects())
2834 .then(|| progress.received_bytes() as u64),
2835 overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
2836 / (progress.total_objects() + progress.total_deltas()) as f32,
2837 });
2838 true
2839 });
2840 }
2841 if let Some(sideband_progress_cb) = self.sideband_progress {
2842 callbacks.sideband_progress(move |data| {
2843 sideband_progress_cb(data);
2844 true
2845 });
2846 }
2847 let mut tried_ssh_agent = false;
2850 let mut ssh_key_paths_to_try: Option<Vec<PathBuf>> = None;
2851 callbacks.credentials(move |url, username_from_url, allowed_types| {
2852 let span = tracing::debug_span!("RemoteCallbacks.credentials");
2853 let _ = span.enter();
2854
2855 let git_config = git2::Config::open_default();
2856 let credential_helper = git_config
2857 .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
2858 if let Ok(creds) = credential_helper {
2859 tracing::info!("using credential_helper");
2860 return Ok(creds);
2861 } else if let Some(username) = username_from_url {
2862 if allowed_types.contains(git2::CredentialType::SSH_KEY) {
2863 if !tried_ssh_agent {
2866 tracing::info!(username, "trying ssh_key_from_agent");
2867 tried_ssh_agent = true;
2868 return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
2869 tracing::error!(err = %err);
2870 err
2871 });
2872 }
2873
2874 let paths = ssh_key_paths_to_try.get_or_insert_with(|| {
2875 if let Some(ref mut cb) = self.get_ssh_keys {
2876 let mut paths = cb(username);
2877 paths.reverse();
2878 paths
2879 } else {
2880 vec![]
2881 }
2882 });
2883
2884 if let Some(path) = paths.pop() {
2885 tracing::info!(username, path = ?path, "trying ssh_key");
2886 return git2::Cred::ssh_key(username, None, &path, None).map_err(|err| {
2887 tracing::error!(err = %err);
2888 err
2889 });
2890 }
2891 }
2892 if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2893 if let Some(ref mut cb) = self.get_password {
2894 if let Some(pw) = cb(url, username) {
2895 tracing::info!(
2896 username,
2897 "using userpass_plaintext with username from url"
2898 );
2899 return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
2900 tracing::error!(err = %err);
2901 err
2902 });
2903 }
2904 }
2905 }
2906 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2907 if let Some(ref mut cb) = self.get_username_password {
2908 if let Some((username, pw)) = cb(url) {
2909 tracing::info!(username, "using userpass_plaintext");
2910 return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
2911 tracing::error!(err = %err);
2912 err
2913 });
2914 }
2915 }
2916 }
2917 tracing::info!("using default");
2918 git2::Cred::default()
2919 });
2920 callbacks
2921 }
2922}
2923
2924#[derive(Clone, Debug)]
2925pub struct Progress {
2926 pub bytes_downloaded: Option<u64>,
2928 pub overall: f32,
2929}