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-05-08 19:54:37 +00:00
|
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
2022-10-31 16:10:22 +00:00
|
|
|
use std::default::Default;
|
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;
|
2021-03-14 17:37:28 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2023-01-01 03:24:32 +00:00
|
|
|
use crate::backend::{CommitId, ObjectId};
|
2020-12-28 01:41:20 +00:00
|
|
|
use crate::commit::Commit;
|
2022-12-03 17:49:09 +00:00
|
|
|
use crate::git_backend::NO_GC_REF_NAMESPACE;
|
2022-12-02 22:08:14 +00:00
|
|
|
use crate::op_store::RefTarget;
|
2023-02-14 21:51:55 +00:00
|
|
|
use crate::repo::{MutableRepo, Repo};
|
2023-04-21 10:12:32 +00:00
|
|
|
use crate::revset;
|
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
|
|
|
|
2021-01-01 20:53:07 +00:00
|
|
|
#[derive(Error, Debug, PartialEq)]
|
2020-12-29 07:31:48 +00:00
|
|
|
pub enum GitImportError {
|
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
|
|
|
}
|
|
|
|
|
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/") {
|
|
|
|
Some(RefName::LocalBranch(branch_name.to_string()))
|
|
|
|
} 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('/')
|
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-05-08 19:30:23 +00:00
|
|
|
fn ref_name_to_local_branch_name(ref_name: &str) -> Option<&str> {
|
|
|
|
ref_name.strip_prefix("refs/heads/")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn local_branch_name_to_ref_name(branch: &str) -> String {
|
|
|
|
format!("refs/heads/{branch}")
|
|
|
|
}
|
|
|
|
|
2023-06-11 04:44:48 +00:00
|
|
|
// TODO: Eventually, git-tracking branches should no longer be stored in
|
|
|
|
// git_refs but with the other remote-tracking branches in BranchTarget. Note
|
|
|
|
// that there are important but subtle differences in behavior for, e.g. `jj
|
|
|
|
// branch forget`.
|
|
|
|
pub fn git_tracking_branches(view: &View) -> impl Iterator<Item = (&str, &RefTarget)> {
|
|
|
|
view.git_refs().iter().filter_map(|(ref_name, target)| {
|
|
|
|
ref_name_to_local_branch_name(ref_name).map(|branch_name| (branch_name, target))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-11 05:54:10 +00:00
|
|
|
pub fn get_git_tracking_branch<'a>(view: &'a View, branch: &str) -> Option<&'a RefTarget> {
|
|
|
|
view.git_refs().get(&local_branch_name_to_ref_name(branch))
|
|
|
|
}
|
|
|
|
|
2023-05-20 06:59:36 +00:00
|
|
|
fn prevent_gc(git_repo: &git2::Repository, id: &CommitId) -> Result<(), git2::Error> {
|
|
|
|
// If multiple processes do git::import_refs() in parallel, this can fail to
|
|
|
|
// acquire a lock file even with force=true.
|
|
|
|
git_repo.reference(
|
|
|
|
&format!("{}{}", NO_GC_REF_NAMESPACE, id.hex()),
|
|
|
|
Oid::from_bytes(id.as_bytes()).unwrap(),
|
|
|
|
true,
|
|
|
|
"used by jj",
|
|
|
|
)?;
|
|
|
|
Ok(())
|
2022-12-03 17:49:09 +00:00
|
|
|
}
|
|
|
|
|
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-02-26 14:23:04 +00:00
|
|
|
) -> Result<(), GitImportError> {
|
|
|
|
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,
|
|
|
|
git_ref_filter: impl Fn(&str) -> bool,
|
2021-01-11 04:13:52 +00:00
|
|
|
) -> Result<(), GitImportError> {
|
2021-03-16 23:08:40 +00:00
|
|
|
let store = mut_repo.store().clone();
|
2023-05-08 19:30:23 +00:00
|
|
|
let mut jj_view_git_refs = mut_repo.view().git_refs().clone();
|
2023-06-15 11:45:16 +00:00
|
|
|
let mut pinned_git_heads = HashMap::new();
|
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).
|
|
|
|
if let Ok(head_git_commit) = git_repo
|
|
|
|
.head()
|
|
|
|
.and_then(|head_ref| head_ref.peel_to_commit())
|
|
|
|
{
|
2023-06-15 11:45:16 +00:00
|
|
|
// Add the current HEAD to `pinned_git_heads` to pin the branch. It's not added
|
2023-06-15 11:01:06 +00:00
|
|
|
// to `hidable_git_heads` because HEAD move doesn't automatically mean the old
|
2023-05-10 10:10:40 +00:00
|
|
|
// 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-06-15 11:45:16 +00:00
|
|
|
pinned_git_heads.insert("HEAD".to_string(), vec![head_commit_id.clone()]);
|
2023-06-15 03:40:49 +00:00
|
|
|
if !matches!(mut_repo.git_head(), Some(RefTarget::Normal(id)) if id == head_commit_id) {
|
|
|
|
let head_commit = store.get_commit(&head_commit_id).unwrap();
|
|
|
|
prevent_gc(git_repo, &head_commit_id)?;
|
|
|
|
mut_repo.add_head(&head_commit);
|
|
|
|
mut_repo.set_git_head(RefTarget::Normal(head_commit_id));
|
|
|
|
}
|
2022-10-28 19:39:28 +00:00
|
|
|
} else {
|
|
|
|
mut_repo.clear_git_head();
|
|
|
|
}
|
|
|
|
|
2021-07-15 08:31:48 +00:00
|
|
|
let mut changed_git_refs = BTreeMap::new();
|
2023-05-08 19:30:23 +00:00
|
|
|
let git_repo_refs = git_repo.references()?;
|
|
|
|
for git_repo_ref in git_repo_refs {
|
|
|
|
let git_repo_ref = git_repo_ref?;
|
|
|
|
if !(git_repo_ref.is_tag() || git_repo_ref.is_branch() || git_repo_ref.is_remote())
|
|
|
|
|| git_repo_ref.name().is_none()
|
2021-01-03 08:26:57 +00:00
|
|
|
{
|
|
|
|
// Skip other refs (such as notes) and symbolic refs, as well as non-utf8 refs.
|
2020-12-29 07:31:48 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-05-08 19:30:23 +00:00
|
|
|
let full_name = git_repo_ref.name().unwrap().to_string();
|
2021-11-28 23:00:46 +00:00
|
|
|
if let Some(RefName::RemoteBranch { branch, remote: _ }) = parse_git_ref(&full_name) {
|
|
|
|
// "refs/remotes/origin/HEAD" isn't a real remote-tracking branch
|
|
|
|
if &branch == "HEAD" {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2023-05-08 19:30:23 +00:00
|
|
|
let git_commit = match git_repo_ref.peel_to_commit() {
|
2021-03-14 20:39:45 +00:00
|
|
|
Ok(git_commit) => git_commit,
|
|
|
|
Err(_) => {
|
|
|
|
// Perhaps a tag pointing to a GPG key or similar. Just skip it.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
2021-11-17 22:20:54 +00:00
|
|
|
let id = CommitId::from_bytes(git_commit.id().as_bytes());
|
2023-06-15 11:45:16 +00:00
|
|
|
pinned_git_heads.insert(full_name.to_string(), vec![id.clone()]);
|
2023-02-26 14:23:04 +00:00
|
|
|
if !git_ref_filter(&full_name) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-10-20 21:21:01 +00:00
|
|
|
// TODO: Make it configurable which remotes are publishing and update public
|
|
|
|
// heads here.
|
2023-05-08 19:30:23 +00:00
|
|
|
let old_target = jj_view_git_refs.remove(&full_name);
|
2021-12-05 20:15:53 +00:00
|
|
|
let new_target = Some(RefTarget::Normal(id.clone()));
|
2021-07-15 08:31:48 +00:00
|
|
|
if new_target != old_target {
|
2023-05-20 06:59:36 +00:00
|
|
|
prevent_gc(git_repo, &id)?;
|
2022-12-03 18:06:22 +00:00
|
|
|
mut_repo.set_git_ref(full_name.clone(), RefTarget::Normal(id.clone()));
|
2021-12-05 20:15:53 +00:00
|
|
|
let commit = store.get_commit(&id).unwrap();
|
|
|
|
mut_repo.add_head(&commit);
|
2021-07-15 08:31:48 +00:00
|
|
|
changed_git_refs.insert(full_name, (old_target, new_target));
|
|
|
|
}
|
|
|
|
}
|
2023-05-08 19:30:23 +00:00
|
|
|
for (full_name, target) in jj_view_git_refs {
|
2023-02-26 14:23:04 +00:00
|
|
|
if git_ref_filter(&full_name) {
|
|
|
|
mut_repo.remove_git_ref(&full_name);
|
|
|
|
changed_git_refs.insert(full_name, (Some(target), None));
|
2023-06-15 12:18:00 +00:00
|
|
|
} else {
|
|
|
|
pinned_git_heads.insert(full_name, target.adds());
|
2023-02-26 14:23:04 +00:00
|
|
|
}
|
2021-07-15 08:31:48 +00:00
|
|
|
}
|
2023-06-15 11:01:06 +00:00
|
|
|
for (full_name, (old_git_target, new_git_target)) in &changed_git_refs {
|
|
|
|
if let Some(ref_name) = parse_git_ref(full_name) {
|
2021-07-15 08:31:48 +00:00
|
|
|
// Apply the change that happened in git since last time we imported refs
|
|
|
|
mut_repo.merge_single_ref(&ref_name, old_git_target.as_ref(), new_git_target.as_ref());
|
|
|
|
// If a git remote-tracking branch changed, apply the change to the local branch
|
|
|
|
// as well
|
2022-12-13 00:18:19 +00:00
|
|
|
if !git_settings.auto_local_branch {
|
|
|
|
continue;
|
|
|
|
}
|
2021-07-15 08:31:48 +00:00
|
|
|
if let RefName::RemoteBranch { branch, remote: _ } = ref_name {
|
|
|
|
mut_repo.merge_single_ref(
|
2023-05-08 19:54:37 +00:00
|
|
|
&RefName::LocalBranch(branch.clone()),
|
2021-07-15 08:31:48 +00:00
|
|
|
old_git_target.as_ref(),
|
|
|
|
new_git_target.as_ref(),
|
|
|
|
);
|
2023-05-08 19:54:37 +00:00
|
|
|
match mut_repo.get_local_branch(&branch) {
|
2023-06-15 11:45:16 +00:00
|
|
|
None => pinned_git_heads.remove(&local_branch_name_to_ref_name(&branch)),
|
2023-05-08 19:54:37 +00:00
|
|
|
Some(target) => {
|
2023-05-08 19:30:23 +00:00
|
|
|
// Note that we are mostly *replacing*, not inserting
|
2023-06-15 11:45:16 +00:00
|
|
|
pinned_git_heads
|
|
|
|
.insert(local_branch_name_to_ref_name(&branch), target.adds())
|
2023-05-08 19:54:37 +00:00
|
|
|
}
|
|
|
|
};
|
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.
|
|
|
|
let hidable_git_heads = changed_git_refs
|
|
|
|
.values()
|
|
|
|
.filter_map(|(old_git_target, _)| old_git_target.as_ref().map(|target| target.adds()))
|
|
|
|
.flatten()
|
|
|
|
.collect_vec();
|
2023-06-15 08:48:20 +00:00
|
|
|
if hidable_git_heads.is_empty() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2023-06-15 11:45:16 +00:00
|
|
|
// We must remove non-existing commits from pinned_git_heads, as they could have
|
2023-06-15 11:01:06 +00:00
|
|
|
// come from branches which were never fetched.
|
2023-06-15 11:45:16 +00:00
|
|
|
let mut pinned_git_heads_set = HashSet::new();
|
|
|
|
for heads_for_ref in pinned_git_heads.into_values() {
|
|
|
|
pinned_git_heads_set.extend(heads_for_ref.into_iter());
|
2023-05-08 19:54:37 +00:00
|
|
|
}
|
2023-06-15 11:45:16 +00:00
|
|
|
pinned_git_heads_set.retain(|id| mut_repo.index().has_id(id));
|
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-04-21 10:12:32 +00:00
|
|
|
let abandoned_commits = revset::walk_revs(
|
|
|
|
mut_repo,
|
2023-06-15 11:01:06 +00:00
|
|
|
&hidable_git_heads,
|
2023-06-15 11:45:16 +00:00
|
|
|
&pinned_git_heads_set.into_iter().collect_vec(),
|
2023-04-21 10:12:32 +00:00
|
|
|
)
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.collect_vec();
|
2022-10-28 23:18:56 +00:00
|
|
|
let root_commit_id = mut_repo.store().root_commit_id().clone();
|
2022-03-27 05:33:08 +00:00
|
|
|
for abandoned_commit in abandoned_commits {
|
2022-10-28 23:18:56 +00:00
|
|
|
if abandoned_commit != root_commit_id {
|
|
|
|
mut_repo.record_abandoned_commit(abandoned_commit);
|
|
|
|
}
|
2022-03-27 05:33:08 +00:00
|
|
|
}
|
|
|
|
|
2020-12-29 07:31:48 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-12-28 01:41:20 +00:00
|
|
|
|
2021-12-04 17:49:50 +00:00
|
|
|
#[derive(Error, Debug, PartialEq)]
|
|
|
|
pub enum GitExportError {
|
|
|
|
#[error("Cannot export conflicted branch '{0}'")]
|
|
|
|
ConflictedBranch(String),
|
2022-10-28 06:15:18 +00:00
|
|
|
#[error("Failed to read export state: {0}")]
|
|
|
|
ReadStateError(String),
|
|
|
|
#[error("Failed to write export state: {0}")]
|
|
|
|
WriteStateError(String),
|
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-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
|
|
|
|
/// names of branches that failed to export.
|
|
|
|
///
|
|
|
|
/// 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-02 23:09:07 +00:00
|
|
|
// TODO: Also indicate why we failed to export these branches
|
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,
|
2022-12-02 22:08:14 +00:00
|
|
|
) -> Result<Vec<String>, GitExportError> {
|
2022-12-24 09:54:36 +00:00
|
|
|
// First find the changes we want need to make without modifying mut_repo
|
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![];
|
2022-12-24 09:54:36 +00:00
|
|
|
let view = mut_repo.view();
|
2023-05-08 19:30:23 +00:00
|
|
|
let all_local_branch_names: HashSet<&str> = view
|
2022-12-24 09:54:36 +00:00
|
|
|
.git_refs()
|
|
|
|
.keys()
|
2023-05-08 19:30:23 +00:00
|
|
|
.filter_map(|r| ref_name_to_local_branch_name(r))
|
2022-12-24 09:54:36 +00:00
|
|
|
.chain(view.branches().keys().map(AsRef::as_ref))
|
|
|
|
.collect();
|
2023-05-08 19:30:23 +00:00
|
|
|
for branch_name in all_local_branch_names {
|
2023-05-08 19:30:23 +00:00
|
|
|
let old_branch = view.get_git_ref(&local_branch_name_to_ref_name(branch_name));
|
2022-12-24 09:54:36 +00:00
|
|
|
let new_branch = view.get_local_branch(branch_name);
|
2021-12-04 17:49:50 +00:00
|
|
|
if new_branch == old_branch {
|
|
|
|
continue;
|
|
|
|
}
|
2022-12-02 23:09:07 +00:00
|
|
|
let old_oid = match old_branch {
|
|
|
|
None => None,
|
|
|
|
Some(RefTarget::Normal(id)) => Some(Oid::from_bytes(id.as_bytes()).unwrap()),
|
|
|
|
Some(RefTarget::Conflict { .. }) => {
|
|
|
|
// The old git ref should only be a conflict if there were concurrent import
|
|
|
|
// operations while the value changed. Don't overwrite these values.
|
2022-12-24 09:54:36 +00:00
|
|
|
failed_branches.push(branch_name.to_owned());
|
2022-12-02 23:09:07 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
2021-12-04 17:49:50 +00:00
|
|
|
if let Some(new_branch) = new_branch {
|
|
|
|
match new_branch {
|
|
|
|
RefTarget::Normal(id) => {
|
2022-12-02 23:09:07 +00:00
|
|
|
let new_oid = Oid::from_bytes(id.as_bytes());
|
2022-12-24 09:54:36 +00:00
|
|
|
branches_to_update.insert(branch_name.to_owned(), (old_oid, new_oid.unwrap()));
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
RefTarget::Conflict { .. } => {
|
2022-12-02 22:08:14 +00:00
|
|
|
// Skip conflicts and leave the old value in git_refs
|
2022-10-31 16:10:22 +00:00
|
|
|
continue;
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-12-24 09:54:36 +00:00
|
|
|
branches_to_delete.insert(branch_name.to_owned(), old_oid.unwrap());
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-31 17:00:44 +00:00
|
|
|
// TODO: Also check other worktrees' HEAD.
|
|
|
|
if let Ok(head_ref) = git_repo.find_reference("HEAD") {
|
2022-11-24 16:45:49 +00:00
|
|
|
if let (Some(head_git_ref), Ok(current_git_commit)) =
|
2022-10-31 17:00:44 +00:00
|
|
|
(head_ref.symbolic_target(), head_ref.peel_to_commit())
|
|
|
|
{
|
2023-05-08 19:30:23 +00:00
|
|
|
if let Some(branch_name) = ref_name_to_local_branch_name(head_git_ref) {
|
2022-12-02 23:09:07 +00:00
|
|
|
let detach_head =
|
|
|
|
if let Some((_old_oid, new_oid)) = branches_to_update.get(branch_name) {
|
|
|
|
*new_oid != current_git_commit.id()
|
|
|
|
} else {
|
|
|
|
branches_to_delete.contains_key(branch_name)
|
|
|
|
};
|
2022-11-24 16:45:49 +00:00
|
|
|
if detach_head {
|
|
|
|
git_repo.set_head_detached(current_git_commit.id())?;
|
|
|
|
}
|
2021-12-05 01:54:06 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
2022-12-02 23:09:07 +00:00
|
|
|
for (branch_name, old_oid) in branches_to_delete {
|
2023-05-08 19:30:23 +00:00
|
|
|
let git_ref_name = local_branch_name_to_ref_name(&branch_name);
|
2022-12-02 23:09:07 +00:00
|
|
|
let success = if let Ok(mut git_ref) = git_repo.find_reference(&git_ref_name) {
|
|
|
|
if git_ref.target() == Some(old_oid) {
|
|
|
|
// The branch has not been updated by git, so go ahead and delete it
|
|
|
|
git_ref.delete().is_ok()
|
|
|
|
} else {
|
|
|
|
// The branch was updated by git
|
|
|
|
false
|
2022-11-25 03:44:10 +00:00
|
|
|
}
|
2022-12-02 23:09:07 +00:00
|
|
|
} else {
|
|
|
|
// The branch is already deleted
|
|
|
|
true
|
|
|
|
};
|
|
|
|
if success {
|
|
|
|
mut_repo.remove_git_ref(&git_ref_name);
|
|
|
|
} else {
|
|
|
|
failed_branches.push(branch_name);
|
2022-11-25 03:44:10 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-02 23:09:07 +00:00
|
|
|
for (branch_name, (old_oid, new_oid)) in branches_to_update {
|
2023-05-08 19:30:23 +00:00
|
|
|
let git_ref_name = local_branch_name_to_ref_name(&branch_name);
|
2022-12-02 23:09:07 +00:00
|
|
|
let success = match old_oid {
|
|
|
|
None => {
|
|
|
|
if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
|
2023-05-12 01:17:18 +00:00
|
|
|
// The branch was added in jj and in git. We're good if and only if git
|
|
|
|
// pointed it to our desired target.
|
2022-12-02 23:09:07 +00:00
|
|
|
git_ref.target() == Some(new_oid)
|
|
|
|
} else {
|
|
|
|
// The branch was added in jj but still doesn't exist in git, so add it
|
|
|
|
git_repo
|
|
|
|
.reference(&git_ref_name, new_oid, true, "export from jj")
|
|
|
|
.is_ok()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(old_oid) => {
|
|
|
|
// The branch was modified in jj. We can use libgit2's API for updating under a
|
|
|
|
// lock.
|
|
|
|
if git_repo
|
|
|
|
.reference_matching(&git_ref_name, new_oid, true, old_oid, "export from jj")
|
|
|
|
.is_ok()
|
|
|
|
{
|
|
|
|
// Successfully updated from old_oid to new_oid (unchanged in git)
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
// The reference was probably updated in git
|
|
|
|
if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
|
2023-05-12 01:17:18 +00:00
|
|
|
// We still consider this a success if it was updated to our desired target
|
2022-12-02 23:09:07 +00:00
|
|
|
git_ref.target() == Some(new_oid)
|
|
|
|
} else {
|
2023-05-12 01:17:18 +00:00
|
|
|
// The reference was deleted in git and moved in jj
|
2022-12-02 23:09:07 +00:00
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if success {
|
|
|
|
mut_repo.set_git_ref(
|
|
|
|
git_ref_name,
|
|
|
|
RefTarget::Normal(CommitId::from_bytes(new_oid.as_bytes())),
|
|
|
|
);
|
|
|
|
} else {
|
2022-11-24 19:03:53 +00:00
|
|
|
failed_branches.push(branch_name);
|
|
|
|
}
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
2022-12-02 22:08:14 +00:00
|
|
|
Ok(failed_branches)
|
2021-12-04 17:49:50 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 20:53:07 +00:00
|
|
|
#[derive(Error, Debug, PartialEq)]
|
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,
|
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
|
|
|
}
|
|
|
|
|
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,
|
2021-09-14 05:01:56 +00:00
|
|
|
) -> Result<Option<String>, GitFetchError> {
|
2020-12-31 17:56:20 +00:00
|
|
|
let mut remote =
|
|
|
|
git_repo
|
|
|
|
.find_remote(remote_name)
|
|
|
|
.map_err(|err| match (err.class(), err.code()) {
|
|
|
|
(git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
|
2021-01-01 20:53:07 +00:00
|
|
|
GitFetchError::NoSuchRemote(remote_name.to_string())
|
2020-12-31 17:56:20 +00:00
|
|
|
}
|
|
|
|
(git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
|
2021-01-01 20:53:07 +00:00
|
|
|
GitFetchError::NoSuchRemote(remote_name.to_string())
|
2020-12-31 17:56:20 +00:00
|
|
|
}
|
2021-01-01 20:53:07 +00:00
|
|
|
_ => GitFetchError::InternalGitError(err),
|
2020-12-31 17:56:20 +00:00
|
|
|
})?;
|
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()?;
|
2022-11-20 03:38:25 +00:00
|
|
|
tracing::debug!("import_refs");
|
2023-02-26 14:23:04 +00:00
|
|
|
if let Some(globs) = branch_name_globs {
|
|
|
|
let pattern = format!(
|
|
|
|
"^refs/remotes/{remote_name}/({})$",
|
|
|
|
globs.iter().map(|glob| glob.replace('*', ".*")).join("|")
|
|
|
|
);
|
|
|
|
tracing::debug!(?globs, ?pattern, "globs as regex");
|
|
|
|
let regex = regex::Regex::new(&pattern).map_err(|_| GitFetchError::InvalidGlob)?;
|
|
|
|
import_some_refs(
|
|
|
|
mut_repo,
|
|
|
|
git_repo,
|
|
|
|
git_settings,
|
|
|
|
move |git_ref_name: &str| -> bool { regex.is_match(git_ref_name) },
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
import_refs(mut_repo, git_repo, git_settings)
|
|
|
|
}
|
|
|
|
.map_err(|err| match err {
|
2021-01-01 20:53:07 +00:00
|
|
|
GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source),
|
2020-12-31 17:56:20 +00:00
|
|
|
})?;
|
2021-09-14 05:01:56 +00:00
|
|
|
Ok(default_branch)
|
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
|
|
|
}
|
|
|
|
|
|
|
|
pub fn push_commit(
|
2021-01-11 04:13:52 +00:00
|
|
|
git_repo: &git2::Repository,
|
2021-08-05 02:28:48 +00:00
|
|
|
target: &Commit,
|
2020-12-28 01:41:20 +00:00
|
|
|
remote_name: &str,
|
|
|
|
remote_branch: &str,
|
2021-08-04 21:30:06 +00:00
|
|
|
// TODO: We want this to be an 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).
|
|
|
|
force: bool,
|
2022-11-06 17:51:23 +00:00
|
|
|
callbacks: RemoteCallbacks<'_>,
|
2020-12-28 01:41:20 +00:00
|
|
|
) -> Result<(), GitPushError> {
|
2021-09-11 05:35:31 +00:00
|
|
|
push_updates(
|
|
|
|
git_repo,
|
|
|
|
remote_name,
|
|
|
|
&[GitRefUpdate {
|
2022-12-15 02:30:06 +00:00
|
|
|
qualified_name: format!("refs/heads/{remote_branch}"),
|
2021-09-11 05:35:31 +00:00
|
|
|
force,
|
|
|
|
new_target: Some(target.id().clone()),
|
|
|
|
}],
|
2022-11-06 17:51:23 +00:00
|
|
|
callbacks,
|
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> {
|
2020-12-29 07:38:20 +00:00
|
|
|
let mut remote =
|
|
|
|
git_repo
|
|
|
|
.find_remote(remote_name)
|
|
|
|
.map_err(|err| match (err.class(), err.code()) {
|
2021-01-01 20:53:07 +00:00
|
|
|
(git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
|
|
|
|
GitPushError::NoSuchRemote(remote_name.to_string())
|
|
|
|
}
|
2020-12-29 07:38:20 +00:00
|
|
|
(git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
|
2021-01-01 20:53:07 +00:00
|
|
|
GitPushError::NoSuchRemote(remote_name.to_string())
|
2020-12-29 07:38:20 +00:00
|
|
|
}
|
2021-01-01 20:53:07 +00:00
|
|
|
_ => GitPushError::InternalGitError(err),
|
2020-12-29 07:38:20 +00:00
|
|
|
})?;
|
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)>,
|
2022-11-06 17:51:23 +00:00
|
|
|
pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<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.
|
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-05-01 09:20:33 +00:00
|
|
|
// Try to get the SSH key from the agent by default, and report an error
|
|
|
|
// only if it _seems_ like that's what the user wanted.
|
|
|
|
//
|
|
|
|
// Note that the env variables read below are **not** the only way to
|
|
|
|
// communicate with the agent, which is why we request a key from it no
|
|
|
|
// matter what.
|
|
|
|
match git2::Cred::ssh_key_from_agent(username) {
|
|
|
|
Ok(key) => {
|
|
|
|
tracing::info!(username, "using ssh_key_from_agent");
|
|
|
|
return Ok(key);
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
if std::env::var("SSH_AUTH_SOCK").is_ok()
|
|
|
|
|| std::env::var("SSH_AGENT_PID").is_ok()
|
|
|
|
{
|
|
|
|
tracing::error!(err = %err);
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
// There is no agent-related env variable so we
|
|
|
|
// consider that the user doesn't care about using
|
|
|
|
// the agent and proceed.
|
|
|
|
}
|
2022-11-06 18:15:44 +00:00
|
|
|
}
|
2023-05-01 09:20:33 +00:00
|
|
|
|
2022-11-06 18:15:44 +00:00
|
|
|
if let Some(ref mut cb) = self.get_ssh_key {
|
|
|
|
if let Some(path) = cb(username) {
|
2023-05-03 04:11:21 +00:00
|
|
|
tracing::info!(username, path = ?path, "using ssh_key");
|
2022-11-20 03:38:25 +00:00
|
|
|
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,
|
|
|
|
}
|