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