mirror of
https://github.com/martinvonz/jj.git
synced 2024-11-24 06:19:42 +00:00
jj-lib: abstract a lower-level api for fetching from git.
* This allows more control over the stages of the fetch. * Implement `git::clone` in terms of the new api. * Update call sites to use new `git::clone`. * Update tests to explicitly use `git::clone` where relevant. Issue: #4923
This commit is contained in:
parent
10c90a5099
commit
cc6382aae5
3 changed files with 212 additions and 6 deletions
|
@ -222,7 +222,7 @@ fn do_git_clone(
|
|||
let mut fetch_tx = workspace_command.start_transaction();
|
||||
|
||||
let stats = with_remote_git_callbacks(ui, None, |cb| {
|
||||
git::fetch(
|
||||
git::clone(
|
||||
fetch_tx.repo_mut(),
|
||||
&git_repo,
|
||||
remote_name,
|
||||
|
|
144
lib/src/git.rs
144
lib/src/git.rs
|
@ -1229,6 +1229,126 @@ pub enum GitFetchError {
|
|||
InternalGitError(#[from] git2::Error),
|
||||
}
|
||||
|
||||
struct GitFetch<'a> {
|
||||
mut_repo: &'a mut MutableRepo,
|
||||
git_repo: &'a git2::Repository,
|
||||
git_settings: &'a GitSettings,
|
||||
fetch_options: git2::FetchOptions<'a>,
|
||||
}
|
||||
|
||||
impl<'a> GitFetch<'a> {
|
||||
fn new(
|
||||
mut_repo: &'a mut MutableRepo,
|
||||
git_repo: &'a git2::Repository,
|
||||
git_settings: &'a GitSettings,
|
||||
fetch_options: git2::FetchOptions<'a>,
|
||||
) -> Self {
|
||||
GitFetch {
|
||||
mut_repo,
|
||||
git_repo,
|
||||
git_settings,
|
||||
fetch_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_options(
|
||||
callbacks: RemoteCallbacks<'_>,
|
||||
depth: Option<NonZeroU32>,
|
||||
) -> git2::FetchOptions<'_> {
|
||||
let mut proxy_options = git2::ProxyOptions::new();
|
||||
proxy_options.auto();
|
||||
|
||||
let mut fetch_options = git2::FetchOptions::new();
|
||||
fetch_options.proxy_options(proxy_options);
|
||||
fetch_options.remote_callbacks(callbacks.into_git());
|
||||
if let Some(depth) = depth {
|
||||
fetch_options.depth(depth.get().try_into().unwrap_or(i32::MAX));
|
||||
}
|
||||
|
||||
fetch_options
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
&mut self,
|
||||
branch_names: &[StringPattern],
|
||||
remote_name: &str,
|
||||
) -> Result<Option<String>, GitFetchError> {
|
||||
// Perform a `git fetch` on the local git repo, updating the remote-tracking
|
||||
// branches in the git repo.
|
||||
let mut remote = self.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)
|
||||
}
|
||||
})?;
|
||||
// At this point, we are only updating Git's remote tracking branches, not the
|
||||
// local branches.
|
||||
let refspecs: Vec<_> = branch_names
|
||||
.iter()
|
||||
.map(|pattern| {
|
||||
pattern
|
||||
.to_glob()
|
||||
.filter(|glob| !glob.contains(INVALID_REFSPEC_CHARS))
|
||||
.map(|glob| format!("+refs/heads/{glob}:refs/remotes/{remote_name}/{glob}"))
|
||||
})
|
||||
.collect::<Option<_>>()
|
||||
.ok_or(GitFetchError::InvalidBranchPattern)?;
|
||||
if refspecs.is_empty() {
|
||||
// Don't fall back to the base refspecs.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::debug!("remote.download");
|
||||
remote.download(&refspecs, Some(&mut self.fetch_options))?;
|
||||
tracing::debug!("remote.prune");
|
||||
remote.prune(None)?;
|
||||
tracing::debug!("remote.update_tips");
|
||||
remote.update_tips(
|
||||
None,
|
||||
git2::RemoteUpdateFlags::empty(),
|
||||
git2::AutotagOption::Unspecified,
|
||||
None,
|
||||
)?;
|
||||
|
||||
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) {
|
||||
tracing::debug!(default_branch = branch_name);
|
||||
default_branch = Some(branch_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::debug!("remote.disconnect");
|
||||
remote.disconnect()?;
|
||||
Ok(default_branch)
|
||||
}
|
||||
|
||||
pub fn import_refs(
|
||||
&mut self,
|
||||
branch_names: &[StringPattern],
|
||||
remote_names: &[&str],
|
||||
) -> Result<GitImportStats, GitImportError> {
|
||||
// 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.
|
||||
tracing::debug!("import_refs");
|
||||
import_some_refs(self.mut_repo, self.git_settings, |ref_name| {
|
||||
remote_names
|
||||
.iter()
|
||||
.filter_map(|remote_name| {
|
||||
to_remote_branch(ref_name, remote_name)
|
||||
.map(|branch| branch_names.iter().any(|pattern| pattern.matches(branch)))
|
||||
})
|
||||
.next()
|
||||
.unwrap_or(matches!(ref_name, RefName::Tag(_)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes successful `fetch()` result.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
pub struct GitFetchStats {
|
||||
|
@ -1239,6 +1359,30 @@ pub struct GitFetchStats {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(mut_repo, git_repo, callbacks))]
|
||||
pub fn clone(
|
||||
mut_repo: &mut MutableRepo,
|
||||
git_repo: &git2::Repository,
|
||||
remote_name: &str,
|
||||
branch_names: &[StringPattern],
|
||||
callbacks: RemoteCallbacks<'_>,
|
||||
git_settings: &GitSettings,
|
||||
depth: Option<NonZeroU32>,
|
||||
) -> Result<GitFetchStats, GitFetchError> {
|
||||
let mut git_fetch = GitFetch::new(
|
||||
mut_repo,
|
||||
git_repo,
|
||||
git_settings,
|
||||
GitFetch::fetch_options(callbacks, depth),
|
||||
);
|
||||
let default_branch = git_fetch.fetch(branch_names, remote_name)?;
|
||||
let import_stats = git_fetch.import_refs(branch_names, &[remote_name])?;
|
||||
let stats = GitFetchStats {
|
||||
default_branch,
|
||||
import_stats,
|
||||
};
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
pub fn fetch(
|
||||
mut_repo: &mut MutableRepo,
|
||||
git_repo: &git2::Repository,
|
||||
|
|
|
@ -2257,12 +2257,12 @@ fn test_init() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_empty_repo() {
|
||||
fn test_clone_empty_repo() {
|
||||
let test_data = GitRepoData::create();
|
||||
let git_settings = GitSettings::default();
|
||||
|
||||
let mut tx = test_data.repo.start_transaction(&test_data.settings);
|
||||
let stats = git::fetch(
|
||||
let stats = git::clone(
|
||||
tx.repo_mut(),
|
||||
&test_data.git_repo,
|
||||
"origin",
|
||||
|
@ -2280,7 +2280,7 @@ fn test_fetch_empty_repo() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_initial_commit() {
|
||||
fn test_clone_initial_commit_head_is_not_set() {
|
||||
let test_data = GitRepoData::create();
|
||||
let git_settings = GitSettings {
|
||||
auto_local_bookmark: true,
|
||||
|
@ -2289,7 +2289,7 @@ fn test_fetch_initial_commit() {
|
|||
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
|
||||
|
||||
let mut tx = test_data.repo.start_transaction(&test_data.settings);
|
||||
let stats = git::fetch(
|
||||
let stats = git::clone(
|
||||
tx.repo_mut(),
|
||||
&test_data.git_repo,
|
||||
"origin",
|
||||
|
@ -2330,6 +2330,68 @@ fn test_fetch_initial_commit() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_initial_commit_head_is_set() {
|
||||
let test_data = GitRepoData::create();
|
||||
let git_settings = GitSettings {
|
||||
auto_local_bookmark: true,
|
||||
..Default::default()
|
||||
};
|
||||
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
|
||||
test_data.origin_repo.set_head("refs/heads/main").unwrap();
|
||||
let new_git_commit = empty_git_commit(
|
||||
&test_data.origin_repo,
|
||||
"refs/heads/main",
|
||||
&[&initial_git_commit],
|
||||
);
|
||||
test_data
|
||||
.origin_repo
|
||||
.reference("refs/tags/v1.0", new_git_commit.id(), false, "")
|
||||
.unwrap();
|
||||
|
||||
let mut tx = test_data.repo.start_transaction(&test_data.settings);
|
||||
let stats = git::clone(
|
||||
tx.repo_mut(),
|
||||
&test_data.git_repo,
|
||||
"origin",
|
||||
&[StringPattern::everything()],
|
||||
git::RemoteCallbacks::default(),
|
||||
&git_settings,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(stats.default_branch, Some("main".to_string()));
|
||||
assert!(stats.import_stats.abandoned_commits.is_empty());
|
||||
let repo = tx.commit("test").unwrap();
|
||||
// The new commit visible after git::clone().
|
||||
let view = repo.view();
|
||||
assert!(view.heads().contains(&jj_id(&new_git_commit)));
|
||||
let commit_target = RefTarget::normal(jj_id(&new_git_commit));
|
||||
let commit_remote_ref = RemoteRef {
|
||||
target: commit_target.clone(),
|
||||
state: RemoteRefState::Tracking,
|
||||
};
|
||||
assert_eq!(
|
||||
*view.git_refs(),
|
||||
btreemap! {
|
||||
"refs/remotes/origin/main".to_string() => commit_target.clone(),
|
||||
"refs/tags/v1.0".to_string() => commit_target.clone(),
|
||||
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
view.bookmarks().collect::<BTreeMap<_, _>>(),
|
||||
btreemap! {
|
||||
"main" => BookmarkTarget {
|
||||
local_target: &commit_target,
|
||||
remote_refs: vec![
|
||||
("origin", &commit_remote_ref),
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_success() {
|
||||
let mut test_data = GitRepoData::create();
|
||||
|
@ -2340,7 +2402,7 @@ fn test_fetch_success() {
|
|||
let initial_git_commit = empty_git_commit(&test_data.origin_repo, "refs/heads/main", &[]);
|
||||
|
||||
let mut tx = test_data.repo.start_transaction(&test_data.settings);
|
||||
git::fetch(
|
||||
git::clone(
|
||||
tx.repo_mut(),
|
||||
&test_data.git_repo,
|
||||
"origin",
|
||||
|
|
Loading…
Reference in a new issue