diff --git a/lib/src/git_store.rs b/lib/src/git_store.rs index 9dbc0c816..b822ad625 100644 --- a/lib/src/git_store.rs +++ b/lib/src/git_store.rs @@ -155,6 +155,10 @@ impl Store for GitStore { 20 } + fn git_repo(&self) -> Option<&Mutex> { + Some(&self.repo) + } + fn read_file(&self, _path: &FileRepoPath, id: &FileId) -> StoreResult> { if id.0.len() != self.hash_length() { return Err(StoreError::NotFound); diff --git a/lib/src/local_store.rs b/lib/src/local_store.rs index c9cafdf9f..81ed55376 100644 --- a/lib/src/local_store.rs +++ b/lib/src/local_store.rs @@ -28,6 +28,7 @@ use crate::store::{ ChangeId, Commit, CommitId, Conflict, ConflictId, ConflictPart, FileId, MillisSinceEpoch, Signature, Store, StoreError, StoreResult, SymlinkId, Timestamp, Tree, TreeId, TreeValue, }; +use std::sync::Mutex; impl From for StoreError { fn from(err: std::io::Error) -> Self { @@ -110,6 +111,10 @@ impl Store for LocalStore { 64 } + fn git_repo(&self) -> Option<&Mutex> { + None + } + fn read_file(&self, _path: &FileRepoPath, id: &FileId) -> StoreResult> { let path = self.file_path(&id); let file = File::open(path).map_err(not_found_to_store_error)?; diff --git a/lib/src/store.rs b/lib/src/store.rs index 7005735bc..78d1fd183 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -21,6 +21,7 @@ use std::vec::Vec; use crate::repo_path::DirRepoPath; use crate::repo_path::FileRepoPath; use std::borrow::Borrow; +use std::sync::Mutex; use thiserror::Error; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] @@ -332,6 +333,8 @@ impl Tree { pub trait Store: Send + Sync + Debug { fn hash_length(&self) -> usize; + fn git_repo(&self) -> Option<&Mutex>; + fn read_file(&self, path: &FileRepoPath, id: &FileId) -> StoreResult>; fn write_file(&self, path: &FileRepoPath, contents: &mut dyn Read) -> StoreResult; diff --git a/lib/src/store_wrapper.rs b/lib/src/store_wrapper.rs index ddf071f2f..0bde25ff1 100644 --- a/lib/src/store_wrapper.rs +++ b/lib/src/store_wrapper.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::collections::HashMap; -use std::sync::{Arc, RwLock, Weak}; +use std::sync::{Arc, Mutex, RwLock, Weak}; use crate::commit::Commit; use crate::repo_path::{DirRepoPath, FileRepoPath}; @@ -24,6 +24,7 @@ use crate::store::{ }; use crate::tree::Tree; use crate::tree_builder::TreeBuilder; +use git2::Repository; use std::io::Read; /// Wraps the low-level store and makes it return more convenient types. Also @@ -59,6 +60,10 @@ impl StoreWrapper { self.store.hash_length() } + pub fn git_repo(&self) -> Option<&Mutex> { + self.store.git_repo() + } + pub fn empty_tree_id(&self) -> &TreeId { self.store.empty_tree_id() } diff --git a/src/commands.rs b/src/commands.rs index 6132de584..26df9b84c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -74,6 +74,12 @@ impl From for CommandError { } } +impl From for CommandError { + fn from(err: git2::Error) -> Self { + CommandError::UserError(format!("Git operation failed: {}", err)) + } +} + fn get_repo(ui: &Ui, matches: &ArgMatches) -> Result, CommandError> { let repo_path_str = matches.value_of("repository").unwrap(); let repo_path = ui.cwd().join(repo_path_str); @@ -411,6 +417,31 @@ fn get_app<'a, 'b>() -> App<'a, 'b> { .about("restore to the state at an operation") .arg(op_arg()), ); + let git_command = SubCommand::with_name("git") + .about("commands for working with the underlying git repo") + .subcommand( + SubCommand::with_name("push") + .about("push a revision to a git remote branch") + .arg( + Arg::with_name("revision") + .long("revision") + .short("r") + .takes_value(true) + .default_value("@^"), + ) + .arg( + Arg::with_name("remote") + .long("remote") + .index(1) + .required(true), + ) + .arg( + Arg::with_name("branch") + .long("branch") + .index(2) + .required(true), + ), + ); let bench_command = SubCommand::with_name("bench") .about("commands for benchmarking internal operations") .subcommand( @@ -499,6 +530,7 @@ fn get_app<'a, 'b>() -> App<'a, 'b> { .subcommand(backout_command) .subcommand(evolve_command) .subcommand(operation_command) + .subcommand(git_command) .subcommand(bench_command) .subcommand(debug_command) } @@ -1908,6 +1940,51 @@ fn cmd_operation( Ok(()) } +fn cmd_git_push( + ui: &mut Ui, + matches: &ArgMatches, + _git_matches: &ArgMatches, + 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 on repos back 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()?; + Ok(()) +} + +fn cmd_git( + ui: &mut Ui, + matches: &ArgMatches, + sub_matches: &ArgMatches, +) -> Result<(), CommandError> { + if let Some(command_matches) = sub_matches.subcommand_matches("push") { + cmd_git_push(ui, matches, sub_matches, command_matches)?; + } else { + panic!("unhandled command: {:#?}", matches); + } + Ok(()) +} + pub fn dispatch(mut ui: Ui, args: I) -> i32 where I: IntoIterator, @@ -1967,6 +2044,8 @@ where cmd_evolve(&mut ui, &matches, &sub_matches) } else if let Some(sub_matches) = matches.subcommand_matches("operation") { cmd_operation(&mut ui, &matches, &sub_matches) + } else if let Some(sub_matches) = matches.subcommand_matches("git") { + cmd_git(&mut ui, &matches, &sub_matches) } else if let Some(sub_matches) = matches.subcommand_matches("bench") { cmd_bench(&mut ui, &matches, &sub_matches) } else if let Some(sub_matches) = matches.subcommand_matches("debug") {