Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Get `rad-checkout` working properly
Alexis Sellier committed 3 years ago
commit bb135b64b3b1f615740aafe05690f6ad78ca2e2f
parent 1f06f5340f81ea8815dae06cb9828da649518cd8
10 files changed +213 -43
added radicle-cli/examples/rad-checkout.md
@@ -0,0 +1,77 @@
+
With the `rad checkout` command, you can create a new working copy from an
+
existing project.
+

+
```
+
$ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+

+
Initializing local checkout for 🌱 rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji (heartwood)
+

+
ok Performing checkout...
+
ok Setting up remotes...
+

+
🌱 Project checkout successful under ./heartwood
+

+
```
+

+
Let's have a look at what the command did. Navigate to the working copy:
+

+
```
+
$ cd heartwood
+
```
+

+
Check the README:
+
```
+
$ cat README
+
Hello World!
+
```
+

+
Check the repository status:
+

+
```
+
$ git status
+
On branch master
+
Your branch is up to date with 'rad/master'.
+

+
nothing to commit, working tree clean
+
```
+

+
Check the remote configuration:
+

+
```
+
$ git remote --verbose
+
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
+
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
+
```
+

+
List the branches:
+

+
```
+
$ git branch --all
+
* master
+
  remotes/rad/master
+
```
+

+
List the references:
+

+
```
+
$ git show-ref
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master
+
```
+

+
View the repository configuration:
+

+
```
+
$ cat .git/config
+
[core]
+
	bare = false
+
	repositoryformatversion = 0
+
	filemode = true
+
	logallrefupdates = true
+
[remote "rad"]
+
	url = rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
	fetch = +refs/heads/*:refs/remotes/rad/*
+
[branch "master"]
+
	remote = rad
+
	merge = refs/heads/master
+
```
modified radicle-cli/examples/rad-init.md
@@ -27,5 +27,5 @@ Projects can be listed with the `ls` command:

```
$ rad ls
-
heartwood rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji cdf76ce Radicle Heartwood Protocol & Stack
+
heartwood rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji f2de534 Radicle Heartwood Protocol & Stack
```
modified radicle-cli/examples/rad-patch.md
@@ -8,18 +8,18 @@ missing from the project.

```
$ git checkout -b flux-capacitor-power
-
$ touch README.md
+
$ touch REQUIREMENTS
```

Here the instructions are added to the project's README for 1.21 gigawatts and
commit the changes to git.

```
-
$ git add README.md
+
$ git add REQUIREMENTS
$ git commit -v -m "Define power requirements"
-
[flux-capacitor-power 9dad201] Define power requirements
+
[flux-capacitor-power 3e674d1] Define power requirements
 1 file changed, 0 insertions(+), 0 deletions(-)
-
 create mode 100644 README.md
+
 create mode 100644 REQUIREMENTS
```

Once the code is ready, we open (or create) a patch with our changes for the project.
@@ -32,10 +32,10 @@ $ rad patch open --message "define power requirements" --no-confirm
ok Pushing HEAD to storage...
ok Analyzing remotes...

-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (cdf76ce) <- z6MknSL…StBU8Vi/flux-capacitor-power (9dad201)
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (f2de534) <- z6MknSL…StBU8Vi/flux-capacitor-power (3e674d1)
1 commit(s) ahead, 0 commit(s) behind

-
9dad201 Define power requirements
+
3e674d1 Define power requirements


╭─ define power requirements ───────
@@ -45,7 +45,7 @@ No description provided.
╰───────────────────────────────────


-
ok Patch b9bb418c6f504ee91e54c555bdc8fc37b4d9b28b created 🌱
+
ok Patch c3bd983b5cf9643f7e98a2e6ee216d6794bac16b created 🌱
```

It will now be listed as one of the project's open patches.
@@ -55,16 +55,16 @@ $ rad patch

- YOU PROPOSED -

-
define power requirements b9bb418c6f5 R0 9dad201 (flux-capacitor-power) ahead 1, behind 0
+
define power requirements c3bd983b5cf R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
└─ * opened by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]

- OTHERS PROPOSED -

Nothing to show.

-
$ rad patch show b9bb418c6f504ee91e54c555bdc8fc37b4d9b28b
+
$ rad patch show c3bd983b5cf9643f7e98a2e6ee216d6794bac16b

-
patch b9bb418c6f504ee91e54c555bdc8fc37b4d9b28b
+
patch c3bd983b5cf9643f7e98a2e6ee216d6794bac16b

╭─ define power requirements ───────

@@ -72,13 +72,13 @@ No description provided.

╰───────────────────────────────────

-
commit 9dad201e2cb3306dd708b4a9ebe811b293e27196
+
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
Author: radicle <radicle@localhost>
Date:   Thu Dec 15 17:28:04 2022 +0000

    Define power requirements

-
diff --git a/README.md b/README.md
+
diff --git a/REQUIREMENTS b/REQUIREMENTS
new file mode 100644
index 0000000..e69de29

modified radicle-cli/src/commands/checkout.rs
@@ -132,10 +132,13 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {

/// Setup a remote and tracking branch for each given remote.
pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow::Result<()> {
+
    let mut spinner = term::spinner("Setting up remotes...");
    for remote_id in remotes {
+
        spinner.message(format!("Setting up remote {remote_id}.."));
+

        if let Some((remote, branch)) = setup.run(*remote_id)? {
            let remote = remote.name().unwrap(); // Only valid UTF-8 is used.
-
                                                 //
+

            term::success!("Remote {} set", term::format::highlight(remote));
            term::success!(
                "Remote-tracking branch {} created for {}",
@@ -144,6 +147,7 @@ pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow:
            );
        }
    }
+
    spinner.finish();

    Ok(())
}
modified radicle-cli/tests/commands.rs
@@ -73,9 +73,12 @@ fn test<'a>(
        .env("GIT_COMMITTER_DATE", "1671125284")
        .env("GIT_COMMITTER_EMAIL", "radicle@localhost")
        .env("GIT_COMMITTER_NAME", "radicle")
+
        .env("GIT_CONFIG", "/dev/null")
+
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
+
        .env("GIT_CONFIG_NOSYSTEM", "1")
        .env("RAD_HOME", home.to_string_lossy())
        .env("RAD_PASSPHRASE", "radicle")
-
        .env("TZ", "Etc/GMT")
+
        .env("TZ", "UTC")
        .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284")
        .envs(envs)
        .cwd(cwd)
@@ -131,6 +134,33 @@ fn rad_init() {
}

#[test]
+
fn rad_checkout() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let working = tempfile::tempdir().unwrap();
+
    let copy = tempfile::tempdir().unwrap();
+

+
    // Setup a test repository.
+
    fixtures::repository(working.path());
+

+
    test(
+
        "examples/rad-init.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    test(
+
        "examples/rad-checkout.md",
+
        copy.path(),
+
        Some(&profile.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
fn rad_delegate() {
    let mut environment = Environment::new();
    let profile = environment.profile("alice");
modified radicle-cli/tests/framework/mod.rs
@@ -2,9 +2,9 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
-
use std::{fs, io, mem};
+
use std::{env, fs, io, mem};

-
use snapbox::cmd::Command;
+
use snapbox::cmd::{Command, OutputAssert};
use snapbox::{Assert, Substitutions};
use thiserror::Error;

@@ -136,25 +136,54 @@ impl TestFormula {
        Ok(self)
    }

-
    pub fn run(&self) -> Result<bool, io::Error> {
+
    pub fn run(&mut self) -> Result<bool, io::Error> {
        let assert = Assert::new().substitutions(self.subs.clone());

        for test in &self.tests {
            for assertion in &test.assertions {
                let program = if assertion.program == "rad" {
                    snapbox::cmd::cargo_bin("rad")
+
                } else if assertion.program == "cd" {
+
                    let path: PathBuf = assertion.args.first().unwrap().into();
+
                    let path = self.cwd.join(path);
+

+
                    // TODO: Add support for `..` and `/`
+
                    // TODO: Error if more than one args are given.
+

+
                    if !path.exists() {
+
                        return Err(io::Error::new(
+
                            io::ErrorKind::NotFound,
+
                            format!("cd: '{}' does not exist", path.display()),
+
                        ));
+
                    }
+
                    self.cwd = path;
+

+
                    continue;
                } else {
                    PathBuf::from(&assertion.program)
                };

-
                Command::new(program)
+
                let result = Command::new(program.clone())
+
                    .env_clear()
+
                    .envs(env::vars().filter(|(k, _)| k == "PATH"))
                    .envs(self.env.clone())
                    .current_dir(&self.cwd)
                    .args(&assertion.args)
                    .with_assert(assert.clone())
-
                    .assert()
-
                    .stdout_matches(&assertion.expected)
-
                    .success();
+
                    .output();
+

+
                match result {
+
                    Ok(output) => {
+
                        let assert = OutputAssert::new(output).with_assert(assert.clone());
+
                        assert.stdout_matches(&assertion.expected).success();
+
                    }
+
                    Err(err) => {
+
                        return Err(io::Error::new(
+
                            err.kind(),
+
                            format!("{err}: `{}`", program.display()),
+
                        ));
+
                    }
+
                }
            }
        }
        Ok(true)
modified radicle/src/git.rs
@@ -255,10 +255,9 @@ pub fn commit<'a>(
    target: &RefStr,
    message: &str,
    sig: &git2::Signature,
+
    tree: &git2::Tree,
) -> Result<git2::Commit<'a>, git2::Error> {
-
    let tree_id = repo.index()?.write_tree()?;
-
    let tree = repo.find_tree(tree_id)?;
-
    let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?;
+
    let oid = repo.commit(Some(target.as_str()), sig, sig, message, tree, &[parent])?;
    let commit = repo.find_commit(oid)?;

    Ok(commit)
@@ -308,7 +307,16 @@ pub fn configure_remote<'r>(

/// Fetch from the given `remote`.
pub fn fetch(repo: &git2::Repository, remote: &str) -> Result<(), git2::Error> {
-
    repo.find_remote(remote)?.fetch::<&str>(&[], None, None)
+
    repo.find_remote(remote)?.fetch::<&str>(
+
        &[],
+
        Some(
+
            git2::FetchOptions::new()
+
                .update_fetchhead(false)
+
                .prune(git2::FetchPrune::On)
+
                .download_tags(git2::AutotagOption::None),
+
        ),
+
        None,
+
    )
}

/// Push `refspecs` to the given `remote` using the provided `namespace`.
modified radicle/src/rad.rs
@@ -285,7 +285,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    let mut opts = git2::RepositoryInitOptions::new();
    opts.no_reinit(true).description(project.description());

-
    let repo = git2::Repository::init_opts(path.as_ref().join(project.name()), &opts)?;
+
    let repo = git2::Repository::init_opts(path.as_ref(), &opts)?;
    let url = git::Url::from(proj).with_namespace(*remote);

    // Configure and fetch all refs from remote.
@@ -298,15 +298,18 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
            git::refs::workdir::remote_branch(&REMOTE_NAME, project.default_branch());

        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
-
        let _ = repo.branch(project.default_branch(), &remote_head_commit, true)?;
+
        let branch = repo
+
            .branch(project.default_branch(), &remote_head_commit, true)?
+
            .into_reference();
+
        let branch_ref = branch
+
            .name()
+
            .expect("checkout: default branch name is valid UTF-8");
+

+
        repo.set_head(branch_ref)?;
+
        repo.checkout_head(None)?;

        // Setup remote tracking for default branch.
-
        git::set_upstream(
-
            &repo,
-
            &REMOTE_NAME,
-
            project.default_branch(),
-
            &git::refs::workdir::branch(project.default_branch()),
-
        )?;
+
        git::set_upstream(&repo, &REMOTE_NAME, project.default_branch(), branch_ref)?;
    }

    Ok(repo)
modified radicle/src/storage/git.rs
@@ -844,12 +844,16 @@ mod tests {
        let alice_proj_storage = alice.repository(proj_id).unwrap();
        let alice_head = proj_repo.find_commit(alice_head).unwrap();
        let alice_sig = git2::Signature::now("Alice", "alice@radicle.xyz").unwrap();
+
        let alice_tree = proj_repo
+
            .find_tree(proj_repo.index().unwrap().write_tree().unwrap())
+
            .unwrap();
        let alice_head = git::commit(
            &proj_repo,
            &alice_head,
            &refname,
            "Making changes",
            &alice_sig,
+
            &alice_tree,
        )
        .unwrap()
        .id();
@@ -911,6 +915,8 @@ mod tests {
        let backend = &project.backend;
        let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
        let head = git::initial_commit(backend, &sig).unwrap();
+
        let tree =
+
            git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), backend).unwrap();

        git::commit(
            backend,
@@ -918,6 +924,7 @@ mod tests {
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
            "Second commit",
            &sig,
+
            &tree,
        )
        .unwrap();

modified radicle/src/test/fixtures.rs
@@ -62,17 +62,25 @@ pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
    )
    .unwrap();
    let head = git::initial_commit(&repo, &sig).unwrap();
-
    let oid = git::commit(
-
        &repo,
-
        &head,
-
        git::refname!("refs/heads/master").as_refstr(),
-
        "Second commit",
-
        &sig,
-
    )
-
    .unwrap()
-
    .id();
+
    let tree = git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
+
    let oid = {
+
        let commit = git::commit(
+
            &repo,
+
            &head,
+
            git::refname!("refs/heads/master").as_refstr(),
+
            "Second commit",
+
            &sig,
+
            &tree,
+
        )
+
        .unwrap();
+

+
        commit.id()
+
    };
+
    repo.set_head("refs/heads/master").unwrap();
+
    repo.checkout_head(None).unwrap();

    // Look, I don't really understand why we have to do this, but we do.
+
    drop(tree);
    drop(head);

    (repo, oid)
@@ -99,18 +107,22 @@ pub mod gen {
        let repo = git2::Repository::init(path).unwrap();
        let sig = git2::Signature::now(string(6).as_str(), email().as_str()).unwrap();
        let head = git::initial_commit(&repo, &sig).unwrap();
+
        let tree =
+
            git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
        let oid = git::commit(
            &repo,
            &head,
            git::refname!("refs/heads/master").as_refstr(),
            string(16).as_str(),
            &sig,
+
            &tree,
        )
        .unwrap()
        .id();

        // Look, I don't really understand why we have to do this, but we do.
        drop(head);
+
        drop(tree);

        (repo, oid)
    }