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        Self {
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        Self {
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        Self {
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        Self::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        Self::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        Self::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)` behavior.
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(&git_repo, &mut index, &parent_tree, &wc_tree).block_on()?;
1349
1350    // Match entries in the new index with entries in the old index, and copy stat
1351    // information if the entry didn't change.
1352    if let Some(old_index) = git_repo.try_index().map_err(GitResetHeadError::from_git)? {
1353        index
1354            .entries_mut_with_paths()
1355            .merge_join_by(old_index.entries(), |(entry, path), old_entry| {
1356                gix::index::Entry::cmp_filepaths(path, old_entry.path(&old_index))
1357                    .then_with(|| entry.stage().cmp(&old_entry.stage()))
1358            })
1359            .filter_map(|merged| merged.both())
1360            .map(|((entry, _), old_entry)| (entry, old_entry))
1361            .filter(|(entry, old_entry)| entry.id == old_entry.id && entry.mode == old_entry.mode)
1362            .for_each(|(entry, old_entry)| entry.stat = old_entry.stat);
1363    }
1364
1365    debug_assert!(index.verify_entries().is_ok());
1366
1367    index
1368        .write(gix::index::write::Options::default())
1369        .map_err(GitResetHeadError::from_git)?;
1370
1371    Ok(())
1372}
1373
1374fn build_index_from_merged_tree(
1375    git_repo: &gix::Repository,
1376    merged_tree: MergedTree,
1377) -> Result<gix::index::File, GitResetHeadError> {
1378    let mut index = gix::index::File::from_state(
1379        gix::index::State::new(git_repo.object_hash()),
1380        git_repo.index_path(),
1381    );
1382
1383    let mut push_index_entry =
1384        |path: &RepoPath, maybe_entry: &Option<TreeValue>, stage: gix::index::entry::Stage| {
1385            let Some(entry) = maybe_entry else {
1386                return;
1387            };
1388
1389            let (id, mode) = match entry {
1390                TreeValue::File {
1391                    id,
1392                    executable,
1393                    copy_id: _,
1394                } => {
1395                    if *executable {
1396                        (id.as_bytes(), gix::index::entry::Mode::FILE_EXECUTABLE)
1397                    } else {
1398                        (id.as_bytes(), gix::index::entry::Mode::FILE)
1399                    }
1400                }
1401                TreeValue::Symlink(id) => (id.as_bytes(), gix::index::entry::Mode::SYMLINK),
1402                TreeValue::Tree(_) => {
1403                    // This case is only possible if there is a file-directory conflict, since
1404                    // `MergedTree::entries` handles the recursion otherwise. We only materialize a
1405                    // file in the working copy for file-directory conflicts, so we don't add the
1406                    // tree to the index here either.
1407                    return;
1408                }
1409                TreeValue::GitSubmodule(id) => (id.as_bytes(), gix::index::entry::Mode::COMMIT),
1410                TreeValue::Conflict(_) => panic!("unexpected merged tree entry: {entry:?}"),
1411            };
1412
1413            let path = BStr::new(path.as_internal_file_string());
1414
1415            // It is safe to push the entry because we ensure that we only add each path to
1416            // a stage once, and we sort the entries after we finish adding them.
1417            index.dangerously_push_entry(
1418                gix::index::entry::Stat::default(),
1419                gix::ObjectId::from_bytes_or_panic(id),
1420                gix::index::entry::Flags::from_stage(stage),
1421                mode,
1422                path,
1423            );
1424        };
1425
1426    let mut has_many_sided_conflict = false;
1427
1428    for (path, entry) in merged_tree.entries() {
1429        let entry = entry?;
1430        if let Some(resolved) = entry.as_resolved() {
1431            push_index_entry(&path, resolved, gix::index::entry::Stage::Unconflicted);
1432            continue;
1433        }
1434
1435        let conflict = entry.simplify();
1436        if let [left, base, right] = conflict.as_slice() {
1437            // 2-sided conflicts can be represented in the Git index
1438            push_index_entry(&path, left, gix::index::entry::Stage::Ours);
1439            push_index_entry(&path, base, gix::index::entry::Stage::Base);
1440            push_index_entry(&path, right, gix::index::entry::Stage::Theirs);
1441        } else {
1442            // We can't represent many-sided conflicts in the Git index, so just add the
1443            // first side as staged. This is preferable to adding the first 2 sides as a
1444            // conflict, since some tools rely on being able to resolve conflicts using the
1445            // index, which could lead to an incorrect conflict resolution if the index
1446            // didn't contain all of the conflict sides. Instead, we add a dummy conflict of
1447            // a file named ".jj-do-not-resolve-this-conflict" to prevent the user from
1448            // accidentally committing the conflict markers.
1449            has_many_sided_conflict = true;
1450            push_index_entry(
1451                &path,
1452                conflict.first(),
1453                gix::index::entry::Stage::Unconflicted,
1454            );
1455        }
1456    }
1457
1458    // Required after `dangerously_push_entry` for correctness. We use do a lookup
1459    // in the index after this, so it must be sorted before we do the lookup.
1460    index.sort_entries();
1461
1462    // If the conflict had an unrepresentable conflict and the dummy file path isn't
1463    // already added in the index, add a dummy file as a conflict.
1464    if has_many_sided_conflict
1465        && index
1466            .entry_index_by_path(INDEX_DUMMY_CONFLICT_FILE.into())
1467            .is_err()
1468    {
1469        let file_blob = git_repo
1470            .write_blob(
1471                b"The working copy commit contains conflicts which cannot be resolved using Git.\n",
1472            )
1473            .map_err(GitResetHeadError::from_git)?;
1474        index.dangerously_push_entry(
1475            gix::index::entry::Stat::default(),
1476            file_blob.detach(),
1477            gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Ours),
1478            gix::index::entry::Mode::FILE,
1479            INDEX_DUMMY_CONFLICT_FILE.into(),
1480        );
1481        // We need to sort again for correctness before writing the index file since we
1482        // added a new entry.
1483        index.sort_entries();
1484    }
1485
1486    Ok(index)
1487}
1488
1489/// Diff `old_tree` to `new_tree` and mark added files as intent-to-add in the
1490/// Git index. Also removes current intent-to-add entries in the index if they
1491/// were removed in the diff.
1492///
1493/// Should be called when the diff between the working-copy commit and its
1494/// parent(s) has changed.
1495pub fn update_intent_to_add(
1496    repo: &dyn Repo,
1497    old_tree: &MergedTree,
1498    new_tree: &MergedTree,
1499) -> Result<(), GitResetHeadError> {
1500    let git_repo = get_git_repo(repo.store())?;
1501    let mut index = git_repo
1502        .index_or_empty()
1503        .map_err(GitResetHeadError::from_git)?;
1504    let mut_index = Arc::make_mut(&mut index);
1505    update_intent_to_add_impl(&git_repo, mut_index, old_tree, new_tree).block_on()?;
1506    debug_assert!(mut_index.verify_entries().is_ok());
1507    mut_index
1508        .write(gix::index::write::Options::default())
1509        .map_err(GitResetHeadError::from_git)?;
1510
1511    Ok(())
1512}
1513
1514async fn update_intent_to_add_impl(
1515    git_repo: &gix::Repository,
1516    index: &mut gix::index::File,
1517    old_tree: &MergedTree,
1518    new_tree: &MergedTree,
1519) -> Result<(), GitResetHeadError> {
1520    let mut diff_stream = old_tree.diff_stream(new_tree, &EverythingMatcher);
1521    let mut added_paths = vec![];
1522    let mut removed_paths = HashSet::new();
1523    while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1524        let (before, after) = values?;
1525        if before.is_absent() {
1526            let executable = match after.as_normal() {
1527                Some(TreeValue::File {
1528                    id: _,
1529                    executable,
1530                    copy_id: _,
1531                }) => *executable,
1532                Some(TreeValue::Symlink(_)) => false,
1533                _ => {
1534                    continue;
1535                }
1536            };
1537            if index
1538                .entry_index_by_path(BStr::new(path.as_internal_file_string()))
1539                .is_err()
1540            {
1541                added_paths.push((BString::from(path.into_internal_string()), executable));
1542            }
1543        } else if after.is_absent() {
1544            removed_paths.insert(BString::from(path.into_internal_string()));
1545        }
1546    }
1547
1548    if added_paths.is_empty() && removed_paths.is_empty() {
1549        return Ok(());
1550    }
1551
1552    if !added_paths.is_empty() {
1553        // We need to write the empty blob, otherwise `jj util gc` will report an error.
1554        let empty_blob = git_repo
1555            .write_blob(b"")
1556            .map_err(GitResetHeadError::from_git)?
1557            .detach();
1558        for (path, executable) in added_paths {
1559            // We have checked that the index doesn't have this entry
1560            index.dangerously_push_entry(
1561                gix::index::entry::Stat::default(),
1562                empty_blob,
1563                gix::index::entry::Flags::INTENT_TO_ADD | gix::index::entry::Flags::EXTENDED,
1564                if executable {
1565                    gix::index::entry::Mode::FILE_EXECUTABLE
1566                } else {
1567                    gix::index::entry::Mode::FILE
1568                },
1569                path.as_ref(),
1570            );
1571        }
1572    }
1573    if !removed_paths.is_empty() {
1574        index.remove_entries(|_size, path, entry| {
1575            entry
1576                .flags
1577                .contains(gix::index::entry::Flags::INTENT_TO_ADD)
1578                && removed_paths.contains(path)
1579        });
1580    }
1581
1582    index.sort_entries();
1583
1584    Ok(())
1585}
1586
1587#[derive(Debug, Error)]
1588pub enum GitRemoteManagementError {
1589    #[error("No git remote named '{}'", .0.as_symbol())]
1590    NoSuchRemote(RemoteNameBuf),
1591    #[error("Git remote named '{}' already exists", .0.as_symbol())]
1592    RemoteAlreadyExists(RemoteNameBuf),
1593    #[error(transparent)]
1594    RemoteName(#[from] GitRemoteNameError),
1595    #[error("Git remote named '{}' has nonstandard configuration", .0.as_symbol())]
1596    NonstandardConfiguration(RemoteNameBuf),
1597    #[error("Error saving Git configuration")]
1598    GitConfigSaveError(#[source] std::io::Error),
1599    #[error("Unexpected Git error when managing remotes")]
1600    InternalGitError(#[source] Box<dyn std::error::Error + Send + Sync>),
1601    #[error(transparent)]
1602    UnexpectedBackend(#[from] UnexpectedGitBackendError),
1603}
1604
1605impl GitRemoteManagementError {
1606    fn from_git(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
1607        Self::InternalGitError(source.into())
1608    }
1609}
1610
1611/// Determine, by its name, if a remote refers to the special local-only "git"
1612/// remote that is used in the Git backend.
1613///
1614/// This function always returns false if the "git" feature is not enabled.
1615pub fn is_special_git_remote(remote: &RemoteName) -> bool {
1616    remote == REMOTE_NAME_FOR_LOCAL_GIT_REPO
1617}
1618
1619fn default_fetch_refspec(remote: &RemoteName) -> String {
1620    format!(
1621        "+refs/heads/*:refs/remotes/{remote}/*",
1622        remote = remote.as_str()
1623    )
1624}
1625
1626fn add_ref(
1627    name: gix::refs::FullName,
1628    target: gix::refs::Target,
1629    message: BString,
1630) -> gix::refs::transaction::RefEdit {
1631    gix::refs::transaction::RefEdit {
1632        change: gix::refs::transaction::Change::Update {
1633            log: gix::refs::transaction::LogChange {
1634                mode: gix::refs::transaction::RefLog::AndReference,
1635                force_create_reflog: false,
1636                message,
1637            },
1638            expected: gix::refs::transaction::PreviousValue::MustNotExist,
1639            new: target,
1640        },
1641        name,
1642        deref: false,
1643    }
1644}
1645
1646fn remove_ref(reference: gix::Reference) -> gix::refs::transaction::RefEdit {
1647    gix::refs::transaction::RefEdit {
1648        change: gix::refs::transaction::Change::Delete {
1649            expected: gix::refs::transaction::PreviousValue::MustExistAndMatch(
1650                reference.target().into_owned(),
1651            ),
1652            log: gix::refs::transaction::RefLog::AndReference,
1653        },
1654        name: reference.name().to_owned(),
1655        deref: false,
1656    }
1657}
1658
1659/// Save an edited [`gix::config::File`] to its original location on disk.
1660///
1661/// Note that the resulting configuration changes are *not* persisted to the
1662/// originating [`gix::Repository`]! The repository must be reloaded with the
1663/// new configuration if necessary.
1664fn save_git_config(config: &gix::config::File) -> std::io::Result<()> {
1665    let mut config_file = File::create(
1666        config
1667            .meta()
1668            .path
1669            .as_ref()
1670            .expect("Git repository to have a config file"),
1671    )?;
1672    config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
1673}
1674
1675fn save_remote(
1676    config: &mut gix::config::File<'static>,
1677    remote_name: &RemoteName,
1678    remote: &mut gix::Remote,
1679) -> Result<(), GitRemoteManagementError> {
1680    // Work around the gitoxide remote management bug
1681    // <https://github.com/GitoxideLabs/gitoxide/issues/1951> by adding
1682    // an empty section.
1683    //
1684    // Note that this will produce useless empty sections if we ever
1685    // support remote configuration keys other than `fetch` and `url`.
1686    config
1687        .new_section(
1688            "remote",
1689            Some(Cow::Owned(BString::from(remote_name.as_str()))),
1690        )
1691        .map_err(GitRemoteManagementError::from_git)?;
1692    remote
1693        .save_as_to(remote_name.as_str(), config)
1694        .map_err(GitRemoteManagementError::from_git)?;
1695    Ok(())
1696}
1697
1698fn git_config_branch_section_ids_by_remote(
1699    config: &gix::config::File,
1700    remote_name: &RemoteName,
1701) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1702    config
1703        .sections_by_name("branch")
1704        .into_iter()
1705        .flatten()
1706        .filter_map(|section| {
1707            let remote_values = section.values("remote");
1708            let push_remote_values = section.values("pushRemote");
1709            if !remote_values
1710                .iter()
1711                .chain(push_remote_values.iter())
1712                .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1713            {
1714                return None;
1715            }
1716            if remote_values.len() > 1
1717                || push_remote_values.len() > 1
1718                || section.value_names().any(|name| {
1719                    !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1720                })
1721            {
1722                return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1723                    remote_name.to_owned(),
1724                )));
1725            }
1726            Some(Ok(section.id()))
1727        })
1728        .collect()
1729}
1730
1731fn rename_remote_in_git_branch_config_sections(
1732    config: &mut gix::config::File,
1733    old_remote_name: &RemoteName,
1734    new_remote_name: &RemoteName,
1735) -> Result<(), GitRemoteManagementError> {
1736    for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1737        config
1738            .section_mut_by_id(id)
1739            .expect("found section to exist")
1740            .set(
1741                "remote"
1742                    .try_into()
1743                    .expect("'remote' to be a valid value name"),
1744                BStr::new(new_remote_name.as_str()),
1745            );
1746    }
1747    Ok(())
1748}
1749
1750fn remove_remote_git_branch_config_sections(
1751    config: &mut gix::config::File,
1752    remote_name: &RemoteName,
1753) -> Result<(), GitRemoteManagementError> {
1754    for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1755        config
1756            .remove_section_by_id(id)
1757            .expect("removed section to exist");
1758    }
1759    Ok(())
1760}
1761
1762fn remove_remote_git_config_sections(
1763    config: &mut gix::config::File,
1764    remote_name: &RemoteName,
1765) -> Result<(), GitRemoteManagementError> {
1766    let section_ids_to_remove: Vec<_> = config
1767        .sections_by_name("remote")
1768        .into_iter()
1769        .flatten()
1770        .filter(|section| {
1771            section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1772        })
1773        .map(|section| {
1774            if section.value_names().any(|name| {
1775                !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1776            }) {
1777                return Err(GitRemoteManagementError::NonstandardConfiguration(
1778                    remote_name.to_owned(),
1779                ));
1780            }
1781            Ok(section.id())
1782        })
1783        .try_collect()?;
1784    for id in section_ids_to_remove {
1785        config
1786            .remove_section_by_id(id)
1787            .expect("removed section to exist");
1788    }
1789    Ok(())
1790}
1791
1792/// Returns a sorted list of configured remote names.
1793pub fn get_all_remote_names(
1794    store: &Store,
1795) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1796    let git_repo = get_git_repo(store)?;
1797    let names = git_repo
1798        .remote_names()
1799        .into_iter()
1800        // exclude empty [remote "<name>"] section
1801        .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1802        // ignore non-UTF-8 remote names which we don't support
1803        .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1804        .map(RemoteNameBuf::from)
1805        .collect();
1806    Ok(names)
1807}
1808
1809pub fn add_remote(
1810    store: &Store,
1811    remote_name: &RemoteName,
1812    url: &str,
1813) -> Result<(), GitRemoteManagementError> {
1814    let git_repo = get_git_repo(store)?;
1815
1816    validate_remote_name(remote_name)?;
1817
1818    if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1819        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1820            remote_name.to_owned(),
1821        ));
1822    }
1823
1824    let mut remote = git_repo
1825        .remote_at(url)
1826        .map_err(GitRemoteManagementError::from_git)?
1827        .with_refspecs(
1828            [default_fetch_refspec(remote_name).as_bytes()],
1829            gix::remote::Direction::Fetch,
1830        )
1831        .expect("default refspec to be valid");
1832
1833    let mut config = git_repo.config_snapshot().clone();
1834    save_remote(&mut config, remote_name, &mut remote)?;
1835    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1836
1837    Ok(())
1838}
1839
1840pub fn remove_remote(
1841    mut_repo: &mut MutableRepo,
1842    remote_name: &RemoteName,
1843) -> Result<(), GitRemoteManagementError> {
1844    let mut git_repo = get_git_repo(mut_repo.store())?;
1845
1846    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1847        return Err(GitRemoteManagementError::NoSuchRemote(
1848            remote_name.to_owned(),
1849        ));
1850    };
1851
1852    let mut config = git_repo.config_snapshot().clone();
1853    remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1854    remove_remote_git_config_sections(&mut config, remote_name)?;
1855    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1856
1857    remove_remote_git_refs(&mut git_repo, remote_name)
1858        .map_err(GitRemoteManagementError::from_git)?;
1859
1860    if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1861        remove_remote_refs(mut_repo, remote_name);
1862    }
1863
1864    Ok(())
1865}
1866
1867fn remove_remote_git_refs(
1868    git_repo: &mut gix::Repository,
1869    remote: &RemoteName,
1870) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1871    let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1872    let edits: Vec<_> = git_repo
1873        .references()?
1874        .prefixed(prefix.as_str())?
1875        .map_ok(remove_ref)
1876        .try_collect()?;
1877    git_repo.edit_references(edits)?;
1878    Ok(())
1879}
1880
1881fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1882    mut_repo.remove_remote(remote);
1883    let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1884    let git_refs_to_delete = mut_repo
1885        .view()
1886        .git_refs()
1887        .keys()
1888        .filter(|&r| r.as_str().starts_with(&prefix))
1889        .cloned()
1890        .collect_vec();
1891    for git_ref in git_refs_to_delete {
1892        mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1893    }
1894}
1895
1896pub fn rename_remote(
1897    mut_repo: &mut MutableRepo,
1898    old_remote_name: &RemoteName,
1899    new_remote_name: &RemoteName,
1900) -> Result<(), GitRemoteManagementError> {
1901    let mut git_repo = get_git_repo(mut_repo.store())?;
1902
1903    validate_remote_name(new_remote_name)?;
1904
1905    let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1906        return Err(GitRemoteManagementError::NoSuchRemote(
1907            old_remote_name.to_owned(),
1908        ));
1909    };
1910    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1911
1912    if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1913        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1914            new_remote_name.to_owned(),
1915        ));
1916    }
1917
1918    match (
1919        remote.refspecs(gix::remote::Direction::Fetch),
1920        remote.refspecs(gix::remote::Direction::Push),
1921    ) {
1922        ([refspec], [])
1923            if refspec.to_ref().to_bstring()
1924                == default_fetch_refspec(old_remote_name).as_bytes() => {}
1925        _ => {
1926            return Err(GitRemoteManagementError::NonstandardConfiguration(
1927                old_remote_name.to_owned(),
1928            ));
1929        }
1930    }
1931
1932    remote
1933        .replace_refspecs(
1934            [default_fetch_refspec(new_remote_name).as_bytes()],
1935            gix::remote::Direction::Fetch,
1936        )
1937        .expect("default refspec to be valid");
1938
1939    let mut config = git_repo.config_snapshot().clone();
1940    save_remote(&mut config, new_remote_name, &mut remote)?;
1941    rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1942    remove_remote_git_config_sections(&mut config, old_remote_name)?;
1943    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1944
1945    rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1946        .map_err(GitRemoteManagementError::from_git)?;
1947
1948    if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1949        rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1950    }
1951
1952    Ok(())
1953}
1954
1955fn rename_remote_git_refs(
1956    git_repo: &mut gix::Repository,
1957    old_remote_name: &RemoteName,
1958    new_remote_name: &RemoteName,
1959) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1960    let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1961    let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1962    let ref_log_message = BString::from(format!(
1963        "renamed remote {old_remote_name} to {new_remote_name}",
1964        old_remote_name = old_remote_name.as_symbol(),
1965        new_remote_name = new_remote_name.as_symbol(),
1966    ));
1967
1968    let edits: Vec<_> = git_repo
1969        .references()?
1970        .prefixed(old_prefix.as_str())?
1971        .map_ok(|old_ref| {
1972            let new_name = BString::new(
1973                [
1974                    new_prefix.as_bytes(),
1975                    &old_ref.name().as_bstr()[old_prefix.len()..],
1976                ]
1977                .concat(),
1978            );
1979            [
1980                add_ref(
1981                    new_name.try_into().expect("new ref name to be valid"),
1982                    old_ref.target().into_owned(),
1983                    ref_log_message.clone(),
1984                ),
1985                remove_ref(old_ref),
1986            ]
1987        })
1988        .flatten_ok()
1989        .try_collect()?;
1990    git_repo.edit_references(edits)?;
1991    Ok(())
1992}
1993
1994/// Set the `url` to be used when fetching data from a remote.
1995///
1996/// Shim for the missing `gix::Remote::fetch_url` API.
1997///
1998/// **TODO:** Upstream an implementation of this to `gix`.
1999fn gix_remote_with_fetch_url<Url, E>(
2000    remote: gix::Remote,
2001    url: Url,
2002) -> Result<gix::Remote, gix::remote::init::Error>
2003where
2004    Url: TryInto<gix::Url, Error = E>,
2005    gix::url::parse::Error: From<E>,
2006{
2007    let mut new_remote = remote.repo().remote_at(url)?;
2008    // Copy the existing data from `remote`.
2009    //
2010    // We don’t copy the push URL, as there does not seem to be any way to reliably
2011    // detect whether one is present with the current API, and `jj git remote
2012    // set-url` refuses to work with them anyway.
2013    new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2014    for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2015        new_remote
2016            .replace_refspecs(
2017                remote
2018                    .refspecs(direction)
2019                    .iter()
2020                    .map(|refspec| refspec.to_ref().to_bstring()),
2021                direction,
2022            )
2023            .expect("existing refspecs to be valid");
2024    }
2025    Ok(new_remote)
2026}
2027
2028pub fn set_remote_url(
2029    store: &Store,
2030    remote_name: &RemoteName,
2031    new_remote_url: &str,
2032) -> Result<(), GitRemoteManagementError> {
2033    let git_repo = get_git_repo(store)?;
2034
2035    validate_remote_name(remote_name)?;
2036
2037    let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2038        return Err(GitRemoteManagementError::NoSuchRemote(
2039            remote_name.to_owned(),
2040        ));
2041    };
2042    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2043
2044    if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2045        return Err(GitRemoteManagementError::NonstandardConfiguration(
2046            remote_name.to_owned(),
2047        ));
2048    }
2049
2050    remote = gix_remote_with_fetch_url(remote, new_remote_url)
2051        .map_err(GitRemoteManagementError::from_git)?;
2052
2053    let mut config = git_repo.config_snapshot().clone();
2054    save_remote(&mut config, remote_name, &mut remote)?;
2055    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2056
2057    Ok(())
2058}
2059
2060fn rename_remote_refs(
2061    mut_repo: &mut MutableRepo,
2062    old_remote_name: &RemoteName,
2063    new_remote_name: &RemoteName,
2064) {
2065    mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2066    let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2067    let git_refs = mut_repo
2068        .view()
2069        .git_refs()
2070        .iter()
2071        .filter_map(|(old, target)| {
2072            old.as_str().strip_prefix(&prefix).map(|p| {
2073                let new: GitRefNameBuf =
2074                    format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2075                (old.clone(), new, target.clone())
2076            })
2077        })
2078        .collect_vec();
2079    for (old, new, target) in git_refs {
2080        mut_repo.set_git_ref_target(&old, RefTarget::absent());
2081        mut_repo.set_git_ref_target(&new, target);
2082    }
2083}
2084
2085const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2086
2087#[derive(Error, Debug)]
2088pub enum GitFetchError {
2089    #[error("No git remote named '{}'", .0.as_symbol())]
2090    NoSuchRemote(RemoteNameBuf),
2091    #[error(
2092        "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2093        chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2094    )]
2095    InvalidBranchPattern(StringPattern),
2096    #[error(transparent)]
2097    RemoteName(#[from] GitRemoteNameError),
2098    #[error(transparent)]
2099    Subprocess(#[from] GitSubprocessError),
2100}
2101
2102struct FetchedBranches {
2103    remote: RemoteNameBuf,
2104    branches: Vec<StringPattern>,
2105}
2106
2107fn expand_fetch_refspecs(
2108    remote: &RemoteName,
2109    branch_names: &[StringPattern],
2110) -> Result<Vec<RefSpec>, GitFetchError> {
2111    branch_names
2112        .iter()
2113        .map(|pattern| {
2114            pattern
2115                .to_glob()
2116                .filter(
2117                    /* This triggered by non-glob `*`s in addition to INVALID_REFSPEC_CHARS
2118                     * because `to_glob()` escapes such `*`s as `[*]`. */
2119                    |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2120                )
2121                .map(|glob| {
2122                    RefSpec::forced(
2123                        format!("refs/heads/{glob}"),
2124                        format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2125                    )
2126                })
2127                .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2128        })
2129        .collect()
2130}
2131
2132/// Helper struct to execute multiple `git fetch` operations
2133pub struct GitFetch<'a> {
2134    mut_repo: &'a mut MutableRepo,
2135    git_repo: Box<gix::Repository>,
2136    git_ctx: GitSubprocessContext<'a>,
2137    git_settings: &'a GitSettings,
2138    fetched: Vec<FetchedBranches>,
2139}
2140
2141impl<'a> GitFetch<'a> {
2142    pub fn new(
2143        mut_repo: &'a mut MutableRepo,
2144        git_settings: &'a GitSettings,
2145    ) -> Result<Self, UnexpectedGitBackendError> {
2146        let git_backend = get_git_backend(mut_repo.store())?;
2147        let git_repo = Box::new(git_backend.git_repo());
2148        let git_ctx =
2149            GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2150        Ok(GitFetch {
2151            mut_repo,
2152            git_repo,
2153            git_ctx,
2154            git_settings,
2155            fetched: vec![],
2156        })
2157    }
2158
2159    /// Perform a `git fetch` on the local git repo, updating the
2160    /// remote-tracking branches in the git repo.
2161    ///
2162    /// Keeps track of the {branch_names, remote_name} pair the refs can be
2163    /// subsequently imported into the `jj` repo by calling `import_refs()`.
2164    #[tracing::instrument(skip(self, callbacks))]
2165    pub fn fetch(
2166        &mut self,
2167        remote_name: &RemoteName,
2168        branch_names: &[StringPattern],
2169        mut callbacks: RemoteCallbacks<'_>,
2170        depth: Option<NonZeroU32>,
2171    ) -> Result<(), GitFetchError> {
2172        validate_remote_name(remote_name)?;
2173
2174        // check the remote exists
2175        if self
2176            .git_repo
2177            .try_find_remote(remote_name.as_str())
2178            .is_none()
2179        {
2180            return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2181        }
2182        // At this point, we are only updating Git's remote tracking branches, not the
2183        // local branches.
2184        let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2185        if remaining_refspecs.is_empty() {
2186            // Don't fall back to the base refspecs.
2187            return Ok(());
2188        }
2189
2190        let mut branches_to_prune = Vec::new();
2191        // git unfortunately errors out if one of the many refspecs is not found
2192        //
2193        // our approach is to filter out failures and retry,
2194        // until either all have failed or an attempt has succeeded
2195        //
2196        // even more unfortunately, git errors out one refspec at a time,
2197        // meaning that the below cycle runs in O(#failed refspecs)
2198        while let Some(failing_refspec) =
2199            self.git_ctx
2200                .spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2201        {
2202            tracing::debug!(failing_refspec, "failed to fetch ref");
2203            remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2204
2205            if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2206                branches_to_prune.push(format!(
2207                    "{remote_name}/{branch_name}",
2208                    remote_name = remote_name.as_str()
2209                ));
2210            }
2211        }
2212
2213        // Even if git fetch has --prune, if a branch is not found it will not be
2214        // pruned on fetch
2215        self.git_ctx.spawn_branch_prune(&branches_to_prune)?;
2216
2217        self.fetched.push(FetchedBranches {
2218            remote: remote_name.to_owned(),
2219            branches: branch_names.to_vec(),
2220        });
2221        Ok(())
2222    }
2223
2224    /// Queries remote for the default branch name.
2225    #[tracing::instrument(skip(self))]
2226    pub fn get_default_branch(
2227        &self,
2228        remote_name: &RemoteName,
2229    ) -> Result<Option<RefNameBuf>, GitFetchError> {
2230        if self
2231            .git_repo
2232            .try_find_remote(remote_name.as_str())
2233            .is_none()
2234        {
2235            return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2236        }
2237        let default_branch = self.git_ctx.spawn_remote_show(remote_name)?;
2238        tracing::debug!(?default_branch);
2239        Ok(default_branch)
2240    }
2241
2242    /// Import the previously fetched remote-tracking branches into the jj repo
2243    /// and update jj's local branches. We also import local tags since remote
2244    /// tags should have been merged by Git.
2245    ///
2246    /// Clears all yet-to-be-imported {branch_names, remote_name} pairs after
2247    /// the import. If `fetch()` has not been called since the last time
2248    /// `import_refs()` was called then this will be a no-op.
2249    #[tracing::instrument(skip(self))]
2250    pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2251        tracing::debug!("import_refs");
2252        let import_stats =
2253            import_some_refs(
2254                self.mut_repo,
2255                self.git_settings,
2256                |kind, symbol| match kind {
2257                    GitRefKind::Bookmark => self
2258                        .fetched
2259                        .iter()
2260                        .filter(|fetched| fetched.remote == symbol.remote)
2261                        .any(|fetched| {
2262                            fetched
2263                                .branches
2264                                .iter()
2265                                .any(|pattern| pattern.is_match(symbol.name.as_str()))
2266                        }),
2267                    GitRefKind::Tag => true,
2268                },
2269            )?;
2270
2271        self.fetched.clear();
2272
2273        Ok(import_stats)
2274    }
2275}
2276
2277#[derive(Error, Debug)]
2278pub enum GitPushError {
2279    #[error("No git remote named '{}'", .0.as_symbol())]
2280    NoSuchRemote(RemoteNameBuf),
2281    #[error(transparent)]
2282    RemoteName(#[from] GitRemoteNameError),
2283    #[error(transparent)]
2284    Subprocess(#[from] GitSubprocessError),
2285    #[error(transparent)]
2286    UnexpectedBackend(#[from] UnexpectedGitBackendError),
2287}
2288
2289#[derive(Clone, Debug)]
2290pub struct GitBranchPushTargets {
2291    pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2292}
2293
2294pub struct GitRefUpdate {
2295    pub qualified_name: GitRefNameBuf,
2296    /// Expected position on the remote or None if we expect the ref to not
2297    /// exist on the remote
2298    ///
2299    /// This is sourced from the local remote-tracking branch.
2300    pub expected_current_target: Option<CommitId>,
2301    pub new_target: Option<CommitId>,
2302}
2303
2304/// Pushes the specified branches and updates the repo view accordingly.
2305pub fn push_branches(
2306    mut_repo: &mut MutableRepo,
2307    git_settings: &GitSettings,
2308    remote: &RemoteName,
2309    targets: &GitBranchPushTargets,
2310    callbacks: RemoteCallbacks<'_>,
2311) -> Result<GitPushStats, GitPushError> {
2312    validate_remote_name(remote)?;
2313
2314    let ref_updates = targets
2315        .branch_updates
2316        .iter()
2317        .map(|(name, update)| GitRefUpdate {
2318            qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2319            expected_current_target: update.old_target.clone(),
2320            new_target: update.new_target.clone(),
2321        })
2322        .collect_vec();
2323
2324    let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2325    tracing::debug!(?push_stats);
2326
2327    // TODO: add support for partially pushed refs? we could update the view
2328    // excluding rejected refs, but the transaction would be aborted anyway
2329    // if we returned an Err.
2330    if push_stats.all_ok() {
2331        for (name, update) in &targets.branch_updates {
2332            let git_ref_name: GitRefNameBuf = format!(
2333                "refs/remotes/{remote}/{name}",
2334                remote = remote.as_str(),
2335                name = name.as_str()
2336            )
2337            .into();
2338            let new_remote_ref = RemoteRef {
2339                target: RefTarget::resolved(update.new_target.clone()),
2340                state: RemoteRefState::Tracked,
2341            };
2342            mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2343            mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2344        }
2345    }
2346
2347    Ok(push_stats)
2348}
2349
2350/// Pushes the specified Git refs without updating the repo view.
2351pub fn push_updates(
2352    repo: &dyn Repo,
2353    git_settings: &GitSettings,
2354    remote_name: &RemoteName,
2355    updates: &[GitRefUpdate],
2356    mut callbacks: RemoteCallbacks<'_>,
2357) -> Result<GitPushStats, GitPushError> {
2358    let mut qualified_remote_refs_expected_locations = HashMap::new();
2359    let mut refspecs = vec![];
2360    for update in updates {
2361        qualified_remote_refs_expected_locations.insert(
2362            update.qualified_name.as_ref(),
2363            update.expected_current_target.as_ref(),
2364        );
2365        if let Some(new_target) = &update.new_target {
2366            // We always force-push. We use the push_negotiation callback in
2367            // `push_refs` to check that the refs did not unexpectedly move on
2368            // the remote.
2369            refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2370        } else {
2371            // Prefixing this with `+` to force-push or not should make no
2372            // difference. The push negotiation happens regardless, and wouldn't
2373            // allow creating a branch if it's not a fast-forward.
2374            refspecs.push(RefSpec::delete(&update.qualified_name));
2375        }
2376    }
2377
2378    let git_backend = get_git_backend(repo.store())?;
2379    let git_repo = git_backend.git_repo();
2380    let git_ctx =
2381        GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2382
2383    // check the remote exists
2384    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2385        return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2386    }
2387
2388    let refs_to_push: Vec<RefToPush> = refspecs
2389        .iter()
2390        .map(|full_refspec| RefToPush::new(full_refspec, &qualified_remote_refs_expected_locations))
2391        .collect();
2392
2393    let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2394    push_stats.pushed.sort();
2395    push_stats.rejected.sort();
2396    push_stats.remote_rejected.sort();
2397    Ok(push_stats)
2398}
2399
2400#[non_exhaustive]
2401#[derive(Default)]
2402#[expect(clippy::type_complexity)]
2403pub struct RemoteCallbacks<'a> {
2404    pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2405    pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2406    pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2407    pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2408    pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2409}
2410
2411#[derive(Clone, Debug)]
2412pub struct Progress {
2413    /// `Some` iff data transfer is currently in progress
2414    pub bytes_downloaded: Option<u64>,
2415    pub overall: f32,
2416}