jj_lib/
git.rs

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