Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
REVIEW
✗ CI failure Fintan Halpenny committed 7 months ago
commit b2b132fa62232528ad402046c0bf67165f8871cb
parent 9e1c2218867d751df2469eb369ad50c67abf7555
1 failed (1 total) View logs
16 files changed +305 -84
modified CHANGELOG.md
@@ -13,6 +13,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## New Features

+
- `rad clone` now supports the flag `--bare` which works analoguously to 
+
  `git clone --bare`.
+
- `rad init --setup-signing` now works on bare repositories.
+

+
## Fixed Bugs
+

+
- `rad init --setup-signing` now works in combination with `--existing`.
+

+
## 1.4.0
+

+
## Release Highlights
+

+
## Deprecations
+

+
## New Features
+

- `rad cob log` now supports the arguments `--from` and `--to` which can be used
  to range over particular operations on a COB.

added crates/radicle-cli/examples/git/git-is-bare-repository.md
@@ -0,0 +1,4 @@
+
```
+
$ git rev-parse --is-bare-repository
+
true
+
```

\ No newline at end of file
added crates/radicle-cli/examples/rad-clone-bare.md
@@ -0,0 +1,81 @@
+
To create a local bare copy of a repository on the radicle network, we use the
+
`clone` command, followed by the identifier or *RID* of the repository:
+

+
```
+
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed --bare
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
+
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found [..] potential seed(s).
+
✓ Target met: [..] seed(s)
+
✓ Creating checkout in ./heartwood..
+
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
+
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Repository successfully cloned under [..]/heartwood/
+
╭────────────────────────────────────╮
+
│ heartwood                          │
+
│ Radicle Heartwood Protocol & Stack │
+
│ 0 issues · 0 patches               │
+
╰────────────────────────────────────╯
+
Run `cd ./heartwood` to go to the repository directory.
+
```
+

+
We can now have a look at the new directory that was created from the cloned
+
repository:
+

+
```
+
$ cd heartwood
+
$ ls
+
FETCH_HEAD
+
HEAD
+
config
+
description
+
hooks
+
info
+
objects
+
refs
+
```
+

+
As expected, some `git` commands fail:
+
``` (stderr) (fail)
+
$ git status
+
fatal: this operation must be run in a work tree
+
```
+

+
Let's check that the remote tracking branch was setup correctly:
+

+
```
+
$ git branch --remotes
+
  alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
+
  rad/master
+
```
+

+
The first branch is ours, and the second points to the repository delegate.
+
We can also take a look at the remotes:
+

+
```
+
$ git remote -v
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
+
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch)
+
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push)
+
```
+

+
Let's check the last commit!
+

+
```
+
$ git log -n 1
+
commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
Author: anonymous <anonymous@radicle.xyz>
+
Date:   Mon Jan 1 14:39:16 2018 +0000
+

+
    Second commit
+
```
+

+
Cloned repositories show up in `rad ls`:
+
```
+
$ rad ls --seeded
+
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ Name        RID                                 Visibility   Head      Description                        │
+
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
```
added crates/radicle-cli/examples/rad-init-existing-bare.md
@@ -0,0 +1,48 @@
+
Let's clone a regular repository via plain Git:
+
```
+
$ git clone --bare $URL heartwood
+
$ cd heartwood
+
$ git rev-parse HEAD
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
```
+

+
We can see it's not a Radicle working copy:
+
``` (fail)
+
$ rad .
+
✗ Error: Current directory is not a Radicle repository
+
```
+

+
Let's pick an existing repository:
+
```
+
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
```
+

+
And initialize this working copy as that existing repository:
+
```
+
$ rad init --setup-signing --existing rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+

+
Configuring radicle signing key SHA256:UIedaL6Cxm6OUErh9GQUzzglSk7VpQlVTI1TAFB/HWA...
+

+
✓ Signing configured in [..]/heartwood/config
+
! Not writing .gitsigners file.
+
✓ Initialized existing repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji in [..]/heartwood/..
+
```
+

+
The warning about not writing `.gitsigners` is expected, as this requires a
+
working directory, which a bare repository does not have.
+

+
We can confirm that the working copy is initialized:
+
```
+
$ rad .
+
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ git remote show rad
+
* remote rad
+
  Fetch URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
  Push  URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
  HEAD branch: (unknown)
+
  Remote branch:
+
    master new (next fetch will store in remotes/rad)
+
  Local ref configured for 'git push':
+
    master pushes to master (up to date)
+
```
modified crates/radicle-cli/examples/rad-init-existing.md
@@ -20,7 +20,12 @@ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji

And initialize this working copy as that existing repository:
```
-
$ rad init --existing rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad init --setup-signing --existing rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+

+
Configuring radicle signing key SHA256:UIedaL6Cxm6OUErh9GQUzzglSk7VpQlVTI1TAFB/HWA...
+

+
✓ Signing configured in [..]/heartwood/.git/config
+
✓ Created .gitsigners file
✓ Initialized existing repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji in [..]/heartwood/..
```

modified crates/radicle-cli/src/commands/checkout.rs
@@ -98,7 +98,7 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
    }

    let mut spinner = term::spinner("Performing checkout...");
-
    let repo = match radicle::rad::checkout(options.id, &remote, path.clone(), &storage) {
+
    let repo = match radicle::rad::checkout(options.id, &remote, path.clone(), &storage, false) {
        Ok(repo) => repo,
        Err(err) => {
            spinner.failed();
modified crates/radicle-cli/src/commands/clone.rs
@@ -46,6 +46,7 @@ Usage

Options

+
        --bare              Make a bare repository
        --scope <scope>     Follow scope: `followed` or `all` (default: all)
    -s, --seed <nid>        Clone from this seed (may be specified multiple times)
        --timeout <secs>    Timeout for fetching repository (default: 9)
@@ -64,6 +65,7 @@ pub struct Options {
    scope: Scope,
    /// Sync settings.
    sync: SyncSettings,
+
    bare: bool,
}

impl Args for Options {
@@ -75,6 +77,7 @@ impl Args for Options {
        let mut scope = Scope::All;
        let mut sync = SyncSettings::default();
        let mut directory = None;
+
        let mut bare = false;

        while let Some(arg) = parser.next()? {
            match arg {
@@ -99,6 +102,9 @@ impl Args for Options {
                    // We keep this flag here for consistency though it doesn't have any effect,
                    // since the command is fully non-interactive.
                }
+
                Long("bare") => {
+
                    bare = true;
+
                }
                Long("help") | Short('h') => {
                    return Err(Error::Help.into());
                }
@@ -125,6 +131,7 @@ impl Args for Options {
                directory,
                scope,
                sync,
+
                bare,
            },
            vec![],
        ))
@@ -153,6 +160,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        options.sync.with_profile(&profile),
        &mut node,
        &profile,
+
        options.bare,
    )?
    .print_or_success()
    .ok_or_else(|| anyhow::anyhow!("failed to clone {}", options.id))?;
@@ -163,7 +171,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        .filter(|id| id != profile.id())
        .collect::<Vec<_>>();
    let default_branch = proj.default_branch().clone();
-
    let path = working.workdir().unwrap(); // SAFETY: The working copy is not bare.
+
    let path = if !options.bare {
+
        working.workdir().unwrap()
+
    } else {
+
        working.path()
+
    };

    // Configure repository and setup tracking for repository delegates.
    radicle::git::configure_repository(&working)?;
@@ -229,6 +241,7 @@ struct Checkout {
    repository: storage::git::Repository,
    doc: Doc,
    project: Project,
+
    bare: bool,
}

impl Checkout {
@@ -236,6 +249,7 @@ impl Checkout {
        repository: storage::git::Repository,
        profile: &Profile,
        directory: Option<PathBuf>,
+
        bare: bool,
    ) -> Result<Self, CheckoutFailure> {
        let rid = repository.rid();
        let doc = repository
@@ -257,6 +271,7 @@ impl Checkout {
            repository,
            doc: doc.doc,
            project: proj,
+
            bare,
        })
    }

@@ -274,7 +289,7 @@ impl Checkout {
            "Creating checkout in ./{}..",
            term::format::tertiary(destination.display())
        ));
-
        match rad::checkout(self.id, &self.remote, self.path, storage) {
+
        match rad::checkout(self.id, &self.remote, self.path, storage, self.bare) {
            Err(err) => {
                spinner.message(format!(
                    "Failed to checkout in ./{}",
@@ -303,6 +318,7 @@ fn clone(
    settings: SyncSettings,
    node: &mut Node,
    profile: &Profile,
+
    bare: bool,
) -> Result<CloneResult, CloneError> {
    // Seed repository.
    if node.seed(id, scope)? {
@@ -322,7 +338,7 @@ fn clone(
                node::sync::FetcherResult::TargetReached(_) => {
                    profile.storage.repository(id).map_or_else(
                        |err| Ok(CloneResult::RepositoryMissing { rid: id, err }),
-
                        |repository| Ok(perform_checkout(repository, profile, directory)?),
+
                        |repository| Ok(perform_checkout(repository, profile, directory, bare)?),
                    )
                }
                node::sync::FetcherResult::TargetError(failure) => {
@@ -330,7 +346,7 @@ fn clone(
                }
            }
        }
-
        Ok(repository) => Ok(perform_checkout(repository, profile, directory)?),
+
        Ok(repository) => Ok(perform_checkout(repository, profile, directory, bare)?),
    }
}

@@ -338,8 +354,9 @@ fn perform_checkout(
    repository: storage::git::Repository,
    profile: &Profile,
    directory: Option<PathBuf>,
+
    bare: bool,
) -> Result<CloneResult, rad::CheckoutError> {
-
    Checkout::new(repository, profile, directory).map_or_else(
+
    Checkout::new(repository, profile, directory, bare).map_or_else(
        |failure| Ok(CloneResult::Failure(failure)),
        |checkout| checkout.run(&profile.storage),
    )
modified crates/radicle-cli/src/commands/init.rs
@@ -403,6 +403,11 @@ pub fn init_existing(
        )?;
    }

+
    if options.setup_signing {
+
        // Setup radicle signing key.
+
        self::setup_signing(profile.id(), &working, options.interactive)?;
+
    }
+

    term::success!(
        "Initialized existing repository {} in {}..",
        term::format::tertiary(rid),
@@ -633,11 +638,13 @@ pub fn setup_signing(
    repo: &git::Repository,
    interactive: Interactive,
) -> anyhow::Result<()> {
-
    let repo = repo
-
        .workdir()
-
        .ok_or(anyhow!("cannot setup signing in bare repository"))?;
+
    const SIGNERS: &str = ".gitsigners";
+

+
    let path = repo.path();
+
    let config = path.join("config");
+

    let key = ssh::fmt::fingerprint(node_id);
-
    let yes = if !git::is_signing_configured(repo)? {
+
    let yes = if !git::is_signing_configured(path)? {
        term::headline(format!(
            "Configuring radicle signing key {}...",
            term::format::tertiary(key)
@@ -645,14 +652,25 @@ pub fn setup_signing(
        true
    } else if interactive.yes() {
        term::confirm(format!(
-
            "Configure radicle signing key {} in local checkout?",
+
            "Configure radicle signing key {} in {}?",
            term::format::tertiary(key),
+
            term::format::tertiary(config.display()),
        ))
    } else {
        true
    };

-
    if yes {
+
    if !yes {
+
        return Ok(());
+
    }
+

+
    git::configure_signing(path, node_id)?;
+
    term::success!(
+
        "Signing configured in {}",
+
        term::format::tertiary(config.display())
+
    );
+

+
    if let Some(repo) = repo.workdir() {
        match git::write_gitsigners(repo, [node_id]) {
            Ok(file) => {
                git::ignore(repo, file.as_path())?;
@@ -661,11 +679,11 @@ pub fn setup_signing(
            }
            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
                let ssh_key = ssh::fmt::key(node_id);
-
                let gitsigners = term::format::tertiary(".gitsigners");
+
                let gitsigners = term::format::tertiary(SIGNERS);
                term::success!("Found existing {} file", gitsigners);

                let ssh_keys =
-
                    git::read_gitsigners(repo).context("error reading .gitsigners file")?;
+
                    git::read_gitsigners(repo).context(format!("error reading {SIGNERS} file"))?;

                if ssh_keys.contains(&ssh_key) {
                    term::success!("Signing key is already in {gitsigners} file");
@@ -677,13 +695,10 @@ pub fn setup_signing(
                return Err(err.into());
            }
        }
-
        git::configure_signing(repo, node_id)?;
-

-
        term::success!(
-
            "Signing configured in {}",
-
            term::format::tertiary(".git/config")
-
        );
+
    } else {
+
        term::notice!("Not writing {SIGNERS} file.")
    }
+

    Ok(())
}

modified crates/radicle-cli/src/git.rs
@@ -351,28 +351,6 @@ pub fn parse_remote(refspec: &str) -> Option<(NodeId, &str)> {
        .and_then(|(peer, r)| NodeId::from_str(peer).ok().map(|p| (p, r)))
}

-
pub fn view_diff(
-
    repo: &git2::Repository,
-
    left: &git2::Oid,
-
    right: &git2::Oid,
-
) -> anyhow::Result<()> {
-
    // TODO(erikli): Replace with repo.diff()
-
    let workdir = repo
-
        .workdir()
-
        .ok_or_else(|| anyhow!("Could not get workdir current repository."))?;
-

-
    let left = format!("{:.7}", left.to_string());
-
    let right = format!("{:.7}", right.to_string());
-

-
    let mut git = Command::new("git")
-
        .current_dir(workdir)
-
        .args(["diff", &left, &right])
-
        .spawn()?;
-
    git.wait()?;
-

-
    Ok(())
-
}
-

pub fn add_tag(
    repo: &git2::Repository,
    message: &str,
modified crates/radicle-cli/tests/commands.rs
@@ -177,6 +177,15 @@ fn rad_init() {
}

#[test]
+
fn rad_init_bare() {
+
    let mut env = Environment::new();
+
    let alice = env.profile("alice");
+
    radicle::test::fixtures::bare_repository(env.work(&alice).as_path());
+
    env.tests(["git/git-is-bare-repository", "rad-init"], &alice)
+
        .unwrap();
+
}
+

+
#[test]
fn rad_init_existing() {
    let mut environment = Environment::new();
    let mut profile = environment.node("alice");
@@ -199,6 +208,28 @@ fn rad_init_existing() {
}

#[test]
+
fn rad_init_existing_bare() {
+
    let mut environment = Environment::new();
+
    let mut profile = environment.node("alice");
+
    let working = tempfile::tempdir().unwrap();
+
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    test(
+
        "examples/rad-init-existing-bare.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [(
+
            "URL",
+
            git::url::File::new(profile.storage.path())
+
                .rid(rid)
+
                .to_string()
+
                .as_str(),
+
        )],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
fn rad_init_no_seed() {
    Environment::alice(["rad-init-no-seed"]);
}
@@ -1125,6 +1156,26 @@ fn rad_clone() {
}

#[test]
+
fn rad_clone_bare() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = environment.tempdir().join("working");
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test("examples/rad-clone-bare.md", working, Some(&bob.home), []).unwrap();
+
}
+

+
#[test]
fn rad_clone_directory() {
    let mut environment = Environment::new();
    let mut alice = environment.node("alice");
modified crates/radicle-node/src/tests/e2e.rs
@@ -562,6 +562,7 @@ fn test_clone() {
        alice.signer.public_key(),
        tmp.path().join("clone"),
        &alice.storage,
+
        false,
    )
    .unwrap();

modified crates/radicle-remote-helper/src/main.rs
@@ -84,12 +84,6 @@ pub enum Error {
    /// I/O error.
    #[error("i/o error: {0}")]
    Io(#[from] io::Error),
-
    /// The `GIT_DIR` env var is not set.
-
    #[error("the `GIT_DIR` environment variable is not set")]
-
    NoGitDir,
-
    /// No parent of `GIT_DIR` was found.
-
    #[error("expected parent of .git but found {path:?}")]
-
    NoWorkingCopy { path: PathBuf },
    /// Git error.
    #[error("git: {0}")]
    Git(#[from] git::raw::Error),
@@ -180,13 +174,14 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
        }
    };

+
    // Assume the default remote if there was no remote.
+
    let remote = remote.unwrap_or_else(|| (*radicle::rad::REMOTE_NAME).clone());
+

    let stored = profile.storage.repository_mut(url.repo)?;
    if stored.is_empty()? {
        return Err(Error::RepositoryNotFound(stored.path().to_path_buf()));
    }

-
    // `GIT_DIR` is set by Git tooling, if we're in a working copy.
-
    let working = env::var("GIT_DIR").map(PathBuf::from);
    // Whether we should output debug logs.
    let debug = radicle::profile::env::debug();

@@ -238,25 +233,23 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
                let oid = git::Oid::from_str(oid)?;
                let refstr = git::RefString::try_from(*refstr)?;

-
                return fetch::run(vec![(oid, refstr)], stored, &stdin, opts.verbosity)
-
                    .map_err(Error::from);
+
                return Ok(fetch::run(
+
                    vec![(oid, refstr)],
+
                    stored,
+
                    &stdin,
+
                    opts.verbosity,
+
                )?);
            }
            ["push", refspec] => {
-
                // We have to be in a working copy to push.
-
                let working = working.map_err(|_| Error::NoGitDir)?;
-

-
                return push::run(
+
                return Ok(push::run(
                    vec![refspec.to_string()],
-
                    &working,
-
                    // N.b. assume the default remote if there was no remote
-
                    remote.unwrap_or((*radicle::rad::REMOTE_NAME).clone()),
+
                    remote,
                    url,
                    &stored,
                    &profile,
                    &stdin,
                    opts,
-
                )
-
                .map_err(Error::from);
+
                )?);
            }
            ["list"] => {
                list::for_fetch(&url, &profile, &stored)?;
modified crates/radicle-remote-helper/src/push.rs
@@ -5,7 +5,6 @@ mod error;

use std::collections::HashMap;
use std::io::IsTerminal;
-
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::str::FromStr;
use std::{assert_eq, io};
@@ -36,10 +35,6 @@ use crate::{hint, read_line, Options, Verbosity};

#[derive(Debug, Error)]
pub enum Error {
-
    #[error(
-
        "the Git repository found at {path:?} is a bare repository, expected a working directory"
-
    )]
-
    BareRepository { path: PathBuf },
    /// Public key doesn't match the remote namespace we're pushing to.
    #[error("cannot push to remote namespace owned by {0}")]
    KeyMismatch(Did),
@@ -251,7 +246,6 @@ impl PushAction {
/// Run a git push command.
pub fn run(
    mut specs: Vec<String>,
-
    working: &Path,
    remote: git::RefString,
    url: Url,
    stored: &storage::git::Repository,
@@ -296,7 +290,7 @@ pub fn run(
    let canonical_ref = git::refs::branch(project.default_branch());
    let mut set_canonical_refs: Vec<(git::Qualified, git::canonical::Object)> =
        Vec::with_capacity(specs.len());
-
    let working = git::raw::Repository::open(working)?;
+
    let working = git::raw::Repository::open_from_env()?;

    // For each refspec, push a ref or delete a ref.
    for spec in specs {
@@ -506,7 +500,7 @@ where
    //
    // In case the reference is not properly deleted, the next attempt to open a patch should
    // not fail, since the reference will already exist with the correct OID.
-
    push_ref(src, &dst, false, working, stored.raw(), opts.verbosity)?;
+
    push_ref(src, &dst, false, stored.raw(), opts.verbosity)?;

    let (_, target) = stored.canonical_head()?;
    let base = if let Some(base) = opts.base {
@@ -620,7 +614,7 @@ where
    let commit = *src;
    let dst = dst.with_namespace(nid.into());

-
    push_ref(src, &dst, force, working, stored.raw(), opts.verbosity)?;
+
    push_ref(src, &dst, force, stored.raw(), opts.verbosity)?;

    let Ok(Some(patch)) = patches.get(&patch_id) else {
        return Err(Error::NotFound(patch_id));
@@ -700,7 +694,7 @@ where
    // It's ok for the destination reference to be unknown, eg. when pushing a new branch.
    let old = stored.backend.find_reference(dst.as_str()).ok();

-
    push_ref(src, &dst, force, working, stored.raw(), verbosity)?;
+
    push_ref(src, &dst, force, stored.raw(), verbosity)?;

    if let Some(old) = old {
        let proj = stored.project()?;
@@ -881,7 +875,6 @@ fn push_ref(
    src: &git::Oid,
    dst: &git::Namespaced,
    force: bool,
-
    working: &git::raw::Repository,
    stored: &git::raw::Repository,
    verbosity: Verbosity,
) -> Result<(), Error> {
@@ -889,9 +882,6 @@ fn push_ref(
    // Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code.
    // This happens during the `list for-push` phase.
    let refspec = git::Refspec { src, dst, force };
-
    /// REVIEW(finto): this isn't used in this commit, but I suppose that's
-
    /// fixed in another commit?
-
    let repo = working.workdir().unwrap_or_else(|| working.path());

    let mut args = vec![
        "push".to_string(),
modified crates/radicle/src/rad.rs
@@ -251,6 +251,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    remote: &RemoteId,
    path: P,
    storage: &S,
+
    bare: bool,
) -> Result<git2::Repository, CheckoutError> {
    // TODO: Decide on whether we can use `clone_local`
    // TODO: Look into sharing object databases.
@@ -260,7 +261,8 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    let mut opts = git2::RepositoryInitOptions::new();
    opts.no_reinit(true)
        .external_template(false)
-
        .description(project.description());
+
        .description(project.description())
+
        .bare(bare);

    let repo = git2::Repository::init_opts(path.as_ref(), &opts)?;
    let url = git::Url::from(proj);
@@ -292,7 +294,8 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
            .display()
            .to_string();

-
        let output = git::run(Some(repo.path()), ["fetch", &stored, &fetchspec]).map_err(CheckoutError::FetchIo)?;
+
        let output = git::run(Some(repo.path()), ["fetch", &stored, &fetchspec])
+
            .map_err(CheckoutError::FetchIo)?;

        if !output.status.success() {
            return Err(CheckoutError::FetchGit {
@@ -317,7 +320,9 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
            .expect("checkout: default branch name is valid UTF-8");

        repo.set_head(branch_ref)?;
-
        repo.checkout_head(None)?;
+
        if !bare {
+
            repo.checkout_head(None)?;
+
        }

        // Setup remote tracking for default branch.
        git::set_upstream(&repo, &*REMOTE_NAME, project.default_branch(), branch_ref)?;
@@ -548,7 +553,7 @@ mod tests {

        // Bob forks it and creates a checkout.
        fork(id, &bob, &storage).unwrap();
-
        checkout(id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
+
        checkout(id, bob_id, tempdir.path().join("copy"), &storage, false).unwrap();

        let bob_remote = storage.repository(id).unwrap().remote(bob_id).unwrap();

@@ -583,7 +588,7 @@ mod tests {
        .unwrap();
        git::set_upstream(&original, "rad", "master", "refs/heads/master").unwrap();

-
        let copy = checkout(id, remote_id, tempdir.path().join("copy"), &storage).unwrap();
+
        let copy = checkout(id, remote_id, tempdir.path().join("copy"), &storage, false).unwrap();

        assert_eq!(
            copy.head().unwrap().target(),
modified crates/radicle/src/storage/refs.rs
@@ -570,6 +570,7 @@ mod tests {
                bob.public_key(),
                tmp.path().join("working"),
                &storage,
+
                false,
            )
            .unwrap();

modified crates/radicle/src/test/fixtures.rs
@@ -95,11 +95,28 @@ where

/// Creates a regular repository at the given path with a couple of commits.
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
-
    let repo = git2::Repository::init_opts(
+
    let (repo, oid) = repository_with(
        path,
        git2::RepositoryInitOptions::new().external_template(false),
+
    );
+
    repo.checkout_head(None).unwrap();
+
    (repo, oid)
+
}
+

+
pub fn bare_repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
+
    repository_with(
+
        path,
+
        git2::RepositoryInitOptions::new()
+
            .external_template(false)
+
            .bare(true),
    )
-
    .unwrap();
+
}
+

+
fn repository_with<P: AsRef<Path>>(
+
    path: P,
+
    opts: &mut git2::RepositoryInitOptions,
+
) -> (git2::Repository, git2::Oid) {
+
    let repo = git2::Repository::init_opts(path, opts).unwrap();

    {
        let mut config = repo.config().unwrap();
@@ -124,7 +141,6 @@ pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
        commit.id()
    };
    repo.set_head("refs/heads/master").unwrap();
-
    repo.checkout_head(None).unwrap();

    drop(tree);
    drop(head);