jj_lib/
git.rs

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