mirror of
https://github.com/martinvonz/jj.git
synced 2024-10-23 15:00:17 +00:00
git: extract function for pushing commit to remote branch, and test it
This commit is contained in:
parent
55a7621c45
commit
e82197d981
5 changed files with 236 additions and 20 deletions
|
@ -67,6 +67,10 @@ impl Commit {
|
|||
Commit { store, id, data }
|
||||
}
|
||||
|
||||
pub fn store(&self) -> &Arc<StoreWrapper> {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &CommitId {
|
||||
&self.id
|
||||
}
|
||||
|
|
72
lib/src/git.rs
Normal file
72
lib/src/git.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
// 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 crate::commit::Commit;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum GitPushError {
|
||||
NotAGitRepo,
|
||||
NoSuchRemote,
|
||||
NotFastForward,
|
||||
// TODO: I'm sure there are other errors possible, such as transport-level errors,
|
||||
// and errors caused by the remote rejecting the push.
|
||||
InternalGitError(String),
|
||||
}
|
||||
|
||||
pub fn push_commit(
|
||||
commit: &Commit,
|
||||
remote_name: &str,
|
||||
remote_branch: &str,
|
||||
) -> Result<(), GitPushError> {
|
||||
let git_repo = commit.store().git_repo().ok_or(GitPushError::NotAGitRepo)?;
|
||||
let locked_git_repo = git_repo.lock().unwrap();
|
||||
// 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 = locked_git_repo
|
||||
.reference(
|
||||
&temp_ref_name,
|
||||
git2::Oid::from_bytes(&commit.id().0).unwrap(),
|
||||
true,
|
||||
"temporary reference for git push",
|
||||
)
|
||||
.map_err(|err| {
|
||||
GitPushError::InternalGitError(format!(
|
||||
"failed to create temporary git ref for push: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let mut remote = locked_git_repo.find_remote(remote_name).map_err(|err| {
|
||||
match (err.class(), err.code()) {
|
||||
(git2::ErrorClass::Config, git2::ErrorCode::NotFound) => GitPushError::NoSuchRemote,
|
||||
(git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => GitPushError::NoSuchRemote,
|
||||
_ => panic!("unhandled git error: {:?}", err),
|
||||
}
|
||||
})?;
|
||||
// Need to add "refs/heads/" prefix due to https://github.com/libgit2/libgit2/issues/1125
|
||||
let refspec = format!("{}:refs/heads/{}", temp_ref_name, remote_branch);
|
||||
remote
|
||||
.push(&[refspec], None)
|
||||
.map_err(|err| match (err.class(), err.code()) {
|
||||
(git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => {
|
||||
GitPushError::NotFastForward
|
||||
}
|
||||
_ => panic!("unhandled git error: {:?}", err),
|
||||
})?;
|
||||
temp_ref.delete().map_err(|err| {
|
||||
GitPushError::InternalGitError(format!(
|
||||
"failed to delete temporary git ref for push: {}",
|
||||
err
|
||||
))
|
||||
})
|
||||
}
|
|
@ -24,6 +24,7 @@ pub mod conflicts;
|
|||
pub mod dag_walk;
|
||||
pub mod evolution;
|
||||
pub mod files;
|
||||
pub mod git;
|
||||
pub mod git_store;
|
||||
pub mod index;
|
||||
pub mod local_store;
|
||||
|
|
144
lib/tests/test_git.rs
Normal file
144
lib/tests/test_git.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
// 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 git2::Oid;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::git;
|
||||
use jj_lib::git::GitPushError;
|
||||
use jj_lib::repo::ReadonlyRepo;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::store::CommitId;
|
||||
use jj_lib::testutils;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Create a Git repo with a single commit in the "main" branch.
|
||||
fn create_source_repo(dir: &Path) -> CommitId {
|
||||
let git_repo = git2::Repository::init_bare(dir).unwrap();
|
||||
let signature = git2::Signature::now("Someone", "someone@example.com").unwrap();
|
||||
let empty_tree_id = Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
|
||||
let empty_tree = git_repo.find_tree(empty_tree_id).unwrap();
|
||||
let oid = git_repo
|
||||
.commit(
|
||||
Some("refs/heads/main"),
|
||||
&signature,
|
||||
&signature,
|
||||
"message",
|
||||
&empty_tree,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
CommitId(oid.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn create_repo_clone(source: &Path, destination: &Path) {
|
||||
git2::Repository::clone(&source.to_str().unwrap(), destination).unwrap();
|
||||
}
|
||||
|
||||
struct PushTestSetup {
|
||||
source_repo_dir: PathBuf,
|
||||
clone_repo_dir: PathBuf,
|
||||
jj_repo: Arc<ReadonlyRepo>,
|
||||
new_commit: Commit,
|
||||
}
|
||||
|
||||
fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSetup {
|
||||
let source_repo_dir = temp_dir.path().join("source");
|
||||
let clone_repo_dir = temp_dir.path().join("clone");
|
||||
let jj_repo_dir = temp_dir.path().join("jj");
|
||||
let initial_commit_id = create_source_repo(&source_repo_dir);
|
||||
create_repo_clone(&source_repo_dir, &clone_repo_dir);
|
||||
std::fs::create_dir(&jj_repo_dir).unwrap();
|
||||
let mut jj_repo =
|
||||
ReadonlyRepo::init_git(&settings, jj_repo_dir.clone(), clone_repo_dir.clone());
|
||||
let new_commit = testutils::create_random_commit(&settings, &jj_repo)
|
||||
.set_parents(vec![initial_commit_id.clone()])
|
||||
.write_to_new_transaction(&jj_repo, "test");
|
||||
Arc::get_mut(&mut jj_repo).unwrap().reload();
|
||||
PushTestSetup {
|
||||
source_repo_dir,
|
||||
clone_repo_dir,
|
||||
jj_repo,
|
||||
new_commit,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_commit_success() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let setup = set_up_push_repos(&settings, &temp_dir);
|
||||
let result = git::push_commit(&setup.new_commit, "origin", "main");
|
||||
assert_eq!(result, Ok(()));
|
||||
|
||||
// Check that the ref got updated in the source repo
|
||||
let source_repo = git2::Repository::open(&setup.source_repo_dir).unwrap();
|
||||
let new_target = source_repo
|
||||
.find_reference("refs/heads/main")
|
||||
.unwrap()
|
||||
.target();
|
||||
let new_oid = Oid::from_bytes(&setup.new_commit.id().0).unwrap();
|
||||
assert_eq!(new_target, Some(new_oid));
|
||||
|
||||
// Check that the ref got updated in the cloned repo. This just tests our
|
||||
// assumptions about libgit2 because we want the refs/remotes/origin/main
|
||||
// branch to be updated.
|
||||
let clone_repo = git2::Repository::open(&setup.clone_repo_dir).unwrap();
|
||||
let new_target = clone_repo
|
||||
.find_reference("refs/remotes/origin/main")
|
||||
.unwrap()
|
||||
.target();
|
||||
assert_eq!(new_target, Some(new_oid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_commit_not_fast_forward() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut jj_repo = set_up_push_repos(&settings, &temp_dir).jj_repo;
|
||||
let new_commit = testutils::create_random_commit(&settings, &jj_repo)
|
||||
.write_to_new_transaction(&jj_repo, "test");
|
||||
Arc::get_mut(&mut jj_repo).unwrap().reload();
|
||||
let result = git::push_commit(&new_commit, "origin", "main");
|
||||
assert_eq!(result, Err(GitPushError::NotFastForward));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_commit_no_such_remote() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let setup = set_up_push_repos(&settings, &temp_dir);
|
||||
let result = git::push_commit(&setup.new_commit, "invalid-remote", "main");
|
||||
assert_eq!(result, Err(GitPushError::NoSuchRemote));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_commit_invalid_remote() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let setup = set_up_push_repos(&settings, &temp_dir);
|
||||
let result = git::push_commit(&setup.new_commit, "http://invalid-remote", "main");
|
||||
assert_eq!(result, Err(GitPushError::NoSuchRemote));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_commit_non_git() {
|
||||
let settings = testutils::user_settings();
|
||||
let (_temp_dir, repo) = testutils::init_repo(&settings, false);
|
||||
let commit =
|
||||
testutils::create_random_commit(&settings, &repo).write_to_new_transaction(&repo, "test");
|
||||
let result = git::push_commit(&commit, "origin", "main");
|
||||
assert_eq!(result, Err(GitPushError::NotAGitRepo));
|
||||
}
|
|
@ -58,6 +58,7 @@ use crate::styler::{ColorStyler, Styler};
|
|||
use crate::template_parser::TemplateParser;
|
||||
use crate::templater::Template;
|
||||
use crate::ui::Ui;
|
||||
use jj_lib::git::GitPushError;
|
||||
use jj_lib::index::{HexPrefix, PrefixResolution};
|
||||
use jj_lib::operation::Operation;
|
||||
use jj_lib::transaction::Transaction;
|
||||
|
@ -1947,30 +1948,24 @@ fn cmd_git_push(
|
|||
cmd_matches: &ArgMatches,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut repo = get_repo(ui, &matches)?;
|
||||
let store = repo.store().clone();
|
||||
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
||||
let git_repo = store.git_repo().ok_or_else(|| {
|
||||
CommandError::UserError(
|
||||
"git push can only be used in repos backed by a git repo".to_string(),
|
||||
)
|
||||
})?;
|
||||
let commit = resolve_revision_arg(ui, mut_repo, cmd_matches)?;
|
||||
let remote_name = cmd_matches.value_of("remote").unwrap();
|
||||
let branch_name = cmd_matches.value_of("branch").unwrap();
|
||||
let locked_git_repo = git_repo.lock().unwrap();
|
||||
// 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 = locked_git_repo.reference(
|
||||
&temp_ref_name,
|
||||
git2::Oid::from_bytes(&commit.id().0).unwrap(),
|
||||
true,
|
||||
"temporary reference for git push",
|
||||
)?;
|
||||
let mut remote = locked_git_repo.find_remote(remote_name)?;
|
||||
// Need to add "refs/heads/" prefix due to https://github.com/libgit2/libgit2/issues/1125
|
||||
let refspec = format!("{}:refs/heads/{}", temp_ref_name, branch_name);
|
||||
remote.push(&[refspec], None)?;
|
||||
temp_ref.delete()?;
|
||||
jj_lib::git::push_commit(&commit, remote_name, branch_name).map_err(|err| match err {
|
||||
GitPushError::NotAGitRepo => CommandError::UserError(
|
||||
"git push can only be used in repos backed by a git repo".to_string(),
|
||||
),
|
||||
GitPushError::NoSuchRemote => {
|
||||
CommandError::UserError(format!("No such git remote: {}", remote_name))
|
||||
}
|
||||
GitPushError::NotFastForward => {
|
||||
CommandError::UserError("Push is not fast-forwardable".to_string())
|
||||
}
|
||||
GitPushError::InternalGitError(err) => {
|
||||
CommandError::UserError(format!("Push failed: {:?}", err))
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue