mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-26 22:10:52 +00:00
git: add subcommand for fetching from remote
This adds `jj git fetch` for fetching from a git remote. There remote has to be added in the underlying git repo if it doesn't already exist. I think command will still be useful on typical small projects with just a single remote on GitHub. With this and the `jj git push` I added recently, I think I have enough for my most of my own interaction with GitHub.
This commit is contained in:
parent
7e65a3d589
commit
e14db781b0
3 changed files with 134 additions and 3 deletions
|
@ -49,6 +49,39 @@ pub fn import_refs(tx: &mut Transaction) -> Result<(), GitImportError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum GitFetchError {
|
||||
NotAGitRepo,
|
||||
NoSuchRemote,
|
||||
// TODO: I'm sure there are other errors possible, such as transport-level errors.
|
||||
InternalGitError(String),
|
||||
}
|
||||
|
||||
pub fn fetch(tx: &mut Transaction, remote_name: &str) -> Result<(), GitFetchError> {
|
||||
let git_repo = tx.store().git_repo().ok_or(GitFetchError::NotAGitRepo)?;
|
||||
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
|
||||
}
|
||||
(git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
|
||||
GitFetchError::NoSuchRemote
|
||||
}
|
||||
_ => GitFetchError::InternalGitError(format!("unhandled git error: {:?}", err)),
|
||||
})?;
|
||||
let refspec: &[&str] = &[];
|
||||
remote.fetch(refspec, None, None).map_err(|err| {
|
||||
GitFetchError::InternalGitError(format!("unhandled git error: {:?}", err))
|
||||
})?;
|
||||
import_refs(tx).map_err(|err| match err {
|
||||
GitImportError::NotAGitRepo => panic!("git repo somehow became a non-git repo"),
|
||||
GitImportError::InternalGitError(err) => GitFetchError::InternalGitError(err),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum GitPushError {
|
||||
NotAGitRepo,
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
use git2::Oid;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::git;
|
||||
use jj_lib::git::{GitImportError, GitPushError};
|
||||
use jj_lib::git::{GitFetchError, GitImportError, GitPushError};
|
||||
use jj_lib::repo::{ReadonlyRepo, Repo};
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::store::CommitId;
|
||||
|
@ -128,6 +128,68 @@ fn test_init() {
|
|||
assert!(!heads.contains(&initial_commit_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_success() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
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 git_repo = git2::Repository::init_bare(&source_repo_dir).unwrap();
|
||||
let initial_git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[]);
|
||||
git2::Repository::clone(&source_repo_dir.to_str().unwrap(), &clone_repo_dir).unwrap();
|
||||
std::fs::create_dir(&jj_repo_dir).unwrap();
|
||||
ReadonlyRepo::init_external_git(&settings, jj_repo_dir.clone(), clone_repo_dir.clone());
|
||||
|
||||
let new_git_commit = empty_git_commit(&git_repo, "refs/heads/main", &[&initial_git_commit]);
|
||||
|
||||
// The new commit is not visible before git::fetch().
|
||||
let jj_repo = ReadonlyRepo::load(&settings, jj_repo_dir.clone());
|
||||
let heads: HashSet<_> = jj_repo.view().heads().cloned().collect();
|
||||
assert!(!heads.contains(&commit_id(&new_git_commit)));
|
||||
|
||||
// The new commit is visible after git::fetch().
|
||||
let mut tx = jj_repo.start_transaction("test");
|
||||
git::fetch(&mut tx, "origin").unwrap();
|
||||
let heads: HashSet<_> = tx.as_repo().view().heads().cloned().collect();
|
||||
assert!(heads.contains(&commit_id(&new_git_commit)));
|
||||
|
||||
tx.discard();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_non_git() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let jj_repo_dir = temp_dir.path().join("jj");
|
||||
std::fs::create_dir(&jj_repo_dir).unwrap();
|
||||
let jj_repo = ReadonlyRepo::init_local(&settings, jj_repo_dir);
|
||||
|
||||
let mut tx = jj_repo.start_transaction("test");
|
||||
let result = git::fetch(&mut tx, "origin");
|
||||
assert_eq!(result, Err(GitFetchError::NotAGitRepo));
|
||||
|
||||
tx.discard();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_no_such_remote() {
|
||||
let settings = testutils::user_settings();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let source_repo_dir = temp_dir.path().join("source");
|
||||
let jj_repo_dir = temp_dir.path().join("jj");
|
||||
git2::Repository::init_bare(&source_repo_dir).unwrap();
|
||||
std::fs::create_dir(&jj_repo_dir).unwrap();
|
||||
let jj_repo =
|
||||
ReadonlyRepo::init_external_git(&settings, jj_repo_dir.clone(), source_repo_dir.clone());
|
||||
|
||||
let mut tx = jj_repo.start_transaction("test");
|
||||
let result = git::fetch(&mut tx, "invalid-remote");
|
||||
assert_eq!(result, Err(GitFetchError::NoSuchRemote));
|
||||
|
||||
tx.discard();
|
||||
}
|
||||
|
||||
struct PushTestSetup {
|
||||
source_repo_dir: PathBuf,
|
||||
clone_repo_dir: PathBuf,
|
||||
|
|
|
@ -59,7 +59,7 @@ use crate::styler::{ColorStyler, Styler};
|
|||
use crate::template_parser::TemplateParser;
|
||||
use crate::templater::Template;
|
||||
use crate::ui::Ui;
|
||||
use jj_lib::git::{GitImportError, GitPushError};
|
||||
use jj_lib::git::{GitFetchError, GitImportError, GitPushError};
|
||||
use jj_lib::index::{HexPrefix, PrefixResolution};
|
||||
use jj_lib::operation::Operation;
|
||||
use jj_lib::transaction::Transaction;
|
||||
|
@ -424,6 +424,16 @@ fn get_app<'a, 'b>() -> App<'a, 'b> {
|
|||
let git_command = SubCommand::with_name("git")
|
||||
.about("commands for working with the underlying git repo")
|
||||
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("fetch")
|
||||
.about("fetch from a git remote")
|
||||
.arg(
|
||||
Arg::with_name("remote")
|
||||
.long("remote")
|
||||
.takes_value(true)
|
||||
.default_value("origin"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("push")
|
||||
.about("push a revision to a git remote branch")
|
||||
|
@ -1964,6 +1974,30 @@ fn cmd_operation(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_git_fetch(
|
||||
ui: &mut Ui,
|
||||
matches: &ArgMatches,
|
||||
_git_matches: &ArgMatches,
|
||||
cmd_matches: &ArgMatches,
|
||||
) -> Result<(), CommandError> {
|
||||
let repo = get_repo(ui, &matches)?;
|
||||
let remote_name = cmd_matches.value_of("remote").unwrap();
|
||||
let mut tx = repo.start_transaction(&format!("fetch from git remote {}", remote_name));
|
||||
git::fetch(&mut tx, remote_name).map_err(|err| match err {
|
||||
GitFetchError::NotAGitRepo => CommandError::UserError(
|
||||
"git push can only be used in repos backed by a git repo".to_string(),
|
||||
),
|
||||
GitFetchError::NoSuchRemote => {
|
||||
CommandError::UserError(format!("No such git remote: {}", remote_name))
|
||||
}
|
||||
GitFetchError::InternalGitError(err) => {
|
||||
CommandError::UserError(format!("Fetch failed: {:?}", err))
|
||||
}
|
||||
})?;
|
||||
tx.commit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_git_push(
|
||||
ui: &mut Ui,
|
||||
matches: &ArgMatches,
|
||||
|
@ -2017,7 +2051,9 @@ fn cmd_git(
|
|||
matches: &ArgMatches,
|
||||
sub_matches: &ArgMatches,
|
||||
) -> Result<(), CommandError> {
|
||||
if let Some(command_matches) = sub_matches.subcommand_matches("push") {
|
||||
if let Some(command_matches) = sub_matches.subcommand_matches("fetch") {
|
||||
cmd_git_fetch(ui, matches, sub_matches, command_matches)?;
|
||||
} else if let Some(command_matches) = sub_matches.subcommand_matches("push") {
|
||||
cmd_git_push(ui, matches, sub_matches, command_matches)?;
|
||||
} else if let Some(command_matches) = sub_matches.subcommand_matches("refresh") {
|
||||
cmd_git_refresh(ui, matches, sub_matches, command_matches)?;
|
||||
|
|
Loading…
Reference in a new issue