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