From bfc3c7164f2e77e797249357910f3f6311442a3b Mon Sep 17 00:00:00 2001 From: cloudhead Date: Fri, 1 Sep 2023 14:33:23 +0200 Subject: [PATCH] tools: Add `rad-merge` tool It happened that a small bug in the remote helper caused a patch not to merge correctly on push. Hence, I implemented a `rad-merge` tool for these cases where we need to fix broken state. --- radicle-remote-helper/src/push.rs | 23 +++----------- radicle-tools/Cargo.toml | 4 +++ radicle-tools/src/rad-merge.rs | 47 ++++++++++++++++++++++++++++ radicle/src/cob/patch.rs | 51 +++++++++++++++++++++++++++++-- 4 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 radicle-tools/src/rad-merge.rs diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs index 9740bd74..0c8eeba6 100644 --- a/radicle-remote-helper/src/push.rs +++ b/radicle-remote-helper/src/push.rs @@ -403,7 +403,7 @@ fn patch_update( // and pushed, but the patch hasn't yet been updated. On push to the patch branch, // it'll seem like the patch is "empty", because the changes are already in the base branch. if base == head && patch.is_open() { - patch_merge(patch, revision, head, working, stored, signer)?; + patch_merge(patch, revision, head, working, signer)?; } Ok(()) @@ -470,7 +470,7 @@ fn patch_merge_all( let revision_id = *revision_id; let patch = patch::PatchMut::new(id, patch, &mut patches); - patch_merge(patch, revision_id, new, working, stored, signer)?; + patch_merge(patch, revision_id, new, working, signer)?; } } Ok(()) @@ -481,33 +481,20 @@ fn patch_merge( revision: patch::RevisionId, commit: git::Oid, working: &git::raw::Repository, - stored: &storage::git::Repository, signer: &G, ) -> Result<(), Error> { - patch.merge(revision, commit, signer)?; + let merged = patch.merge(revision, commit, signer)?; eprintln!( "{} Patch {} merged", cli::format::positive("✓"), - cli::format::tertiary(patch.id) + cli::format::tertiary(merged.patch) ); // Delete patch references that were created when the patch was opened. // Note that we don't return an error if we can't delete the refs, since it's // not critical. - let nid = signer.public_key(); - let stored_ref = git::refs::storage::patch(&patch.id).with_namespace(nid.into()); - let working_ref = git::refs::workdir::patch_upstream(&patch.id); - - stored - .raw() - .find_reference(&stored_ref) - .and_then(|mut r| r.delete()) - .ok(); - working - .find_reference(&working_ref) - .and_then(|mut r| r.delete()) - .ok(); + merged.cleanup(working, signer).ok(); Ok(()) } diff --git a/radicle-tools/Cargo.toml b/radicle-tools/Cargo.toml index 3f5ebe62..447b3bbf 100644 --- a/radicle-tools/Cargo.toml +++ b/radicle-tools/Cargo.toml @@ -27,6 +27,10 @@ path = "src/rad-init.rs" name = "rad-self" path = "src/rad-self.rs" +[[bin]] +name = "rad-merge" +path = "src/rad-merge.rs" + [[bin]] name = "rad-set-canonical-refs" path = "src/rad-set-canonical-refs.rs" diff --git a/radicle-tools/src/rad-merge.rs b/radicle-tools/src/rad-merge.rs new file mode 100644 index 00000000..bb4b24cf --- /dev/null +++ b/radicle-tools/src/rad-merge.rs @@ -0,0 +1,47 @@ +use std::collections::HashSet; +use std::env; + +use anyhow::anyhow; +use radicle::cob::patch::{PatchId, Patches}; +use radicle::git::Oid; +use radicle::storage::ReadStorage; +use radicle_cli::terminal as term; + +fn main() -> anyhow::Result<()> { + let pid: PatchId = env::args() + .nth(1) + .ok_or_else(|| anyhow!("usage: rad-merge "))? + .parse()?; + let profile = radicle::Profile::load()?; + let (working, rid) = radicle::rad::cwd()?; + let stored = profile.storage.repository(rid)?; + let mut patches = Patches::open(&stored)?; + let mut patch = patches.get_mut(&pid)?; + + if patch.is_merged() { + anyhow::bail!("fatal: patch {pid} is already merged"); + } + let (revision, r) = patch.latest(); + let head = r.head(); + + let mut revwalk = stored.backend.revwalk()?; + revwalk.push_head()?; + + let commits = revwalk + .map(|r| r.map(Oid::from)) + .collect::, _>>()?; + + if !commits.contains(&head) { + anyhow::bail!("fatal: patch head {head} is not in default branch"); + } + let signer = term::signer(&profile)?; + + patch + .merge(*revision, head, &signer)? + .cleanup(&working, &signer)?; + + println!("✓ Patch {pid} merged at commit {head}"); + println!("You may now run `rad sync --announce`."); + + Ok(()) +} diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 791a0472..23eef35d 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -25,6 +25,7 @@ use crate::identity; use crate::identity::doc::DocError; use crate::identity::PayloadError; use crate::prelude::*; +use crate::storage; /// Type name of a patch. pub static TYPENAME: Lazy = @@ -235,6 +236,46 @@ impl HistoryAction for Action { } } +/// Output of a merge. +#[derive(Debug)] +#[must_use] +pub struct Merged<'a, R> { + pub patch: PatchId, + pub entry: EntryId, + + stored: &'a R, +} + +impl<'a, R: WriteRepository> Merged<'a, R> { + /// Cleanup after merging a patch. + /// + /// This removes Git refs relating to the patch, both in the working copy, + /// and the stored copy; and updates `rad/sigrefs`. + pub fn cleanup( + self, + working: &git::raw::Repository, + signer: &G, + ) -> Result<(), storage::Error> { + let nid = signer.public_key(); + let stored_ref = git::refs::storage::patch(&self.patch).with_namespace(nid.into()); + let working_ref = git::refs::workdir::patch_upstream(&self.patch); + + working + .find_reference(&working_ref) + .and_then(|mut r| r.delete()) + .ok(); + + self.stored + .raw() + .find_reference(&stored_ref) + .and_then(|mut r| r.delete()) + .ok(); + self.stored.sign_refs(signer)?; + + Ok(()) + } +} + /// Where a patch is intended to be merged. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1501,9 +1542,15 @@ where revision: RevisionId, commit: git::Oid, signer: &G, - ) -> Result { + ) -> Result, Error> { // TODO: Don't allow merging the same revision twice? - self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit)) + let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?; + + Ok(Merged { + entry, + patch: self.id, + stored: self.store.as_ref(), + }) } /// Update a patch with a new revision.