From d481001271fe8adebb2fd4c93e50f9d1633c6255 Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Sat, 26 Dec 2020 18:24:29 -0800 Subject: [PATCH] commands: add a `jj git push` command This commit starts adding support for working with a Jujube repo's underlyng Git repo (if there is one). It does so by adding a command for pushing from the Git repo to a remote, so you can work with your anonymous branches in Jujube and push to a remote Git repo without having to switch repos and copy commit hashes. For example, `jj git push origin main` will push to the "main" branch on the remote called "origin". The remote name (such as "origin") is resolved in that repo. Unlike most commands, it defaults to pushing the working copy's parent, since it is probably a mistake to push a working copy commit to a Git repo. I plan to add more `jj git` subcommands later. There will probably be at least a command (or several?) for making the Git repo's refs available in the Jujube repo. --- lib/src/git_store.rs | 4 ++ lib/src/local_store.rs | 5 +++ lib/src/store.rs | 3 ++ lib/src/store_wrapper.rs | 7 +++- src/commands.rs | 79 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) 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") {