From bb135b64b3b1f615740aafe05690f6ad78ca2e2f Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Tue, 24 Jan 2023 12:51:41 +0100 Subject: [PATCH] cli: Get `rad-checkout` working properly --- radicle-cli/examples/rad-checkout.md | 77 ++++++++++++++++++++++++++++ radicle-cli/examples/rad-init.md | 2 +- radicle-cli/examples/rad-patch.md | 24 ++++----- radicle-cli/src/commands/checkout.rs | 6 ++- radicle-cli/tests/commands.rs | 32 +++++++++++- radicle-cli/tests/framework/mod.rs | 43 +++++++++++++--- radicle/src/git.rs | 16 ++++-- radicle/src/rad.rs | 19 ++++--- radicle/src/storage/git.rs | 7 +++ radicle/src/test/fixtures.rs | 30 +++++++---- 10 files changed, 213 insertions(+), 43 deletions(-) create mode 100644 radicle-cli/examples/rad-checkout.md diff --git a/radicle-cli/examples/rad-checkout.md b/radicle-cli/examples/rad-checkout.md new file mode 100644 index 00000000..26b005c4 --- /dev/null +++ b/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 +``` diff --git a/radicle-cli/examples/rad-init.md b/radicle-cli/examples/rad-init.md index 40702208..3cbfb7de 100644 --- a/radicle-cli/examples/rad-init.md +++ b/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 ``` diff --git a/radicle-cli/examples/rad-patch.md b/radicle-cli/examples/rad-patch.md index 247c8e2a..6661fdb7 100644 --- a/radicle-cli/examples/rad-patch.md +++ b/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 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 diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs index efbdca35..d8a09996 100644 --- a/radicle-cli/src/commands/checkout.rs +++ b/radicle-cli/src/commands/checkout.rs @@ -132,10 +132,13 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result { /// 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(()) } diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index f09f1cad..ec77d7c0 100644 --- a/radicle-cli/tests/commands.rs +++ b/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) @@ -130,6 +133,33 @@ fn rad_init() { .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] fn rad_delegate() { let mut environment = Environment::new(); diff --git a/radicle-cli/tests/framework/mod.rs b/radicle-cli/tests/framework/mod.rs index a4af6a17..0755afb6 100644 --- a/radicle-cli/tests/framework/mod.rs +++ b/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 { + pub fn run(&mut self) -> Result { 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) diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 1c611820..837305e9 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -255,10 +255,9 @@ pub fn commit<'a>( target: &RefStr, message: &str, sig: &git2::Signature, + tree: &git2::Tree, ) -> Result, 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`. diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 48a5f639..18381105 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -285,7 +285,7 @@ pub fn checkout, 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, 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) diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 0ab700e4..79ce93c1 100644 --- a/radicle/src/storage/git.rs +++ b/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(); diff --git a/radicle/src/test/fixtures.rs b/radicle/src/test/fixtures.rs index 57bab18a..d722ebd4 100644 --- a/radicle/src/test/fixtures.rs +++ b/radicle/src/test/fixtures.rs @@ -62,17 +62,25 @@ pub fn repository>(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) }