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.
This commit is contained in:
parent
6c5cba3ca6
commit
bfc3c7164f
|
|
@ -403,7 +403,7 @@ fn patch_update<G: Signer>(
|
||||||
// and pushed, but the patch hasn't yet been updated. On push to the patch branch,
|
// 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.
|
// it'll seem like the patch is "empty", because the changes are already in the base branch.
|
||||||
if base == head && patch.is_open() {
|
if base == head && patch.is_open() {
|
||||||
patch_merge(patch, revision, head, working, stored, signer)?;
|
patch_merge(patch, revision, head, working, signer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -470,7 +470,7 @@ fn patch_merge_all<G: Signer>(
|
||||||
let revision_id = *revision_id;
|
let revision_id = *revision_id;
|
||||||
let patch = patch::PatchMut::new(id, patch, &mut patches);
|
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(())
|
Ok(())
|
||||||
|
|
@ -481,33 +481,20 @@ fn patch_merge<G: Signer>(
|
||||||
revision: patch::RevisionId,
|
revision: patch::RevisionId,
|
||||||
commit: git::Oid,
|
commit: git::Oid,
|
||||||
working: &git::raw::Repository,
|
working: &git::raw::Repository,
|
||||||
stored: &storage::git::Repository,
|
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
patch.merge(revision, commit, signer)?;
|
let merged = patch.merge(revision, commit, signer)?;
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Patch {} merged",
|
"{} Patch {} merged",
|
||||||
cli::format::positive("✓"),
|
cli::format::positive("✓"),
|
||||||
cli::format::tertiary(patch.id)
|
cli::format::tertiary(merged.patch)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete patch references that were created when the patch was opened.
|
// 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
|
// Note that we don't return an error if we can't delete the refs, since it's
|
||||||
// not critical.
|
// not critical.
|
||||||
let nid = signer.public_key();
|
merged.cleanup(working, signer).ok();
|
||||||
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();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ path = "src/rad-init.rs"
|
||||||
name = "rad-self"
|
name = "rad-self"
|
||||||
path = "src/rad-self.rs"
|
path = "src/rad-self.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rad-merge"
|
||||||
|
path = "src/rad-merge.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rad-set-canonical-refs"
|
name = "rad-set-canonical-refs"
|
||||||
path = "src/rad-set-canonical-refs.rs"
|
path = "src/rad-set-canonical-refs.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 <patch-id>"))?
|
||||||
|
.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::<Result<HashSet<Oid>, _>>()?;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ use crate::identity;
|
||||||
use crate::identity::doc::DocError;
|
use crate::identity::doc::DocError;
|
||||||
use crate::identity::PayloadError;
|
use crate::identity::PayloadError;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::storage;
|
||||||
|
|
||||||
/// Type name of a patch.
|
/// Type name of a patch.
|
||||||
pub static TYPENAME: Lazy<TypeName> =
|
pub static TYPENAME: Lazy<TypeName> =
|
||||||
|
|
@ -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<G: Signer>(
|
||||||
|
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.
|
/// Where a patch is intended to be merged.
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
@ -1501,9 +1542,15 @@ where
|
||||||
revision: RevisionId,
|
revision: RevisionId,
|
||||||
commit: git::Oid,
|
commit: git::Oid,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<EntryId, Error> {
|
) -> Result<Merged<R>, Error> {
|
||||||
// TODO: Don't allow merging the same revision twice?
|
// 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.
|
/// Update a patch with a new revision.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue