2020-12-28 01:41:20 +00:00
|
|
|
// Copyright 2020 Google LLC
|
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
|
2021-03-14 17:37:28 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2020-12-28 01:41:20 +00:00
|
|
|
use crate::commit::Commit;
|
2021-03-16 23:08:40 +00:00
|
|
|
use crate::repo::MutableRepo;
|
2020-12-29 07:31:48 +00:00
|
|
|
use crate::store::CommitId;
|
|
|
|
|
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-05-15 16:16:31 +00:00
|
|
|
// Reflect changes made in the underlying Git repo in the Jujutsu repo.
|
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,
|
|
|
|
) -> Result<(), GitImportError> {
|
2021-03-16 23:08:40 +00:00
|
|
|
let store = mut_repo.store().clone();
|
2020-12-29 07:31:48 +00:00
|
|
|
let git_refs = git_repo.references()?;
|
2021-03-16 23:08:40 +00:00
|
|
|
let existing_git_refs: Vec<_> = mut_repo.view().git_refs().keys().cloned().collect();
|
2021-01-03 08:26:57 +00:00
|
|
|
// TODO: Store the id of the previous import and read it back here, so we can
|
|
|
|
// merge the views instead of overwriting.
|
|
|
|
for existing_git_ref in existing_git_refs {
|
2021-03-16 23:08:40 +00:00
|
|
|
mut_repo.remove_git_ref(&existing_git_ref);
|
2021-01-03 08:26:57 +00:00
|
|
|
// TODO: We should probably also remove heads pointing to the same
|
|
|
|
// commits and commits no longer reachable from other refs.
|
|
|
|
// If the underlying git repo has a branch that gets rewritten, we
|
|
|
|
// should probably not keep the commits it used to point to.
|
|
|
|
}
|
2020-12-29 07:31:48 +00:00
|
|
|
for git_ref in git_refs {
|
|
|
|
let git_ref = git_ref?;
|
2021-01-03 08:26:57 +00:00
|
|
|
if !(git_ref.is_tag() || git_ref.is_branch() || git_ref.is_remote())
|
|
|
|
|| git_ref.name().is_none()
|
|
|
|
{
|
|
|
|
// Skip other refs (such as notes) and symbolic refs, as well as non-utf8 refs.
|
2020-12-29 07:31:48 +00:00
|
|
|
// TODO: Is it useful to import HEAD (especially if it's detached)?
|
|
|
|
continue;
|
|
|
|
}
|
2021-03-14 20:39:45 +00:00
|
|
|
let git_commit = match git_ref.peel_to_commit() {
|
|
|
|
Ok(git_commit) => git_commit,
|
|
|
|
Err(_) => {
|
|
|
|
// Perhaps a tag pointing to a GPG key or similar. Just skip it.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
2020-12-29 07:31:48 +00:00
|
|
|
let id = CommitId(git_commit.id().as_bytes().to_vec());
|
|
|
|
let commit = store.get_commit(&id).unwrap();
|
2021-03-16 23:08:40 +00:00
|
|
|
mut_repo.add_head(&commit);
|
|
|
|
mut_repo.insert_git_ref(git_ref.name().unwrap().to_string(), id);
|
2021-01-16 19:48:35 +00:00
|
|
|
// For now, we consider all remotes "publishing".
|
|
|
|
// TODO: Make it configurable which remotes are publishing.
|
|
|
|
if git_ref.is_remote() {
|
2021-03-16 23:08:40 +00:00
|
|
|
mut_repo.add_public_head(&commit);
|
2021-01-16 19:48:35 +00:00
|
|
|
}
|
2020-12-29 07:31:48 +00:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2020-12-28 01:41:20 +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),
|
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
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
) -> Result<(), 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 callbacks = git2::RemoteCallbacks::new();
|
|
|
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
|
|
|
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
|
|
|
|
});
|
|
|
|
let mut fetch_options = git2::FetchOptions::new();
|
|
|
|
fetch_options.remote_callbacks(callbacks);
|
2020-12-31 17:56:20 +00:00
|
|
|
let refspec: &[&str] = &[];
|
2021-03-15 00:11:54 +00:00
|
|
|
remote.fetch(refspec, Some(&mut fetch_options), None)?;
|
2021-03-16 23:08:40 +00:00
|
|
|
import_refs(mut_repo, git_repo).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
|
|
|
})?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
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),
|
|
|
|
#[error("Push is not fast-forwardable'")]
|
2020-12-28 01:41:20 +00:00
|
|
|
NotFastForward,
|
2021-01-02 18:08:23 +00:00
|
|
|
#[error("Remote reject the update'")]
|
|
|
|
RefUpdateRejected,
|
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,
|
2020-12-28 01:41:20 +00:00
|
|
|
commit: &Commit,
|
|
|
|
remote_name: &str,
|
|
|
|
remote_branch: &str,
|
|
|
|
) -> Result<(), GitPushError> {
|
|
|
|
// Create a temporary ref to work around https://github.com/libgit2/libgit2/issues/3178
|
|
|
|
let temp_ref_name = format!("refs/jj/git-push/{}", commit.id().hex());
|
2021-01-01 20:53:07 +00:00
|
|
|
let mut temp_ref = git_repo.reference(
|
|
|
|
&temp_ref_name,
|
|
|
|
git2::Oid::from_bytes(&commit.id().0).unwrap(),
|
|
|
|
true,
|
|
|
|
"temporary reference for git push",
|
|
|
|
)?;
|
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
|
|
|
})?;
|
2020-12-28 01:41:20 +00:00
|
|
|
// Need to add "refs/heads/" prefix due to https://github.com/libgit2/libgit2/issues/1125
|
2021-01-02 08:24:10 +00:00
|
|
|
let qualified_remote_branch = format!("refs/heads/{}", remote_branch);
|
|
|
|
let mut callbacks = git2::RemoteCallbacks::new();
|
2021-01-02 18:08:23 +00:00
|
|
|
let mut updated = false;
|
2021-01-02 08:24:10 +00:00
|
|
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
|
|
|
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
|
|
|
|
});
|
2021-01-02 18:08:23 +00:00
|
|
|
callbacks.push_update_reference(|refname, status| {
|
2021-01-03 03:27:51 +00:00
|
|
|
if refname == qualified_remote_branch && status.is_none() {
|
2021-01-02 18:08:23 +00:00
|
|
|
updated = true;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
});
|
2021-01-02 08:24:10 +00:00
|
|
|
let refspec = format!("{}:{}", temp_ref_name, qualified_remote_branch);
|
|
|
|
let mut push_options = git2::PushOptions::new();
|
|
|
|
push_options.remote_callbacks(callbacks);
|
2020-12-28 01:41:20 +00:00
|
|
|
remote
|
2021-01-02 08:24:10 +00:00
|
|
|
.push(&[refspec], 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-01-01 20:53:07 +00:00
|
|
|
temp_ref.delete()?;
|
2021-01-02 18:08:23 +00:00
|
|
|
if updated {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err(GitPushError::RefUpdateRejected)
|
|
|
|
}
|
2020-12-28 01:41:20 +00:00
|
|
|
}
|