Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: improve default branch pick
✗ CI failure Sekhat Temporus committed 9 months ago
commit 37ea81766ec8a008420c43091364130bd61016f1
parent 85ddcace0a9fc888aa05bc0e8519ecf8b8a63911
1 failed (1 total) View logs
5 files changed +116 -9
added 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 <default branch>` or set `git config set --local init.defaultBranch <default branch>`
+
✗ 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.
modified 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.
modified 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 <default branch>` or set `git config set --local init.defaultBranch <default branch>`");
+
            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<String, DefaultBranchError> {
+
    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<Option<String>, 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<String, DefaultBranchError> {
+
    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()),
+
    }
+
}
modified crates/radicle-cli/tests/commands.rs
@@ -213,6 +213,18 @@ fn rad_init_no_git() {
}

#[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();
    let profile = environment.profile("alice");
modified crates/radicle/src/git.rs
@@ -701,6 +701,22 @@ pub fn set_upstream(
    Ok(())
}

+
pub fn init_default_branch(repo: &git2::Repository) -> Result<Option<String>, 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<Option<String>, 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<P, S, K, V>(
    repo: P,