From 5ccf38079c7a3b51b83105acd6fe83d97a8f8ec0 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Fri, 17 Mar 2023 16:58:29 +0100 Subject: [PATCH] cli: Make `patch checkout` fetch missing objects Before this change, `rad patch checkout` would fail if the patch commits were not in the working copy. --- Cargo.lock | 1 + radicle-cli/Cargo.toml | 1 + radicle-cli/examples/rad-patch.md | 1 - radicle-cli/src/commands/patch/checkout.rs | 91 ++++++++++++++++------ 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f80140dc..18b4aaf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1823,6 +1823,7 @@ version = "0.8.0" dependencies = [ "anyhow", "chrono", + "git-ref-format", "json-color", "lexopt", "log", diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml index a95ada96..9e0878af 100644 --- a/radicle-cli/Cargo.toml +++ b/radicle-cli/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = { version = "1" } chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +git-ref-format = { version = "0", features = ["macro"] } json-color = { version = "0.7" } lexopt = { version = "0.2" } log = { version = "0.4", features = ["std"] } diff --git a/radicle-cli/examples/rad-patch.md b/radicle-cli/examples/rad-patch.md index 1d122eb3..921c5455 100644 --- a/radicle-cli/examples/rad-patch.md +++ b/radicle-cli/examples/rad-patch.md @@ -112,6 +112,5 @@ Now, let's checkout the patch that we just created: ``` $ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 -✓ Performing patch checkout... ✓ Switched to branch patch/191a14e520f ``` diff --git a/radicle-cli/src/commands/patch/checkout.rs b/radicle-cli/src/commands/patch/checkout.rs index 73a4e6fd..f54184fb 100644 --- a/radicle-cli/src/commands/patch/checkout.rs +++ b/radicle-cli/src/commands/patch/checkout.rs @@ -1,38 +1,85 @@ -use crate::terminal as term; use anyhow::anyhow; -use radicle::cob::patch::{self, PatchId}; -use radicle::git::{self, RefString}; + +use radicle::cob::patch; +use radicle::cob::patch::{Patch, PatchId}; +use radicle::git; +use radicle::git::RefString; use radicle::storage::git::Repository; +use radicle::storage::ReadRepository; + +use crate::terminal as term; pub fn run( - storage: &Repository, - git_workdir: &git::raw::Repository, + stored: &Repository, + working: &git::raw::Repository, patch_id: &PatchId, ) -> anyhow::Result<()> { - let patches = patch::Patches::open(storage)?; + let patches = patch::Patches::open(stored)?; let patch = patches .get(patch_id)? .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?; - let spinner = term::spinner("Performing patch checkout..."); + let mut spinner = term::spinner("Performing checkout..."); + let patch_branch = + // SAFETY: Patch IDs are valid refstrings. + git::refname!("patch").join(RefString::try_from(term::format::cob(patch_id)).unwrap()); + let commit = find_patch_commit(&patch, &patch_branch, stored, working)?; - // Getting the patch obj! - let patch_head = *patch.head(); - let commit = git_workdir.find_commit(patch_head.into())?; + // Create patch branch and switch to it. + working.branch(patch_branch.as_str(), &commit, false)?; + working.checkout_tree(commit.as_object(), None)?; + working.set_head(&git::refs::workdir::branch(&patch_branch))?; - let name = RefString::try_from(format!("patch/{}", term::format::cob(patch_id)))?; - let branch = git::refs::workdir::branch(&name); - // checkout the patch in a new branch! - git_workdir.branch(branch.as_str(), &commit, false)?; - // and then point the current `HEAD` inside the new branch. - git_workdir.set_head(branch.as_str())?; - spinner.finish(); - - // 3. Write to the UI Terminal - term::success!( + spinner.message(format!( "Switched to branch {}", - term::format::highlight(name.as_str()) - ); + term::format::highlight(patch_branch.as_str()) + )); + spinner.finish(); Ok(()) } + +/// Try to find the patch head in our working copy, and if we don't find it, +/// fetch it from storage first. +fn find_patch_commit<'a>( + patch: &Patch, + patch_branch: &RefString, + stored: &Repository, + working: &'a git::raw::Repository, +) -> anyhow::Result> { + let patch_head = *patch.head(); + + match working.find_commit(patch_head.into()) { + Ok(commit) => Ok(commit), + Err(e) if git::ext::is_not_found_err(&e) => { + // TODO: Handle case of concurrent revisions. + let (_, rev) = patch + .latest() + .ok_or(anyhow!("patch does not have any revisions"))?; + let author = **rev.author.id(); + let remote = stored.remote(&author)?; + + // Find a ref in storage that points to our patch, so that we can fetch the patch + // objects into our working copy. + let (refstr, _) = remote + .refs + .iter() + .find(|(_, o)| **o == patch_head) + .ok_or(anyhow!("patch ref for {patch_head} not found in storage"))?; + let remote_branch = git::refs::workdir::remote_branch( + &RefString::try_from(author.to_string())?, + patch_branch, + ); + let url = git::Url::from(stored.id).with_namespace(author); + + // Fetch only the ref pointing to the patch revision. + working.remote_anonymous(url.to_string().as_str())?.fetch( + &[&format!("{refstr}:{remote_branch}")], + None, + None, + )?; + working.find_commit(patch_head.into()).map_err(|e| e.into()) + } + Err(e) => Err(e.into()), + } +}