2022-11-26 23:57:50 +00:00
|
|
|
// Copyright 2020 The Jujutsu Authors
|
2020-12-28 01:41:20 +00:00
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2023-07-10 15:17:00 +00:00
|
|
|
#![allow(missing_docs)]
|
|
|
|
|
2023-08-04 23:02:06 +00:00
|
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
2022-10-31 16:10:22 +00:00
|
|
|
use std::default::Default;
|
2023-04-03 23:28:19 +00:00
|
|
|
use std::io::Read;
|
2023-08-04 01:10:32 +00:00
|
|
|
use std::iter;
|
2022-11-06 17:51:23 +00:00
|
|
|
use std::path::PathBuf;
|
2021-07-15 08:31:48 +00:00
|
|
|
|
2022-11-06 17:36:52 +00:00
|
|
|
use git2::Oid;
|
2021-09-11 05:35:31 +00:00
|
|
|
use itertools::Itertools;
|
2023-04-03 23:28:19 +00:00
|
|
|
use tempfile::NamedTempFile;
|
2021-03-14 17:37:28 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2023-08-09 21:21:13 +00:00
|
|
|
use crate::backend::{BackendError, CommitId, ObjectId};
|
2023-10-03 10:11:55 +00:00
|
|
|
use crate::commit::Commit;
|
2023-09-08 07:29:09 +00:00
|
|
|
use crate::git_backend::GitBackend;
|
2023-09-25 14:34:09 +00:00
|
|
|
use crate::op_store::{RefTarget, RefTargetOptionExt};
|
2023-02-14 21:51:55 +00:00
|
|
|
use crate::repo::{MutableRepo, Repo};
|
2023-09-30 05:58:32 +00:00
|
|
|
use crate::revset::{self, RevsetExpression};
|
2022-12-13 00:18:19 +00:00
|
|
|
use crate::settings::GitSettings;
|
2023-06-11 04:44:48 +00:00
|
|
|
use crate::view::{RefName, View};
|
2020-12-29 07:31:48 +00:00
|
|
|
|
2023-08-22 07:02:54 +00:00
|
|
|
/// Reserved remote name for the backing Git repo.
|
|
|
|
pub const REMOTE_NAME_FOR_LOCAL_GIT_REPO: &str = "git";
|
2023-10-03 10:45:03 +00:00
|
|
|
/// Ref name used as a placeholder to unset HEAD without a commit.
|
|
|
|
const UNBORN_ROOT_REF_NAME: &str = "refs/jj/root";
|
2023-08-22 07:02:54 +00:00
|
|
|
|
2023-08-09 21:21:13 +00:00
|
|
|
#[derive(Error, Debug)]
|
2020-12-29 07:31:48 +00:00
|
|
|
pub enum GitImportError {
|
2023-08-09 21:21:13 +00:00
|
|
|
#[error("Failed to read Git HEAD target commit {id}: {err}", id=id.hex())]
|
|
|
|
MissingHeadTarget {
|
|
|
|
id: CommitId,
|
|
|
|
#[source]
|
|
|
|
err: BackendError,
|
|
|
|
},
|
|
|
|
#[error("Ancestor of Git ref {ref_name} is missing: {err}")]
|
|
|
|
MissingRefAncestor {
|
|
|
|
ref_name: String,
|
|
|
|
#[source]
|
|
|
|
err: BackendError,
|
|
|
|
},
|
2023-08-29 04:45:56 +00:00
|
|
|
#[error(
|
|
|
|
"Git remote named '{name}' is reserved for local Git repository",
|
|
|
|
name = REMOTE_NAME_FOR_LOCAL_GIT_REPO
|
|
|
|
)]
|
|
|
|
RemoteReservedForLocalGitRepo,
|
2021-01-01 20:53:07 +00:00
|
|
|
#[error("Unexpected git error when importing refs: {0}")]
|
|
|
|
InternalGitError(#[from] git2::Error),
|
2020-12-29 07:31:48 +00:00
|
|
|
}
|
|
|
|
|
2023-09-30 04:29:58 +00:00
|
|
|
/// Describes changes made by `import_refs()` or `fetch()`.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub struct GitImportStats {
|
|
|
|
/// Commits superseded by newly imported commits.
|
|
|
|
pub abandoned_commits: Vec<CommitId>,
|
|
|
|
}
|
|
|
|
|
2021-07-15 08:31:48 +00:00
|
|
|
fn parse_git_ref(ref_name: &str) -> Option<RefName> {
|
|
|
|
if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {
|
2023-07-11 04:40:03 +00:00
|
|
|
// Git CLI says 'HEAD' is not a valid branch name
|
|
|
|
(branch_name != "HEAD").then(|| RefName::LocalBranch(branch_name.to_string()))
|
2021-07-15 08:31:48 +00:00
|
|
|
} else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") {
|
|
|
|
remote_and_branch
|
2021-12-09 08:12:10 +00:00
|
|
|
.split_once('/')
|
2023-06-28 09:48:16 +00:00
|
|
|
// "refs/remotes/origin/HEAD" isn't a real remote-tracking branch
|
|
|
|
.filter(|&(_, branch)| branch != "HEAD")
|
2021-07-15 08:31:48 +00:00
|
|
|
.map(|(remote, branch)| RefName::RemoteBranch {
|
|
|
|
remote: remote.to_string(),
|
|
|
|
branch: branch.to_string(),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
ref_name
|
|
|
|
.strip_prefix("refs/tags/")
|
|
|
|
.map(|tag_name| RefName::Tag(tag_name.to_string()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-11 04:13:50 +00:00
|
|
|
fn to_git_ref_name(parsed_ref: &RefName) -> Option<String> {
|
2023-06-27 22:18:15 +00:00
|
|
|
match parsed_ref {
|
2023-09-03 23:53:11 +00:00
|
|
|
RefName::LocalBranch(branch) => {
|
|
|
|
(!branch.is_empty() && branch != "HEAD").then(|| format!("refs/heads/{branch}"))
|
2023-07-11 04:13:50 +00:00
|
|
|
}
|
2023-09-03 23:53:11 +00:00
|
|
|
RefName::RemoteBranch { branch, remote } => (!branch.is_empty() && branch != "HEAD")
|
|
|
|
.then(|| format!("refs/remotes/{remote}/{branch}")),
|
2023-07-11 04:13:50 +00:00
|
|
|
RefName::Tag(tag) => Some(format!("refs/tags/{tag}")),
|
|
|
|
RefName::GitRef(name) => Some(name.to_owned()),
|
2023-06-27 22:18:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-04 07:18:17 +00:00
|
|
|
fn to_remote_branch<'a>(parsed_ref: &'a RefName, remote_name: &str) -> Option<&'a str> {
|
|
|
|
match parsed_ref {
|
|
|
|
RefName::RemoteBranch { branch, remote } => (remote == remote_name).then_some(branch),
|
|
|
|
RefName::LocalBranch(..) | RefName::Tag(..) | RefName::GitRef(..) => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-29 04:45:56 +00:00
|
|
|
/// Returns true if the `parsed_ref` won't be imported because its remote name
|
|
|
|
/// is reserved.
|
|
|
|
///
|
|
|
|
/// Use this as a negative `git_ref_filter` to be passed in to
|
|
|
|
/// `import_some_refs()`.
|
|
|
|
pub fn is_reserved_git_remote_ref(parsed_ref: &RefName) -> bool {
|
|
|
|
to_remote_branch(parsed_ref, REMOTE_NAME_FOR_LOCAL_GIT_REPO).is_some()
|
|
|
|
}
|
|
|
|
|
2023-06-16 13:49:13 +00:00
|
|
|
/// Checks if `git_ref` points to a Git commit object, and returns its id.
|
|
|
|
///
|
|
|
|
/// If the ref points to the previously `known_target` (i.e. unchanged), this
|
|
|
|
/// should be faster than `git_ref.peel_to_commit()`.
|
|
|
|
fn resolve_git_ref_to_commit_id(
|
|
|
|
git_ref: &git2::Reference<'_>,
|
2023-07-12 22:20:44 +00:00
|
|
|
known_target: &RefTarget,
|
2023-06-16 13:49:13 +00:00
|
|
|
) -> Option<CommitId> {
|
|
|
|
// Try fast path if we have a candidate id which is known to be a commit object.
|
2023-07-11 18:23:09 +00:00
|
|
|
if let Some(id) = known_target.as_normal() {
|
2023-06-16 13:49:13 +00:00
|
|
|
if matches!(git_ref.target(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
|
|
|
|
return Some(id.clone());
|
|
|
|
}
|
|
|
|
if matches!(git_ref.target_peel(), Some(oid) if oid.as_bytes() == id.as_bytes()) {
|
|
|
|
// Perhaps an annotated tag stored in packed-refs file, and pointing to the
|
|
|
|
// already known target commit.
|
|
|
|
return Some(id.clone());
|
|
|
|
}
|
|
|
|
// A tag (according to ref name.) Try to peel one more level. This is slightly
|
|
|
|
// faster than recurse into peel_to_commit(). If we recorded a tag oid, we
|
|
|
|
// could skip this at all.
|
|
|
|
if let Some(Ok(tag)) = git_ref.is_tag().then(|| git_ref.peel_to_tag()) {
|
|
|
|
if tag.target_id().as_bytes() == id.as_bytes() {
|
|
|
|
// An annotated tag pointing to the already known target commit.
|
|
|
|
return Some(id.clone());
|
|
|
|
} else {
|
|
|
|
// Unknown id. Recurse from the current state as git_object_peel() of
|
|
|
|
// libgit2 would do. A tag may point to non-commit object.
|
|
|
|
let git_commit = tag.into_object().peel_to_commit().ok()?;
|
|
|
|
return Some(CommitId::from_bytes(git_commit.id().as_bytes()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let git_commit = git_ref.peel_to_commit().ok()?;
|
|
|
|
Some(CommitId::from_bytes(git_commit.id().as_bytes()))
|
|
|
|
}
|
|
|
|
|
2021-12-04 17:49:50 +00:00
|
|
|
/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
|
2023-05-12 01:17:18 +00:00
|
|
|
///
|
|
|
|
/// This function detects conflicts (if both Git and JJ modified a branch) and
|
|
|
|
/// records them in JJ's view.
|
2021-01-11 04:13:52 +00:00
|
|
|
pub fn import_refs(
|
2021-03-16 23:08:40 +00:00
|
|
|
mut_repo: &mut MutableRepo,
|
2021-01-11 04:13:52 +00:00
|
|
|
git_repo: &git2::Repository,
|
2022-12-13 00:18:19 +00:00
|
|
|
git_settings: &GitSettings,
|
2023-09-30 04:29:58 +00:00
|
|
|
) -> Result<GitImportStats, GitImportError> {
|
2023-02-26 14:23:04 +00:00
|
|
|
import_some_refs(mut_repo, git_repo, git_settings, |_| true)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
|
2023-05-12 01:17:18 +00:00
|
|
|
///
|
2023-02-26 14:23:04 +00:00
|
|
|
/// Only branches whose git full reference name pass the filter will be
|
|
|
|
/// considered for addition, update, or deletion.
|
|
|
|
pub fn import_some_refs(
|
|
|
|
mut_repo: &mut MutableRepo,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
git_settings: &GitSettings,
|
2023-06-28 10:32:49 +00:00
|
|
|
git_ref_filter: impl Fn(&RefName) -> bool,
|
2023-09-30 04:29:58 +00:00
|
|
|
) -> Result<GitImportStats, GitImportError> {
|
2022-10-28 19:39:28 +00:00
|
|
|
// TODO: Should this be a separate function? We may not always want to import
|
|
|
|
// the Git HEAD (and add it to our set of heads).
|
2023-08-11 23:43:17 +00:00
|
|
|
let old_git_head = mut_repo.view().git_head();
|
|
|
|
let changed_git_head = if let Ok(head_git_commit) = git_repo
|
2022-10-28 19:39:28 +00:00
|
|
|
.head()
|
|
|
|
.and_then(|head_ref| head_ref.peel_to_commit())
|
|
|
|
{
|
2023-08-04 01:10:32 +00:00
|
|
|
// The current HEAD is not added to `hidable_git_heads` because HEAD move
|
|
|
|
// doesn't automatically mean the old HEAD branch has been rewritten.
|
2022-10-28 19:39:28 +00:00
|
|
|
let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes());
|
2023-08-11 23:43:17 +00:00
|
|
|
let new_head_target = RefTarget::normal(head_commit_id);
|
|
|
|
(*old_git_head != new_head_target).then_some(new_head_target)
|
2022-10-28 19:39:28 +00:00
|
|
|
} else {
|
2023-08-11 23:43:17 +00:00
|
|
|
old_git_head.is_present().then(RefTarget::absent)
|
|
|
|
};
|
2023-10-01 15:57:17 +00:00
|
|
|
let changed_remote_refs = diff_refs_to_import(mut_repo.view(), git_repo, git_ref_filter)?;
|
2023-08-04 22:13:09 +00:00
|
|
|
|
|
|
|
// Import new heads
|
2023-08-12 00:31:43 +00:00
|
|
|
let store = mut_repo.store();
|
2023-09-07 10:43:07 +00:00
|
|
|
// TODO: It might be better to obtain both git_repo and git_backend from
|
|
|
|
// mut_repo, and return error if the repo isn't backed by Git.
|
|
|
|
let git_backend = store.backend_impl().downcast_ref::<GitBackend>().unwrap();
|
2023-09-08 02:05:54 +00:00
|
|
|
// Bulk-import all reachable commits to reduce overhead of table merging.
|
|
|
|
let head_ids = itertools::chain(
|
|
|
|
&changed_git_head,
|
2023-10-01 15:57:17 +00:00
|
|
|
changed_remote_refs
|
|
|
|
.values()
|
|
|
|
.map(|(_, new_target)| new_target),
|
2023-09-08 02:05:54 +00:00
|
|
|
)
|
|
|
|
.flat_map(|target| target.added_ids());
|
2023-09-07 13:11:51 +00:00
|
|
|
let heads_imported = git_backend
|
|
|
|
.import_head_commits(head_ids, store.use_tree_conflict_format())
|
|
|
|
.is_ok();
|
2023-08-12 00:31:43 +00:00
|
|
|
let mut head_commits = Vec::new();
|
2023-09-07 10:43:07 +00:00
|
|
|
let get_commit = |id| {
|
2023-09-08 02:05:54 +00:00
|
|
|
// If bulk-import failed, try again to find bad head or ref.
|
|
|
|
if !heads_imported {
|
2023-09-07 13:11:51 +00:00
|
|
|
git_backend.import_head_commits([id], store.use_tree_conflict_format())?;
|
2023-09-08 02:05:54 +00:00
|
|
|
}
|
2023-09-07 10:43:07 +00:00
|
|
|
store.get_commit(id)
|
|
|
|
};
|
2023-08-11 23:43:17 +00:00
|
|
|
if let Some(new_head_target) = &changed_git_head {
|
|
|
|
for id in new_head_target.added_ids() {
|
2023-09-07 10:43:07 +00:00
|
|
|
let commit = get_commit(id).map_err(|err| GitImportError::MissingHeadTarget {
|
|
|
|
id: id.clone(),
|
|
|
|
err,
|
|
|
|
})?;
|
2023-08-12 00:31:43 +00:00
|
|
|
head_commits.push(commit);
|
2023-08-11 23:43:17 +00:00
|
|
|
}
|
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
for (ref_name, (_, new_target)) in &changed_remote_refs {
|
|
|
|
for id in new_target.added_ids() {
|
2023-09-07 10:43:07 +00:00
|
|
|
let commit = get_commit(id).map_err(|err| GitImportError::MissingRefAncestor {
|
|
|
|
ref_name: ref_name.to_string(),
|
|
|
|
err,
|
|
|
|
})?;
|
2023-08-12 00:31:43 +00:00
|
|
|
head_commits.push(commit);
|
2023-08-09 21:21:13 +00:00
|
|
|
}
|
2023-08-04 22:13:09 +00:00
|
|
|
}
|
2023-08-12 00:31:43 +00:00
|
|
|
mut_repo.add_heads(&head_commits);
|
2023-08-04 22:13:09 +00:00
|
|
|
|
2023-08-11 23:43:17 +00:00
|
|
|
// Apply the change that happened in git since last time we imported refs.
|
|
|
|
if let Some(new_head_target) = changed_git_head {
|
|
|
|
mut_repo.set_git_head_target(new_head_target);
|
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
for (ref_name, (old_target, new_target)) in &changed_remote_refs {
|
2023-08-04 22:13:09 +00:00
|
|
|
let full_name = to_git_ref_name(ref_name).unwrap();
|
2023-10-01 15:57:17 +00:00
|
|
|
mut_repo.set_git_ref_target(&full_name, new_target.clone());
|
2023-08-07 10:48:55 +00:00
|
|
|
if let RefName::RemoteBranch { branch, remote } = ref_name {
|
|
|
|
// Remote-tracking branch is the last known state of the branch in the remote.
|
|
|
|
// It shouldn't diverge even if we had inconsistent view.
|
2023-10-01 15:57:17 +00:00
|
|
|
mut_repo.set_remote_branch_target(branch, remote, new_target.clone());
|
2023-08-07 10:48:55 +00:00
|
|
|
// If a git remote-tracking branch changed, apply the change to the local branch
|
|
|
|
// as well.
|
|
|
|
if git_settings.auto_local_branch {
|
|
|
|
let local_ref_name = RefName::LocalBranch(branch.clone());
|
2023-10-01 15:57:17 +00:00
|
|
|
mut_repo.merge_single_ref(&local_ref_name, old_target, new_target);
|
2023-08-07 10:48:55 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-09-25 11:05:24 +00:00
|
|
|
if let RefName::LocalBranch(branch) = ref_name {
|
|
|
|
// Update Git-tracking branch like the other remote branches.
|
|
|
|
mut_repo.set_remote_branch_target(
|
|
|
|
branch,
|
|
|
|
REMOTE_NAME_FOR_LOCAL_GIT_REPO,
|
2023-10-01 15:57:17 +00:00
|
|
|
new_target.clone(),
|
2023-09-25 11:05:24 +00:00
|
|
|
);
|
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
mut_repo.merge_single_ref(ref_name, old_target, new_target);
|
2021-07-15 08:31:48 +00:00
|
|
|
}
|
2020-12-29 07:31:48 +00:00
|
|
|
}
|
2022-03-27 05:33:08 +00:00
|
|
|
|
|
|
|
// Find commits that are no longer referenced in the git repo and abandon them
|
2023-06-15 11:01:06 +00:00
|
|
|
// in jj as well.
|
2023-10-01 15:57:17 +00:00
|
|
|
let hidable_git_heads = changed_remote_refs
|
2023-06-15 11:01:06 +00:00
|
|
|
.values()
|
2023-10-01 15:57:17 +00:00
|
|
|
.flat_map(|(old_target, _)| old_target.added_ids())
|
2023-07-01 07:07:18 +00:00
|
|
|
.cloned()
|
2023-06-15 11:01:06 +00:00
|
|
|
.collect_vec();
|
2023-06-15 08:48:20 +00:00
|
|
|
if hidable_git_heads.is_empty() {
|
2023-09-30 04:29:58 +00:00
|
|
|
let stats = GitImportStats {
|
|
|
|
abandoned_commits: vec![],
|
|
|
|
};
|
|
|
|
return Ok(stats);
|
2023-06-15 08:48:20 +00:00
|
|
|
}
|
2023-09-30 04:29:58 +00:00
|
|
|
let pinned_heads = itertools::chain(
|
|
|
|
pinned_commit_ids(mut_repo.view()),
|
|
|
|
iter::once(mut_repo.store().root_commit_id()),
|
|
|
|
)
|
|
|
|
.cloned()
|
|
|
|
.collect_vec();
|
2022-03-27 05:33:08 +00:00
|
|
|
// We could use mut_repo.record_rewrites() here but we know we only need to care
|
|
|
|
// about abandoned commits for now. We may want to change this if we ever
|
|
|
|
// add a way of preserving change IDs across rewrites by `git` (e.g. by
|
|
|
|
// putting them in the commit message).
|
2023-09-30 05:58:32 +00:00
|
|
|
let abandoned_expression = RevsetExpression::commits(pinned_heads)
|
|
|
|
.range(&RevsetExpression::commits(hidable_git_heads))
|
|
|
|
.intersection(&RevsetExpression::visible_heads().ancestors());
|
|
|
|
let abandoned_commits = revset::optimize(abandoned_expression)
|
|
|
|
.resolve(mut_repo)
|
|
|
|
.unwrap()
|
|
|
|
.evaluate(mut_repo)
|
2023-08-04 01:10:32 +00:00
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.collect_vec();
|
2023-09-30 04:29:58 +00:00
|
|
|
for abandoned_commit in &abandoned_commits {
|
|
|
|
mut_repo.record_abandoned_commit(abandoned_commit.clone());
|
2022-03-27 05:33:08 +00:00
|
|
|
}
|
|
|
|
|
2023-09-30 04:29:58 +00:00
|
|
|
let stats = GitImportStats { abandoned_commits };
|
|
|
|
Ok(stats)
|
2020-12-29 07:31:48 +00:00
|
|
|
}
|
2020-12-28 01:41:20 +00:00
|
|
|
|
2023-08-04 23:23:17 +00:00
|
|
|
/// Calculates diff of git refs to be imported.
|
|
|
|
fn diff_refs_to_import(
|
|
|
|
view: &View,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
git_ref_filter: impl Fn(&RefName) -> bool,
|
|
|
|
) -> Result<BTreeMap<RefName, (RefTarget, RefTarget)>, GitImportError> {
|
2023-10-01 15:57:17 +00:00
|
|
|
let mut known_remote_refs: HashMap<RefName, &RefTarget> = view
|
2023-08-04 23:02:06 +00:00
|
|
|
.git_refs()
|
|
|
|
.iter()
|
|
|
|
.filter_map(|(full_name, target)| {
|
|
|
|
// TODO: or clean up invalid ref in case it was stored due to historical bug?
|
|
|
|
let ref_name = parse_git_ref(full_name).expect("stored git ref should be parsable");
|
|
|
|
git_ref_filter(&ref_name).then_some((ref_name, target))
|
|
|
|
})
|
|
|
|
.collect();
|
2023-10-01 15:57:17 +00:00
|
|
|
let mut changed_remote_refs = BTreeMap::new();
|
|
|
|
for git_ref in git_repo.references()? {
|
|
|
|
let git_ref = git_ref?;
|
|
|
|
let Some(full_name) = git_ref.name() else {
|
2023-08-04 23:23:17 +00:00
|
|
|
// Skip non-utf8 refs.
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
let Some(ref_name) = parse_git_ref(full_name) else {
|
|
|
|
// Skip other refs (such as notes) and symbolic refs.
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
if !git_ref_filter(&ref_name) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-10-01 16:07:32 +00:00
|
|
|
if is_reserved_git_remote_ref(&ref_name) {
|
|
|
|
return Err(GitImportError::RemoteReservedForLocalGitRepo);
|
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
let old_target = known_remote_refs.get(&ref_name).copied().flatten();
|
|
|
|
let Some(id) = resolve_git_ref_to_commit_id(&git_ref, old_target) else {
|
2023-08-04 23:02:06 +00:00
|
|
|
// Skip (or remove existing) invalid refs.
|
|
|
|
continue;
|
|
|
|
};
|
2023-08-04 23:23:17 +00:00
|
|
|
// TODO: Make it configurable which remotes are publishing and update public
|
|
|
|
// heads here.
|
2023-10-01 15:57:17 +00:00
|
|
|
known_remote_refs.remove(&ref_name);
|
2023-08-12 00:10:19 +00:00
|
|
|
let new_target = RefTarget::normal(id);
|
2023-08-04 23:02:06 +00:00
|
|
|
if new_target != *old_target {
|
2023-10-01 15:57:17 +00:00
|
|
|
changed_remote_refs.insert(ref_name, (old_target.clone(), new_target));
|
2023-08-04 23:23:17 +00:00
|
|
|
}
|
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
for (ref_name, old_target) in known_remote_refs {
|
|
|
|
changed_remote_refs.insert(ref_name, (old_target.clone(), RefTarget::absent()));
|
2023-08-04 23:23:17 +00:00
|
|
|
}
|
2023-10-01 15:57:17 +00:00
|
|
|
Ok(changed_remote_refs)
|
2023-08-04 23:23:17 +00:00
|
|
|
}
|
|
|
|
|
2023-09-27 11:56:59 +00:00
|
|
|
/// Commits referenced by local branches, tags, or HEAD@git.
|
2023-08-04 01:10:32 +00:00
|
|
|
///
|
|
|
|
/// On `import_refs()`, this is similar to collecting commits referenced by
|
|
|
|
/// `view.git_refs()`. Main difference is that local branches can be moved by
|
|
|
|
/// tracking remotes, and such mutation isn't applied to `view.git_refs()` yet.
|
|
|
|
fn pinned_commit_ids(view: &View) -> impl Iterator<Item = &CommitId> {
|
|
|
|
itertools::chain!(
|
2023-09-21 09:49:27 +00:00
|
|
|
view.local_branches().map(|(_, target)| target),
|
2023-08-04 01:10:32 +00:00
|
|
|
view.tags().values(),
|
|
|
|
iter::once(view.git_head()),
|
|
|
|
)
|
|
|
|
.flat_map(|target| target.added_ids())
|
|
|
|
}
|
|
|
|
|
2021-12-04 17:49:50 +00:00
|
|
|
#[derive(Error, Debug, PartialEq)]
|
|
|
|
pub enum GitExportError {
|
2022-10-28 06:15:18 +00:00
|
|
|
#[error("Git error: {0}")]
|
2021-12-04 17:49:50 +00:00
|
|
|
InternalGitError(#[from] git2::Error),
|
|
|
|
}
|
|
|
|
|
2023-09-03 23:53:11 +00:00
|
|
|
/// A ref we failed to export to Git, along with the reason it failed.
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
pub struct FailedRefExport {
|
|
|
|
pub name: RefName,
|
|
|
|
pub reason: FailedRefExportReason,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The reason we failed to export a ref to Git.
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
pub enum FailedRefExportReason {
|
|
|
|
/// The name is not allowed in Git.
|
|
|
|
InvalidGitName,
|
|
|
|
/// The ref was in a conflicted state from the last import. A re-import
|
|
|
|
/// should fix it.
|
|
|
|
ConflictedOldState,
|
2023-09-03 21:11:00 +00:00
|
|
|
/// The branch points to the root commit, which Git doesn't have
|
|
|
|
OnRootCommit,
|
2023-09-03 23:53:11 +00:00
|
|
|
/// We wanted to delete it, but it had been modified in Git.
|
|
|
|
DeletedInJjModifiedInGit,
|
|
|
|
/// We wanted to add it, but Git had added it with a different target
|
|
|
|
AddedInJjAddedInGit,
|
|
|
|
/// We wanted to modify it, but Git had deleted it
|
|
|
|
ModifiedInJjDeletedInGit,
|
|
|
|
/// Failed to delete the ref from the Git repo
|
|
|
|
FailedToDelete(git2::Error),
|
|
|
|
/// Failed to set the ref in the Git repo
|
|
|
|
FailedToSet(git2::Error),
|
|
|
|
}
|
|
|
|
|
2023-09-05 06:38:25 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct RefsToExport {
|
|
|
|
branches_to_update: BTreeMap<RefName, (Option<Oid>, Oid)>,
|
|
|
|
branches_to_delete: BTreeMap<RefName, Oid>,
|
|
|
|
failed_branches: Vec<FailedRefExport>,
|
|
|
|
}
|
|
|
|
|
2023-05-12 01:17:18 +00:00
|
|
|
/// Export changes to branches made in the Jujutsu repo compared to our last
|
|
|
|
/// seen view of the Git repo in `mut_repo.view().git_refs()`. Returns a list of
|
2023-06-27 22:18:15 +00:00
|
|
|
/// refs that failed to export.
|
2023-05-12 01:17:18 +00:00
|
|
|
///
|
|
|
|
/// We ignore changed branches that are conflicted (were also changed in the Git
|
|
|
|
/// repo compared to our last remembered view of the Git repo). These will be
|
|
|
|
/// marked conflicted by the next `jj git import`.
|
|
|
|
///
|
|
|
|
/// We do not export tags and other refs at the moment, since these aren't
|
|
|
|
/// supposed to be modified by JJ. For them, the Git state is considered
|
|
|
|
/// authoritative.
|
2022-12-03 07:04:20 +00:00
|
|
|
pub fn export_refs(
|
2022-11-03 05:30:18 +00:00
|
|
|
mut_repo: &mut MutableRepo,
|
2021-12-04 17:49:50 +00:00
|
|
|
git_repo: &git2::Repository,
|
2023-09-03 23:53:11 +00:00
|
|
|
) -> Result<Vec<FailedRefExport>, GitExportError> {
|
2023-06-30 02:22:23 +00:00
|
|
|
export_some_refs(mut_repo, git_repo, |_| true)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn export_some_refs(
|
|
|
|
mut_repo: &mut MutableRepo,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
git_ref_filter: impl Fn(&RefName) -> bool,
|
2023-09-03 23:53:11 +00:00
|
|
|
) -> Result<Vec<FailedRefExport>, GitExportError> {
|
2023-09-05 06:38:25 +00:00
|
|
|
let RefsToExport {
|
|
|
|
branches_to_update,
|
|
|
|
branches_to_delete,
|
|
|
|
mut failed_branches,
|
|
|
|
} = diff_refs_to_export(
|
|
|
|
mut_repo.view(),
|
|
|
|
mut_repo.store().root_commit_id(),
|
|
|
|
git_ref_filter,
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODO: Also check other worktrees' HEAD.
|
|
|
|
if let Ok(head_ref) = git_repo.find_reference("HEAD") {
|
|
|
|
if let (Some(head_git_ref), Ok(current_git_commit)) =
|
|
|
|
(head_ref.symbolic_target(), head_ref.peel_to_commit())
|
|
|
|
{
|
|
|
|
if let Some(parsed_ref) = parse_git_ref(head_git_ref) {
|
|
|
|
let detach_head =
|
|
|
|
if let Some((_old_oid, new_oid)) = branches_to_update.get(&parsed_ref) {
|
|
|
|
*new_oid != current_git_commit.id()
|
|
|
|
} else {
|
|
|
|
branches_to_delete.contains_key(&parsed_ref)
|
|
|
|
};
|
|
|
|
if detach_head {
|
|
|
|
git_repo.set_head_detached(current_git_commit.id())?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (parsed_ref_name, old_oid) in branches_to_delete {
|
|
|
|
let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap();
|
|
|
|
if let Err(reason) = delete_git_ref(git_repo, &git_ref_name, old_oid) {
|
|
|
|
failed_branches.push(FailedRefExport {
|
|
|
|
name: parsed_ref_name,
|
|
|
|
reason,
|
|
|
|
});
|
|
|
|
} else {
|
2023-09-25 11:05:24 +00:00
|
|
|
let new_target = RefTarget::absent();
|
|
|
|
if let RefName::LocalBranch(branch) = &parsed_ref_name {
|
|
|
|
mut_repo.set_remote_branch_target(
|
|
|
|
branch,
|
|
|
|
REMOTE_NAME_FOR_LOCAL_GIT_REPO,
|
|
|
|
new_target.clone(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
mut_repo.set_git_ref_target(&git_ref_name, new_target);
|
2023-09-05 06:38:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for (parsed_ref_name, (old_oid, new_oid)) in branches_to_update {
|
|
|
|
let git_ref_name = to_git_ref_name(&parsed_ref_name).unwrap();
|
|
|
|
if let Err(reason) = update_git_ref(git_repo, &git_ref_name, old_oid, new_oid) {
|
|
|
|
failed_branches.push(FailedRefExport {
|
|
|
|
name: parsed_ref_name,
|
|
|
|
reason,
|
|
|
|
});
|
|
|
|
} else {
|
2023-09-25 11:05:24 +00:00
|
|
|
let new_target = RefTarget::normal(CommitId::from_bytes(new_oid.as_bytes()));
|
|
|
|
if let RefName::LocalBranch(branch) = &parsed_ref_name {
|
|
|
|
mut_repo.set_remote_branch_target(
|
|
|
|
branch,
|
|
|
|
REMOTE_NAME_FOR_LOCAL_GIT_REPO,
|
|
|
|
new_target.clone(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
mut_repo.set_git_ref_target(&git_ref_name, new_target);
|
2023-09-05 06:38:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
failed_branches.sort_by_key(|failed| failed.name.clone());
|
|
|
|
Ok(failed_branches)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates diff of branches to be exported.
|
|
|
|
fn diff_refs_to_export(
|
|
|
|
view: &View,
|
|
|
|
root_commit_id: &CommitId,
|
|
|
|
git_ref_filter: impl Fn(&RefName) -> bool,
|
|
|
|
) -> RefsToExport {
|
2022-11-24 16:45:49 +00:00
|
|
|
let mut branches_to_update = BTreeMap::new();
|
2022-12-02 23:09:07 +00:00
|
|
|
let mut branches_to_delete = BTreeMap::new();
|
|
|
|
let mut failed_branches = vec![];
|
2023-09-05 06:38:25 +00:00
|
|
|
let root_commit_target = RefTarget::normal(root_commit_id.clone());
|
2023-06-27 22:18:15 +00:00
|
|
|
let jj_repo_iter_all_branches = view.branches().iter().flat_map(|(branch, target)| {
|
|
|
|
itertools::chain(
|
|
|
|
target
|
|
|
|
.local_target
|
2023-07-12 22:58:28 +00:00
|
|
|
.is_present()
|
|
|
|
.then(|| RefName::LocalBranch(branch.to_owned())),
|
2023-06-27 22:18:15 +00:00
|
|
|
target
|
|
|
|
.remote_targets
|
|
|
|
.keys()
|
2023-09-25 11:05:24 +00:00
|
|
|
.filter(|&remote| remote != REMOTE_NAME_FOR_LOCAL_GIT_REPO)
|
2023-06-27 22:18:15 +00:00
|
|
|
.map(|remote| RefName::RemoteBranch {
|
|
|
|
branch: branch.to_string(),
|
|
|
|
remote: remote.to_string(),
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
});
|
2023-09-05 08:39:45 +00:00
|
|
|
let all_ref_names_passing_filter: HashSet<_> = view
|
2022-12-24 09:54:36 +00:00
|
|
|
.git_refs()
|
|
|
|
.keys()
|
2023-06-27 22:18:15 +00:00
|
|
|
.filter_map(|name| parse_git_ref(name))
|
2023-06-27 22:18:15 +00:00
|
|
|
.chain(jj_repo_iter_all_branches)
|
2023-06-30 02:22:23 +00:00
|
|
|
.filter(git_ref_filter)
|
2022-12-24 09:54:36 +00:00
|
|
|
.collect();
|
2023-09-05 08:39:45 +00:00
|
|
|
for ref_name in all_ref_names_passing_filter {
|
|
|
|
let new_target = match &ref_name {
|
2023-06-27 22:18:15 +00:00
|
|
|
RefName::LocalBranch(branch) => view.get_local_branch(branch),
|
2023-06-27 22:18:15 +00:00
|
|
|
RefName::RemoteBranch { remote, branch } => {
|
2023-09-22 11:28:26 +00:00
|
|
|
// There are two situations where remote-tracking branches get out of sync:
|
|
|
|
// 1. `jj branch forget`
|
|
|
|
// 2. `jj op undo`/`restore` in colocated repo
|
2023-06-27 22:18:15 +00:00
|
|
|
view.get_remote_branch(branch, remote)
|
|
|
|
}
|
2023-06-27 22:18:15 +00:00
|
|
|
_ => continue,
|
|
|
|
};
|
2023-09-05 08:39:45 +00:00
|
|
|
let old_target = if let Some(name) = to_git_ref_name(&ref_name) {
|
2023-07-11 04:13:50 +00:00
|
|
|
view.get_git_ref(&name)
|
|
|
|
} else {
|
|
|
|
// Invalid branch name in Git sense
|
2023-09-03 23:53:11 +00:00
|
|
|
failed_branches.push(FailedRefExport {
|
2023-09-05 08:39:45 +00:00
|
|
|
name: ref_name,
|
2023-09-03 23:53:11 +00:00
|
|
|
reason: FailedRefExportReason::InvalidGitName,
|
|
|
|
});
|
2023-07-11 04:13:50 +00:00
|
|
|
continue;
|
|
|
|
};
|
2023-09-05 08:39:45 +00:00
|
|
|
if new_target == old_target {
|
2021-12-04 17:49:50 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-09-05 08:39:45 +00:00
|
|
|
if *new_target == root_commit_target {
|
2023-09-03 21:11:00 +00:00
|
|
|
// Git doesn't have a root commit
|
|
|
|
failed_branches.push(FailedRefExport {
|
2023-09-05 08:39:45 +00:00
|
|
|
name: ref_name,
|
2023-09-03 21:11:00 +00:00
|
|
|
reason: FailedRefExportReason::OnRootCommit,
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
2023-09-05 08:39:45 +00:00
|
|
|
let old_oid = if let Some(id) = old_target.as_normal() {
|
2023-07-11 18:23:09 +00:00
|
|
|
Some(Oid::from_bytes(id.as_bytes()).unwrap())
|
2023-09-05 08:39:45 +00:00
|
|
|
} else if old_target.has_conflict() {
|
2023-07-11 18:23:09 +00:00
|
|
|
// The old git ref should only be a conflict if there were concurrent import
|
|
|
|
// operations while the value changed. Don't overwrite these values.
|
2023-09-03 23:53:11 +00:00
|
|
|
failed_branches.push(FailedRefExport {
|
2023-09-05 08:39:45 +00:00
|
|
|
name: ref_name,
|
2023-09-03 23:53:11 +00:00
|
|
|
reason: FailedRefExportReason::ConflictedOldState,
|
|
|
|
});
|
2023-07-11 18:23:09 +00:00
|
|
|
continue;
|
|
|
|
} else {
|
2023-09-05 08:39:45 +00:00
|
|
|
assert!(old_target.is_absent());
|
2023-07-11 18:23:09 +00:00
|
|
|
None
|
2022-12-02 23:09:07 +00:00
|
|
|
};
|
2023-09-05 08:39:45 +00:00
|
|
|
if let Some(id) = new_target.as_normal() {
|
2023-07-11 18:23:09 +00:00
|
|
|
let new_oid = Oid::from_bytes(id.as_bytes());
|
2023-09-05 08:39:45 +00:00
|
|
|
branches_to_update.insert(ref_name, (old_oid, new_oid.unwrap()));
|
|
|
|
} else if new_target.has_conflict() {
|
2023-07-11 18:23:09 +00:00
|
|
|
// Skip conflicts and leave the old value in git_refs
|
|
|
|
continue;
|
2021-12-04 17:49:50 +00:00
|
|
|
} else {
|
2023-09-05 08:39:45 +00:00
|
|
|
assert!(new_target.is_absent());
|
|
|
|
branches_to_delete.insert(ref_name, old_oid.unwrap());
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-05 06:38:25 +00:00
|
|
|
|
|
|
|
RefsToExport {
|
|
|
|
branches_to_update,
|
|
|
|
branches_to_delete,
|
|
|
|
failed_branches,
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-05 07:04:09 +00:00
|
|
|
fn delete_git_ref(
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
git_ref_name: &str,
|
|
|
|
old_oid: Oid,
|
|
|
|
) -> Result<(), FailedRefExportReason> {
|
|
|
|
if let Ok(mut git_repo_ref) = git_repo.find_reference(git_ref_name) {
|
|
|
|
if git_repo_ref.target() == Some(old_oid) {
|
|
|
|
// The branch has not been updated by git, so go ahead and delete it
|
|
|
|
git_repo_ref
|
|
|
|
.delete()
|
|
|
|
.map_err(FailedRefExportReason::FailedToDelete)?;
|
|
|
|
} else {
|
|
|
|
// The branch was updated by git
|
|
|
|
return Err(FailedRefExportReason::DeletedInJjModifiedInGit);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// The branch is already deleted
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-09-05 07:16:37 +00:00
|
|
|
fn update_git_ref(
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
git_ref_name: &str,
|
|
|
|
old_oid: Option<Oid>,
|
|
|
|
new_oid: Oid,
|
|
|
|
) -> Result<(), FailedRefExportReason> {
|
|
|
|
match old_oid {
|
|
|
|
None => {
|
|
|
|
if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name) {
|
|
|
|
// The branch was added in jj and in git. We're good if and only if git
|
|
|
|
// pointed it to our desired target.
|
|
|
|
if git_repo_ref.target() != Some(new_oid) {
|
|
|
|
return Err(FailedRefExportReason::AddedInJjAddedInGit);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// The branch was added in jj but still doesn't exist in git, so add it
|
|
|
|
git_repo
|
2023-09-05 07:48:37 +00:00
|
|
|
.reference(git_ref_name, new_oid, false, "export from jj")
|
2023-09-05 07:16:37 +00:00
|
|
|
.map_err(FailedRefExportReason::FailedToSet)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(old_oid) => {
|
|
|
|
// The branch was modified in jj. We can use libgit2's API for updating under a
|
|
|
|
// lock.
|
|
|
|
if let Err(err) =
|
|
|
|
git_repo.reference_matching(git_ref_name, new_oid, true, old_oid, "export from jj")
|
|
|
|
{
|
|
|
|
// The reference was probably updated in git
|
|
|
|
if let Ok(git_repo_ref) = git_repo.find_reference(git_ref_name) {
|
|
|
|
// We still consider this a success if it was updated to our desired target
|
|
|
|
if git_repo_ref.target() != Some(new_oid) {
|
|
|
|
return Err(FailedRefExportReason::FailedToSet(err));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// The reference was deleted in git and moved in jj
|
|
|
|
return Err(FailedRefExportReason::ModifiedInJjDeletedInGit);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Successfully updated from old_oid to new_oid (unchanged in
|
|
|
|
// git)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-10-03 10:11:55 +00:00
|
|
|
/// Sets `HEAD@git` to the parent of the given working-copy commit and resets
|
|
|
|
/// the Git index.
|
|
|
|
pub fn reset_head(
|
|
|
|
mut_repo: &mut MutableRepo,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
wc_commit: &Commit,
|
|
|
|
) -> Result<(), git2::Error> {
|
|
|
|
let first_parent_id = &wc_commit.parent_ids()[0];
|
|
|
|
if first_parent_id != mut_repo.store().root_commit_id() {
|
|
|
|
let new_git_commit_id = Oid::from_bytes(first_parent_id.as_bytes()).unwrap();
|
|
|
|
let new_git_commit = git_repo.find_commit(new_git_commit_id)?;
|
|
|
|
git_repo.set_head_detached(new_git_commit_id)?;
|
|
|
|
git_repo.reset(new_git_commit.as_object(), git2::ResetType::Mixed, None)?;
|
|
|
|
mut_repo.set_git_head_target(RefTarget::normal(first_parent_id.clone()));
|
2023-10-03 10:45:03 +00:00
|
|
|
} else {
|
|
|
|
// Can't detach HEAD without a commit. Use placeholder ref to nullify the HEAD.
|
|
|
|
// We can't set_head() an arbitrary unborn ref, so use reference_symbolic()
|
|
|
|
// instead. Git CLI appears to deal with that. It would be nice if Git CLI
|
|
|
|
// couldn't create a commit without setting a valid branch name.
|
|
|
|
if mut_repo.git_head().is_present() {
|
|
|
|
match git_repo.find_reference(UNBORN_ROOT_REF_NAME) {
|
|
|
|
Ok(mut git_repo_ref) => git_repo_ref.delete()?,
|
|
|
|
Err(err) if err.code() == git2::ErrorCode::NotFound => {}
|
|
|
|
Err(err) => return Err(err),
|
|
|
|
}
|
|
|
|
git_repo.reference_symbolic("HEAD", UNBORN_ROOT_REF_NAME, true, "unset HEAD by jj")?;
|
|
|
|
}
|
|
|
|
// git_reset() of libgit2 requires a commit object. Do that manually.
|
|
|
|
let mut index = git_repo.index()?;
|
|
|
|
index.clear()?; // or read empty tree
|
|
|
|
index.write()?;
|
|
|
|
git_repo.cleanup_state()?;
|
|
|
|
mut_repo.set_git_head_target(RefTarget::absent());
|
2023-10-03 10:11:55 +00:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-08-19 03:58:18 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum GitRemoteManagementError {
|
|
|
|
#[error("No git remote named '{0}'")]
|
|
|
|
NoSuchRemote(String),
|
2023-08-19 04:37:07 +00:00
|
|
|
#[error("Git remote named '{0}' already exists")]
|
|
|
|
RemoteAlreadyExists(String),
|
2023-08-19 05:01:49 +00:00
|
|
|
#[error(
|
|
|
|
"Git remote named '{name}' is reserved for local Git repository",
|
|
|
|
name = REMOTE_NAME_FOR_LOCAL_GIT_REPO
|
|
|
|
)]
|
|
|
|
RemoteReservedForLocalGitRepo,
|
2023-08-19 03:58:18 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
InternalGitError(git2::Error),
|
|
|
|
}
|
|
|
|
|
2023-08-19 04:13:02 +00:00
|
|
|
fn is_remote_not_found_err(err: &git2::Error) -> bool {
|
|
|
|
matches!(
|
|
|
|
(err.class(), err.code()),
|
|
|
|
(
|
|
|
|
git2::ErrorClass::Config,
|
|
|
|
git2::ErrorCode::NotFound | git2::ErrorCode::InvalidSpec
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-19 04:37:07 +00:00
|
|
|
fn is_remote_exists_err(err: &git2::Error) -> bool {
|
|
|
|
matches!(
|
|
|
|
(err.class(), err.code()),
|
|
|
|
(git2::ErrorClass::Config, git2::ErrorCode::Exists)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-19 04:46:30 +00:00
|
|
|
pub fn add_remote(
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
remote_name: &str,
|
|
|
|
url: &str,
|
|
|
|
) -> Result<(), GitRemoteManagementError> {
|
2023-08-19 05:01:49 +00:00
|
|
|
if remote_name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
|
|
|
return Err(GitRemoteManagementError::RemoteReservedForLocalGitRepo);
|
|
|
|
}
|
2023-08-19 04:46:30 +00:00
|
|
|
git_repo.remote(remote_name, url).map_err(|err| {
|
|
|
|
if is_remote_exists_err(&err) {
|
|
|
|
GitRemoteManagementError::RemoteAlreadyExists(remote_name.to_owned())
|
|
|
|
} else {
|
|
|
|
GitRemoteManagementError::InternalGitError(err)
|
|
|
|
}
|
|
|
|
})?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-07-07 13:03:36 +00:00
|
|
|
pub fn remove_remote(
|
|
|
|
mut_repo: &mut MutableRepo,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
remote_name: &str,
|
2023-08-19 03:58:18 +00:00
|
|
|
) -> Result<(), GitRemoteManagementError> {
|
|
|
|
git_repo.remote_delete(remote_name).map_err(|err| {
|
|
|
|
if is_remote_not_found_err(&err) {
|
|
|
|
GitRemoteManagementError::NoSuchRemote(remote_name.to_owned())
|
|
|
|
} else {
|
|
|
|
GitRemoteManagementError::InternalGitError(err)
|
|
|
|
}
|
|
|
|
})?;
|
2023-09-25 11:05:24 +00:00
|
|
|
if remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
|
|
|
remove_remote_refs(mut_repo, remote_name);
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn remove_remote_refs(mut_repo: &mut MutableRepo, remote_name: &str) {
|
2023-07-07 13:03:36 +00:00
|
|
|
let mut branches_to_delete = vec![];
|
|
|
|
for (branch, target) in mut_repo.view().branches() {
|
|
|
|
if target.remote_targets.contains_key(remote_name) {
|
|
|
|
branches_to_delete.push(branch.clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let prefix = format!("refs/remotes/{remote_name}/");
|
|
|
|
let git_refs_to_delete = mut_repo
|
|
|
|
.view()
|
|
|
|
.git_refs()
|
|
|
|
.keys()
|
2023-08-15 03:18:52 +00:00
|
|
|
.filter(|&r| r.starts_with(&prefix))
|
|
|
|
.cloned()
|
2023-07-07 13:03:36 +00:00
|
|
|
.collect_vec();
|
|
|
|
for branch in branches_to_delete {
|
2023-07-12 16:56:02 +00:00
|
|
|
mut_repo.set_remote_branch_target(&branch, remote_name, RefTarget::absent());
|
2023-07-07 13:03:36 +00:00
|
|
|
}
|
|
|
|
for git_ref in git_refs_to_delete {
|
2023-07-12 16:56:02 +00:00
|
|
|
mut_repo.set_git_ref_target(&git_ref, RefTarget::absent());
|
2023-07-07 13:03:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn rename_remote(
|
|
|
|
mut_repo: &mut MutableRepo,
|
|
|
|
git_repo: &git2::Repository,
|
|
|
|
old_remote_name: &str,
|
|
|
|
new_remote_name: &str,
|
2023-08-19 03:58:18 +00:00
|
|
|
) -> Result<(), GitRemoteManagementError> {
|
2023-08-19 05:01:49 +00:00
|
|
|
if new_remote_name == REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
|
|
|
return Err(GitRemoteManagementError::RemoteReservedForLocalGitRepo);
|
|
|
|
}
|
2023-08-19 03:58:18 +00:00
|
|
|
git_repo
|
|
|
|
.remote_rename(old_remote_name, new_remote_name)
|
|
|
|
.map_err(|err| {
|
|
|
|
if is_remote_not_found_err(&err) {
|
|
|
|
GitRemoteManagementError::NoSuchRemote(old_remote_name.to_owned())
|
2023-08-19 04:37:07 +00:00
|
|
|
} else if is_remote_exists_err(&err) {
|
|
|
|
GitRemoteManagementError::RemoteAlreadyExists(new_remote_name.to_owned())
|
2023-08-19 03:58:18 +00:00
|
|
|
} else {
|
|
|
|
GitRemoteManagementError::InternalGitError(err)
|
|
|
|
}
|
|
|
|
})?;
|
2023-09-25 11:05:24 +00:00
|
|
|
if old_remote_name != REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
|
|
|
rename_remote_refs(mut_repo, old_remote_name, new_remote_name);
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rename_remote_refs(mut_repo: &mut MutableRepo, old_remote_name: &str, new_remote_name: &str) {
|
2023-07-07 13:03:36 +00:00
|
|
|
mut_repo.rename_remote(old_remote_name, new_remote_name);
|
|
|
|
let prefix = format!("refs/remotes/{old_remote_name}/");
|
|
|
|
let git_refs = mut_repo
|
|
|
|
.view()
|
|
|
|
.git_refs()
|
|
|
|
.iter()
|
|
|
|
.filter_map(|(r, target)| {
|
|
|
|
r.strip_prefix(&prefix).map(|p| {
|
|
|
|
(
|
|
|
|
r.clone(),
|
|
|
|
format!("refs/remotes/{new_remote_name}/{p}"),
|
|
|
|
target.clone(),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.collect_vec();
|
|
|
|
for (old, new, target) in git_refs {
|
2023-07-12 16:56:02 +00:00
|
|
|
mut_repo.set_git_ref_target(&old, RefTarget::absent());
|
2023-07-12 14:41:38 +00:00
|
|
|
mut_repo.set_git_ref_target(&new, target);
|
2023-07-07 13:03:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-09 21:21:13 +00:00
|
|
|
#[derive(Error, Debug)]
|
2020-12-31 17:56:20 +00:00
|
|
|
pub enum GitFetchError {
|
2021-01-01 20:53:07 +00:00
|
|
|
#[error("No git remote named '{0}'")]
|
|
|
|
NoSuchRemote(String),
|
2023-01-12 22:53:26 +00:00
|
|
|
#[error("Invalid glob provided. Globs may not contain the characters `:` or `^`.")]
|
|
|
|
InvalidGlob,
|
2023-08-29 05:38:12 +00:00
|
|
|
#[error("Failed to import Git refs: {0}")]
|
2023-08-09 21:21:13 +00:00
|
|
|
GitImportError(#[from] GitImportError),
|
2020-12-31 17:56:20 +00:00
|
|
|
// TODO: I'm sure there are other errors possible, such as transport-level errors.
|
2021-01-01 20:53:07 +00:00
|
|
|
#[error("Unexpected git error when fetching: {0}")]
|
|
|
|
InternalGitError(#[from] git2::Error),
|
2020-12-31 17:56:20 +00:00
|
|
|
}
|
|
|
|
|
2023-09-30 04:29:58 +00:00
|
|
|
/// Describes successful `fetch()` result.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub struct GitFetchStats {
|
|
|
|
/// Remote's default branch.
|
|
|
|
pub default_branch: Option<String>,
|
|
|
|
/// Changes made by the import.
|
|
|
|
pub import_stats: GitImportStats,
|
|
|
|
}
|
|
|
|
|
2022-11-20 03:38:25 +00:00
|
|
|
#[tracing::instrument(skip(mut_repo, git_repo, callbacks))]
|
2021-01-11 04:13:52 +00:00
|
|
|
pub fn fetch(
|
2021-03-16 23:08:40 +00:00
|
|
|
mut_repo: &mut MutableRepo,
|
2021-01-11 04:13:52 +00:00
|
|
|
git_repo: &git2::Repository,
|
|
|
|
remote_name: &str,
|
2023-02-26 14:23:04 +00:00
|
|
|
branch_name_globs: Option<&[&str]>,
|
2022-11-06 17:36:52 +00:00
|
|
|
callbacks: RemoteCallbacks<'_>,
|
2022-12-13 00:18:19 +00:00
|
|
|
git_settings: &GitSettings,
|
2023-09-30 04:29:58 +00:00
|
|
|
) -> Result<GitFetchStats, GitFetchError> {
|
2023-07-04 07:26:13 +00:00
|
|
|
let branch_name_filter = {
|
|
|
|
let regex = if let Some(globs) = branch_name_globs {
|
|
|
|
let result = regex::RegexSet::new(
|
|
|
|
globs
|
|
|
|
.iter()
|
|
|
|
.map(|glob| format!("^{}$", glob.replace('*', ".*"))),
|
|
|
|
)
|
|
|
|
.map_err(|_| GitFetchError::InvalidGlob)?;
|
|
|
|
tracing::debug!(?globs, ?result, "globs as regex");
|
|
|
|
Some(result)
|
2023-06-30 02:20:00 +00:00
|
|
|
} else {
|
2023-07-04 07:26:13 +00:00
|
|
|
None
|
|
|
|
};
|
|
|
|
move |branch: &str| regex.as_ref().map(|r| r.is_match(branch)).unwrap_or(true)
|
2023-06-30 02:20:00 +00:00
|
|
|
};
|
|
|
|
|
2023-06-30 02:20:00 +00:00
|
|
|
// In non-colocated repositories, it's possible that `jj branch forget` was run
|
|
|
|
// at some point and no `jj git export` happened since.
|
|
|
|
//
|
|
|
|
// This would mean that remote-tracking branches, forgotten in the jj repo,
|
|
|
|
// still exist in the git repo. If the branches didn't move on the remote, and
|
|
|
|
// we fetched them, jj would think that they are unmodified and wouldn't
|
|
|
|
// resurrect them.
|
|
|
|
//
|
|
|
|
// Export will delete the remote-tracking branches in the git repo, so it's
|
|
|
|
// possible to fetch them again.
|
|
|
|
//
|
|
|
|
// For more details, see the `test_branch_forget_fetched_branch` test, and PRs
|
|
|
|
// #1714 and #1771
|
|
|
|
//
|
|
|
|
// Apart from `jj branch forget`, jj doesn't provide commands to manipulate
|
|
|
|
// remote-tracking branches, and local git branches don't affect fetch
|
|
|
|
// behaviors. So, it's unnecessary to export anything else.
|
|
|
|
//
|
|
|
|
// TODO: Create a command the user can use to reset jj's
|
|
|
|
// branch state to the git repo's state. In this case, `jj branch forget`
|
|
|
|
// doesn't work as it tries to delete the latter. One possible name is `jj
|
|
|
|
// git import --reset BRANCH`.
|
|
|
|
// TODO: Once the command described above exists, it should be mentioned in `jj
|
|
|
|
// help branch forget`.
|
2023-07-04 07:26:13 +00:00
|
|
|
let nonempty_branches: HashSet<_> = mut_repo
|
2023-06-30 02:20:00 +00:00
|
|
|
.view()
|
2023-09-21 09:49:27 +00:00
|
|
|
.local_branches()
|
2023-08-15 03:18:52 +00:00
|
|
|
.map(|(branch, _target)| branch.to_owned())
|
2023-07-04 07:26:13 +00:00
|
|
|
.collect();
|
2023-06-30 02:20:00 +00:00
|
|
|
// TODO: Inform the user if the export failed? In most cases, export is not
|
|
|
|
// essential for fetch to work.
|
|
|
|
let _ = export_some_refs(mut_repo, git_repo, |ref_name| {
|
2023-07-04 07:26:13 +00:00
|
|
|
to_remote_branch(ref_name, remote_name)
|
|
|
|
.map(|branch| branch_name_filter(branch) && !nonempty_branches.contains(branch))
|
|
|
|
.unwrap_or(false)
|
2023-06-30 02:20:00 +00:00
|
|
|
});
|
|
|
|
|
2023-06-30 02:20:00 +00:00
|
|
|
// Perform a `git fetch` on the local git repo, updating the remote-tracking
|
|
|
|
// branches in the git repo.
|
2023-08-19 04:13:02 +00:00
|
|
|
let mut remote = git_repo.find_remote(remote_name).map_err(|err| {
|
|
|
|
if is_remote_not_found_err(&err) {
|
|
|
|
GitFetchError::NoSuchRemote(remote_name.to_string())
|
|
|
|
} else {
|
|
|
|
GitFetchError::InternalGitError(err)
|
|
|
|
}
|
|
|
|
})?;
|
2021-03-15 00:11:54 +00:00
|
|
|
let mut fetch_options = git2::FetchOptions::new();
|
2021-10-09 15:54:26 +00:00
|
|
|
let mut proxy_options = git2::ProxyOptions::new();
|
|
|
|
proxy_options.auto();
|
|
|
|
fetch_options.proxy_options(proxy_options);
|
2022-11-06 17:36:52 +00:00
|
|
|
let callbacks = callbacks.into_git();
|
2021-03-15 00:11:54 +00:00
|
|
|
fetch_options.remote_callbacks(callbacks);
|
2023-03-01 21:10:48 +00:00
|
|
|
let refspecs = {
|
|
|
|
// If no globs have been given, import all branches
|
|
|
|
let globs = branch_name_globs.unwrap_or(&["*"]);
|
2023-02-26 14:23:04 +00:00
|
|
|
if globs.iter().any(|g| g.contains(|c| ":^".contains(c))) {
|
|
|
|
return Err(GitFetchError::InvalidGlob);
|
|
|
|
}
|
|
|
|
// At this point, we are only updating Git's remote tracking branches, not the
|
|
|
|
// local branches.
|
|
|
|
globs
|
|
|
|
.iter()
|
|
|
|
.map(|glob| format!("+refs/heads/{glob}:refs/remotes/{remote_name}/{glob}"))
|
|
|
|
.collect_vec()
|
|
|
|
};
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!("remote.download");
|
2023-01-12 22:53:26 +00:00
|
|
|
remote.download(&refspecs, Some(&mut fetch_options))?;
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!("remote.prune");
|
2021-11-07 23:18:24 +00:00
|
|
|
remote.prune(None)?;
|
2023-02-12 00:43:38 +00:00
|
|
|
tracing::debug!("remote.update_tips");
|
|
|
|
remote.update_tips(None, false, git2::AutotagOption::Unspecified, None)?;
|
2021-09-14 05:01:56 +00:00
|
|
|
// TODO: We could make it optional to get the default branch since we only care
|
|
|
|
// about it on clone.
|
|
|
|
let mut default_branch = None;
|
|
|
|
if let Ok(default_ref_buf) = remote.default_branch() {
|
|
|
|
if let Some(default_ref) = default_ref_buf.as_str() {
|
|
|
|
// LocalBranch here is the local branch on the remote, so it's really the remote
|
|
|
|
// branch
|
|
|
|
if let Some(RefName::LocalBranch(branch_name)) = parse_git_ref(default_ref) {
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!(default_branch = branch_name);
|
2021-09-14 05:01:56 +00:00
|
|
|
default_branch = Some(branch_name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!("remote.disconnect");
|
2021-09-22 18:19:41 +00:00
|
|
|
remote.disconnect()?;
|
2023-06-30 02:20:00 +00:00
|
|
|
|
2023-09-25 10:31:01 +00:00
|
|
|
// Import the remote-tracking branches into the jj repo and update jj's
|
|
|
|
// local branches. We also import local tags since remote tags should have
|
|
|
|
// been merged by Git.
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!("import_refs");
|
2023-09-30 04:29:58 +00:00
|
|
|
let import_stats = import_some_refs(mut_repo, git_repo, git_settings, |ref_name| {
|
2023-07-04 07:26:13 +00:00
|
|
|
to_remote_branch(ref_name, remote_name)
|
|
|
|
.map(&branch_name_filter)
|
2023-09-25 10:31:01 +00:00
|
|
|
.unwrap_or_else(|| matches!(ref_name, RefName::Tag(_)))
|
2023-07-04 07:26:13 +00:00
|
|
|
})?;
|
2023-09-30 04:29:58 +00:00
|
|
|
let stats = GitFetchStats {
|
|
|
|
default_branch,
|
|
|
|
import_stats,
|
|
|
|
};
|
|
|
|
Ok(stats)
|
2020-12-31 17:56:20 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 20:53:07 +00:00
|
|
|
#[derive(Error, Debug, PartialEq)]
|
2020-12-28 01:41:20 +00:00
|
|
|
pub enum GitPushError {
|
2021-01-01 20:53:07 +00:00
|
|
|
#[error("No git remote named '{0}'")]
|
|
|
|
NoSuchRemote(String),
|
2021-08-11 15:16:19 +00:00
|
|
|
#[error("Push is not fast-forwardable")]
|
2020-12-28 01:41:20 +00:00
|
|
|
NotFastForward,
|
2022-09-26 15:42:41 +00:00
|
|
|
#[error("Remote rejected the update of some refs (do you have permission to push to {0:?}?)")]
|
2021-09-11 05:35:31 +00:00
|
|
|
RefUpdateRejected(Vec<String>),
|
2020-12-28 01:41:20 +00:00
|
|
|
// TODO: I'm sure there are other errors possible, such as transport-level errors,
|
|
|
|
// and errors caused by the remote rejecting the push.
|
2021-01-01 20:53:07 +00:00
|
|
|
#[error("Unexpected git error when pushing: {0}")]
|
|
|
|
InternalGitError(#[from] git2::Error),
|
2020-12-28 01:41:20 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 05:35:31 +00:00
|
|
|
pub struct GitRefUpdate {
|
|
|
|
pub qualified_name: String,
|
|
|
|
// TODO: We want this to be a `current_target: Option<CommitId>` for the expected current
|
|
|
|
// commit on the remote. It's a blunt "force" option instead until git2-rs supports the
|
|
|
|
// "push negotiation" callback (https://github.com/rust-lang/git2-rs/issues/733).
|
|
|
|
pub force: bool,
|
|
|
|
pub new_target: Option<CommitId>,
|
2021-08-05 02:28:48 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 05:35:31 +00:00
|
|
|
pub fn push_updates(
|
2021-08-05 05:05:58 +00:00
|
|
|
git_repo: &git2::Repository,
|
|
|
|
remote_name: &str,
|
2021-09-11 05:35:31 +00:00
|
|
|
updates: &[GitRefUpdate],
|
2022-11-06 17:51:23 +00:00
|
|
|
callbacks: RemoteCallbacks<'_>,
|
2021-08-05 05:05:58 +00:00
|
|
|
) -> Result<(), GitPushError> {
|
2021-09-11 05:35:31 +00:00
|
|
|
let mut temp_refs = vec![];
|
|
|
|
let mut qualified_remote_refs = vec![];
|
|
|
|
let mut refspecs = vec![];
|
|
|
|
for update in updates {
|
|
|
|
qualified_remote_refs.push(update.qualified_name.as_str());
|
|
|
|
if let Some(new_target) = &update.new_target {
|
|
|
|
// Create a temporary ref to work around https://github.com/libgit2/libgit2/issues/3178
|
|
|
|
let temp_ref_name = format!("refs/jj/git-push/{}", new_target.hex());
|
|
|
|
temp_refs.push(git_repo.reference(
|
|
|
|
&temp_ref_name,
|
2021-11-17 22:20:54 +00:00
|
|
|
git2::Oid::from_bytes(new_target.as_bytes()).unwrap(),
|
2021-09-11 05:35:31 +00:00
|
|
|
true,
|
|
|
|
"temporary reference for git push",
|
|
|
|
)?);
|
|
|
|
refspecs.push(format!(
|
|
|
|
"{}{}:{}",
|
|
|
|
(if update.force { "+" } else { "" }),
|
|
|
|
temp_ref_name,
|
|
|
|
update.qualified_name
|
|
|
|
));
|
|
|
|
} else {
|
|
|
|
refspecs.push(format!(":{}", update.qualified_name));
|
|
|
|
}
|
|
|
|
}
|
2022-11-06 17:51:23 +00:00
|
|
|
let result = push_refs(
|
|
|
|
git_repo,
|
|
|
|
remote_name,
|
|
|
|
&qualified_remote_refs,
|
|
|
|
&refspecs,
|
|
|
|
callbacks,
|
|
|
|
);
|
2021-09-11 05:35:31 +00:00
|
|
|
for mut temp_ref in temp_refs {
|
|
|
|
// TODO: Figure out how to do the equivalent of absl::Cleanup for
|
|
|
|
// temp_ref.delete().
|
2022-07-02 02:50:51 +00:00
|
|
|
if let Err(err) = temp_ref.delete() {
|
|
|
|
// Propagate error only if we don't already have an error to return and it's not
|
|
|
|
// NotFound (there may be duplicates if the list if multiple branches moved to
|
|
|
|
// the same commit).
|
|
|
|
if result.is_ok() && err.code() != git2::ErrorCode::NotFound {
|
|
|
|
return Err(GitPushError::InternalGitError(err));
|
|
|
|
}
|
|
|
|
}
|
2021-09-11 05:35:31 +00:00
|
|
|
}
|
|
|
|
result
|
2021-08-05 05:05:58 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 05:35:31 +00:00
|
|
|
fn push_refs(
|
2021-08-05 02:28:48 +00:00
|
|
|
git_repo: &git2::Repository,
|
|
|
|
remote_name: &str,
|
2021-09-11 05:35:31 +00:00
|
|
|
qualified_remote_refs: &[&str],
|
|
|
|
refspecs: &[String],
|
2022-11-06 17:51:23 +00:00
|
|
|
callbacks: RemoteCallbacks<'_>,
|
2021-08-05 02:28:48 +00:00
|
|
|
) -> Result<(), GitPushError> {
|
2023-08-19 04:13:02 +00:00
|
|
|
let mut remote = git_repo.find_remote(remote_name).map_err(|err| {
|
|
|
|
if is_remote_not_found_err(&err) {
|
|
|
|
GitPushError::NoSuchRemote(remote_name.to_string())
|
|
|
|
} else {
|
|
|
|
GitPushError::InternalGitError(err)
|
|
|
|
}
|
|
|
|
})?;
|
2021-09-11 05:35:31 +00:00
|
|
|
let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs.iter().copied().collect();
|
|
|
|
let mut push_options = git2::PushOptions::new();
|
2021-10-09 15:54:26 +00:00
|
|
|
let mut proxy_options = git2::ProxyOptions::new();
|
|
|
|
proxy_options.auto();
|
|
|
|
push_options.proxy_options(proxy_options);
|
2022-11-06 17:51:23 +00:00
|
|
|
let mut callbacks = callbacks.into_git();
|
2021-01-02 18:08:23 +00:00
|
|
|
callbacks.push_update_reference(|refname, status| {
|
2021-09-11 05:35:31 +00:00
|
|
|
// The status is Some if the ref update was rejected
|
|
|
|
if status.is_none() {
|
|
|
|
remaining_remote_refs.remove(refname);
|
2021-01-02 18:08:23 +00:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
});
|
2021-01-02 08:24:10 +00:00
|
|
|
push_options.remote_callbacks(callbacks);
|
2020-12-28 01:41:20 +00:00
|
|
|
remote
|
2021-09-11 05:35:31 +00:00
|
|
|
.push(refspecs, Some(&mut push_options))
|
2020-12-28 01:41:20 +00:00
|
|
|
.map_err(|err| match (err.class(), err.code()) {
|
|
|
|
(git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => {
|
|
|
|
GitPushError::NotFastForward
|
|
|
|
}
|
2021-01-01 20:53:07 +00:00
|
|
|
_ => GitPushError::InternalGitError(err),
|
2020-12-28 01:41:20 +00:00
|
|
|
})?;
|
2021-01-02 18:08:23 +00:00
|
|
|
drop(push_options);
|
2021-09-11 05:35:31 +00:00
|
|
|
if remaining_remote_refs.is_empty() {
|
2021-01-02 18:08:23 +00:00
|
|
|
Ok(())
|
|
|
|
} else {
|
2021-09-11 05:35:31 +00:00
|
|
|
Err(GitPushError::RefUpdateRejected(
|
|
|
|
remaining_remote_refs
|
|
|
|
.iter()
|
|
|
|
.sorted()
|
|
|
|
.map(|name| name.to_string())
|
|
|
|
.collect(),
|
|
|
|
))
|
2021-01-02 18:08:23 +00:00
|
|
|
}
|
2020-12-28 01:41:20 +00:00
|
|
|
}
|
2021-10-09 16:03:17 +00:00
|
|
|
|
2022-11-06 17:36:52 +00:00
|
|
|
#[non_exhaustive]
|
|
|
|
#[derive(Default)]
|
2022-11-06 17:51:23 +00:00
|
|
|
#[allow(clippy::type_complexity)]
|
2022-11-06 17:36:52 +00:00
|
|
|
pub struct RemoteCallbacks<'a> {
|
|
|
|
pub progress: Option<&'a mut dyn FnMut(&Progress)>,
|
2023-08-08 16:43:28 +00:00
|
|
|
pub get_ssh_keys: Option<&'a mut dyn FnMut(&str) -> Vec<PathBuf>>,
|
2022-11-06 18:15:44 +00:00
|
|
|
pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
|
|
|
|
pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
|
2022-11-06 17:36:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> RemoteCallbacks<'a> {
|
2022-11-06 17:51:23 +00:00
|
|
|
fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
|
2022-11-06 17:36:52 +00:00
|
|
|
let mut callbacks = git2::RemoteCallbacks::new();
|
|
|
|
if let Some(progress_cb) = self.progress {
|
|
|
|
callbacks.transfer_progress(move |progress| {
|
|
|
|
progress_cb(&Progress {
|
2023-01-20 15:19:07 +00:00
|
|
|
bytes_downloaded: (progress.received_objects() < progress.total_objects())
|
|
|
|
.then(|| progress.received_bytes() as u64),
|
2022-11-06 17:36:52 +00:00
|
|
|
overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
|
|
|
|
/ (progress.total_objects() + progress.total_deltas()) as f32,
|
|
|
|
});
|
|
|
|
true
|
2022-10-21 23:38:25 +00:00
|
|
|
});
|
2022-11-06 17:36:52 +00:00
|
|
|
}
|
|
|
|
// TODO: We should expose the callbacks to the caller instead -- the library
|
2022-11-06 17:51:23 +00:00
|
|
|
// crate shouldn't read environment variables.
|
2023-08-08 04:30:23 +00:00
|
|
|
let mut tried_ssh_agent = false;
|
2023-08-08 16:43:28 +00:00
|
|
|
let mut ssh_key_paths_to_try: Option<Vec<PathBuf>> = None;
|
2022-11-06 18:15:44 +00:00
|
|
|
callbacks.credentials(move |url, username_from_url, allowed_types| {
|
2022-11-20 03:38:25 +00:00
|
|
|
let span = tracing::debug_span!("RemoteCallbacks.credentials");
|
|
|
|
let _ = span.enter();
|
|
|
|
|
2022-11-20 00:13:39 +00:00
|
|
|
let git_config = git2::Config::open_default();
|
|
|
|
let credential_helper = git_config
|
|
|
|
.and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
|
|
|
|
if let Ok(creds) = credential_helper {
|
2023-05-03 04:11:21 +00:00
|
|
|
tracing::info!("using credential_helper");
|
2022-11-20 00:13:39 +00:00
|
|
|
return Ok(creds);
|
|
|
|
} else if let Some(username) = username_from_url {
|
2022-11-06 18:15:44 +00:00
|
|
|
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
|
2023-08-08 04:30:23 +00:00
|
|
|
// Try to get the SSH key from the agent once. We don't even check if
|
|
|
|
// $SSH_AUTH_SOCK is set because Windows uses another mechanism.
|
|
|
|
if !tried_ssh_agent {
|
2023-08-08 16:43:28 +00:00
|
|
|
tracing::info!(username, "trying ssh_key_from_agent");
|
2023-08-08 04:30:23 +00:00
|
|
|
tried_ssh_agent = true;
|
|
|
|
return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
|
|
|
|
tracing::error!(err = %err);
|
|
|
|
err
|
|
|
|
});
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
2023-05-01 09:20:33 +00:00
|
|
|
|
2023-08-08 16:43:28 +00:00
|
|
|
let paths = ssh_key_paths_to_try.get_or_insert_with(|| {
|
|
|
|
if let Some(ref mut cb) = self.get_ssh_keys {
|
|
|
|
let mut paths = cb(username);
|
|
|
|
paths.reverse();
|
|
|
|
paths
|
|
|
|
} else {
|
|
|
|
vec![]
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
2023-08-08 16:43:28 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if let Some(path) = paths.pop() {
|
|
|
|
tracing::info!(username, path = ?path, "trying ssh_key");
|
|
|
|
return git2::Cred::ssh_key(username, None, &path, None).map_err(|err| {
|
|
|
|
tracing::error!(err = %err);
|
|
|
|
err
|
|
|
|
});
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
|
|
|
if let Some(ref mut cb) = self.get_password {
|
|
|
|
if let Some(pw) = cb(url, username) {
|
2023-05-03 04:11:21 +00:00
|
|
|
tracing::info!(
|
2022-11-20 03:38:25 +00:00
|
|
|
username,
|
|
|
|
"using userpass_plaintext with username from url"
|
|
|
|
);
|
|
|
|
return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
|
|
|
|
tracing::error!(err = %err);
|
|
|
|
err
|
|
|
|
});
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-06 17:36:52 +00:00
|
|
|
}
|
2022-11-06 18:15:44 +00:00
|
|
|
} else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
|
|
|
if let Some(ref mut cb) = self.get_username_password {
|
|
|
|
if let Some((username, pw)) = cb(url) {
|
2023-05-03 04:11:21 +00:00
|
|
|
tracing::info!(username, "using userpass_plaintext");
|
2022-11-20 03:38:25 +00:00
|
|
|
return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
|
|
|
|
tracing::error!(err = %err);
|
|
|
|
err
|
|
|
|
});
|
2022-11-06 17:36:52 +00:00
|
|
|
}
|
2021-10-09 16:13:06 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-03 04:11:21 +00:00
|
|
|
tracing::info!("using default");
|
2022-11-06 17:36:52 +00:00
|
|
|
git2::Cred::default()
|
|
|
|
});
|
|
|
|
callbacks
|
|
|
|
}
|
2021-10-09 16:03:17 +00:00
|
|
|
}
|
2022-10-21 23:38:25 +00:00
|
|
|
|
|
|
|
pub struct Progress {
|
|
|
|
/// `Some` iff data transfer is currently in progress
|
|
|
|
pub bytes_downloaded: Option<u64>,
|
|
|
|
pub overall: f32,
|
|
|
|
}
|
2023-04-03 23:28:19 +00:00
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct PartialSubmoduleConfig {
|
|
|
|
path: Option<String>,
|
|
|
|
url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Represents configuration from a submodule, e.g. in .gitmodules
|
|
|
|
/// This doesn't include all possible fields, only the ones we care about
|
2023-06-30 21:04:24 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
2023-04-03 23:28:19 +00:00
|
|
|
pub struct SubmoduleConfig {
|
|
|
|
pub name: String,
|
|
|
|
pub path: String,
|
|
|
|
pub url: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
pub enum GitConfigParseError {
|
|
|
|
#[error("Unexpected io error when parsing config: {0}")]
|
|
|
|
IoError(#[from] std::io::Error),
|
|
|
|
#[error("Unexpected git error when parsing config: {0}")]
|
|
|
|
InternalGitError(#[from] git2::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_gitmodules(
|
|
|
|
config: &mut dyn Read,
|
|
|
|
) -> Result<BTreeMap<String, SubmoduleConfig>, GitConfigParseError> {
|
|
|
|
// git2 can only read from a path, so set one up
|
|
|
|
let mut temp_file = NamedTempFile::new()?;
|
|
|
|
std::io::copy(config, &mut temp_file)?;
|
|
|
|
let path = temp_file.into_temp_path();
|
|
|
|
let git_config = git2::Config::open(&path)?;
|
|
|
|
// Partial config value for each submodule name
|
|
|
|
let mut partial_configs: BTreeMap<String, PartialSubmoduleConfig> = BTreeMap::new();
|
|
|
|
|
2023-07-10 04:45:26 +00:00
|
|
|
let entries = git_config.entries(Some(r"submodule\..+\."))?;
|
2023-04-03 23:28:19 +00:00
|
|
|
entries.for_each(|entry| {
|
|
|
|
let (config_name, config_value) = match (entry.name(), entry.value()) {
|
|
|
|
// Reject non-utf8 entries
|
|
|
|
(Some(name), Some(value)) => (name, value),
|
|
|
|
_ => return,
|
|
|
|
};
|
|
|
|
|
|
|
|
// config_name is of the form submodule.<name>.<variable>
|
|
|
|
let (submod_name, submod_var) = config_name
|
|
|
|
.strip_prefix("submodule.")
|
|
|
|
.unwrap()
|
|
|
|
.split_once('.')
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let map_entry = partial_configs.entry(submod_name.to_string()).or_default();
|
|
|
|
|
|
|
|
match (submod_var.to_ascii_lowercase().as_str(), &map_entry) {
|
|
|
|
// TODO Git warns when a duplicate config entry is found, we should
|
|
|
|
// consider doing the same.
|
|
|
|
("path", PartialSubmoduleConfig { path: None, .. }) => {
|
|
|
|
map_entry.path = Some(config_value.to_string())
|
|
|
|
}
|
|
|
|
("url", PartialSubmoduleConfig { url: None, .. }) => {
|
|
|
|
map_entry.url = Some(config_value.to_string())
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
};
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let ret = partial_configs
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|(name, val)| {
|
|
|
|
Some((
|
|
|
|
name.clone(),
|
|
|
|
SubmoduleConfig {
|
|
|
|
name,
|
|
|
|
path: val.path?,
|
|
|
|
url: val.url?,
|
|
|
|
},
|
|
|
|
))
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
Ok(ret)
|
|
|
|
}
|