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