Skip to main content

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