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