From 30ad7e4d67a079bd129d8cbf75504f6118d56947 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Thu, 16 Nov 2023 14:27:12 +0000 Subject: [PATCH] cli: add force flag to rad patch checkout Add a `--force` flag to forcibly checkout the branch to the latest revision, if the branch already exists. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- .../examples/rad-patch-checkout-force.md | 87 +++++++++++++++++++ radicle-cli/src/commands/patch.rs | 21 ++++- radicle-cli/src/commands/patch/checkout.rs | 37 +++++--- radicle-cli/tests/commands.rs | 49 +++++++++++ 4 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 radicle-cli/examples/rad-patch-checkout-force.md diff --git a/radicle-cli/examples/rad-patch-checkout-force.md b/radicle-cli/examples/rad-patch-checkout-force.md new file mode 100644 index 00000000..2e7a237f --- /dev/null +++ b/radicle-cli/examples/rad-patch-checkout-force.md @@ -0,0 +1,87 @@ +A common workflow is to use `rad patch checkout` to view a +collaborator's changes. So, first off, we create a patch: + +``` ~alice +$ git checkout -b flux-capacitor-power +$ touch REQUIREMENTS +$ git add REQUIREMENTS +$ git commit -v -m "Define power requirements" +[flux-capacitor-power 3e674d1] Define power requirements + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 REQUIREMENTS +``` + +``` ~alice (stderr) +$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches +✓ Patch 6ff4f09c1b5a81347981f59b02ef43a31a07cdae opened +✓ Synced with 1 node(s) +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +On the other end, Bob uses `rad patch checkout` to view the patch: + +``` ~bob +$ cd heartwood +$ rad sync -f +✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi.. +✓ Fetched repository from 1 seed(s) +$ rad patch checkout 6ff4f09 +✓ Switched to branch patch/6ff4f09 +✓ Branch patch/6ff4f09 setup to track rad/patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae +``` + +Meanwhile, we may see some more changes that we need to make, so we +add a `README.md`: + +``` ~alice +$ touch README.md +$ git add README.md +$ git commit --message "Add README, just for the fun" +[flux-capacitor-power 27857ec] Add README, just for the fun + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 README.md +``` + +``` ~alice (stderr) +$ git push rad -o patch.message="Add README, just for the fun" +✓ Patch 6ff4f09 updated to 0c0942e2ff2488617d950ede15567ca39a29972e +✓ Synced with 1 node(s) +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + 3e674d1..27857ec flux-capacitor-power -> patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae +``` + +Bob fetches these new changes and can see their branch is now behind: + +``` ~bob (stderr) +$ git fetch rad +✓ Synced with 1 peer(s) +From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji + 3e674d1..27857ec patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae -> rad/patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae +``` + +``` ~bob +$ git status +On branch patch/6ff4f09 +Your branch is behind 'rad/patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae' by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + +nothing to commit, working tree clean +``` + +If Bob was to run `rad patch checkout` again, their branch would not +update to the latest commit. This is because it sees that the branch +already exists and does not want overwrite any changes. Bob can choose +to use the `--force` (`-f`) flag to ensure that they are looking at +the latest changes: + +``` ~bob +$ rad patch checkout 6ff4f09 -f +✓ Switched to branch patch/6ff4f09 +✓ Branch patch/6ff4f09 setup to track rad/patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae +$ git status +On branch patch/6ff4f09 +Your branch is up to date with 'rad/patches/6ff4f09c1b5a81347981f59b02ef43a31a07cdae'. + +nothing to commit, working tree clean +``` diff --git a/radicle-cli/src/commands/patch.rs b/radicle-cli/src/commands/patch.rs index d060913e..eeaf3543 100644 --- a/radicle-cli/src/commands/patch.rs +++ b/radicle-cli/src/commands/patch.rs @@ -85,6 +85,10 @@ Ready options --undo Convert a patch back to a draft +Checkout options + + -f, --force Checkout the head of the revision, even if the branch already exists + Other options -q, --quiet Quiet output @@ -151,6 +155,7 @@ pub enum Operation { }, Checkout { revision_id: Rev, + force: bool, }, Comment { revision_id: Rev, @@ -198,6 +203,7 @@ impl Args for Options { let mut diff = false; let mut undo = false; let mut reply_to: Option = None; + let mut force = false; while let Some(arg) = parser.next()? { match arg { @@ -251,6 +257,11 @@ impl Args for Options { reply_to = Some(rev); } + // Checkout options + Long("force") | Short('f') if op == Some(OperationName::Checkout) => { + force = true; + } + // List options. Long("all") => { filter = Filter::all(); @@ -340,6 +351,7 @@ impl Args for Options { }, OperationName::Checkout => Operation::Checkout { revision_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, + force, }, OperationName::Comment => Operation::Comment { revision_id: patch_id @@ -419,9 +431,14 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let patch_id = patch_id.resolve::(&repository.backend)?; delete::run(&patch_id, &profile, &repository)?; } - Operation::Checkout { revision_id } => { + Operation::Checkout { revision_id, force } => { let revision_id = revision_id.resolve::(&repository.backend)?; - checkout::run(&patch::RevisionId::from(revision_id), &repository, &workdir)?; + checkout::run( + &patch::RevisionId::from(revision_id), + &repository, + &workdir, + force, + )?; } Operation::Comment { revision_id, diff --git a/radicle-cli/src/commands/patch/checkout.rs b/radicle-cli/src/commands/patch/checkout.rs index 32873149..5f026e10 100644 --- a/radicle-cli/src/commands/patch/checkout.rs +++ b/radicle-cli/src/commands/patch/checkout.rs @@ -13,6 +13,7 @@ pub fn run( revision_id: &RevisionId, stored: &Repository, working: &git::raw::Repository, + force: bool, ) -> anyhow::Result<()> { let patches = patch::Patches::open(stored)?; @@ -35,18 +36,30 @@ pub fn run( // SAFETY: Patch IDs are valid refstrings. git::refname!("patch").join(RefString::try_from(term::format::cob(&patch_id)).unwrap()); - match working.find_branch(patch_branch.as_str(), radicle::git::raw::BranchType::Local) { - Ok(branch) => { - let commit = branch.get().peel_to_commit()?; - working.checkout_tree(commit.as_object(), None)?; - } - Err(e) if radicle::git::is_not_found_err(&e) => { - let commit = find_patch_commit(revision, stored, working)?; - // Create patch branch and switch to it. - working.branch(patch_branch.as_str(), &commit, true)?; - working.checkout_tree(commit.as_object(), None)?; - } - Err(e) => return Err(e.into()), + let commit = + match working.find_branch(patch_branch.as_str(), radicle::git::raw::BranchType::Local) { + Ok(branch) if !force => branch.get().peel_to_commit()?, + Ok(branch) => { + let commit = find_patch_commit(revision, stored, working)?; + let mut r = branch.into_reference(); + r.set_target(commit.id(), &format!("force update '{patch_branch}'"))?; + commit + } + Err(e) if radicle::git::is_not_found_err(&e) => { + let commit = find_patch_commit(revision, stored, working)?; + // Create patch branch and switch to it. + working.branch(patch_branch.as_str(), &commit, true)?; + commit + } + Err(e) => return Err(e.into()), + }; + + if force { + let mut opts = radicle::git::raw::build::CheckoutBuilder::new(); + opts.force(); + working.checkout_tree(commit.as_object(), Some(&mut opts))?; + } else { + working.checkout_tree(commit.as_object(), None)?; } working.set_head(&git::refs::workdir::branch(&patch_branch))?; diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 1b063c46..0dd76e3c 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -440,6 +440,55 @@ fn rad_patch_checkout() { .unwrap(); } +#[test] +fn rad_patch_checkout_force() { + let mut environment = Environment::new(); + let alice = environment.node(Config::test(Alias::new("alice"))); + let bob = environment.node(Config::test(Alias::new("bob"))); + let working = environment.tmp().join("working"); + let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); + + // Setup a test repository. + fixtures::repository(working.join("alice")); + + test( + "examples/rad-init.md", + working.join("alice"), + Some(&alice.home), + [], + ) + .unwrap(); + + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + + bob.handle.track_repo(acme, Scope::All).unwrap(); + alice.connect(&bob).converge([&bob]); + + test( + "examples/rad-clone.md", + working.join("bob"), + Some(&bob.home), + [], + ) + .unwrap(); + + formula(&environment.tmp(), "examples/rad-patch-checkout-force.md") + .unwrap() + .home( + "alice", + working.join("alice"), + [("RAD_HOME", alice.home.path().display())], + ) + .home( + "bob", + working.join("bob"), + [("RAD_HOME", bob.home.path().display())], + ) + .run() + .unwrap(); +} + #[test] fn rad_patch_update() { let mut environment = Environment::new();