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