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:
Martin von Zweigbergk 2020-12-31 09:56:20 -08:00
parent 7e65a3d589
commit e14db781b0
3 changed files with 134 additions and 3 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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)?;