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