jj_lib/
git.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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;
45use crate::matchers::EverythingMatcher;
46use crate::merged_tree::MergedTree;
47use crate::merged_tree::TreeDiffEntry;
48use crate::object_id::ObjectId as _;
49use crate::op_store::RefTarget;
50use crate::op_store::RefTargetOptionExt as _;
51use crate::op_store::RemoteRef;
52use crate::op_store::RemoteRefState;
53use crate::ref_name::GitRefName;
54use crate::ref_name::GitRefNameBuf;
55use crate::ref_name::RefName;
56use crate::ref_name::RefNameBuf;
57use crate::ref_name::RemoteName;
58use crate::ref_name::RemoteNameBuf;
59use crate::ref_name::RemoteRefSymbol;
60use crate::ref_name::RemoteRefSymbolBuf;
61use crate::refs::BookmarkPushUpdate;
62use crate::repo::MutableRepo;
63use crate::repo::Repo;
64use crate::repo_path::RepoPath;
65use crate::revset::RevsetExpression;
66use crate::settings::GitSettings;
67use crate::store::Store;
68use crate::str_util::StringPattern;
69use crate::view::View;
70
71/// Reserved remote name for the backing Git repo.
72pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &RemoteName = RemoteName::new("git");
73/// Git ref prefix that would conflict with the reserved "git" remote.
74pub const RESERVED_REMOTE_REF_NAMESPACE: &str = "refs/remotes/git/";
75/// Ref name used as a placeholder to unset HEAD without a commit.
76const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
77/// Dummy file to be added to the index to indicate that the user is editing a
78/// commit with a conflict that isn't represented in the Git index.
79const INDEX_DUMMY_CONFLICT_FILE: &str = ".jj-do-not-resolve-this-conflict";
80
81#[derive(Debug, Error)]
82pub enum GitRemoteNameError {
83    #[error(
84        "Git remote named '{name}' is reserved for local Git repository",
85        name = REMOTE_NAME_FOR_LOCAL_GIT_REPO.as_symbol()
86    )]
87    ReservedForLocalGitRepo,
88    #[error("Git remotes with slashes are incompatible with jj: {}", .0.as_symbol())]
89    WithSlash(RemoteNameBuf),
90}
91
92fn validate_remote_name(name: &RemoteName) -> Result<(), GitRemoteNameError> {
93    if name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
94        Err(GitRemoteNameError::ReservedForLocalGitRepo)
95    } else if name.as_str().contains("/") {
96        Err(GitRemoteNameError::WithSlash(name.to_owned()))
97    } else {
98        Ok(())
99    }
100}
101
102/// Type of Git ref to be imported or exported.
103#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub enum GitRefKind {
105    Bookmark,
106    Tag,
107}
108
109/// Stats from a git push
110#[derive(Clone, Debug, Default, Eq, PartialEq)]
111pub struct GitPushStats {
112    /// reference accepted by the remote
113    pub pushed: Vec<GitRefNameBuf>,
114    /// rejected reference, due to lease failure, with an optional reason
115    pub rejected: Vec<(GitRefNameBuf, Option<String>)>,
116    /// reference rejected by the remote, with an optional reason
117    pub remote_rejected: Vec<(GitRefNameBuf, Option<String>)>,
118}
119
120impl GitPushStats {
121    pub fn all_ok(&self) -> bool {
122        self.rejected.is_empty() && self.remote_rejected.is_empty()
123    }
124}
125
126/// Newtype to look up `HashMap` entry by key of shorter lifetime.
127///
128/// https://users.rust-lang.org/t/unexpected-lifetime-issue-with-hashmap-remove/113961/6
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130struct RemoteRefKey<'a>(RemoteRefSymbol<'a>);
131
132impl<'a: 'b, 'b> Borrow<RemoteRefSymbol<'b>> for RemoteRefKey<'a> {
133    fn borrow(&self) -> &RemoteRefSymbol<'b> {
134        &self.0
135    }
136}
137
138/// Representation of a Git refspec
139///
140/// It is often the case that we need only parts of the refspec,
141/// Passing strings around and repeatedly parsing them is sub-optimal, confusing
142/// and error prone
143#[derive(Debug, Hash, PartialEq, Eq)]
144pub(crate) struct RefSpec {
145    forced: bool,
146    // Source and destination may be fully-qualified ref name, glob pattern, or
147    // object ID. The GitRefNameBuf type shouldn't be used.
148    source: Option<String>,
149    destination: String,
150}
151
152impl RefSpec {
153    fn forced(source: impl Into<String>, destination: impl Into<String>) -> Self {
154        RefSpec {
155            forced: true,
156            source: Some(source.into()),
157            destination: destination.into(),
158        }
159    }
160
161    fn delete(destination: impl Into<String>) -> Self {
162        // We don't force push on branch deletion
163        RefSpec {
164            forced: false,
165            source: None,
166            destination: destination.into(),
167        }
168    }
169
170    pub(crate) fn to_git_format(&self) -> String {
171        format!(
172            "{}{}",
173            if self.forced { "+" } else { "" },
174            self.to_git_format_not_forced()
175        )
176    }
177
178    /// Format git refspec without the leading force flag '+'
179    ///
180    /// When independently setting --force-with-lease, having the
181    /// leading flag overrides the lease, so we need to print it
182    /// without it
183    pub(crate) fn to_git_format_not_forced(&self) -> String {
184        if let Some(s) = &self.source {
185            format!("{}:{}", s, self.destination)
186        } else {
187            format!(":{}", self.destination)
188        }
189    }
190}
191
192/// Helper struct that matches a refspec with its expected location in the
193/// remote it's being pushed to
194pub(crate) struct RefToPush<'a> {
195    pub(crate) refspec: &'a RefSpec,
196    pub(crate) expected_location: Option<&'a CommitId>,
197}
198
199impl<'a> RefToPush<'a> {
200    fn new(
201        refspec: &'a RefSpec,
202        expected_locations: &'a HashMap<&GitRefName, Option<&CommitId>>,
203    ) -> Self {
204        let expected_location = *expected_locations
205            .get(GitRefName::new(&refspec.destination))
206            .expect(
207                "The refspecs and the expected locations were both constructed from the same \
208                 source of truth. This means the lookup should always work.",
209            );
210
211        RefToPush {
212            refspec,
213            expected_location,
214        }
215    }
216
217    pub(crate) fn to_git_lease(&self) -> String {
218        format!(
219            "{}:{}",
220            self.refspec.destination,
221            self.expected_location
222                .map(|x| x.to_string())
223                .as_deref()
224                .unwrap_or("")
225        )
226    }
227}
228
229/// Translates Git ref name to jj's `name@remote` symbol. Returns `None` if the
230/// ref cannot be represented in jj.
231pub fn parse_git_ref(full_name: &GitRefName) -> Option<(GitRefKind, RemoteRefSymbol<'_>)> {
232    if let Some(name) = full_name.as_str().strip_prefix("refs/heads/") {
233        // Git CLI says 'HEAD' is not a valid branch name
234        if name == "HEAD" {
235            return None;
236        }
237        let name = RefName::new(name);
238        let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
239        Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
240    } else if let Some(remote_and_name) = full_name.as_str().strip_prefix("refs/remotes/") {
241        let (remote, name) = remote_and_name.split_once('/')?;
242        // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch
243        if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || name == "HEAD" {
244            return None;
245        }
246        let name = RefName::new(name);
247        let remote = RemoteName::new(remote);
248        Some((GitRefKind::Bookmark, RemoteRefSymbol { name, remote }))
249    } else if let Some(name) = full_name.as_str().strip_prefix("refs/tags/") {
250        let name = RefName::new(name);
251        let remote = REMOTE_NAME_FOR_LOCAL_GIT_REPO;
252        Some((GitRefKind::Tag, RemoteRefSymbol { name, remote }))
253    } else {
254        None
255    }
256}
257
258fn to_git_ref_name(kind: GitRefKind, symbol: RemoteRefSymbol<'_>) -> Option<GitRefNameBuf> {
259    let RemoteRefSymbol { name, remote } = symbol;
260    let name = name.as_str();
261    let remote = remote.as_str();
262    if name.is_empty() || remote.is_empty() {
263        return None;
264    }
265    match kind {
266        GitRefKind::Bookmark => {
267            if name == "HEAD" {
268                return None;
269            }
270            if remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
271                Some(format!("refs/heads/{name}").into())
272            } else {
273                Some(format!("refs/remotes/{remote}/{name}").into())
274            }
275        }
276        GitRefKind::Tag => {
277            (remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO).then(|| format!("refs/tags/{name}").into())
278        }
279    }
280}
281
282#[derive(Debug, Error)]
283#[error("The repo is not backed by a Git repo")]
284pub struct UnexpectedGitBackendError;
285
286/// Returns the underlying `GitBackend` implementation.
287pub fn get_git_backend(store: &Store) -> Result<&GitBackend, UnexpectedGitBackendError> {
288    store
289        .backend_impl()
290        .downcast_ref()
291        .ok_or(UnexpectedGitBackendError)
292}
293
294/// Returns new thread-local instance to access to the underlying Git repo.
295pub fn get_git_repo(store: &Store) -> Result<gix::Repository, UnexpectedGitBackendError> {
296    get_git_backend(store).map(|backend| backend.git_repo())
297}
298
299/// Checks if `git_ref` points to a Git commit object, and returns its id.
300///
301/// If the ref points to the previously `known_target` (i.e. unchanged), this
302/// should be faster than `git_ref.into_fully_peeled_id()`.
303fn resolve_git_ref_to_commit_id(
304    git_ref: &gix::Reference,
305    known_target: &RefTarget,
306) -> Option<CommitId> {
307    let mut peeling_ref = Cow::Borrowed(git_ref);
308
309    // Try fast path if we have a candidate id which is known to be a commit object.
310    if let Some(id) = known_target.as_normal() {
311        let raw_ref = &git_ref.inner;
312        if matches!(raw_ref.target.try_id(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
313            return Some(id.clone());
314        }
315        if matches!(raw_ref.peeled, Some(oid) if oid.as_bytes() == id.as_bytes()) {
316            // Perhaps an annotated tag stored in packed-refs file, and pointing to the
317            // already known target commit.
318            return Some(id.clone());
319        }
320        // A tag (according to ref name.) Try to peel one more level. This is slightly
321        // faster than recurse into into_fully_peeled_id(). If we recorded a tag oid, we
322        // could skip this at all.
323        if raw_ref.peeled.is_none() && git_ref.name().as_bstr().starts_with(b"refs/tags/") {
324            let maybe_tag = git_ref
325                .try_id()
326                .and_then(|id| id.object().ok())
327                .and_then(|object| object.try_into_tag().ok());
328            if let Some(oid) = maybe_tag.as_ref().and_then(|tag| tag.target_id().ok()) {
329                if oid.as_bytes() == id.as_bytes() {
330                    // An annotated tag pointing to the already known target commit.
331                    return Some(id.clone());
332                }
333                // Unknown id. Recurse from the current state. A tag may point to
334                // non-commit object.
335                peeling_ref.to_mut().inner.target = gix::refs::Target::Object(oid.detach());
336            }
337        }
338    }
339
340    // Alternatively, we might want to inline the first half of the peeling
341    // loop. into_fully_peeled_id() looks up the target object to see if it's
342    // a tag or not, and we need to check if it's a commit object.
343    let peeled_id = peeling_ref.into_owned().into_fully_peeled_id().ok()?;
344    let is_commit = peeled_id
345        .object()
346        .is_ok_and(|object| object.kind.is_commit());
347    is_commit.then(|| CommitId::from_bytes(peeled_id.as_bytes()))
348}
349
350#[derive(Error, Debug)]
351pub enum GitImportError {
352    #[error("Failed to read Git HEAD target commit {id}")]
353    MissingHeadTarget {
354        id: CommitId,
355        #[source]
356        err: BackendError,
357    },
358    #[error("Ancestor of Git ref {symbol} is missing")]
359    MissingRefAncestor {
360        symbol: RemoteRefSymbolBuf,
361        #[source]
362        err: BackendError,
363    },
364    #[error(transparent)]
365    Backend(BackendError),
366    #[error(transparent)]
367    Git(Box<dyn std::error::Error + Send + Sync>),
368    #[error(transparent)]
369    UnexpectedBackend(#[from] UnexpectedGitBackendError),
370}
371
372impl GitImportError {
373    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
374        GitImportError::Git(source.into())
375    }
376}
377
378/// Describes changes made by `import_refs()` or `fetch()`.
379#[derive(Clone, Debug, Eq, PartialEq, Default)]
380pub struct GitImportStats {
381    /// Commits superseded by newly imported commits.
382    pub abandoned_commits: Vec<CommitId>,
383    /// Remote bookmark `(symbol, (old_remote_ref, new_target))`s to be merged
384    /// in to the local bookmarks, sorted by `symbol`.
385    pub changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
386    /// Remote tag `(symbol, (old_remote_ref, new_target))`s to be merged in to
387    /// the local tags, sorted by `symbol`.
388    pub changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
389    /// Git ref names that couldn't be imported, sorted by name.
390    ///
391    /// This list doesn't include refs that are supposed to be ignored, such as
392    /// refs pointing to non-commit objects.
393    pub failed_ref_names: Vec<BString>,
394}
395
396#[derive(Debug)]
397struct RefsToImport {
398    /// Git ref `(full_name, new_target)`s to be copied to the view, sorted by
399    /// `full_name`.
400    changed_git_refs: Vec<(GitRefNameBuf, RefTarget)>,
401    /// Remote bookmark `(symbol, (old_remote_ref, new_target))`s to be merged
402    /// in to the local bookmarks, sorted by `symbol`.
403    changed_remote_bookmarks: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
404    /// Remote tag `(symbol, (old_remote_ref, new_target))`s to be merged in to
405    /// the local tags, sorted by `symbol`.
406    changed_remote_tags: Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
407    /// Git ref names that couldn't be imported, sorted by name.
408    failed_ref_names: Vec<BString>,
409}
410
411/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
412///
413/// This function detects conflicts (if both Git and JJ modified a bookmark) and
414/// records them in JJ's view.
415pub fn import_refs(
416    mut_repo: &mut MutableRepo,
417    git_settings: &GitSettings,
418) -> Result<GitImportStats, GitImportError> {
419    import_some_refs(mut_repo, git_settings, |_, _| true)
420}
421
422/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
423///
424/// Only bookmarks and tags whose remote symbol pass the filter will be
425/// considered for addition, update, or deletion.
426pub fn import_some_refs(
427    mut_repo: &mut MutableRepo,
428    git_settings: &GitSettings,
429    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
430) -> Result<GitImportStats, GitImportError> {
431    let store = mut_repo.store();
432    let git_backend = get_git_backend(store)?;
433    let git_repo = git_backend.git_repo();
434
435    let RefsToImport {
436        changed_git_refs,
437        changed_remote_bookmarks,
438        changed_remote_tags,
439        failed_ref_names,
440    } = diff_refs_to_import(mut_repo.view(), &git_repo, git_ref_filter)?;
441
442    // Bulk-import all reachable Git commits to the backend to reduce overhead
443    // of table merging and ref updates.
444    //
445    // changed_remote_bookmarks/tags might contain new_targets that are not in
446    // changed_git_refs, but such targets should have already been imported to
447    // the backend.
448    let index = mut_repo.index();
449    let missing_head_ids = changed_git_refs
450        .iter()
451        .flat_map(|(_, new_target)| new_target.added_ids())
452        .filter(|&id| !index.has_id(id));
453    let heads_imported = git_backend.import_head_commits(missing_head_ids).is_ok();
454
455    // Import new remote heads
456    let mut head_commits = Vec::new();
457    let get_commit = |id| {
458        // If bulk-import failed, try again to find bad head or ref.
459        if !heads_imported && !index.has_id(id) {
460            git_backend.import_head_commits([id])?;
461        }
462        store.get_commit(id)
463    };
464    for (symbol, (_, new_target)) in
465        itertools::chain(&changed_remote_bookmarks, &changed_remote_tags)
466    {
467        for id in new_target.added_ids() {
468            let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {
469                symbol: symbol.clone(),
470                err,
471            })?;
472            head_commits.push(commit);
473        }
474    }
475    // It's unlikely the imported commits were missing, but I/O-related error
476    // can still occur.
477    mut_repo
478        .add_heads(&head_commits)
479        .map_err(GitImportError::Backend)?;
480
481    // Apply the change that happened in git since last time we imported refs.
482    for (full_name, new_target) in changed_git_refs {
483        mut_repo.set_git_ref_target(&full_name, new_target);
484    }
485    for (symbol, (old_remote_ref, new_target)) in &changed_remote_bookmarks {
486        let symbol = symbol.as_ref();
487        let base_target = old_remote_ref.tracked_target();
488        let new_remote_ref = RemoteRef {
489            target: new_target.clone(),
490            state: if old_remote_ref.is_present() {
491                old_remote_ref.state
492            } else {
493                default_remote_ref_state_for(GitRefKind::Bookmark, symbol, git_settings)
494            },
495        };
496        if new_remote_ref.is_tracked() {
497            mut_repo.merge_local_bookmark(symbol.name, base_target, &new_remote_ref.target);
498        }
499        // Remote-tracking branch is the last known state of the branch in the remote.
500        // It shouldn't diverge even if we had inconsistent view.
501        mut_repo.set_remote_bookmark(symbol, new_remote_ref);
502    }
503    for (symbol, (old_remote_ref, new_target)) in &changed_remote_tags {
504        let symbol = symbol.as_ref();
505        let base_target = old_remote_ref.tracked_target();
506        let new_remote_ref = RemoteRef {
507            target: new_target.clone(),
508            state: if old_remote_ref.is_present() {
509                old_remote_ref.state
510            } else {
511                default_remote_ref_state_for(GitRefKind::Tag, symbol, git_settings)
512            },
513        };
514        if new_remote_ref.is_tracked() {
515            mut_repo.merge_tag(symbol.name, base_target, &new_remote_ref.target);
516        }
517        // TODO: If we add Git-tracking tag, it will be updated here.
518    }
519
520    let abandoned_commits = if git_settings.abandon_unreachable_commits {
521        abandon_unreachable_commits(mut_repo, &changed_remote_bookmarks, &changed_remote_tags)
522            .map_err(GitImportError::Backend)?
523    } else {
524        vec![]
525    };
526    let stats = GitImportStats {
527        abandoned_commits,
528        changed_remote_bookmarks,
529        changed_remote_tags,
530        failed_ref_names,
531    };
532    Ok(stats)
533}
534
535/// Finds commits that used to be reachable in git that no longer are reachable.
536/// Those commits will be recorded as abandoned in the `MutableRepo`.
537fn abandon_unreachable_commits(
538    mut_repo: &mut MutableRepo,
539    changed_remote_bookmarks: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
540    changed_remote_tags: &[(RemoteRefSymbolBuf, (RemoteRef, RefTarget))],
541) -> BackendResult<Vec<CommitId>> {
542    let hidable_git_heads = itertools::chain(changed_remote_bookmarks, changed_remote_tags)
543        .flat_map(|(_, (old_remote_ref, _))| old_remote_ref.target.added_ids())
544        .cloned()
545        .collect_vec();
546    if hidable_git_heads.is_empty() {
547        return Ok(vec![]);
548    }
549    let pinned_expression = RevsetExpression::union_all(&[
550        // Local refs are usually visible, no need to filter out hidden
551        RevsetExpression::commits(pinned_commit_ids(mut_repo.view())),
552        RevsetExpression::commits(remotely_pinned_commit_ids(mut_repo.view()))
553            // Hidden remote branches should not contribute to pinning
554            .intersection(&RevsetExpression::visible_heads().ancestors()),
555        RevsetExpression::root(),
556    ]);
557    let abandoned_expression = pinned_expression
558        .range(&RevsetExpression::commits(hidable_git_heads))
559        // Don't include already-abandoned commits in GitImportStats
560        .intersection(&RevsetExpression::visible_heads().ancestors());
561    let abandoned_commit_ids: Vec<_> = abandoned_expression
562        .evaluate(mut_repo)
563        .map_err(|err| err.into_backend_error())?
564        .iter()
565        .try_collect()
566        .map_err(|err| err.into_backend_error())?;
567    for id in &abandoned_commit_ids {
568        let commit = mut_repo.store().get_commit(id)?;
569        mut_repo.record_abandoned_commit(&commit);
570    }
571    Ok(abandoned_commit_ids)
572}
573
574/// Calculates diff of git refs to be imported.
575fn diff_refs_to_import(
576    view: &View,
577    git_repo: &gix::Repository,
578    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
579) -> Result<RefsToImport, GitImportError> {
580    let mut known_git_refs = view
581        .git_refs()
582        .iter()
583        .filter_map(|(full_name, target)| {
584            // TODO: or clean up invalid ref in case it was stored due to historical bug?
585            let (kind, symbol) =
586                parse_git_ref(full_name).expect("stored git ref should be parsable");
587            git_ref_filter(kind, symbol).then_some((full_name.as_ref(), target))
588        })
589        .collect();
590    // TODO: migrate tags to the remote view, and don't destructure &RemoteRef
591    let mut known_remote_bookmarks = view
592        .all_remote_bookmarks()
593        .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
594        .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), (&remote_ref.target, remote_ref.state)))
595        .collect();
596    // TODO: compare to tags stored in the "git" remote view. Since tags should
597    // never be moved locally in jj, we can consider local tags as merge base.
598    let mut known_remote_tags = view
599        .tags()
600        .iter()
601        .map(|(name, target)| {
602            let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
603            let state = RemoteRefState::Tracked;
604            (symbol, (target, state))
605        })
606        .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Tag, symbol))
607        .map(|(symbol, remote_ref)| (RemoteRefKey(symbol), remote_ref))
608        .collect();
609
610    let mut changed_git_refs = Vec::new();
611    let mut changed_remote_bookmarks = Vec::new();
612    let mut changed_remote_tags = Vec::new();
613    let mut failed_ref_names = Vec::new();
614    let actual = git_repo.references().map_err(GitImportError::from_git)?;
615    collect_changed_refs_to_import(
616        actual.local_branches().map_err(GitImportError::from_git)?,
617        &mut known_git_refs,
618        &mut known_remote_bookmarks,
619        &mut changed_git_refs,
620        &mut changed_remote_bookmarks,
621        &mut failed_ref_names,
622        &git_ref_filter,
623    )?;
624    collect_changed_refs_to_import(
625        actual.remote_branches().map_err(GitImportError::from_git)?,
626        &mut known_git_refs,
627        &mut known_remote_bookmarks,
628        &mut changed_git_refs,
629        &mut changed_remote_bookmarks,
630        &mut failed_ref_names,
631        &git_ref_filter,
632    )?;
633    collect_changed_refs_to_import(
634        actual.tags().map_err(GitImportError::from_git)?,
635        &mut known_git_refs,
636        &mut known_remote_tags,
637        &mut changed_git_refs,
638        &mut changed_remote_tags,
639        &mut failed_ref_names,
640        &git_ref_filter,
641    )?;
642    for full_name in known_git_refs.into_keys() {
643        changed_git_refs.push((full_name.to_owned(), RefTarget::absent()));
644    }
645    for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_bookmarks {
646        let old_remote_ref = RemoteRef {
647            target: old_target.clone(),
648            state: old_state,
649        };
650        changed_remote_bookmarks.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
651    }
652    for (RemoteRefKey(symbol), (old_target, old_state)) in known_remote_tags {
653        let old_remote_ref = RemoteRef {
654            target: old_target.clone(),
655            state: old_state,
656        };
657        changed_remote_tags.push((symbol.to_owned(), (old_remote_ref, RefTarget::absent())));
658    }
659
660    // Stabilize merge order and output.
661    changed_git_refs.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
662    changed_remote_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
663    changed_remote_tags.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
664    failed_ref_names.sort_unstable();
665    Ok(RefsToImport {
666        changed_git_refs,
667        changed_remote_bookmarks,
668        changed_remote_tags,
669        failed_ref_names,
670    })
671}
672
673fn collect_changed_refs_to_import(
674    actual_git_refs: gix::reference::iter::Iter<'_>,
675    known_git_refs: &mut HashMap<&GitRefName, &RefTarget>,
676    known_remote_refs: &mut HashMap<RemoteRefKey<'_>, (&RefTarget, RemoteRefState)>,
677    changed_git_refs: &mut Vec<(GitRefNameBuf, RefTarget)>,
678    changed_remote_refs: &mut Vec<(RemoteRefSymbolBuf, (RemoteRef, RefTarget))>,
679    failed_ref_names: &mut Vec<BString>,
680    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
681) -> Result<(), GitImportError> {
682    for git_ref in actual_git_refs {
683        let git_ref = git_ref.map_err(GitImportError::from_git)?;
684        let full_name_bytes = git_ref.name().as_bstr();
685        let Ok(full_name) = str::from_utf8(full_name_bytes) else {
686            // Non-utf8 refs cannot be imported.
687            failed_ref_names.push(full_name_bytes.to_owned());
688            continue;
689        };
690        if full_name.starts_with(RESERVED_REMOTE_REF_NAMESPACE) {
691            failed_ref_names.push(full_name_bytes.to_owned());
692            continue;
693        }
694        let full_name = GitRefName::new(full_name);
695        let Some((kind, symbol)) = parse_git_ref(full_name) else {
696            // Skip special refs such as refs/remotes/*/HEAD.
697            continue;
698        };
699        if !git_ref_filter(kind, symbol) {
700            continue;
701        }
702        let old_git_target = known_git_refs.get(full_name).copied().flatten();
703        let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_git_target) else {
704            // Skip (or remove existing) invalid refs.
705            continue;
706        };
707        let new_target = RefTarget::normal(id);
708        known_git_refs.remove(full_name);
709        if new_target != *old_git_target {
710            changed_git_refs.push((full_name.to_owned(), new_target.clone()));
711        }
712        // TODO: Make it configurable which remotes are publishing and update public
713        // heads here.
714        let (old_remote_target, old_remote_state) = known_remote_refs
715            .remove(&symbol)
716            .unwrap_or_else(|| (RefTarget::absent_ref(), RemoteRefState::New));
717        if new_target != *old_remote_target {
718            let old_remote_ref = RemoteRef {
719                target: old_remote_target.clone(),
720                state: old_remote_state,
721            };
722            changed_remote_refs.push((symbol.to_owned(), (old_remote_ref, new_target)));
723        }
724    }
725    Ok(())
726}
727
728fn default_remote_ref_state_for(
729    kind: GitRefKind,
730    symbol: RemoteRefSymbol<'_>,
731    git_settings: &GitSettings,
732) -> RemoteRefState {
733    match kind {
734        GitRefKind::Bookmark => {
735            if symbol.remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO || git_settings.auto_local_bookmark {
736                RemoteRefState::Tracked
737            } else {
738                RemoteRefState::New
739            }
740        }
741        GitRefKind::Tag => RemoteRefState::Tracked,
742    }
743}
744
745/// Commits referenced by local branches or tags.
746///
747/// On `import_refs()`, this is similar to collecting commits referenced by
748/// `view.git_refs()`. Main difference is that local branches can be moved by
749/// tracking remotes, and such mutation isn't applied to `view.git_refs()` yet.
750fn pinned_commit_ids(view: &View) -> Vec<CommitId> {
751    itertools::chain(
752        view.local_bookmarks().map(|(_, target)| target),
753        view.tags().values(),
754    )
755    .flat_map(|target| target.added_ids())
756    .cloned()
757    .collect()
758}
759
760/// Commits referenced by untracked remote branches including hidden ones.
761///
762/// Tracked remote branches aren't included because they should have been merged
763/// into the local counterparts, and the changes pulled from one remote should
764/// propagate to the other remotes on later push. OTOH, untracked remote
765/// branches are considered independent refs.
766fn remotely_pinned_commit_ids(view: &View) -> Vec<CommitId> {
767    view.all_remote_bookmarks()
768        .filter(|(_, remote_ref)| !remote_ref.is_tracked())
769        .map(|(_, remote_ref)| &remote_ref.target)
770        .flat_map(|target| target.added_ids())
771        .cloned()
772        .collect()
773}
774
775/// Imports HEAD from the underlying Git repo.
776///
777/// Unlike `import_refs()`, the old HEAD branch is not abandoned because HEAD
778/// move doesn't always mean the old HEAD branch has been rewritten.
779///
780/// Unlike `reset_head()`, this function doesn't move the working-copy commit to
781/// the child of the new HEAD revision.
782pub fn import_head(mut_repo: &mut MutableRepo) -> Result<(), GitImportError> {
783    let store = mut_repo.store();
784    let git_backend = get_git_backend(store)?;
785    let git_repo = git_backend.git_repo();
786
787    let old_git_head = mut_repo.view().git_head();
788    let new_git_head_id = if let Ok(oid) = git_repo.head_id() {
789        Some(CommitId::from_bytes(oid.as_bytes()))
790    } else {
791        None
792    };
793    if old_git_head.as_resolved() == Some(&new_git_head_id) {
794        return Ok(());
795    }
796
797    // Import new head
798    if let Some(head_id) = &new_git_head_id {
799        let index = mut_repo.index();
800        if !index.has_id(head_id) {
801            git_backend.import_head_commits([head_id]).map_err(|err| {
802                GitImportError::MissingHeadTarget {
803                    id: head_id.clone(),
804                    err,
805                }
806            })?;
807        }
808        // It's unlikely the imported commits were missing, but I/O-related
809        // error can still occur.
810        store
811            .get_commit(head_id)
812            .and_then(|commit| mut_repo.add_head(&commit))
813            .map_err(GitImportError::Backend)?;
814    }
815
816    mut_repo.set_git_head_target(RefTarget::resolved(new_git_head_id));
817    Ok(())
818}
819
820#[derive(Error, Debug)]
821pub enum GitExportError {
822    #[error(transparent)]
823    Git(Box<dyn std::error::Error + Send + Sync>),
824    #[error(transparent)]
825    UnexpectedBackend(#[from] UnexpectedGitBackendError),
826}
827
828impl GitExportError {
829    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
830        GitExportError::Git(source.into())
831    }
832}
833
834/// The reason we failed to export a ref to Git.
835#[derive(Debug, Error)]
836pub enum FailedRefExportReason {
837    /// The name is not allowed in Git.
838    #[error("Name is not allowed in Git")]
839    InvalidGitName,
840    /// The ref was in a conflicted state from the last import. A re-import
841    /// should fix it.
842    #[error("Ref was in a conflicted state from the last import")]
843    ConflictedOldState,
844    /// The ref points to the root commit, which Git doesn't have.
845    #[error("Ref cannot point to the root commit in Git")]
846    OnRootCommit,
847    /// We wanted to delete it, but it had been modified in Git.
848    #[error("Deleted ref had been modified in Git")]
849    DeletedInJjModifiedInGit,
850    /// We wanted to add it, but Git had added it with a different target
851    #[error("Added ref had been added with a different target in Git")]
852    AddedInJjAddedInGit,
853    /// We wanted to modify it, but Git had deleted it
854    #[error("Modified ref had been deleted in Git")]
855    ModifiedInJjDeletedInGit,
856    /// Failed to delete the ref from the Git repo
857    #[error("Failed to delete")]
858    FailedToDelete(#[source] Box<gix::reference::edit::Error>),
859    /// Failed to set the ref in the Git repo
860    #[error("Failed to set")]
861    FailedToSet(#[source] Box<gix::reference::edit::Error>),
862}
863
864/// Describes changes made by [`export_refs()`].
865#[derive(Debug)]
866pub struct GitExportStats {
867    /// Remote bookmarks that couldn't be exported, sorted by `symbol`.
868    pub failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
869}
870
871#[derive(Debug)]
872struct RefsToExport {
873    /// Remote bookmark `(symbol, (old_oid, new_oid))`s to update, sorted by
874    /// `symbol`.
875    bookmarks_to_update: Vec<(RemoteRefSymbolBuf, (Option<gix::ObjectId>, gix::ObjectId))>,
876    /// Remote bookmark `(symbol, old_oid)`s to delete, sorted by `symbol`.
877    ///
878    /// Deletion has to be exported first to avoid conflict with new bookmarks
879    /// on file-system.
880    bookmarks_to_delete: Vec<(RemoteRefSymbolBuf, gix::ObjectId)>,
881    /// Remote bookmarks that couldn't be exported, sorted by `symbol`.
882    failed_bookmarks: Vec<(RemoteRefSymbolBuf, FailedRefExportReason)>,
883}
884
885/// Export changes to bookmarks made in the Jujutsu repo compared to our last
886/// seen view of the Git repo in `mut_repo.view().git_refs()`.
887///
888/// We ignore changed bookmarks that are conflicted (were also changed in the
889/// Git repo compared to our last remembered view of the Git repo). These will
890/// be marked conflicted by the next `jj git import`.
891///
892/// We do not export tags and other refs at the moment, since these aren't
893/// supposed to be modified by JJ. For them, the Git state is considered
894/// authoritative.
895pub fn export_refs(mut_repo: &mut MutableRepo) -> Result<GitExportStats, GitExportError> {
896    export_some_refs(mut_repo, |_, _| true)
897}
898
899pub fn export_some_refs(
900    mut_repo: &mut MutableRepo,
901    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
902) -> Result<GitExportStats, GitExportError> {
903    fn get<'a, V>(map: &'a [(RemoteRefSymbolBuf, V)], key: RemoteRefSymbol<'_>) -> Option<&'a V> {
904        debug_assert!(map.is_sorted_by_key(|(k, _)| k));
905        let index = map.binary_search_by_key(&key, |(k, _)| k.as_ref()).ok()?;
906        let (_, value) = &map[index];
907        Some(value)
908    }
909
910    let git_repo = get_git_repo(mut_repo.store())?;
911
912    let RefsToExport {
913        bookmarks_to_update,
914        bookmarks_to_delete,
915        mut failed_bookmarks,
916    } = diff_refs_to_export(
917        mut_repo.view(),
918        mut_repo.store().root_commit_id(),
919        &git_ref_filter,
920    );
921
922    // TODO: Also check other worktrees' HEAD.
923    if let Ok(head_ref) = git_repo.find_reference("HEAD") {
924        let target_name = head_ref.target().try_name().map(|name| name.to_owned());
925        if let Some((GitRefKind::Bookmark, symbol)) = target_name
926            .as_ref()
927            .and_then(|name| str::from_utf8(name.as_bstr()).ok())
928            .and_then(|name| parse_git_ref(name.as_ref()))
929        {
930            let old_target = head_ref.inner.target.clone();
931            let current_oid = match head_ref.into_fully_peeled_id() {
932                Ok(id) => Some(id.detach()),
933                Err(gix::reference::peel::Error::ToId(
934                    gix::refs::peel::to_id::Error::FollowToObject(
935                        gix::refs::peel::to_object::Error::Follow(
936                            gix::refs::file::find::existing::Error::NotFound { .. },
937                        ),
938                    ),
939                )) => None, // Unborn ref should be considered absent
940                Err(err) => return Err(GitExportError::from_git(err)),
941            };
942            let new_oid = if let Some((_old_oid, new_oid)) = get(&bookmarks_to_update, symbol) {
943                Some(new_oid)
944            } else if get(&bookmarks_to_delete, symbol).is_some() {
945                None
946            } else {
947                current_oid.as_ref()
948            };
949            if new_oid != current_oid.as_ref() {
950                update_git_head(
951                    &git_repo,
952                    gix::refs::transaction::PreviousValue::MustExistAndMatch(old_target),
953                    current_oid,
954                )
955                .map_err(GitExportError::from_git)?;
956            }
957        }
958    }
959    for (symbol, old_oid) in bookmarks_to_delete {
960        let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
961            failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
962            continue;
963        };
964        if let Err(reason) = delete_git_ref(&git_repo, &git_ref_name, &old_oid) {
965            failed_bookmarks.push((symbol, reason));
966        } else {
967            let new_target = RefTarget::absent();
968            mut_repo.set_git_ref_target(&git_ref_name, new_target);
969        }
970    }
971    for (symbol, (old_oid, new_oid)) in bookmarks_to_update {
972        let Some(git_ref_name) = to_git_ref_name(GitRefKind::Bookmark, symbol.as_ref()) else {
973            failed_bookmarks.push((symbol, FailedRefExportReason::InvalidGitName));
974            continue;
975        };
976        if let Err(reason) = update_git_ref(&git_repo, &git_ref_name, old_oid, new_oid) {
977            failed_bookmarks.push((symbol, reason));
978        } else {
979            let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
980            mut_repo.set_git_ref_target(&git_ref_name, new_target);
981        }
982    }
983
984    // Stabilize output, allow binary search.
985    failed_bookmarks.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2));
986
987    copy_exportable_local_bookmarks_to_remote_view(
988        mut_repo,
989        REMOTE_NAME_FOR_LOCAL_GIT_REPO,
990        |name| {
991            let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
992            git_ref_filter(GitRefKind::Bookmark, symbol) && get(&failed_bookmarks, symbol).is_none()
993        },
994    );
995
996    Ok(GitExportStats { failed_bookmarks })
997}
998
999fn copy_exportable_local_bookmarks_to_remote_view(
1000    mut_repo: &mut MutableRepo,
1001    remote: &RemoteName,
1002    name_filter: impl Fn(&RefName) -> bool,
1003) {
1004    let new_local_bookmarks = mut_repo
1005        .view()
1006        .local_remote_bookmarks(remote)
1007        .filter_map(|(name, targets)| {
1008            // TODO: filter out untracked bookmarks (if we add support for untracked @git
1009            // bookmarks)
1010            let old_target = &targets.remote_ref.target;
1011            let new_target = targets.local_target;
1012            (!new_target.has_conflict() && old_target != new_target).then_some((name, new_target))
1013        })
1014        .filter(|&(name, _)| name_filter(name))
1015        .map(|(name, new_target)| (name.to_owned(), new_target.clone()))
1016        .collect_vec();
1017    for (name, new_target) in new_local_bookmarks {
1018        let new_remote_ref = RemoteRef {
1019            target: new_target,
1020            state: RemoteRefState::Tracked,
1021        };
1022        mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
1023    }
1024}
1025
1026/// Calculates diff of bookmarks to be exported.
1027fn diff_refs_to_export(
1028    view: &View,
1029    root_commit_id: &CommitId,
1030    git_ref_filter: impl Fn(GitRefKind, RemoteRefSymbol<'_>) -> bool,
1031) -> RefsToExport {
1032    // Local targets will be copied to the "git" remote if successfully exported. So
1033    // the local bookmarks are considered to be the new "git" remote bookmarks.
1034    let mut all_bookmark_targets: HashMap<RemoteRefSymbol, (&RefTarget, &RefTarget)> =
1035        itertools::chain(
1036            view.local_bookmarks().map(|(name, target)| {
1037                let symbol = name.to_remote_symbol(REMOTE_NAME_FOR_LOCAL_GIT_REPO);
1038                (symbol, target)
1039            }),
1040            view.all_remote_bookmarks()
1041                .filter(|&(symbol, _)| symbol.remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
1042                .map(|(symbol, remote_ref)| (symbol, &remote_ref.target)),
1043        )
1044        .filter(|&(symbol, _)| git_ref_filter(GitRefKind::Bookmark, symbol))
1045        .map(|(symbol, new_target)| (symbol, (RefTarget::absent_ref(), new_target)))
1046        .collect();
1047    let known_git_refs = view
1048        .git_refs()
1049        .iter()
1050        .map(|(full_name, target)| {
1051            let (kind, symbol) =
1052                parse_git_ref(full_name).expect("stored git ref should be parsable");
1053            ((kind, symbol), target)
1054        })
1055        .filter(|&((kind, symbol), _)| {
1056            // There are two situations where remote bookmarks get out of sync:
1057            // 1. `jj bookmark forget --include-remotes`
1058            // 2. `jj op undo`/`restore` in colocated repo
1059            kind == GitRefKind::Bookmark && git_ref_filter(kind, symbol)
1060        });
1061    for ((_kind, symbol), target) in known_git_refs {
1062        all_bookmark_targets
1063            .entry(symbol)
1064            .and_modify(|(old_target, _)| *old_target = target)
1065            .or_insert((target, RefTarget::absent_ref()));
1066    }
1067
1068    let mut bookmarks_to_update = Vec::new();
1069    let mut bookmarks_to_delete = Vec::new();
1070    let mut failed_bookmarks = Vec::new();
1071    let root_commit_target = RefTarget::normal(root_commit_id.clone());
1072    for (symbol, (old_target, new_target)) in all_bookmark_targets {
1073        if new_target == old_target {
1074            continue;
1075        }
1076        if *new_target == root_commit_target {
1077            // Git doesn't have a root commit
1078            failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::OnRootCommit));
1079            continue;
1080        }
1081        let old_oid = if let Some(id) = old_target.as_normal() {
1082            Some(gix::ObjectId::from_bytes_or_panic(id.as_bytes()))
1083        } else if old_target.has_conflict() {
1084            // The old git ref should only be a conflict if there were concurrent import
1085            // operations while the value changed. Don't overwrite these values.
1086            failed_bookmarks.push((symbol.to_owned(), FailedRefExportReason::ConflictedOldState));
1087            continue;
1088        } else {
1089            assert!(old_target.is_absent());
1090            None
1091        };
1092        if let Some(id) = new_target.as_normal() {
1093            let new_oid = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1094            bookmarks_to_update.push((symbol.to_owned(), (old_oid, new_oid)));
1095        } else if new_target.has_conflict() {
1096            // Skip conflicts and leave the old value in git_refs
1097            continue;
1098        } else {
1099            assert!(new_target.is_absent());
1100            bookmarks_to_delete.push((symbol.to_owned(), old_oid.unwrap()));
1101        }
1102    }
1103
1104    // Stabilize export order and output, allow binary search.
1105    bookmarks_to_update.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1106    bookmarks_to_delete.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1107    failed_bookmarks.sort_unstable_by(|(sym1, _), (sym2, _)| sym1.cmp(sym2));
1108    RefsToExport {
1109        bookmarks_to_update,
1110        bookmarks_to_delete,
1111        failed_bookmarks,
1112    }
1113}
1114
1115fn delete_git_ref(
1116    git_repo: &gix::Repository,
1117    git_ref_name: &GitRefName,
1118    old_oid: &gix::oid,
1119) -> Result<(), FailedRefExportReason> {
1120    if let Ok(git_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1121        if git_ref.inner.target.try_id() == Some(old_oid) {
1122            // The ref has not been updated by git, so go ahead and delete it
1123            git_ref
1124                .delete()
1125                .map_err(|err| FailedRefExportReason::FailedToDelete(err.into()))?;
1126        } else {
1127            // The ref was updated by git
1128            return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
1129        }
1130    } else {
1131        // The ref is already deleted
1132    }
1133    Ok(())
1134}
1135
1136fn update_git_ref(
1137    git_repo: &gix::Repository,
1138    git_ref_name: &GitRefName,
1139    old_oid: Option<gix::ObjectId>,
1140    new_oid: gix::ObjectId,
1141) -> Result<(), FailedRefExportReason> {
1142    match old_oid {
1143        None => {
1144            if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1145                // The ref was added in jj and in git. We're good if and only if git
1146                // pointed it to our desired target.
1147                if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1148                    return Err(FailedRefExportReason::AddedInJjAddedInGit);
1149                }
1150            } else {
1151                // The ref was added in jj but still doesn't exist in git, so add it
1152                git_repo
1153                    .reference(
1154                        git_ref_name.as_str(),
1155                        new_oid,
1156                        gix::refs::transaction::PreviousValue::MustNotExist,
1157                        "export from jj",
1158                    )
1159                    .map_err(|err| FailedRefExportReason::FailedToSet(err.into()))?;
1160            }
1161        }
1162        Some(old_oid) => {
1163            // The ref was modified in jj. We can use gix API for updating under a lock.
1164            if let Err(err) = git_repo.reference(
1165                git_ref_name.as_str(),
1166                new_oid,
1167                gix::refs::transaction::PreviousValue::MustExistAndMatch(old_oid.into()),
1168                "export from jj",
1169            ) {
1170                // The reference was probably updated in git
1171                if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name.as_str()) {
1172                    // We still consider this a success if it was updated to our desired target
1173                    if git_repo_ref.inner.target.try_id() != Some(&new_oid) {
1174                        return Err(FailedRefExportReason::FailedToSet(err.into()));
1175                    }
1176                } else {
1177                    // The reference was deleted in git and moved in jj
1178                    return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
1179                }
1180            } else {
1181                // Successfully updated from old_oid to new_oid (unchanged in
1182                // git)
1183            }
1184        }
1185    }
1186    Ok(())
1187}
1188
1189/// Ensures Git HEAD is detached and pointing to the `new_oid`. If `new_oid`
1190/// is `None` (meaning absent), dummy placeholder ref will be set.
1191fn update_git_head(
1192    git_repo: &gix::Repository,
1193    expected_ref: gix::refs::transaction::PreviousValue,
1194    new_oid: Option<gix::ObjectId>,
1195) -> Result<(), gix::reference::edit::Error> {
1196    let mut ref_edits = Vec::new();
1197    let new_target = if let Some(oid) = new_oid {
1198        gix::refs::Target::Object(oid)
1199    } else {
1200        // Can't detach HEAD without a commit. Use placeholder ref to nullify
1201        // the HEAD. The placeholder ref isn't a normal branch ref. Git CLI
1202        // appears to deal with that, and can move the placeholder ref. So we
1203        // need to ensure that the ref doesn't exist.
1204        ref_edits.push(gix::refs::transaction::RefEdit {
1205            change: gix::refs::transaction::Change::Delete {
1206                expected: gix::refs::transaction::PreviousValue::Any,
1207                log: gix::refs::transaction::RefLog::AndReference,
1208            },
1209            name: UNBORN_ROOT_REF_NAME.try_into().unwrap(),
1210            deref: false,
1211        });
1212        gix::refs::Target::Symbolic(UNBORN_ROOT_REF_NAME.try_into().unwrap())
1213    };
1214    ref_edits.push(gix::refs::transaction::RefEdit {
1215        change: gix::refs::transaction::Change::Update {
1216            log: gix::refs::transaction::LogChange {
1217                message: "export from jj".into(),
1218                ..Default::default()
1219            },
1220            expected: expected_ref,
1221            new: new_target,
1222        },
1223        name: "HEAD".try_into().unwrap(),
1224        deref: false,
1225    });
1226    git_repo.edit_references(ref_edits)?;
1227    Ok(())
1228}
1229
1230#[derive(Debug, Error)]
1231pub enum GitResetHeadError {
1232    #[error(transparent)]
1233    Backend(#[from] BackendError),
1234    #[error(transparent)]
1235    Git(Box<dyn std::error::Error + Send + Sync>),
1236    #[error("Failed to update Git HEAD ref")]
1237    UpdateHeadRef(#[source] Box<gix::reference::edit::Error>),
1238    #[error(transparent)]
1239    UnexpectedBackend(#[from] UnexpectedGitBackendError),
1240}
1241
1242impl GitResetHeadError {
1243    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1244        GitResetHeadError::Git(source.into())
1245    }
1246}
1247
1248/// Sets Git HEAD to the parent of the given working-copy commit and resets
1249/// the Git index.
1250pub fn reset_head(mut_repo: &mut MutableRepo, wc_commit: &Commit) -> Result<(), GitResetHeadError> {
1251    let git_repo = get_git_repo(mut_repo.store())?;
1252
1253    let first_parent_id = &wc_commit.parent_ids()[0];
1254    let new_head_target = if first_parent_id != mut_repo.store().root_commit_id() {
1255        RefTarget::normal(first_parent_id.clone())
1256    } else {
1257        RefTarget::absent()
1258    };
1259
1260    // If the first parent of the working copy has changed, reset the Git HEAD.
1261    let old_head_target = mut_repo.git_head();
1262    if old_head_target != new_head_target {
1263        let expected_ref = if let Some(id) = old_head_target.as_normal() {
1264            // We have to check the actual HEAD state because we don't record a
1265            // symbolic ref as such.
1266            let actual_head = git_repo.head().map_err(GitResetHeadError::from_git)?;
1267            if actual_head.is_detached() {
1268                let id = gix::ObjectId::from_bytes_or_panic(id.as_bytes());
1269                gix::refs::transaction::PreviousValue::MustExistAndMatch(id.into())
1270            } else {
1271                // Just overwrite symbolic ref, which is unusual. Alternatively,
1272                // maybe we can test the target ref by issuing noop edit.
1273                gix::refs::transaction::PreviousValue::MustExist
1274            }
1275        } else {
1276            // Just overwrite if unborn (or conflict), which is also unusual.
1277            gix::refs::transaction::PreviousValue::MustExist
1278        };
1279        let new_oid = new_head_target
1280            .as_normal()
1281            .map(|id| gix::ObjectId::from_bytes_or_panic(id.as_bytes()));
1282        update_git_head(&git_repo, expected_ref, new_oid)
1283            .map_err(|err| GitResetHeadError::UpdateHeadRef(err.into()))?;
1284        mut_repo.set_git_head_target(new_head_target);
1285    }
1286
1287    // If there is an ongoing operation (merge, rebase, etc.), we need to clean it
1288    // up.
1289    //
1290    // TODO: Polish and upstream this to `gix`.
1291    if git_repo.state().is_some() {
1292        // Based on the files `git2::Repository::cleanup_state` deletes; when
1293        // upstreaming this logic should probably become more elaborate to match
1294        // `git(1)` behaviour.
1295        const STATE_FILE_NAMES: &[&str] = &[
1296            "MERGE_HEAD",
1297            "MERGE_MODE",
1298            "MERGE_MSG",
1299            "REVERT_HEAD",
1300            "CHERRY_PICK_HEAD",
1301            "BISECT_LOG",
1302        ];
1303        const STATE_DIR_NAMES: &[&str] = &["rebase-merge", "rebase-apply", "sequencer"];
1304        let handle_err = |err: PathError| match err.error.kind() {
1305            std::io::ErrorKind::NotFound => Ok(()),
1306            _ => Err(GitResetHeadError::from_git(err)),
1307        };
1308        for file_name in STATE_FILE_NAMES {
1309            let path = git_repo.path().join(file_name);
1310            std::fs::remove_file(&path)
1311                .context(&path)
1312                .or_else(handle_err)?;
1313        }
1314        for dir_name in STATE_DIR_NAMES {
1315            let path = git_repo.path().join(dir_name);
1316            std::fs::remove_dir_all(&path)
1317                .context(&path)
1318                .or_else(handle_err)?;
1319        }
1320    }
1321
1322    let parent_tree = wc_commit.parent_tree(mut_repo)?;
1323
1324    // Use the merged parent tree as the Git index, allowing `git diff` to show the
1325    // same changes as `jj diff`. If the merged parent tree has conflicts, then the
1326    // Git index will also be conflicted.
1327    let mut index = if let Some(tree) = parent_tree.as_merge().as_resolved() {
1328        if tree.id() == mut_repo.store().empty_tree_id() {
1329            // If the tree is empty, gix can fail to load the object (since Git doesn't
1330            // require the empty tree to actually be present in the object database), so we
1331            // just use an empty index directly.
1332            gix::index::File::from_state(
1333                gix::index::State::new(git_repo.object_hash()),
1334                git_repo.index_path(),
1335            )
1336        } else {
1337            // If the parent tree is resolved, we can use gix's `index_from_tree` method.
1338            // This is more efficient than iterating over the tree and adding each entry.
1339            git_repo
1340                .index_from_tree(&gix::ObjectId::from_bytes_or_panic(tree.id().as_bytes()))
1341                .map_err(GitResetHeadError::from_git)?
1342        }
1343    } else {
1344        build_index_from_merged_tree(&git_repo, parent_tree.clone())?
1345    };
1346
1347    let wc_tree = wc_commit.tree()?;
1348    update_intent_to_add_impl(&mut index, &parent_tree, &wc_tree, git_repo.object_hash())
1349        .block_on()?;
1350
1351    // Match entries in the new index with entries in the old index, and copy stat
1352    // information if the entry didn't change.
1353    if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1354        index
1355            .entries_mut_with_paths()
1356            .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1357                gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1358                    .then_with(|| entry.stage().cmp(&old_entry.stage()))
1359            })
1360            .filter_map(|merged| merged.both())
1361            .map(|((entry, _), old_entry)| (entry, old_entry))
1362            .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1363            .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1364    }
1365
1366    debug_assert!(index.verify_entries().is_ok());
1367
1368    index
1369        .write(gix::index::write::Options::default())
1370        .map_err(GitResetHeadError::from_git)?;
1371
1372    Ok(())
1373}
1374
1375fn build_index_from_merged_tree(
1376    git_repo: &gix::Repository,
1377    merged_tree: MergedTree,
1378) -> Result<gix::index::File, GitResetHeadError> {
1379    let mut index = gix::index::File::from_state(
1380        gix::index::State::new(git_repo.object_hash()),
1381        git_repo.index_path(),
1382    );
1383
1384    let mut push_index_entry =
1385        |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1386            let Some(entry) = maybe_entry else {
1387                return;
1388            };
1389
1390            let (id, mode) = match entry {
1391                TreeValue::File {
1392                    id,
1393                    executable,
1394                    copy_id: _,
1395                } => {
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                    // This case is only possible if there is a file-directory conflict, since
1405                    // `MergedTree::entries` handles the recursion otherwise. We only materialize a
1406                    // file in the working copy for file-directory conflicts, so we don't add the
1407                    // tree to the index here either.
1408                    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            // It is safe to push the entry because we ensure that we only add each path to
1417            // a stage once, and we sort the entries after we finish adding them.
1418            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            // 2-sided conflicts can be represented in the Git index
1439            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            // We can't represent many-sided conflicts in the Git index, so just add the
1444            // first side as staged. This is preferable to adding the first 2 sides as a
1445            // conflict, since some tools rely on being able to resolve conflicts using the
1446            // index, which could lead to an incorrect conflict resolution if the index
1447            // didn't contain all of the conflict sides. Instead, we add a dummy conflict of
1448            // a file named ".jj-do-not-resolve-this-conflict" to prevent the user from
1449            // accidentally committing the conflict markers.
1450            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    // Required after `dangerously_push_entry` for correctness. We use do a lookup
1460    // in the index after this, so it must be sorted before we do the lookup.
1461    index.sort_entries();
1462
1463    // If the conflict had an unrepresentable conflict and the dummy file path isn't
1464    // already added in the index, add a dummy file as a conflict.
1465    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        // We need to sort again for correctness before writing the index file since we
1483        // added a new entry.
1484        index.sort_entries();
1485    }
1486
1487    Ok(index)
1488}
1489
1490/// Diff `old_tree` to `new_tree` and mark added files as intent-to-add in the
1491/// Git index. Also removes current intent-to-add entries in the index if they
1492/// were removed in the diff.
1493///
1494/// Should be called when the diff between the working-copy commit and its
1495/// parent(s) has changed.
1496pub 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 {
1529                    id: _,
1530                    executable,
1531                    copy_id: _,
1532                }) => *executable,
1533                Some(TreeValue::Symlink(_)) => false,
1534                _ => {
1535                    continue;
1536                }
1537            };
1538            if index
1539                .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1540                .is_err()
1541            {
1542                added_paths.push((BString::from(path.into_internal_string()), executable));
1543            }
1544        } else if after.is_absent() {
1545            removed_paths.insert(BString::from(path.into_internal_string()));
1546        }
1547    }
1548
1549    if added_paths.is_empty() && removed_paths.is_empty() {
1550        return Ok(());
1551    }
1552
1553    for (path, executable) in added_paths {
1554        // We have checked that the index doesn't have this entry
1555        index.dangerously_push_entry(
1556            gix::index::entry::Stat::default(),
1557            gix::ObjectId::empty_blob(hash_kind),
1558            gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1559            if executable {
1560                gix::index::entry::Mode::FILE_EXECUTABLE
1561            } else {
1562                gix::index::entry::Mode::FILE
1563            },
1564            path.as_ref(),
1565        );
1566    }
1567    if !removed_paths.is_empty() {
1568        index.remove_entries(|_size, path, entry| {
1569            entry
1570                .flags
1571                .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1572                && removed_paths.contains(path)
1573        });
1574    }
1575
1576    index.sort_entries();
1577
1578    Ok(())
1579}
1580
1581#[derive(Debug, Error)]
1582pub enum GitRemoteManagementError {
1583    #[error("No git remote named '{}'", .0.as_symbol())]
1584    NoSuchRemote(RemoteNameBuf),
1585    #[error("Git remote named '{}' already exists", .0.as_symbol())]
1586    RemoteAlreadyExists(RemoteNameBuf),
1587    #[error(transparent)]
1588    RemoteName(#[from] GitRemoteNameError),
1589    #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1590    NonstandardConfiguration(RemoteNameBuf),
1591    #[error("Error saving Git configuration")]
1592    GitConfigSaveError(#[source] std::io::Error),
1593    #[error("Unexpected Git error when managing remotes")]
1594    InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1595    #[error(transparent)]
1596    UnexpectedBackend(#[from] UnexpectedGitBackendError),
1597}
1598
1599impl GitRemoteManagementError {
1600    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1601        GitRemoteManagementError::InternalGitError(source.into())
1602    }
1603}
1604
1605/// Determine, by its name, if a remote refers to the special local-only "git"
1606/// remote that is used in the Git backend.
1607///
1608/// This function always returns false if the "git" feature is not enabled.
1609pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1610    remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1611}
1612
1613fn default_fetch_refspec(remote: &RemoteName) -> String {
1614    format!(
1615        "+refs/heads/*:refs/remotes/{remote}/*",
1616        remote = remote.as_str()
1617    )
1618}
1619
1620fn add_ref(
1621    name: gix::refs::FullName,
1622    target: gix::refs::Target,
1623    message: BString,
1624) -> gix::refs::transaction::RefEdit {
1625    gix::refs::transaction::RefEdit {
1626        change: gix::refs::transaction::Change::Update {
1627            log: gix::refs::transaction::LogChange {
1628                mode: gix::refs::transaction::RefLog::AndReference,
1629                force_create_reflog: false,
1630                message,
1631            },
1632            expected: gix::refs::transaction::PreviousValue::MustNotExist,
1633            new: target,
1634        },
1635        name,
1636        deref: false,
1637    }
1638}
1639
1640fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1641    gix::refs::transaction::RefEdit {
1642        change: gix::refs::transaction::Change::Delete {
1643            expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1644                reference.target().into_owned(),
1645            ),
1646            log: gix::refs::transaction::RefLog::AndReference,
1647        },
1648        name: reference.name().to_owned(),
1649        deref: false,
1650    }
1651}
1652
1653/// Save an edited [`gix::config::File`] to its original location on disk.
1654///
1655/// Note that the resulting configuration changes are *not* persisted to the
1656/// originating [`gix::Repository`]! The repository must be reloaded with the
1657/// new configuration if necessary.
1658fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1659    let mut config_file = File::create(
1660        config
1661            .meta()
1662            .path
1663            .as_ref()
1664            .expect("Git repository to have a config file"),
1665    )?;
1666    config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1667}
1668
1669fn save_remote(
1670    config: &mut gix::config::File<'static>,
1671    remote_name: &RemoteName,
1672    remote: &mut gix::Remote,
1673) -> Result<(), GitRemoteManagementError> {
1674    // Work around the gitoxide remote management bug
1675    // <https://github.com/GitoxideLabs/gitoxide/issues/1951> by adding
1676    // an empty section.
1677    //
1678    // Note that this will produce useless empty sections if we ever
1679    // support remote configuration keys other than `fetch` and `url`.
1680    config
1681        .new_section(
1682            "remote",
1683            Some(Cow::Owned(BString::from(remote_name.as_str()))),
1684        )
1685        .map_err(GitRemoteManagementError::from_git)?;
1686    remote
1687        .save_as_to(remote_name.as_str(), config)
1688        .map_err(GitRemoteManagementError::from_git)?;
1689    Ok(())
1690}
1691
1692fn git_config_branch_section_ids_by_remote(
1693    config: &gix::config::File,
1694    remote_name: &RemoteName,
1695) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1696    config
1697        .sections_by_name("branch")
1698        .into_iter()
1699        .flatten()
1700        .filter_map(|section| {
1701            let remote_values = section.values("remote");
1702            let push_remote_values = section.values("pushRemote");
1703            if !remote_values
1704                .iter()
1705                .chain(push_remote_values.iter())
1706                .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1707            {
1708                return None;
1709            }
1710            if remote_values.len() > 1
1711                || push_remote_values.len() > 1
1712                || section.value_names().any(|name| {
1713                    !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1714                })
1715            {
1716                return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1717                    remote_name.to_owned(),
1718                )));
1719            }
1720            Some(Ok(section.id()))
1721        })
1722        .collect()
1723}
1724
1725fn rename_remote_in_git_branch_config_sections(
1726    config: &mut gix::config::File,
1727    old_remote_name: &RemoteName,
1728    new_remote_name: &RemoteName,
1729) -> Result<(), GitRemoteManagementError> {
1730    for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1731        config
1732            .section_mut_by_id(id)
1733            .expect("found section to exist")
1734            .set(
1735                "remote"
1736                    .try_into()
1737                    .expect("'remote' to be a valid value name"),
1738                BStr::new(new_remote_name.as_str()),
1739            );
1740    }
1741    Ok(())
1742}
1743
1744fn remove_remote_git_branch_config_sections(
1745    config: &mut gix::config::File,
1746    remote_name: &RemoteName,
1747) -> Result<(), GitRemoteManagementError> {
1748    for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1749        config
1750            .remove_section_by_id(id)
1751            .expect("removed section to exist");
1752    }
1753    Ok(())
1754}
1755
1756fn remove_remote_git_config_sections(
1757    config: &mut gix::config::File,
1758    remote_name: &RemoteName,
1759) -> Result<(), GitRemoteManagementError> {
1760    let section_ids_to_remove: Vec<_> = config
1761        .sections_by_name("remote")
1762        .into_iter()
1763        .flatten()
1764        .filter(|section| {
1765            section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1766        })
1767        .map(|section| {
1768            if section.value_names().any(|name| {
1769                !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1770            }) {
1771                return Err(GitRemoteManagementError::NonstandardConfiguration(
1772                    remote_name.to_owned(),
1773                ));
1774            }
1775            Ok(section.id())
1776        })
1777        .try_collect()?;
1778    for id in section_ids_to_remove {
1779        config
1780            .remove_section_by_id(id)
1781            .expect("removed section to exist");
1782    }
1783    Ok(())
1784}
1785
1786/// Returns a sorted list of configured remote names.
1787pub fn get_all_remote_names(
1788    store: &Store,
1789) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1790    let git_repo = get_git_repo(store)?;
1791    let names = git_repo
1792        .remote_names()
1793        .into_iter()
1794        // exclude empty [remote "<name>"] section
1795        .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1796        // ignore non-UTF-8 remote names which we don't support
1797        .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1798        .map(RemoteNameBuf::from)
1799        .collect();
1800    Ok(names)
1801}
1802
1803pub fn add_remote(
1804    store: &Store,
1805    remote_name: &RemoteName,
1806    url: &str,
1807) -> Result<(), GitRemoteManagementError> {
1808    let git_repo = get_git_repo(store)?;
1809
1810    validate_remote_name(remote_name)?;
1811
1812    if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1813        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1814            remote_name.to_owned(),
1815        ));
1816    }
1817
1818    let mut remote = git_repo
1819        .remote_at(url)
1820        .map_err(GitRemoteManagementError::from_git)?
1821        .with_refspecs(
1822            [default_fetch_refspec(remote_name).as_bytes()],
1823            gix::remote::Direction::Fetch,
1824        )
1825        .expect("default refspec to be valid");
1826
1827    let mut config = git_repo.config_snapshot().clone();
1828    save_remote(&mut config, remote_name, &mut remote)?;
1829    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1830
1831    Ok(())
1832}
1833
1834pub fn remove_remote(
1835    mut_repo: &mut MutableRepo,
1836    remote_name: &RemoteName,
1837) -> Result<(), GitRemoteManagementError> {
1838    let mut git_repo = get_git_repo(mut_repo.store())?;
1839
1840    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1841        return Err(GitRemoteManagementError::NoSuchRemote(
1842            remote_name.to_owned(),
1843        ));
1844    };
1845
1846    let mut config = git_repo.config_snapshot().clone();
1847    remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1848    remove_remote_git_config_sections(&mut config, remote_name)?;
1849    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1850
1851    remove_remote_git_refs(&mut git_repo, remote_name)
1852        .map_err(GitRemoteManagementError::from_git)?;
1853
1854    if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1855        remove_remote_refs(mut_repo, remote_name);
1856    }
1857
1858    Ok(())
1859}
1860
1861fn remove_remote_git_refs(
1862    git_repo: &mut gix::Repository,
1863    remote: &RemoteName,
1864) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1865    let edits: Vec<_> = git_repo
1866        .references()?
1867        .prefixed(format!("refs/remotes/{remote}/", remote = remote.as_str()))?
1868        .map_ok(remove_ref)
1869        .try_collect()?;
1870    git_repo.edit_references(edits)?;
1871    Ok(())
1872}
1873
1874fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1875    mut_repo.remove_remote(remote);
1876    let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1877    let git_refs_to_delete = mut_repo
1878        .view()
1879        .git_refs()
1880        .keys()
1881        .filter(|&r| r.as_str().starts_with(&prefix))
1882        .cloned()
1883        .collect_vec();
1884    for git_ref in git_refs_to_delete {
1885        mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1886    }
1887}
1888
1889pub fn rename_remote(
1890    mut_repo: &mut MutableRepo,
1891    old_remote_name: &RemoteName,
1892    new_remote_name: &RemoteName,
1893) -> Result<(), GitRemoteManagementError> {
1894    let mut git_repo = get_git_repo(mut_repo.store())?;
1895
1896    validate_remote_name(new_remote_name)?;
1897
1898    let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1899        return Err(GitRemoteManagementError::NoSuchRemote(
1900            old_remote_name.to_owned(),
1901        ));
1902    };
1903    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1904
1905    if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1906        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1907            new_remote_name.to_owned(),
1908        ));
1909    }
1910
1911    match (
1912        remote.refspecs(gix::remote::Direction::Fetch),
1913        remote.refspecs(gix::remote::Direction::Push),
1914    ) {
1915        ([refspec], [])
1916            if refspec.to_ref().to_bstring()
1917                == default_fetch_refspec(old_remote_name).as_bytes() => {}
1918        _ => {
1919            return Err(GitRemoteManagementError::NonstandardConfiguration(
1920                old_remote_name.to_owned(),
1921            ))
1922        }
1923    }
1924
1925    remote
1926        .replace_refspecs(
1927            [default_fetch_refspec(new_remote_name).as_bytes()],
1928            gix::remote::Direction::Fetch,
1929        )
1930        .expect("default refspec to be valid");
1931
1932    let mut config = git_repo.config_snapshot().clone();
1933    save_remote(&mut config, new_remote_name, &mut remote)?;
1934    rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1935    remove_remote_git_config_sections(&mut config, old_remote_name)?;
1936    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1937
1938    rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1939        .map_err(GitRemoteManagementError::from_git)?;
1940
1941    if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1942        rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1943    }
1944
1945    Ok(())
1946}
1947
1948fn rename_remote_git_refs(
1949    git_repo: &mut gix::Repository,
1950    old_remote_name: &RemoteName,
1951    new_remote_name: &RemoteName,
1952) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1953    let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1954    let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1955    let ref_log_message = BString::from(format!(
1956        "renamed remote {old_remote_name} to {new_remote_name}",
1957        old_remote_name = old_remote_name.as_symbol(),
1958        new_remote_name = new_remote_name.as_symbol(),
1959    ));
1960
1961    let edits: Vec<_> = git_repo
1962        .references()?
1963        .prefixed(old_prefix.clone())?
1964        .map_ok(|old_ref| {
1965            let new_name = BString::new(
1966                [
1967                    new_prefix.as_bytes(),
1968                    &old_ref.name().as_bstr()[old_prefix.len()..],
1969                ]
1970                .concat(),
1971            );
1972            [
1973                add_ref(
1974                    new_name.try_into().expect("new ref name to be valid"),
1975                    old_ref.target().into_owned(),
1976                    ref_log_message.clone(),
1977                ),
1978                remove_ref(old_ref),
1979            ]
1980        })
1981        .flatten_ok()
1982        .try_collect()?;
1983    git_repo.edit_references(edits)?;
1984    Ok(())
1985}
1986
1987/// Set the `url` to be used when fetching data from a remote.
1988///
1989/// Shim for the missing `gix::Remote::fetch_url` API.
1990///
1991/// **TODO:** Upstream an implementation of this to `gix`.
1992fn gix_remote_with_fetch_url<Url, E>(
1993    remote: gix::Remote,
1994    url: Url,
1995) -> Result<gix::Remote, gix::remote::init::Error>
1996where
1997    Url: TryInto<gix::Url, Error = E>,
1998    gix::url::parse::Error: From<E>,
1999{
2000    let mut new_remote = remote.repo().remote_at(url)?;
2001    // Copy the existing data from `remote`.
2002    //
2003    // We don’t copy the push URL, as there does not seem to be any way to reliably
2004    // detect whether one is present with the current API, and `jj git remote
2005    // set-url` refuses to work with them anyway.
2006    new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2007    for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2008        new_remote
2009            .replace_refspecs(
2010                remote
2011                    .refspecs(direction)
2012                    .iter()
2013                    .map(|refspec| refspec.to_ref().to_bstring()),
2014                direction,
2015            )
2016            .expect("existing refspecs to be valid");
2017    }
2018    Ok(new_remote)
2019}
2020
2021pub fn set_remote_url(
2022    store: &Store,
2023    remote_name: &RemoteName,
2024    new_remote_url: &str,
2025) -> Result<(), GitRemoteManagementError> {
2026    let git_repo = get_git_repo(store)?;
2027
2028    validate_remote_name(remote_name)?;
2029
2030    let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2031        return Err(GitRemoteManagementError::NoSuchRemote(
2032            remote_name.to_owned(),
2033        ));
2034    };
2035    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2036
2037    if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2038        return Err(GitRemoteManagementError::NonstandardConfiguration(
2039            remote_name.to_owned(),
2040        ));
2041    }
2042
2043    remote = gix_remote_with_fetch_url(remote, new_remote_url)
2044        .map_err(GitRemoteManagementError::from_git)?;
2045
2046    let mut config = git_repo.config_snapshot().clone();
2047    save_remote(&mut config, remote_name, &mut remote)?;
2048    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2049
2050    Ok(())
2051}
2052
2053fn rename_remote_refs(
2054    mut_repo: &mut MutableRepo,
2055    old_remote_name: &RemoteName,
2056    new_remote_name: &RemoteName,
2057) {
2058    mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2059    let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2060    let git_refs = mut_repo
2061        .view()
2062        .git_refs()
2063        .iter()
2064        .filter_map(|(old, target)| {
2065            old.as_str().strip_prefix(&prefix).map(|p| {
2066                let new: GitRefNameBuf =
2067                    format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2068                (old.clone(), new, target.clone())
2069            })
2070        })
2071        .collect_vec();
2072    for (old, new, target) in git_refs {
2073        mut_repo.set_git_ref_target(&old, RefTarget::absent());
2074        mut_repo.set_git_ref_target(&new, target);
2075    }
2076}
2077
2078const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2079
2080#[derive(Error, Debug)]
2081pub enum GitFetchError {
2082    #[error("No git remote named '{}'", .0.as_symbol())]
2083    NoSuchRemote(RemoteNameBuf),
2084    #[error(
2085        "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2086        chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2087    )]
2088    InvalidBranchPattern(StringPattern),
2089    #[error(transparent)]
2090    RemoteName(#[from] GitRemoteNameError),
2091    #[error(transparent)]
2092    Subprocess(#[from] GitSubprocessError),
2093}
2094
2095struct FetchedBranches {
2096    remote: RemoteNameBuf,
2097    branches: Vec<StringPattern>,
2098}
2099
2100fn expand_fetch_refspecs(
2101    remote: &RemoteName,
2102    branch_names: &[StringPattern],
2103) -> Result<Vec<RefSpec>, GitFetchError> {
2104    branch_names
2105        .iter()
2106        .map(|pattern| {
2107            pattern
2108                .to_glob()
2109                .filter(
2110                    /* This triggered by non-glob `*`s in addition to INVALID_REFSPEC_CHARS
2111                     * because `to_glob()` escapes such `*`s as `[*]`. */
2112                    |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2113                )
2114                .map(|glob| {
2115                    RefSpec::forced(
2116                        format!("refs/heads/{glob}"),
2117                        format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2118                    )
2119                })
2120                .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2121        })
2122        .collect()
2123}
2124
2125/// Helper struct to execute multiple `git fetch` operations
2126pub struct GitFetch<'a> {
2127    mut_repo: &'a mut MutableRepo,
2128    git_repo: Box<gix::Repository>,
2129    git_ctx: GitSubprocessContext<'a>,
2130    git_settings: &'a GitSettings,
2131    fetched: Vec<FetchedBranches>,
2132}
2133
2134impl<'a> GitFetch<'a> {
2135    pub fn new(
2136        mut_repo: &'a mut MutableRepo,
2137        git_settings: &'a GitSettings,
2138    ) -> Result<Self, UnexpectedGitBackendError> {
2139        let git_backend = get_git_backend(mut_repo.store())?;
2140        let git_repo = Box::new(git_backend.git_repo());
2141        let git_ctx =
2142            GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2143        Ok(GitFetch {
2144            mut_repo,
2145            git_repo,
2146            git_ctx,
2147            git_settings,
2148            fetched: vec![],
2149        })
2150    }
2151
2152    /// Perform a `git fetch` on the local git repo, updating the
2153    /// remote-tracking branches in the git repo.
2154    ///
2155    /// Keeps track of the {branch_names, remote_name} pair the refs can be
2156    /// subsequently imported into the `jj` repo by calling `import_refs()`.
2157    #[tracing::instrument(skip(self, callbacks))]
2158    pub fn fetch(
2159        &mut self,
2160        remote_name: &RemoteName,
2161        branch_names: &[StringPattern],
2162        mut callbacks: RemoteCallbacks<'_>,
2163        depth: Option<NonZeroU32>,
2164    ) -> Result<(), GitFetchError> {
2165        validate_remote_name(remote_name)?;
2166
2167        // check the remote exists
2168        if self
2169            .git_repo
2170            .try_find_remote(remote_name.as_str())
2171            .is_none()
2172        {
2173            return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2174        }
2175        // At this point, we are only updating Git's remote tracking branches, not the
2176        // local branches.
2177        let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2178        if remaining_refspecs.is_empty() {
2179            // Don't fall back to the base refspecs.
2180            return Ok(());
2181        }
2182
2183        let mut branches_to_prune = Vec::new();
2184        // git unfortunately errors out if one of the many refspecs is not found
2185        //
2186        // our approach is to filter out failures and retry,
2187        // until either all have failed or an attempt has succeeded
2188        //
2189        // even more unfortunately, git errors out one refspec at a time,
2190        // meaning that the below cycle runs in O(#failed refspecs)
2191        while let Some(failing_refspec) =
2192            self.git_ctx
2193                .spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2194        {
2195            tracing::debug!(failing_refspec, "failed to fetch ref");
2196            remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2197
2198            if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2199                branches_to_prune.push(format!(
2200                    "{remote_name}/{branch_name}",
2201                    remote_name = remote_name.as_str()
2202                ));
2203            }
2204        }
2205
2206        // Even if git fetch has --prune, if a branch is not found it will not be
2207        // pruned on fetch
2208        self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2209
2210        self.fetched.push(FetchedBranches {
2211            remote: remote_name.to_owned(),
2212            branches: branch_names.to_vec(),
2213        });
2214        Ok(())
2215    }
2216
2217    /// Queries remote for the default branch name.
2218    #[tracing::instrument(skip(self))]
2219    pub fn get_default_branch(
2220        &self,
2221        remote_name: &RemoteName,
2222    ) -> Result<Option<RefNameBuf>, GitFetchError> {
2223        if self
2224            .git_repo
2225            .try_find_remote(remote_name.as_str())
2226            .is_none()
2227        {
2228            return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2229        }
2230        let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2231        tracing::debug!(?default_branch);
2232        Ok(default_branch)
2233    }
2234
2235    /// Import the previously fetched remote-tracking branches into the jj repo
2236    /// and update jj's local branches. We also import local tags since remote
2237    /// tags should have been merged by Git.
2238    ///
2239    /// Clears all yet-to-be-imported {branch_names, remote_name} pairs after
2240    /// the import. If `fetch()` has not been called since the last time
2241    /// `import_refs()` was called then this will be a no-op.
2242    #[tracing::instrument(skip(self))]
2243    pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2244        tracing::debug!("import_refs");
2245        let import_stats =
2246            import_some_refs(
2247                self.mut_repo,
2248                self.git_settings,
2249                |kind, symbol| match kind {
2250                    GitRefKind::Bookmark => self
2251                        .fetched
2252                        .iter()
2253                        .filter(|fetched| fetched.remote == symbol.remote)
2254                        .any(|fetched| {
2255                            fetched
2256                                .branches
2257                                .iter()
2258                                .any(|pattern| pattern.matches(symbol.name.as_str()))
2259                        }),
2260                    GitRefKind::Tag => true,
2261                },
2262            )?;
2263
2264        self.fetched.clear();
2265
2266        Ok(import_stats)
2267    }
2268}
2269
2270#[derive(Error, Debug)]
2271pub enum GitPushError {
2272    #[error("No git remote named '{}'", .0.as_symbol())]
2273    NoSuchRemote(RemoteNameBuf),
2274    #[error(transparent)]
2275    RemoteName(#[from] GitRemoteNameError),
2276    #[error(transparent)]
2277    Subprocess(#[from] GitSubprocessError),
2278    #[error(transparent)]
2279    UnexpectedBackend(#[from] UnexpectedGitBackendError),
2280}
2281
2282#[derive(Clone, Debug)]
2283pub struct GitBranchPushTargets {
2284    pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2285}
2286
2287pub struct GitRefUpdate {
2288    pub qualified_name: GitRefNameBuf,
2289    /// Expected position on the remote or None if we expect the ref to not
2290    /// exist on the remote
2291    ///
2292    /// This is sourced from the local remote-tracking branch.
2293    pub expected_current_target: Option<CommitId>,
2294    pub new_target: Option<CommitId>,
2295}
2296
2297/// Pushes the specified branches and updates the repo view accordingly.
2298pub fn push_branches(
2299    mut_repo: &mut MutableRepo,
2300    git_settings: &GitSettings,
2301    remote: &RemoteName,
2302    targets: &GitBranchPushTargets,
2303    callbacks: RemoteCallbacks<'_>,
2304) -> Result<GitPushStats, GitPushError> {
2305    validate_remote_name(remote)?;
2306
2307    let ref_updates = targets
2308        .branch_updates
2309        .iter()
2310        .map(|(name, update)| GitRefUpdate {
2311            qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2312            expected_current_target: update.old_target.clone(),
2313            new_target: update.new_target.clone(),
2314        })
2315        .collect_vec();
2316
2317    let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2318    tracing::debug!(?push_stats);
2319
2320    // TODO: add support for partially pushed refs? we could update the view
2321    // excluding rejected refs, but the transaction would be aborted anyway
2322    // if we returned an Err.
2323    if push_stats.all_ok() {
2324        for (name, update) in &targets.branch_updates {
2325            let git_ref_name: GitRefNameBuf = format!(
2326                "refs/remotes/{remote}/{name}",
2327                remote = remote.as_str(),
2328                name = name.as_str()
2329            )
2330            .into();
2331            let new_remote_ref = RemoteRef {
2332                target: RefTarget::resolved(update.new_target.clone()),
2333                state: RemoteRefState::Tracked,
2334            };
2335            mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2336            mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2337        }
2338    }
2339
2340    Ok(push_stats)
2341}
2342
2343/// Pushes the specified Git refs without updating the repo view.
2344pub fn push_updates(
2345    repo: &dyn Repo,
2346    git_settings: &GitSettings,
2347    remote_name: &RemoteName,
2348    updates: &[GitRefUpdate],
2349    mut callbacks: RemoteCallbacks<'_>,
2350) -> Result<GitPushStats, GitPushError> {
2351    let mut qualified_remote_refs_expected_locations = HashMap::new();
2352    let mut refspecs = vec![];
2353    for update in updates {
2354        qualified_remote_refs_expected_locations.insert(
2355            update.qualified_name.as_ref(),
2356            update.expected_current_target.as_ref(),
2357        );
2358        if let Some(new_target) = &update.new_target {
2359            // We always force-push. We use the push_negotiation callback in
2360            // `push_refs` to check that the refs did not unexpectedly move on
2361            // the remote.
2362            refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2363        } else {
2364            // Prefixing this with `+` to force-push or not should make no
2365            // difference. The push negotiation happens regardless, and wouldn't
2366            // allow creating a branch if it's not a fast-forward.
2367            refspecs.push(RefSpec::delete(&update.qualified_name));
2368        }
2369    }
2370
2371    let git_backend = get_git_backend(repo.store())?;
2372    let git_repo = git_backend.git_repo();
2373    let git_ctx =
2374        GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2375
2376    // check the remote exists
2377    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2378        return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2379    }
2380
2381    let refs_to_push: Vec<RefToPush> = refspecs
2382        .iter()
2383        .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2384        .collect();
2385
2386    let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2387    push_stats.pushed.sort();
2388    push_stats.rejected.sort();
2389    push_stats.remote_rejected.sort();
2390    Ok(push_stats)
2391}
2392
2393#[non_exhaustive]
2394#[derive(Default)]
2395#[expect(clippy::type_complexity)]
2396pub struct RemoteCallbacks<'a> {
2397    pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2398    pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2399    pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2400    pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2401    pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2402}
2403
2404#[derive(Clone, Debug)]
2405pub struct Progress {
2406    /// `Some` iff data transfer is currently in progress
2407    pub bytes_downloaded: Option<u64>,
2408    pub overall: f32,
2409}