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