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