mirror of
https://github.com/martinvonz/jj.git
synced 2024-12-28 23:55:54 +00:00
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.
This commit is contained in:
parent
09e474a05a
commit
d481001271
5 changed files with 97 additions and 1 deletions
|
@ -155,6 +155,10 @@ impl Store for GitStore {
|
|||
20
|
||||
}
|
||||
|
||||
fn git_repo(&self) -> Option<&Mutex<git2::Repository>> {
|
||||
Some(&self.repo)
|
||||
}
|
||||
|
||||
fn read_file(&self, _path: &FileRepoPath, id: &FileId) -> StoreResult<Box<dyn Read>> {
|
||||
if id.0.len() != self.hash_length() {
|
||||
return Err(StoreError::NotFound);
|
||||
|
|
|
@ -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<std::io::Error> for StoreError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
|
@ -110,6 +111,10 @@ impl Store for LocalStore {
|
|||
64
|
||||
}
|
||||
|
||||
fn git_repo(&self) -> Option<&Mutex<git2::Repository>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_file(&self, _path: &FileRepoPath, id: &FileId) -> StoreResult<Box<dyn Read>> {
|
||||
let path = self.file_path(&id);
|
||||
let file = File::open(path).map_err(not_found_to_store_error)?;
|
||||
|
|
|
@ -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<git2::Repository>>;
|
||||
|
||||
fn read_file(&self, path: &FileRepoPath, id: &FileId) -> StoreResult<Box<dyn Read>>;
|
||||
|
||||
fn write_file(&self, path: &FileRepoPath, contents: &mut dyn Read) -> StoreResult<FileId>;
|
||||
|
|
|
@ -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<Repository>> {
|
||||
self.store.git_repo()
|
||||
}
|
||||
|
||||
pub fn empty_tree_id(&self) -> &TreeId {
|
||||
self.store.empty_tree_id()
|
||||
}
|
||||
|
|
|
@ -74,6 +74,12 @@ impl From<DiffEditError> for CommandError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<git2::Error> for CommandError {
|
||||
fn from(err: git2::Error) -> Self {
|
||||
CommandError::UserError(format!("Git operation failed: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_repo(ui: &Ui, matches: &ArgMatches) -> Result<Arc<ReadonlyRepo>, 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<I, T>(mut ui: Ui, args: I) -> i32
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
|
@ -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") {
|
||||
|
|
Loading…
Reference in a new issue