diff --git a/crates/radicle-cli/examples/rad-init-detached-head.md b/crates/radicle-cli/examples/rad-init-detached-head.md new file mode 100644 index 00000000..8935abd0 --- /dev/null +++ b/crates/radicle-cli/examples/rad-init-detached-head.md @@ -0,0 +1,29 @@ +Let's assume you created a Git repository containing two commits and checked out +the first one, leaving you in a detached `HEAD` state: + +``` +$ git init -q +$ touch file1.txt +$ git add . +$ git commit -m "Create file" -q +$ touch file2.txt +$ git add . +$ git commit -m "Create a second file" -q +$ git checkout HEAD~1 +``` + +If you try to `rad init` the repository in this state, it will attempt to +determine a default branch. However, it cannot because we are currently not on +any branch nor have we set the `init.defaultBranch` option in our `git config`. + +Using `rad init` will fail, providing a hint on how to fix the issue: + +``` (fail) +$ rad init +✗ Error: in detached HEAD state +✗ Hint: try `git checkout ` or set `git config set --local init.defaultBranch ` +✗ Error: aborting `rad init` +``` + +Alternatively, if you know a branch that already exists, and you would like to +use, you can use the `--default-branch` option. diff --git a/crates/radicle-cli/examples/rad-init-no-git.md b/crates/radicle-cli/examples/rad-init-no-git.md index 505c426a..b39e914d 100644 --- a/crates/radicle-cli/examples/rad-init-no-git.md +++ b/crates/radicle-cli/examples/rad-init-no-git.md @@ -16,7 +16,10 @@ Now we try again. ``` (fail) $ rad init -✗ Error: repository head must point to a commit +✗ Error: could not determine default branch in repository +✗ Hint: perhaps you need to create a branch? +✗ Error: aborting `rad init` ``` -Looks like we need a commit. +Looks like we need to get to work and start working on a branch and add commits +to it. diff --git a/crates/radicle-cli/src/commands/init.rs b/crates/radicle-cli/src/commands/init.rs index b837f558..76822e0b 100644 --- a/crates/radicle-cli/src/commands/init.rs +++ b/crates/radicle-cli/src/commands/init.rs @@ -12,6 +12,7 @@ use serde_json as json; use radicle::crypto::ssh; use radicle::explorer::ExplorerUrl; +use radicle::git::raw; use radicle::git::RefString; use radicle::identity::project::ProjectName; use radicle::identity::{Doc, RepoId, Visibility}; @@ -216,11 +217,21 @@ pub fn init( .unwrap_or_else(|| repo.path()) .canonicalize()?; let interactive = options.interactive; - let head: String = repo - .head() - .ok() - .and_then(|head| head.shorthand().map(|h| h.to_owned())) - .ok_or_else(|| anyhow!("repository head must point to a commit"))?; + + let default_branch = match find_default_branch(&repo) { + Err(err @ DefaultBranchError::Head) => { + term::error(err); + term::hint("try `git checkout ` or set `git config set --local init.defaultBranch `"); + anyhow::bail!("aborting `rad init`") + } + Err(err @ DefaultBranchError::NoHead) => { + term::error(err); + term::hint("perhaps you need to create a branch?"); + anyhow::bail!("aborting `rad init`") + } + Err(err) => anyhow::bail!(err), + Ok(branch) => branch, + }; term::headline(format!( "Initializing{}radicle 👾 repository in {}..", @@ -252,10 +263,10 @@ pub fn init( Some(branch) => branch, None if interactive.yes() => term::input( "Default branch", - Some(head), + Some(default_branch), Some("Please specify an existing branch"), )?, - None => head, + None => default_branch, }; let branch = RefString::try_from(branch.clone()) .map_err(|e| anyhow!("invalid branch name {:?}: {}", branch, e))?; @@ -671,3 +682,39 @@ pub fn setup_signing( } Ok(()) } + +#[derive(Debug, thiserror::Error)] +enum DefaultBranchError { + #[error("could not determine default branch in repository")] + NoHead, + #[error("in detached HEAD state")] + Head, + #[error("could not determine default branch in repository: {0}")] + Git(raw::Error), +} + +fn find_default_branch(repo: &raw::Repository) -> Result { + match find_init_default_branch(repo).ok().flatten() { + Some(refname) => Ok(refname), + None => Ok(find_repository_head(repo)?), + } +} + +fn find_init_default_branch(repo: &raw::Repository) -> Result, raw::Error> { + let config = repo.config().and_then(|mut c| c.snapshot())?; + let default_branch = config.get_str("init.defaultbranch")?; + let branch = repo.find_branch(default_branch, raw::BranchType::Local)?; + Ok(branch.into_reference().shorthand().map(ToOwned::to_owned)) +} + +fn find_repository_head(repo: &raw::Repository) -> Result { + match repo.head() { + Err(e) if e.code() == raw::ErrorCode::UnbornBranch => Err(DefaultBranchError::NoHead), + Err(e) => Err(DefaultBranchError::Git(e)), + Ok(head) => head + .shorthand() + .filter(|refname| *refname != "HEAD") + .ok_or(DefaultBranchError::Head) + .map(|refname| refname.to_owned()), + } +} diff --git a/crates/radicle-cli/tests/commands.rs b/crates/radicle-cli/tests/commands.rs index 51699600..9ef56f75 100644 --- a/crates/radicle-cli/tests/commands.rs +++ b/crates/radicle-cli/tests/commands.rs @@ -212,6 +212,18 @@ fn rad_init_no_git() { environment.test("rad-init-no-git", &profile).unwrap(); } +#[test] +fn rad_init_detached_head() { + let mut environment = Environment::new(); + let profile = environment.profile("alice"); + + // NOTE: There is no repository set up here. + + environment + .test("rad-init-detached-head", &profile) + .unwrap(); +} + #[test] fn rad_inspect() { let mut environment = Environment::new(); diff --git a/crates/radicle/src/git.rs b/crates/radicle/src/git.rs index 7a67f4fa..e7d13117 100644 --- a/crates/radicle/src/git.rs +++ b/crates/radicle/src/git.rs @@ -701,6 +701,22 @@ pub fn set_upstream( Ok(()) } +pub fn init_default_branch(repo: &git2::Repository) -> Result, git2::Error> { + let config = repo.config().and_then(|mut c| c.snapshot())?; + let default_branch = config.get_str("init.defaultbranch")?; + let branch = repo.find_branch(default_branch, git2::BranchType::Local)?; + Ok(branch.into_reference().shorthand().map(ToOwned::to_owned)) +} + +pub fn head_refname(repo: &git2::Repository) -> Result, git2::Error> { + let head = repo.head()?; + match head.shorthand() { + Some("HEAD") => Ok(None), + Some(refname) => Ok(Some(refname.to_owned())), + None => Ok(None), + } +} + /// Execute a git command by spawning a child process. pub fn run( repo: P,