cli: Get `rad-checkout` working properly

This commit is contained in:
Alexis Sellier 2023-01-24 12:51:41 +01:00
parent 1f06f5340f
commit bb135b64b3
No known key found for this signature in database
10 changed files with 213 additions and 43 deletions

View File

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

View File

@ -27,5 +27,5 @@ Projects can be listed with the `ls` command:
``` ```
$ rad ls $ rad ls
heartwood rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji cdf76ce Radicle Heartwood Protocol & Stack heartwood rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji f2de534 Radicle Heartwood Protocol & Stack
``` ```

View File

@ -8,18 +8,18 @@ missing from the project.
``` ```
$ git checkout -b flux-capacitor-power $ 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 Here the instructions are added to the project's README for 1.21 gigawatts and
commit the changes to git. commit the changes to git.
``` ```
$ git add README.md $ git add REQUIREMENTS
$ git commit -v -m "Define power 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(-) 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. 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 Pushing HEAD to storage...
ok Analyzing remotes... ok Analyzing remotes...
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (cdf76ce) <- z6MknSLStBU8Vi/flux-capacitor-power (9dad201) z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (f2de534) <- z6MknSLStBU8Vi/flux-capacitor-power (3e674d1)
1 commit(s) ahead, 0 commit(s) behind 1 commit(s) ahead, 0 commit(s) behind
9dad201 Define power requirements 3e674d1 Define power requirements
╭─ 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. It will now be listed as one of the project's open patches.
@ -55,16 +55,16 @@ $ rad patch
- YOU PROPOSED - - 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) [..] └─ * opened by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
- OTHERS PROPOSED - - OTHERS PROPOSED -
Nothing to show. Nothing to show.
$ rad patch show b9bb418c6f504ee91e54c555bdc8fc37b4d9b28b $ rad patch show c3bd983b5cf9643f7e98a2e6ee216d6794bac16b
patch b9bb418c6f504ee91e54c555bdc8fc37b4d9b28b patch c3bd983b5cf9643f7e98a2e6ee216d6794bac16b
╭─ define power requirements ─────── ╭─ define power requirements ───────
@ -72,13 +72,13 @@ No description provided.
╰─────────────────────────────────── ╰───────────────────────────────────
commit 9dad201e2cb3306dd708b4a9ebe811b293e27196 commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
Author: radicle <radicle@localhost> Author: radicle <radicle@localhost>
Date: Thu Dec 15 17:28:04 2022 +0000 Date: Thu Dec 15 17:28:04 2022 +0000
Define power requirements Define power requirements
diff --git a/README.md b/README.md diff --git a/REQUIREMENTS b/REQUIREMENTS
new file mode 100644 new file mode 100644
index 0000000..e69de29 index 0000000..e69de29

View File

@ -132,10 +132,13 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
/// Setup a remote and tracking branch for each given remote. /// Setup a remote and tracking branch for each given remote.
pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow::Result<()> { pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow::Result<()> {
let mut spinner = term::spinner("Setting up remotes...");
for remote_id in remotes { for remote_id in remotes {
spinner.message(format!("Setting up remote {remote_id}.."));
if let Some((remote, branch)) = setup.run(*remote_id)? { if let Some((remote, branch)) = setup.run(*remote_id)? {
let remote = remote.name().unwrap(); // Only valid UTF-8 is used. let remote = remote.name().unwrap(); // Only valid UTF-8 is used.
//
term::success!("Remote {} set", term::format::highlight(remote)); term::success!("Remote {} set", term::format::highlight(remote));
term::success!( term::success!(
"Remote-tracking branch {} created for {}", "Remote-tracking branch {} created for {}",
@ -144,6 +147,7 @@ pub fn setup_remotes(setup: project::SetupRemote, remotes: &[NodeId]) -> anyhow:
); );
} }
} }
spinner.finish();
Ok(()) Ok(())
} }

View File

@ -73,9 +73,12 @@ fn test<'a>(
.env("GIT_COMMITTER_DATE", "1671125284") .env("GIT_COMMITTER_DATE", "1671125284")
.env("GIT_COMMITTER_EMAIL", "radicle@localhost") .env("GIT_COMMITTER_EMAIL", "radicle@localhost")
.env("GIT_COMMITTER_NAME", "radicle") .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_HOME", home.to_string_lossy())
.env("RAD_PASSPHRASE", "radicle") .env("RAD_PASSPHRASE", "radicle")
.env("TZ", "Etc/GMT") .env("TZ", "UTC")
.env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284") .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284")
.envs(envs) .envs(envs)
.cwd(cwd) .cwd(cwd)
@ -130,6 +133,33 @@ fn rad_init() {
.unwrap(); .unwrap();
} }
#[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] #[test]
fn rad_delegate() { fn rad_delegate() {
let mut environment = Environment::new(); let mut environment = Environment::new();

View File

@ -2,9 +2,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; 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 snapbox::{Assert, Substitutions};
use thiserror::Error; use thiserror::Error;
@ -136,25 +136,54 @@ impl TestFormula {
Ok(self) 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()); let assert = Assert::new().substitutions(self.subs.clone());
for test in &self.tests { for test in &self.tests {
for assertion in &test.assertions { for assertion in &test.assertions {
let program = if assertion.program == "rad" { let program = if assertion.program == "rad" {
snapbox::cmd::cargo_bin("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 { } else {
PathBuf::from(&assertion.program) 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()) .envs(self.env.clone())
.current_dir(&self.cwd) .current_dir(&self.cwd)
.args(&assertion.args) .args(&assertion.args)
.with_assert(assert.clone()) .with_assert(assert.clone())
.assert() .output();
.stdout_matches(&assertion.expected)
.success(); 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) Ok(true)

View File

@ -255,10 +255,9 @@ pub fn commit<'a>(
target: &RefStr, target: &RefStr,
message: &str, message: &str,
sig: &git2::Signature, sig: &git2::Signature,
tree: &git2::Tree,
) -> Result<git2::Commit<'a>, git2::Error> { ) -> Result<git2::Commit<'a>, git2::Error> {
let tree_id = repo.index()?.write_tree()?; let oid = repo.commit(Some(target.as_str()), sig, sig, message, tree, &[parent])?;
let tree = repo.find_tree(tree_id)?;
let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?;
let commit = repo.find_commit(oid)?; let commit = repo.find_commit(oid)?;
Ok(commit) Ok(commit)
@ -308,7 +307,16 @@ pub fn configure_remote<'r>(
/// Fetch from the given `remote`. /// Fetch from the given `remote`.
pub fn fetch(repo: &git2::Repository, remote: &str) -> Result<(), git2::Error> { 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`. /// Push `refspecs` to the given `remote` using the provided `namespace`.

View File

@ -285,7 +285,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
let mut opts = git2::RepositoryInitOptions::new(); let mut opts = git2::RepositoryInitOptions::new();
opts.no_reinit(true).description(project.description()); 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); let url = git::Url::from(proj).with_namespace(*remote);
// Configure and fetch all refs from 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()); git::refs::workdir::remote_branch(&REMOTE_NAME, project.default_branch());
let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?; 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. // Setup remote tracking for default branch.
git::set_upstream( git::set_upstream(&repo, &REMOTE_NAME, project.default_branch(), branch_ref)?;
&repo,
&REMOTE_NAME,
project.default_branch(),
&git::refs::workdir::branch(project.default_branch()),
)?;
} }
Ok(repo) Ok(repo)

View File

@ -844,12 +844,16 @@ mod tests {
let alice_proj_storage = alice.repository(proj_id).unwrap(); let alice_proj_storage = alice.repository(proj_id).unwrap();
let alice_head = proj_repo.find_commit(alice_head).unwrap(); let alice_head = proj_repo.find_commit(alice_head).unwrap();
let alice_sig = git2::Signature::now("Alice", "alice@radicle.xyz").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( let alice_head = git::commit(
&proj_repo, &proj_repo,
&alice_head, &alice_head,
&refname, &refname,
"Making changes", "Making changes",
&alice_sig, &alice_sig,
&alice_tree,
) )
.unwrap() .unwrap()
.id(); .id();
@ -911,6 +915,8 @@ mod tests {
let backend = &project.backend; let backend = &project.backend;
let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap(); let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
let head = git::initial_commit(backend, &sig).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( git::commit(
backend, backend,
@ -918,6 +924,7 @@ mod tests {
&git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(), &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
"Second commit", "Second commit",
&sig, &sig,
&tree,
) )
.unwrap(); .unwrap();

View File

@ -62,17 +62,25 @@ pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
) )
.unwrap(); .unwrap();
let head = git::initial_commit(&repo, &sig).unwrap(); let head = git::initial_commit(&repo, &sig).unwrap();
let oid = git::commit( let tree = git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap();
&repo, let oid = {
&head, let commit = git::commit(
git::refname!("refs/heads/master").as_refstr(), &repo,
"Second commit", &head,
&sig, git::refname!("refs/heads/master").as_refstr(),
) "Second commit",
.unwrap() &sig,
.id(); &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. // Look, I don't really understand why we have to do this, but we do.
drop(tree);
drop(head); drop(head);
(repo, oid) (repo, oid)
@ -99,18 +107,22 @@ pub mod gen {
let repo = git2::Repository::init(path).unwrap(); let repo = git2::Repository::init(path).unwrap();
let sig = git2::Signature::now(string(6).as_str(), email().as_str()).unwrap(); let sig = git2::Signature::now(string(6).as_str(), email().as_str()).unwrap();
let head = git::initial_commit(&repo, &sig).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( let oid = git::commit(
&repo, &repo,
&head, &head,
git::refname!("refs/heads/master").as_refstr(), git::refname!("refs/heads/master").as_refstr(),
string(16).as_str(), string(16).as_str(),
&sig, &sig,
&tree,
) )
.unwrap() .unwrap()
.id(); .id();
// Look, I don't really understand why we have to do this, but we do. // Look, I don't really understand why we have to do this, but we do.
drop(head); drop(head);
drop(tree);
(repo, oid) (repo, oid)
} }