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.into_backend_error())?
568        .iter()
569        .try_collect()
570        .map_err(|err| err.into_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 save_remote(
1677    config: &mut gix::config::File<'static>,
1678    remote_name: &RemoteName,
1679    remote: &mut gix::Remote,
1680) -> Result<(), GitRemoteManagementError> {
1681    // Work around the gitoxide remote management bug
1682    // <https://github.com/GitoxideLabs/gitoxide/issues/1951> by adding
1683    // an empty section.
1684    //
1685    // Note that this will produce useless empty sections if we ever
1686    // support remote configuration keys other than `fetch` and `url`.
1687    config
1688        .new_section(
1689            "remote",
1690            Some(Cow::Owned(BString::from(remote_name.as_str()))),
1691        )
1692        .map_err(GitRemoteManagementError::from_git)?;
1693    remote
1694        .save_as_to(remote_name.as_str(), config)
1695        .map_err(GitRemoteManagementError::from_git)?;
1696    Ok(())
1697}
1698
1699fn git_config_branch_section_ids_by_remote(
1700    config: &gix::config::File,
1701    remote_name: &RemoteName,
1702) -> Result<Vec<gix::config::file::SectionId>, GitRemoteManagementError> {
1703    config
1704        .sections_by_name("branch")
1705        .into_iter()
1706        .flatten()
1707        .filter_map(|section| {
1708            let remote_values = section.values("remote");
1709            let push_remote_values = section.values("pushRemote");
1710            if !remote_values
1711                .iter()
1712                .chain(push_remote_values.iter())
1713                .any(|branch_remote_name| **branch_remote_name == remote_name.as_str())
1714            {
1715                return None;
1716            }
1717            if remote_values.len() > 1
1718                || push_remote_values.len() > 1
1719                || section.value_names().any(|name| {
1720                    !name.eq_ignore_ascii_case(b"remote") && !name.eq_ignore_ascii_case(b"merge")
1721                })
1722            {
1723                return Some(Err(GitRemoteManagementError::NonstandardConfiguration(
1724                    remote_name.to_owned(),
1725                )));
1726            }
1727            Some(Ok(section.id()))
1728        })
1729        .collect()
1730}
1731
1732fn rename_remote_in_git_branch_config_sections(
1733    config: &mut gix::config::File,
1734    old_remote_name: &RemoteName,
1735    new_remote_name: &RemoteName,
1736) -> Result<(), GitRemoteManagementError> {
1737    for id in git_config_branch_section_ids_by_remote(config, old_remote_name)? {
1738        config
1739            .section_mut_by_id(id)
1740            .expect("found section to exist")
1741            .set(
1742                "remote"
1743                    .try_into()
1744                    .expect("'remote' to be a valid value name"),
1745                BStr::new(new_remote_name.as_str()),
1746            );
1747    }
1748    Ok(())
1749}
1750
1751fn remove_remote_git_branch_config_sections(
1752    config: &mut gix::config::File,
1753    remote_name: &RemoteName,
1754) -> Result<(), GitRemoteManagementError> {
1755    for id in git_config_branch_section_ids_by_remote(config, remote_name)? {
1756        config
1757            .remove_section_by_id(id)
1758            .expect("removed section to exist");
1759    }
1760    Ok(())
1761}
1762
1763fn remove_remote_git_config_sections(
1764    config: &mut gix::config::File,
1765    remote_name: &RemoteName,
1766) -> Result<(), GitRemoteManagementError> {
1767    let section_ids_to_remove: Vec<_> = config
1768        .sections_by_name("remote")
1769        .into_iter()
1770        .flatten()
1771        .filter(|section| {
1772            section.header().subsection_name() == Some(BStr::new(remote_name.as_str()))
1773        })
1774        .map(|section| {
1775            if section.value_names().any(|name| {
1776                !name.eq_ignore_ascii_case(b"url") && !name.eq_ignore_ascii_case(b"fetch")
1777            }) {
1778                return Err(GitRemoteManagementError::NonstandardConfiguration(
1779                    remote_name.to_owned(),
1780                ));
1781            }
1782            Ok(section.id())
1783        })
1784        .try_collect()?;
1785    for id in section_ids_to_remove {
1786        config
1787            .remove_section_by_id(id)
1788            .expect("removed section to exist");
1789    }
1790    Ok(())
1791}
1792
1793/// Returns a sorted list of configured remote names.
1794pub fn get_all_remote_names(
1795    store: &Store,
1796) -> Result<Vec<RemoteNameBuf>, UnexpectedGitBackendError> {
1797    let git_repo = get_git_repo(store)?;
1798    let names = git_repo
1799        .remote_names()
1800        .into_iter()
1801        // exclude empty [remote "<name>"] section
1802        .filter(|name| git_repo.try_find_remote(name.as_ref()).is_some())
1803        // ignore non-UTF-8 remote names which we don't support
1804        .filter_map(|name| String::from_utf8(name.into_owned().into()).ok())
1805        .map(RemoteNameBuf::from)
1806        .collect();
1807    Ok(names)
1808}
1809
1810pub fn add_remote(
1811    store: &Store,
1812    remote_name: &RemoteName,
1813    url: &str,
1814) -> Result<(), GitRemoteManagementError> {
1815    let git_repo = get_git_repo(store)?;
1816
1817    validate_remote_name(remote_name)?;
1818
1819    if git_repo.try_find_remote(remote_name.as_str()).is_some() {
1820        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1821            remote_name.to_owned(),
1822        ));
1823    }
1824
1825    let mut remote = git_repo
1826        .remote_at(url)
1827        .map_err(GitRemoteManagementError::from_git)?
1828        .with_refspecs(
1829            [default_fetch_refspec(remote_name).as_bytes()],
1830            gix::remote::Direction::Fetch,
1831        )
1832        .expect("default refspec to be valid");
1833
1834    let mut config = git_repo.config_snapshot().clone();
1835    save_remote(&mut config, remote_name, &mut remote)?;
1836    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1837
1838    Ok(())
1839}
1840
1841pub fn remove_remote(
1842    mut_repo: &mut MutableRepo,
1843    remote_name: &RemoteName,
1844) -> Result<(), GitRemoteManagementError> {
1845    let mut git_repo = get_git_repo(mut_repo.store())?;
1846
1847    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
1848        return Err(GitRemoteManagementError::NoSuchRemote(
1849            remote_name.to_owned(),
1850        ));
1851    };
1852
1853    let mut config = git_repo.config_snapshot().clone();
1854    remove_remote_git_branch_config_sections(&mut config, remote_name)?;
1855    remove_remote_git_config_sections(&mut config, remote_name)?;
1856    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1857
1858    remove_remote_git_refs(&mut git_repo, remote_name)
1859        .map_err(GitRemoteManagementError::from_git)?;
1860
1861    if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1862        remove_remote_refs(mut_repo, remote_name);
1863    }
1864
1865    Ok(())
1866}
1867
1868fn remove_remote_git_refs(
1869    git_repo: &mut gix::Repository,
1870    remote: &RemoteName,
1871) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1872    let edits: Vec<_> = git_repo
1873        .references()?
1874        .prefixed(format!("refs/remotes/{remote}/", remote = remote.as_str()))?
1875        .map_ok(remove_ref)
1876        .try_collect()?;
1877    git_repo.edit_references(edits)?;
1878    Ok(())
1879}
1880
1881fn remove_remote_refs(mut_repo: &mut MutableRepo, remote: &RemoteName) {
1882    mut_repo.remove_remote(remote);
1883    let prefix = format!("refs/remotes/{remote}/", remote = remote.as_str());
1884    let git_refs_to_delete = mut_repo
1885        .view()
1886        .git_refs()
1887        .keys()
1888        .filter(|&r| r.as_str().starts_with(&prefix))
1889        .cloned()
1890        .collect_vec();
1891    for git_ref in git_refs_to_delete {
1892        mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
1893    }
1894}
1895
1896pub fn rename_remote(
1897    mut_repo: &mut MutableRepo,
1898    old_remote_name: &RemoteName,
1899    new_remote_name: &RemoteName,
1900) -> Result<(), GitRemoteManagementError> {
1901    let mut git_repo = get_git_repo(mut_repo.store())?;
1902
1903    validate_remote_name(new_remote_name)?;
1904
1905    let Some(result) = git_repo.try_find_remote(old_remote_name.as_str()) else {
1906        return Err(GitRemoteManagementError::NoSuchRemote(
1907            old_remote_name.to_owned(),
1908        ));
1909    };
1910    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
1911
1912    if git_repo.try_find_remote(new_remote_name.as_str()).is_some() {
1913        return Err(GitRemoteManagementError::RemoteAlreadyExists(
1914            new_remote_name.to_owned(),
1915        ));
1916    }
1917
1918    match (
1919        remote.refspecs(gix::remote::Direction::Fetch),
1920        remote.refspecs(gix::remote::Direction::Push),
1921    ) {
1922        ([refspec], [])
1923            if refspec.to_ref().to_bstring()
1924                == default_fetch_refspec(old_remote_name).as_bytes() => {}
1925        _ => {
1926            return Err(GitRemoteManagementError::NonstandardConfiguration(
1927                old_remote_name.to_owned(),
1928            ))
1929        }
1930    }
1931
1932    remote
1933        .replace_refspecs(
1934            [default_fetch_refspec(new_remote_name).as_bytes()],
1935            gix::remote::Direction::Fetch,
1936        )
1937        .expect("default refspec to be valid");
1938
1939    let mut config = git_repo.config_snapshot().clone();
1940    save_remote(&mut config, new_remote_name, &mut remote)?;
1941    rename_remote_in_git_branch_config_sections(&mut config, old_remote_name, new_remote_name)?;
1942    remove_remote_git_config_sections(&mut config, old_remote_name)?;
1943    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
1944
1945    rename_remote_git_refs(&mut git_repo, old_remote_name, new_remote_name)
1946        .map_err(GitRemoteManagementError::from_git)?;
1947
1948    if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
1949        rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
1950    }
1951
1952    Ok(())
1953}
1954
1955fn rename_remote_git_refs(
1956    git_repo: &mut gix::Repository,
1957    old_remote_name: &RemoteName,
1958    new_remote_name: &RemoteName,
1959) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
1960    let old_prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
1961    let new_prefix = format!("refs/remotes/{}/", new_remote_name.as_str());
1962    let ref_log_message = BString::from(format!(
1963        "renamed remote {old_remote_name} to {new_remote_name}",
1964        old_remote_name = old_remote_name.as_symbol(),
1965        new_remote_name = new_remote_name.as_symbol(),
1966    ));
1967
1968    let edits: Vec<_> = git_repo
1969        .references()?
1970        .prefixed(old_prefix.clone())?
1971        .map_ok(|old_ref| {
1972            let new_name = BString::new(
1973                [
1974                    new_prefix.as_bytes(),
1975                    &old_ref.name().as_bstr()[old_prefix.len()..],
1976                ]
1977                .concat(),
1978            );
1979            [
1980                add_ref(
1981                    new_name.try_into().expect("new ref name to be valid"),
1982                    old_ref.target().into_owned(),
1983                    ref_log_message.clone(),
1984                ),
1985                remove_ref(old_ref),
1986            ]
1987        })
1988        .flatten_ok()
1989        .try_collect()?;
1990    git_repo.edit_references(edits)?;
1991    Ok(())
1992}
1993
1994/// Set the `url` to be used when fetching data from a remote.
1995///
1996/// Shim for the missing `gix::Remote::fetch_url` API.
1997///
1998/// **TODO:** Upstream an implementation of this to `gix`.
1999fn gix_remote_with_fetch_url<Url, E>(
2000    remote: gix::Remote,
2001    url: Url,
2002) -> Result<gix::Remote, gix::remote::init::Error>
2003where
2004    Url: TryInto<gix::Url, Error = E>,
2005    gix::url::parse::Error: From<E>,
2006{
2007    let mut new_remote = remote.repo().remote_at(url)?;
2008    // Copy the existing data from `remote`.
2009    //
2010    // We don’t copy the push URL, as there does not seem to be any way to reliably
2011    // detect whether one is present with the current API, and `jj git remote
2012    // set-url` refuses to work with them anyway.
2013    new_remote = new_remote.with_fetch_tags(remote.fetch_tags());
2014    for direction in [gix::remote::Direction::Fetch, gix::remote::Direction::Push] {
2015        new_remote
2016            .replace_refspecs(
2017                remote
2018                    .refspecs(direction)
2019                    .iter()
2020                    .map(|refspec| refspec.to_ref().to_bstring()),
2021                direction,
2022            )
2023            .expect("existing refspecs to be valid");
2024    }
2025    Ok(new_remote)
2026}
2027
2028pub fn set_remote_url(
2029    store: &Store,
2030    remote_name: &RemoteName,
2031    new_remote_url: &str,
2032) -> Result<(), GitRemoteManagementError> {
2033    let git_repo = get_git_repo(store)?;
2034
2035    validate_remote_name(remote_name)?;
2036
2037    let Some(result) = git_repo.try_find_remote_without_url_rewrite(remote_name.as_str()) else {
2038        return Err(GitRemoteManagementError::NoSuchRemote(
2039            remote_name.to_owned(),
2040        ));
2041    };
2042    let mut remote = result.map_err(GitRemoteManagementError::from_git)?;
2043
2044    if remote.url(gix::remote::Direction::Push) != remote.url(gix::remote::Direction::Fetch) {
2045        return Err(GitRemoteManagementError::NonstandardConfiguration(
2046            remote_name.to_owned(),
2047        ));
2048    }
2049
2050    remote = gix_remote_with_fetch_url(remote, new_remote_url)
2051        .map_err(GitRemoteManagementError::from_git)?;
2052
2053    let mut config = git_repo.config_snapshot().clone();
2054    save_remote(&mut config, remote_name, &mut remote)?;
2055    save_git_config(&config).map_err(GitRemoteManagementError::GitConfigSaveError)?;
2056
2057    Ok(())
2058}
2059
2060fn rename_remote_refs(
2061    mut_repo: &mut MutableRepo,
2062    old_remote_name: &RemoteName,
2063    new_remote_name: &RemoteName,
2064) {
2065    mut_repo.rename_remote(old_remote_name.as_ref(), new_remote_name.as_ref());
2066    let prefix = format!("refs/remotes/{}/", old_remote_name.as_str());
2067    let git_refs = mut_repo
2068        .view()
2069        .git_refs()
2070        .iter()
2071        .filter_map(|(old, target)| {
2072            old.as_str().strip_prefix(&prefix).map(|p| {
2073                let new: GitRefNameBuf =
2074                    format!("refs/remotes/{}/{p}", new_remote_name.as_str()).into();
2075                (old.clone(), new, target.clone())
2076            })
2077        })
2078        .collect_vec();
2079    for (old, new, target) in git_refs {
2080        mut_repo.set_git_ref_target(&old, RefTarget::absent());
2081        mut_repo.set_git_ref_target(&new, target);
2082    }
2083}
2084
2085#[cfg(feature = "git2")]
2086fn open_git2_repo(git_backend: &GitBackend) -> Result<git2::Repository, git2::Error> {
2087    let mut flags = git2::RepositoryOpenFlags::NO_SEARCH;
2088    if std::env::var("JJ_DEBUG_HERMETIC_GIT2").as_deref() == Ok("1") {
2089        flags.insert(git2::RepositoryOpenFlags::FROM_ENV);
2090    }
2091    git2::Repository::open_ext(
2092        git_backend.git_repo_path(),
2093        flags,
2094        &[] as &[&std::ffi::OsStr],
2095    )
2096}
2097
2098const INVALID_REFSPEC_CHARS: [char; 5] = [':', '^', '?', '[', ']'];
2099
2100#[derive(Error, Debug)]
2101pub enum GitFetchError {
2102    #[error("No git remote named '{}'", .0.as_symbol())]
2103    NoSuchRemote(RemoteNameBuf),
2104    #[error(
2105        "Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `{chars}`",
2106        chars = INVALID_REFSPEC_CHARS.iter().join("`, `")
2107    )]
2108    InvalidBranchPattern(StringPattern),
2109    #[error(transparent)]
2110    RemoteName(#[from] GitRemoteNameError),
2111    #[cfg(feature = "git2")]
2112    #[error(transparent)]
2113    Git2(#[from] git2::Error),
2114    #[error(transparent)]
2115    Subprocess(#[from] GitSubprocessError),
2116}
2117
2118// TODO: If Git2 implementation is removed, this can be replaced with
2119// UnexpectedGitBackendError.
2120#[derive(Debug, Error)]
2121pub enum GitFetchPrepareError {
2122    #[cfg(feature = "git2")]
2123    #[error(transparent)]
2124    Git2(#[from] git2::Error),
2125    #[error(transparent)]
2126    UnexpectedBackend(#[from] UnexpectedGitBackendError),
2127}
2128
2129#[cfg(feature = "git2")]
2130fn git2_fetch_options(
2131    mut callbacks: RemoteCallbacks<'_>,
2132    depth: Option<NonZeroU32>,
2133) -> git2::FetchOptions<'_> {
2134    let mut proxy_options = git2::ProxyOptions::new();
2135    proxy_options.auto();
2136
2137    let mut fetch_options = git2::FetchOptions::new();
2138    fetch_options.proxy_options(proxy_options);
2139    // git2 doesn't provide API to set "no-progress" protocol option. If
2140    // sideband callback were enabled, remote progress messages would be written
2141    // no matter if the process was attached to a tty or not.
2142    if callbacks.progress.is_none() {
2143        callbacks.sideband_progress = None;
2144    }
2145    fetch_options.remote_callbacks(callbacks.into_git());
2146    if let Some(depth) = depth {
2147        fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX));
2148    }
2149
2150    fetch_options
2151}
2152
2153struct FetchedBranches {
2154    remote: RemoteNameBuf,
2155    branches: Vec<StringPattern>,
2156}
2157
2158/// Helper struct to execute multiple `git fetch` operations
2159pub struct GitFetch<'a> {
2160    mut_repo: &'a mut MutableRepo,
2161    fetch_impl: GitFetchImpl<'a>,
2162    git_settings: &'a GitSettings,
2163    fetched: Vec<FetchedBranches>,
2164}
2165
2166impl<'a> GitFetch<'a> {
2167    pub fn new(
2168        mut_repo: &'a mut MutableRepo,
2169        git_settings: &'a GitSettings,
2170    ) -> Result<Self, GitFetchPrepareError> {
2171        let fetch_impl = GitFetchImpl::new(mut_repo.store(), git_settings)?;
2172        Ok(GitFetch {
2173            mut_repo,
2174            fetch_impl,
2175            git_settings,
2176            fetched: vec![],
2177        })
2178    }
2179
2180    /// Perform a `git fetch` on the local git repo, updating the
2181    /// remote-tracking branches in the git repo.
2182    ///
2183    /// Keeps track of the {branch_names, remote_name} pair the refs can be
2184    /// subsequently imported into the `jj` repo by calling `import_refs()`.
2185    #[tracing::instrument(skip(self, callbacks))]
2186    pub fn fetch(
2187        &mut self,
2188        remote_name: &RemoteName,
2189        branch_names: &[StringPattern],
2190        callbacks: RemoteCallbacks<'_>,
2191        depth: Option<NonZeroU32>,
2192    ) -> Result<(), GitFetchError> {
2193        validate_remote_name(remote_name)?;
2194        self.fetch_impl
2195            .fetch(remote_name, branch_names, callbacks, depth)?;
2196        self.fetched.push(FetchedBranches {
2197            remote: remote_name.to_owned(),
2198            branches: branch_names.to_vec(),
2199        });
2200        Ok(())
2201    }
2202
2203    /// Queries remote for the default branch name.
2204    #[tracing::instrument(skip(self, callbacks))]
2205    pub fn get_default_branch(
2206        &self,
2207        remote_name: &RemoteName,
2208        callbacks: RemoteCallbacks<'_>,
2209    ) -> Result<Option<RefNameBuf>, GitFetchError> {
2210        self.fetch_impl.get_default_branch(remote_name, callbacks)
2211    }
2212
2213    /// Import the previously fetched remote-tracking branches into the jj repo
2214    /// and update jj's local branches. We also import local tags since remote
2215    /// tags should have been merged by Git.
2216    ///
2217    /// Clears all yet-to-be-imported {branch_names, remote_name} pairs after
2218    /// the import. If `fetch()` has not been called since the last time
2219    /// `import_refs()` was called then this will be a no-op.
2220    #[tracing::instrument(skip(self))]
2221    pub fn import_refs(&mut self) -> Result<GitImportStats, GitImportError> {
2222        tracing::debug!("import_refs");
2223        let import_stats =
2224            import_some_refs(
2225                self.mut_repo,
2226                self.git_settings,
2227                |kind, symbol| match kind {
2228                    GitRefKind::Bookmark => self
2229                        .fetched
2230                        .iter()
2231                        .filter(|fetched| fetched.remote == symbol.remote)
2232                        .any(|fetched| {
2233                            fetched
2234                                .branches
2235                                .iter()
2236                                .any(|pattern| pattern.matches(symbol.name.as_str()))
2237                        }),
2238                    GitRefKind::Tag => true,
2239                },
2240            )?;
2241
2242        self.fetched.clear();
2243
2244        Ok(import_stats)
2245    }
2246}
2247
2248fn expand_fetch_refspecs(
2249    remote: &RemoteName,
2250    branch_names: &[StringPattern],
2251) -> Result<Vec<RefSpec>, GitFetchError> {
2252    branch_names
2253        .iter()
2254        .map(|pattern| {
2255            pattern
2256                .to_glob()
2257                .filter(
2258                    /* This triggered by non-glob `*`s in addition to INVALID_REFSPEC_CHARS
2259                     * because `to_glob()` escapes such `*`s as `[*]`. */
2260                    |glob| !glob.contains(INVALID_REFSPEC_CHARS),
2261                )
2262                .map(|glob| {
2263                    RefSpec::forced(
2264                        format!("refs/heads/{glob}"),
2265                        format!("refs/remotes/{remote}/{glob}", remote = remote.as_str()),
2266                    )
2267                })
2268                .ok_or_else(|| GitFetchError::InvalidBranchPattern(pattern.clone()))
2269        })
2270        .collect()
2271}
2272
2273enum GitFetchImpl<'a> {
2274    #[cfg(feature = "git2")]
2275    Git2 { git_repo: git2::Repository },
2276    Subprocess {
2277        git_repo: Box<gix::Repository>,
2278        git_ctx: GitSubprocessContext<'a>,
2279    },
2280}
2281
2282impl<'a> GitFetchImpl<'a> {
2283    fn new(store: &Store, git_settings: &'a GitSettings) -> Result<Self, GitFetchPrepareError> {
2284        let git_backend = get_git_backend(store)?;
2285        #[cfg(feature = "git2")]
2286        if !git_settings.subprocess {
2287            let git_repo = open_git2_repo(git_backend)?;
2288            return Ok(GitFetchImpl::Git2 { git_repo });
2289        }
2290        let git_repo = Box::new(git_backend.git_repo());
2291        let git_ctx =
2292            GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2293        Ok(GitFetchImpl::Subprocess { git_repo, git_ctx })
2294    }
2295
2296    fn fetch(
2297        &self,
2298        remote_name: &RemoteName,
2299        branch_names: &[StringPattern],
2300        callbacks: RemoteCallbacks<'_>,
2301        depth: Option<NonZeroU32>,
2302    ) -> Result<(), GitFetchError> {
2303        match self {
2304            #[cfg(feature = "git2")]
2305            GitFetchImpl::Git2 { git_repo } => {
2306                git2_fetch(git_repo, remote_name, branch_names, callbacks, depth)
2307            }
2308            GitFetchImpl::Subprocess { git_repo, git_ctx } => subprocess_fetch(
2309                git_repo,
2310                git_ctx,
2311                remote_name,
2312                branch_names,
2313                callbacks,
2314                depth,
2315            ),
2316        }
2317    }
2318
2319    fn get_default_branch(
2320        &self,
2321        remote_name: &RemoteName,
2322        callbacks: RemoteCallbacks<'_>,
2323    ) -> Result<Option<RefNameBuf>, GitFetchError> {
2324        match self {
2325            #[cfg(feature = "git2")]
2326            GitFetchImpl::Git2 { git_repo } => {
2327                git2_get_default_branch(git_repo, remote_name, callbacks)
2328            }
2329            GitFetchImpl::Subprocess { git_repo, git_ctx } => {
2330                subprocess_get_default_branch(git_repo, git_ctx, remote_name, callbacks)
2331            }
2332        }
2333    }
2334}
2335
2336#[cfg(feature = "git2")]
2337fn git2_fetch(
2338    git_repo: &git2::Repository,
2339    remote_name: &RemoteName,
2340    branch_names: &[StringPattern],
2341    callbacks: RemoteCallbacks<'_>,
2342    depth: Option<NonZeroU32>,
2343) -> Result<(), GitFetchError> {
2344    let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2345        if is_remote_not_found_err(&err) {
2346            GitFetchError::NoSuchRemote(remote_name.to_owned())
2347        } else {
2348            GitFetchError::Git2(err)
2349        }
2350    })?;
2351    // At this point, we are only updating Git's remote tracking branches, not the
2352    // local branches.
2353    let refspecs: Vec<String> = expand_fetch_refspecs(remote_name, branch_names)?
2354        .iter()
2355        .map(|refspec| refspec.to_git_format())
2356        .collect();
2357
2358    if refspecs.is_empty() {
2359        // Don't fall back to the base refspecs.
2360        return Ok(());
2361    }
2362
2363    tracing::debug!("remote.download");
2364    remote.download(&refspecs, Some(&mut git2_fetch_options(callbacks, depth)))?;
2365    tracing::debug!("remote.prune");
2366    remote.prune(None)?;
2367    tracing::debug!("remote.update_tips");
2368    remote.update_tips(
2369        None,
2370        git2::RemoteUpdateFlags::empty(),
2371        git2::AutotagOption::Unspecified,
2372        None,
2373    )?;
2374    tracing::debug!("remote.disconnect");
2375    remote.disconnect()?;
2376    Ok(())
2377}
2378
2379#[cfg(feature = "git2")]
2380fn git2_get_default_branch(
2381    git_repo: &git2::Repository,
2382    remote_name: &RemoteName,
2383    callbacks: RemoteCallbacks<'_>,
2384) -> Result<Option<RefNameBuf>, GitFetchError> {
2385    let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2386        if is_remote_not_found_err(&err) {
2387            GitFetchError::NoSuchRemote(remote_name.to_owned())
2388        } else {
2389            GitFetchError::Git2(err)
2390        }
2391    })?;
2392    // Unlike .download(), connect_auth() returns RAII object.
2393    tracing::debug!("remote.connect");
2394    let connection = {
2395        let mut proxy_options = git2::ProxyOptions::new();
2396        proxy_options.auto();
2397        remote.connect_auth(
2398            git2::Direction::Fetch,
2399            Some(callbacks.into_git()),
2400            Some(proxy_options),
2401        )?
2402    };
2403    let mut default_branch = None;
2404    tracing::debug!("remote.default_branch");
2405    if let Ok(default_ref_buf) = connection.default_branch() {
2406        if let Some(default_ref) = default_ref_buf.as_str() {
2407            // Here the ref should point to local branch on the remote
2408            if let Some(branch_name) = default_ref
2409                .strip_prefix("refs/heads/")
2410                .filter(|&name| name != "HEAD")
2411            {
2412                tracing::debug!(default_branch = branch_name);
2413                default_branch = Some(branch_name.into());
2414            }
2415        }
2416    }
2417    Ok(default_branch)
2418}
2419
2420fn subprocess_fetch(
2421    git_repo: &gix::Repository,
2422    git_ctx: &GitSubprocessContext,
2423    remote_name: &RemoteName,
2424    branch_names: &[StringPattern],
2425    mut callbacks: RemoteCallbacks<'_>,
2426    depth: Option<NonZeroU32>,
2427) -> Result<(), GitFetchError> {
2428    // check the remote exists
2429    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2430        return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2431    }
2432    // At this point, we are only updating Git's remote tracking branches, not the
2433    // local branches.
2434    let mut remaining_refspecs: Vec<_> = expand_fetch_refspecs(remote_name, branch_names)?;
2435    if remaining_refspecs.is_empty() {
2436        // Don't fall back to the base refspecs.
2437        return Ok(());
2438    }
2439
2440    let mut branches_to_prune = Vec::new();
2441    // git unfortunately errors out if one of the many refspecs is not found
2442    //
2443    // our approach is to filter out failures and retry,
2444    // until either all have failed or an attempt has succeeded
2445    //
2446    // even more unfortunately, git errors out one refspec at a time,
2447    // meaning that the below cycle runs in O(#failed refspecs)
2448    while let Some(failing_refspec) =
2449        git_ctx.spawn_fetch(remote_name, &remaining_refspecs, &mut callbacks, depth)?
2450    {
2451        tracing::debug!(failing_refspec, "failed to fetch ref");
2452        remaining_refspecs.retain(|r| r.source.as_ref() != Some(&failing_refspec));
2453
2454        if let Some(branch_name) = failing_refspec.strip_prefix("refs/heads/") {
2455            branches_to_prune.push(format!(
2456                "{remote_name}/{branch_name}",
2457                remote_name = remote_name.as_str()
2458            ));
2459        }
2460    }
2461
2462    // Even if git fetch has --prune, if a branch is not found it will not be
2463    // pruned on fetch
2464    git_ctx.spawn_branch_prune(&branches_to_prune)?;
2465    Ok(())
2466}
2467
2468fn subprocess_get_default_branch(
2469    git_repo: &gix::Repository,
2470    git_ctx: &GitSubprocessContext,
2471    remote_name: &RemoteName,
2472    _callbacks: RemoteCallbacks<'_>,
2473) -> Result<Option<RefNameBuf>, GitFetchError> {
2474    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2475        return Err(GitFetchError::NoSuchRemote(remote_name.to_owned()));
2476    }
2477    let default_branch = git_ctx.spawn_remote_show(remote_name)?;
2478    tracing::debug!(?default_branch);
2479    Ok(default_branch)
2480}
2481
2482#[derive(Error, Debug)]
2483pub enum GitPushError {
2484    #[error("No git remote named '{}'", .0.as_symbol())]
2485    NoSuchRemote(RemoteNameBuf),
2486    #[error(transparent)]
2487    RemoteName(#[from] GitRemoteNameError),
2488    #[cfg(feature = "git2")]
2489    #[error(transparent)]
2490    Git2(#[from] git2::Error),
2491    #[error(transparent)]
2492    Subprocess(#[from] GitSubprocessError),
2493    #[error(transparent)]
2494    UnexpectedBackend(#[from] UnexpectedGitBackendError),
2495}
2496
2497#[derive(Clone, Debug)]
2498pub struct GitBranchPushTargets {
2499    pub branch_updates: Vec<(RefNameBuf, BookmarkPushUpdate)>,
2500}
2501
2502pub struct GitRefUpdate {
2503    pub qualified_name: GitRefNameBuf,
2504    /// Expected position on the remote or None if we expect the ref to not
2505    /// exist on the remote
2506    ///
2507    /// This is sourced from the local remote-tracking branch.
2508    pub expected_current_target: Option<CommitId>,
2509    pub new_target: Option<CommitId>,
2510}
2511
2512/// Pushes the specified branches and updates the repo view accordingly.
2513pub fn push_branches(
2514    mut_repo: &mut MutableRepo,
2515    git_settings: &GitSettings,
2516    remote: &RemoteName,
2517    targets: &GitBranchPushTargets,
2518    callbacks: RemoteCallbacks<'_>,
2519) -> Result<GitPushStats, GitPushError> {
2520    validate_remote_name(remote)?;
2521
2522    let ref_updates = targets
2523        .branch_updates
2524        .iter()
2525        .map(|(name, update)| GitRefUpdate {
2526            qualified_name: format!("refs/heads/{name}", name = name.as_str()).into(),
2527            expected_current_target: update.old_target.clone(),
2528            new_target: update.new_target.clone(),
2529        })
2530        .collect_vec();
2531
2532    let push_stats = push_updates(mut_repo, git_settings, remote, &ref_updates, callbacks)?;
2533    tracing::debug!(?push_stats);
2534
2535    // TODO: add support for partially pushed refs? we could update the view
2536    // excluding rejected refs, but the transaction would be aborted anyway
2537    // if we returned an Err.
2538    if push_stats.all_ok() {
2539        for (name, update) in &targets.branch_updates {
2540            let git_ref_name: GitRefNameBuf = format!(
2541                "refs/remotes/{remote}/{name}",
2542                remote = remote.as_str(),
2543                name = name.as_str()
2544            )
2545            .into();
2546            let new_remote_ref = RemoteRef {
2547                target: RefTarget::resolved(update.new_target.clone()),
2548                state: RemoteRefState::Tracked,
2549            };
2550            mut_repo.set_git_ref_target(&git_ref_name, new_remote_ref.target.clone());
2551            mut_repo.set_remote_bookmark(name.to_remote_symbol(remote), new_remote_ref);
2552        }
2553    }
2554
2555    Ok(push_stats)
2556}
2557
2558/// Pushes the specified Git refs without updating the repo view.
2559pub fn push_updates(
2560    repo: &dyn Repo,
2561    git_settings: &GitSettings,
2562    remote_name: &RemoteName,
2563    updates: &[GitRefUpdate],
2564    callbacks: RemoteCallbacks<'_>,
2565) -> Result<GitPushStats, GitPushError> {
2566    let mut qualified_remote_refs_expected_locations = HashMap::new();
2567    let mut refspecs = vec![];
2568    for update in updates {
2569        qualified_remote_refs_expected_locations.insert(
2570            update.qualified_name.as_ref(),
2571            update.expected_current_target.as_ref(),
2572        );
2573        if let Some(new_target) = &update.new_target {
2574            // We always force-push. We use the push_negotiation callback in
2575            // `push_refs` to check that the refs did not unexpectedly move on
2576            // the remote.
2577            refspecs.push(RefSpec::forced(new_target.hex(), &update.qualified_name));
2578        } else {
2579            // Prefixing this with `+` to force-push or not should make no
2580            // difference. The push negotiation happens regardless, and wouldn't
2581            // allow creating a branch if it's not a fast-forward.
2582            refspecs.push(RefSpec::delete(&update.qualified_name));
2583        }
2584    }
2585    // TODO(ilyagr): `push_refs`, or parts of it, should probably be inlined. This
2586    // requires adjusting some tests.
2587
2588    let git_backend = get_git_backend(repo.store())?;
2589    #[cfg(feature = "git2")]
2590    if !git_settings.subprocess {
2591        let git_repo = open_git2_repo(git_backend)?;
2592        let refspecs: Vec<String> = refspecs.iter().map(RefSpec::to_git_format).collect();
2593        return git2_push_refs(
2594            repo,
2595            &git_repo,
2596            remote_name,
2597            &qualified_remote_refs_expected_locations,
2598            &refspecs,
2599            callbacks,
2600        );
2601    }
2602    let git_repo = git_backend.git_repo();
2603    let git_ctx =
2604        GitSubprocessContext::from_git_backend(git_backend, &git_settings.executable_path);
2605    subprocess_push_refs(
2606        &git_repo,
2607        &git_ctx,
2608        remote_name,
2609        &qualified_remote_refs_expected_locations,
2610        &refspecs,
2611        callbacks,
2612    )
2613}
2614
2615#[cfg(feature = "git2")]
2616fn git2_push_refs(
2617    repo: &dyn Repo,
2618    git_repo: &git2::Repository,
2619    remote_name: &RemoteName,
2620    qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2621    refspecs: &[String],
2622    callbacks: RemoteCallbacks<'_>,
2623) -> Result<GitPushStats, GitPushError> {
2624    let mut remote = git_repo.find_remote(remote_name.as_str()).map_err(|err| {
2625        if is_remote_not_found_err(&err) {
2626            GitPushError::NoSuchRemote(remote_name.to_owned())
2627        } else {
2628            GitPushError::Git2(err)
2629        }
2630    })?;
2631
2632    let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs_expected_locations
2633        .keys()
2634        .copied()
2635        .collect();
2636    let mut failed_push_negotiations = vec![];
2637    let mut pushed_refs = vec![];
2638
2639    let push_result = {
2640        let mut push_options = git2::PushOptions::new();
2641        let mut proxy_options = git2::ProxyOptions::new();
2642        proxy_options.auto();
2643        push_options.proxy_options(proxy_options);
2644        let mut callbacks = callbacks.into_git();
2645        callbacks.push_negotiation(|updates| {
2646            for update in updates {
2647                let dst_refname: &GitRefName = update
2648                    .dst_refname()
2649                    .expect("Expect reference name to be valid UTF-8")
2650                    .as_ref();
2651                let expected_remote_location = *qualified_remote_refs_expected_locations
2652                    .get(dst_refname)
2653                    .expect("Push is trying to move a ref it wasn't asked to move");
2654                let oid_to_maybe_commitid =
2655                    |oid: git2::Oid| (!oid.is_zero()).then(|| CommitId::from_bytes(oid.as_bytes()));
2656                let actual_remote_location = oid_to_maybe_commitid(update.src());
2657                let local_location = oid_to_maybe_commitid(update.dst());
2658
2659                match allow_push(
2660                    repo.index(),
2661                    actual_remote_location.as_ref(),
2662                    expected_remote_location,
2663                    local_location.as_ref(),
2664                ) {
2665                    Ok(PushAllowReason::NormalMatch) => {}
2666                    Ok(PushAllowReason::UnexpectedNoop) => {
2667                        tracing::info!(
2668                            "The push of {dst_refname:?} is unexpectedly a no-op, the remote \
2669                             branch is already at {actual_remote_location:?}. We expected it to \
2670                             be at {expected_remote_location:?}. We don't consider this an error.",
2671                        );
2672                    }
2673                    Ok(PushAllowReason::ExceptionalFastforward) => {
2674                        // TODO(ilyagr): We could consider printing a user-facing message at
2675                        // this point.
2676                        tracing::info!(
2677                            "We allow the push of {dst_refname:?} to {local_location:?}, even \
2678                             though it is unexpectedly at {actual_remote_location:?} on the \
2679                             server rather than the expected {expected_remote_location:?}. The \
2680                             desired location is a descendant of the actual location, and the \
2681                             actual location is a descendant of the expected location.",
2682                        );
2683                    }
2684                    Err(()) => {
2685                        // While we show debug info in the message with `--debug`,
2686                        // there's probably no need to show the detailed commit
2687                        // locations to the user normally. They should do a `jj git
2688                        // fetch`, and the resulting branch conflicts should contain
2689                        // all the information they need.
2690                        tracing::info!(
2691                            "Cannot push {dst_refname:?} to {local_location:?}; it is at \
2692                             unexpectedly at {actual_remote_location:?} on the server as opposed \
2693                             to the expected {expected_remote_location:?}",
2694                        );
2695                        failed_push_negotiations.push(dst_refname.to_owned());
2696                    }
2697                }
2698            }
2699
2700            if failed_push_negotiations.is_empty() {
2701                Ok(())
2702            } else {
2703                Err(git2::Error::from_str("failed push negotiation"))
2704            }
2705        });
2706        callbacks.push_update_reference(|refname, status| {
2707            let refname = GitRefName::new(refname);
2708            // The status is Some if the ref update was rejected by the remote
2709            if status.is_none() {
2710                remaining_remote_refs.remove(refname);
2711                pushed_refs.push(refname.to_owned());
2712            }
2713            Ok(())
2714        });
2715        push_options.remote_callbacks(callbacks);
2716        remote.push(refspecs, Some(&mut push_options))
2717    };
2718
2719    for failed_update in &failed_push_negotiations {
2720        remaining_remote_refs.remove(&**failed_update);
2721    }
2722    let rejected: Vec<_> = failed_push_negotiations
2723        .into_iter()
2724        .sorted()
2725        .map(|name| (name, None))
2726        .collect();
2727    let remote_rejected: Vec<_> = remaining_remote_refs
2728        .into_iter()
2729        .sorted()
2730        .map(|name| (name.to_owned(), None))
2731        .collect();
2732    pushed_refs.sort();
2733
2734    let push_stats = if !rejected.is_empty() {
2735        // If the push negotiation returned an error, `remote.push` would not
2736        // have pushed anything and would have returned an error, as expected.
2737        // However, the error it returns is not necessarily the error we'd
2738        // expect. It also depends on the exact versions of `libgit2` and
2739        // `git2.rs`. So, we cannot rely on it containing any useful
2740        // information. See https://github.com/rust-lang/git2-rs/issues/1042.
2741
2742        assert!(push_result.is_err());
2743        GitPushStats {
2744            rejected,
2745            remote_rejected,
2746            ..Default::default()
2747        }
2748    } else {
2749        push_result?;
2750        GitPushStats {
2751            pushed: pushed_refs,
2752            remote_rejected,
2753            ..Default::default()
2754        }
2755    };
2756
2757    Ok(push_stats)
2758}
2759
2760fn subprocess_push_refs(
2761    git_repo: &gix::Repository,
2762    git_ctx: &GitSubprocessContext,
2763    remote_name: &RemoteName,
2764    qualified_remote_refs_expected_locations: &HashMap<&GitRefName, Option<&CommitId>>,
2765    refspecs: &[RefSpec],
2766    mut callbacks: RemoteCallbacks<'_>,
2767) -> Result<GitPushStats, GitPushError> {
2768    // check the remote exists
2769    if git_repo.try_find_remote(remote_name.as_str()).is_none() {
2770        return Err(GitPushError::NoSuchRemote(remote_name.to_owned()));
2771    }
2772
2773    let refs_to_push: Vec<RefToPush> = refspecs
2774        .iter()
2775        .map(|full_refspec| RefToPush::new(full_refspec, qualified_remote_refs_expected_locations))
2776        .collect();
2777
2778    let mut push_stats = git_ctx.spawn_push(remote_name, &refs_to_push, &mut callbacks)?;
2779    push_stats.pushed.sort();
2780    push_stats.rejected.sort();
2781    push_stats.remote_rejected.sort();
2782    Ok(push_stats)
2783}
2784
2785#[cfg(feature = "git2")]
2786#[derive(Debug, Clone, PartialEq, Eq)]
2787enum PushAllowReason {
2788    NormalMatch,
2789    ExceptionalFastforward,
2790    UnexpectedNoop,
2791}
2792
2793#[cfg(feature = "git2")]
2794fn allow_push(
2795    index: &dyn Index,
2796    actual_remote_location: Option<&CommitId>,
2797    expected_remote_location: Option<&CommitId>,
2798    destination_location: Option<&CommitId>,
2799) -> Result<PushAllowReason, ()> {
2800    if actual_remote_location == expected_remote_location {
2801        return Ok(PushAllowReason::NormalMatch);
2802    }
2803
2804    // If the remote ref is in an unexpected location, we still allow some
2805    // pushes, based on whether `jj git fetch` would result in a conflicted ref.
2806    //
2807    // For `merge_ref_targets` to work correctly, `actual_remote_location` must
2808    // be a commit that we locally know about.
2809    //
2810    // This does not lose any generality since for `merge_ref_targets` to
2811    // resolve to `local_target` below, it is conceptually necessary (but not
2812    // sufficient) for the destination_location to be either a descendant of
2813    // actual_remote_location or equal to it. Either way, we would know about that
2814    // commit locally.
2815    if !actual_remote_location.is_none_or(|id| index.has_id(id)) {
2816        return Err(());
2817    }
2818    let remote_target = RefTarget::resolved(actual_remote_location.cloned());
2819    let base_target = RefTarget::resolved(expected_remote_location.cloned());
2820    // The push destination is the local position of the ref
2821    let local_target = RefTarget::resolved(destination_location.cloned());
2822    if refs::merge_ref_targets(index, &remote_target, &base_target, &local_target) == local_target {
2823        // Fetch would not change the local branch, so the push is OK in spite of
2824        // the discrepancy with the expected location. We return some debug info and
2825        // verify some invariants before OKing the push.
2826        Ok(if actual_remote_location == destination_location {
2827            // This is the situation of what we call "A - B + A = A"
2828            // conflicts, see also test_refs.rs and
2829            // https://github.com/jj-vcs/jj/blob/c9b44f382824301e6c0fdd6f4cbc52bb00c50995/lib/src/merge.rs#L92.
2830            PushAllowReason::UnexpectedNoop
2831        } else {
2832            // Due to our ref merge rules, this case should happen if an only
2833            // if:
2834            //
2835            // 1. This is a fast-forward.
2836            // 2. The expected location is an ancestor of both the actual location and the
2837            //    destination (local position).
2838            PushAllowReason::ExceptionalFastforward
2839        })
2840    } else {
2841        Err(())
2842    }
2843}
2844
2845#[non_exhaustive]
2846#[derive(Default)]
2847#[expect(clippy::type_complexity)]
2848pub struct RemoteCallbacks<'a> {
2849    pub progress: Option<&'a mut dyn FnMut(&Progress)>,
2850    pub sideband_progress: Option<&'a mut dyn FnMut(&[u8])>,
2851    pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
2852    pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
2853    pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
2854}
2855
2856#[cfg(feature = "git2")]
2857impl<'a> RemoteCallbacks<'a> {
2858    fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
2859        let mut callbacks = git2::RemoteCallbacks::new();
2860        if let Some(progress_cb) = self.progress {
2861            callbacks.transfer_progress(move |progress| {
2862                progress_cb(&Progress {
2863                    bytes_downloaded: (progress.received_objects() < progress.total_objects())
2864                        .then(|| progress.received_bytes() as u64),
2865                    overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
2866                        / (progress.total_objects() + progress.total_deltas()) as f32,
2867                });
2868                true
2869            });
2870        }
2871        if let Some(sideband_progress_cb) = self.sideband_progress {
2872            callbacks.sideband_progress(move |data| {
2873                sideband_progress_cb(data);
2874                true
2875            });
2876        }
2877        // TODO: We should expose the callbacks to the caller instead -- the library
2878        // crate shouldn't read environment variables.
2879        let mut tried_ssh_agent = false;
2880        let mut ssh_key_paths_to_try: Option<Vec<PathBuf>> = None;
2881        callbacks.credentials(move |url, username_from_url, allowed_types| {
2882            let span = tracing::debug_span!("RemoteCallbacks.credentials");
2883            let _ = span.enter();
2884
2885            let git_config = git2::Config::open_default();
2886            let credential_helper = git_config
2887                .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
2888            if let Ok(creds) = credential_helper {
2889                tracing::info!("using credential_helper");
2890                return Ok(creds);
2891            } else if let Some(username) = username_from_url {
2892                if allowed_types.contains(git2::CredentialType::SSH_KEY) {
2893                    // Try to get the SSH key from the agent once. We don't even check if
2894                    // $SSH_AUTH_SOCK is set because Windows uses another mechanism.
2895                    if !tried_ssh_agent {
2896                        tracing::info!(username, "trying ssh_key_from_agent");
2897                        tried_ssh_agent = true;
2898                        return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
2899                            tracing::error!(err = %err);
2900                            err
2901                        });
2902                    }
2903
2904                    let paths = ssh_key_paths_to_try.get_or_insert_with(|| {
2905                        if let Some(ref mut cb) = self.get_ssh_keys {
2906                            let mut paths = cb(username);
2907                            paths.reverse();
2908                            paths
2909                        } else {
2910                            vec![]
2911                        }
2912                    });
2913
2914                    if let Some(path) = paths.pop() {
2915                        tracing::info!(username, path = ?path, "trying ssh_key");
2916                        return git2::Cred::ssh_key(username, None, &path, None).map_err(|err| {
2917                            tracing::error!(err = %err);
2918                            err
2919                        });
2920                    }
2921                }
2922                if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2923                    if let Some(ref mut cb) = self.get_password {
2924                        if let Some(pw) = cb(url, username) {
2925                            tracing::info!(
2926                                username,
2927                                "using userpass_plaintext with username from url"
2928                            );
2929                            return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
2930                                tracing::error!(err = %err);
2931                                err
2932                            });
2933                        }
2934                    }
2935                }
2936            } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
2937                if let Some(ref mut cb) = self.get_username_password {
2938                    if let Some((username, pw)) = cb(url) {
2939                        tracing::info!(username, "using userpass_plaintext");
2940                        return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
2941                            tracing::error!(err = %err);
2942                            err
2943                        });
2944                    }
2945                }
2946            }
2947            tracing::info!("using default");
2948            git2::Cred::default()
2949        });
2950        callbacks
2951    }
2952}
2953
2954#[derive(Clone, Debug)]
2955pub struct Progress {
2956    /// `Some` iff data transfer is currently in progress
2957    pub bytes_downloaded: Option<u64>,
2958    pub overall: f32,
2959}