// 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. use thiserror::Error; use crate::commit::Commit; use crate::store::CommitId; use crate::transaction::Transaction; #[derive(Error, Debug, PartialEq)] pub enum GitImportError { #[error("Unexpected git error when importing refs: {0}")] InternalGitError(#[from] git2::Error), } // Reflect changes made in the underlying Git repo in the Jujube repo. pub fn import_refs( tx: &mut Transaction, git_repo: &git2::Repository, ) -> Result<(), GitImportError> { let store = tx.store().clone(); let git_refs = git_repo.references()?; let existing_git_refs: Vec<_> = tx.view().git_refs().keys().cloned().collect(); // 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 { tx.remove_git_ref(&existing_git_ref); // 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. } for git_ref in git_refs { let git_ref = git_ref?; 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. // TODO: Is it useful to import HEAD (especially if it's detached)? continue; } let git_commit = git_ref.peel_to_commit()?; let id = CommitId(git_commit.id().as_bytes().to_vec()); let commit = store.get_commit(&id).unwrap(); tx.add_head(&commit); tx.insert_git_ref(git_ref.name().unwrap().to_string(), id); // For now, we consider all remotes "publishing". // TODO: Make it configurable which remotes are publishing. if git_ref.is_remote() { tx.add_public_head(&commit); } } Ok(()) } #[derive(Error, Debug, PartialEq)] pub enum GitFetchError { #[error("No git remote named '{0}'")] NoSuchRemote(String), // TODO: I'm sure there are other errors possible, such as transport-level errors. #[error("Unexpected git error when fetching: {0}")] InternalGitError(#[from] git2::Error), } pub fn fetch( tx: &mut Transaction, git_repo: &git2::Repository, remote_name: &str, ) -> Result<(), GitFetchError> { let mut remote = git_repo .find_remote(remote_name) .map_err(|err| match (err.class(), err.code()) { (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => { GitFetchError::NoSuchRemote(remote_name.to_string()) } (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => { GitFetchError::NoSuchRemote(remote_name.to_string()) } _ => GitFetchError::InternalGitError(err), })?; let refspec: &[&str] = &[]; remote.fetch(refspec, None, None)?; import_refs(tx, git_repo).map_err(|err| match err { GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source), })?; Ok(()) } #[derive(Error, Debug, PartialEq)] pub enum GitPushError { #[error("No git remote named '{0}'")] NoSuchRemote(String), #[error("Push is not fast-forwardable'")] NotFastForward, #[error("Remote reject the update'")] RefUpdateRejected, // TODO: I'm sure there are other errors possible, such as transport-level errors, // and errors caused by the remote rejecting the push. #[error("Unexpected git error when pushing: {0}")] InternalGitError(#[from] git2::Error), } pub fn push_commit( git_repo: &git2::Repository, 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()); let mut temp_ref = git_repo.reference( &temp_ref_name, git2::Oid::from_bytes(&commit.id().0).unwrap(), true, "temporary reference for git push", )?; let mut remote = git_repo .find_remote(remote_name) .map_err(|err| match (err.class(), err.code()) { (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => { GitPushError::NoSuchRemote(remote_name.to_string()) } (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => { GitPushError::NoSuchRemote(remote_name.to_string()) } _ => GitPushError::InternalGitError(err), })?; // Need to add "refs/heads/" prefix due to https://github.com/libgit2/libgit2/issues/1125 let qualified_remote_branch = format!("refs/heads/{}", remote_branch); let mut callbacks = git2::RemoteCallbacks::new(); let mut updated = false; callbacks.credentials(|_url, username_from_url, _allowed_types| { git2::Cred::ssh_key_from_agent(username_from_url.unwrap()) }); callbacks.push_update_reference(|refname, status| { if refname == qualified_remote_branch && status.is_none() { updated = true; } Ok(()) }); let refspec = format!("{}:{}", temp_ref_name, qualified_remote_branch); let mut push_options = git2::PushOptions::new(); push_options.remote_callbacks(callbacks); remote .push(&[refspec], Some(&mut push_options)) .map_err(|err| match (err.class(), err.code()) { (git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => { GitPushError::NotFastForward } _ => GitPushError::InternalGitError(err), })?; drop(push_options); temp_ref.delete()?; if updated { Ok(()) } else { Err(GitPushError::RefUpdateRejected) } }