cli: Enable redacting patch revisions
Use `rad patch redact <revision-id>` to redact (delete) a patch revision.
This commit is contained in:
parent
13bea902b2
commit
65763a2ad2
|
|
@ -14,6 +14,8 @@ mod edit;
|
||||||
mod list;
|
mod list;
|
||||||
#[path = "patch/ready.rs"]
|
#[path = "patch/ready.rs"]
|
||||||
mod ready;
|
mod ready;
|
||||||
|
#[path = "patch/redact.rs"]
|
||||||
|
mod redact;
|
||||||
#[path = "patch/show.rs"]
|
#[path = "patch/show.rs"]
|
||||||
mod show;
|
mod show;
|
||||||
#[path = "patch/update.rs"]
|
#[path = "patch/update.rs"]
|
||||||
|
|
@ -49,6 +51,7 @@ Usage
|
||||||
rad patch update <patch-id> [<option>...]
|
rad patch update <patch-id> [<option>...]
|
||||||
rad patch checkout <patch-id> [<option>...]
|
rad patch checkout <patch-id> [<option>...]
|
||||||
rad patch delete <patch-id> [<option>...]
|
rad patch delete <patch-id> [<option>...]
|
||||||
|
rad patch redact <revision-id> [<option>...]
|
||||||
rad patch ready <patch-id> [--undo] [<option>...]
|
rad patch ready <patch-id> [--undo] [<option>...]
|
||||||
rad patch edit <patch-id> [<option>...]
|
rad patch edit <patch-id> [<option>...]
|
||||||
|
|
||||||
|
|
@ -99,6 +102,7 @@ pub enum OperationName {
|
||||||
#[default]
|
#[default]
|
||||||
List,
|
List,
|
||||||
Edit,
|
Edit,
|
||||||
|
Redact,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Filter(fn(&patch::State) -> bool);
|
pub struct Filter(fn(&patch::State) -> bool);
|
||||||
|
|
@ -158,6 +162,9 @@ pub enum Operation {
|
||||||
patch_id: Rev,
|
patch_id: Rev,
|
||||||
message: Message,
|
message: Message,
|
||||||
},
|
},
|
||||||
|
Redact {
|
||||||
|
revision_id: Rev,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -179,6 +186,7 @@ impl Args for Options {
|
||||||
let mut fetch = false;
|
let mut fetch = false;
|
||||||
let mut announce = false;
|
let mut announce = false;
|
||||||
let mut patch_id = None;
|
let mut patch_id = None;
|
||||||
|
let mut revision_id = None;
|
||||||
let mut message = Message::default();
|
let mut message = Message::default();
|
||||||
let mut push = true;
|
let mut push = true;
|
||||||
let mut filter = Filter::default();
|
let mut filter = Filter::default();
|
||||||
|
|
@ -274,8 +282,13 @@ impl Args for Options {
|
||||||
"a" | "archive" => op = Some(OperationName::Archive),
|
"a" | "archive" => op = Some(OperationName::Archive),
|
||||||
"y" | "ready" => op = Some(OperationName::Ready),
|
"y" | "ready" => op = Some(OperationName::Ready),
|
||||||
"e" | "edit" => op = Some(OperationName::Edit),
|
"e" | "edit" => op = Some(OperationName::Edit),
|
||||||
|
"r" | "redact" => op = Some(OperationName::Redact),
|
||||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||||
},
|
},
|
||||||
|
Value(val) if op == Some(OperationName::Redact) => {
|
||||||
|
let val = string(&val);
|
||||||
|
revision_id = Some(Rev::from(val));
|
||||||
|
}
|
||||||
Value(val)
|
Value(val)
|
||||||
if patch_id.is_none()
|
if patch_id.is_none()
|
||||||
&& [
|
&& [
|
||||||
|
|
@ -329,6 +342,9 @@ impl Args for Options {
|
||||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
|
OperationName::Redact => Operation::Redact {
|
||||||
|
revision_id: revision_id.ok_or_else(|| anyhow!("a revision must be provided"))?,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|
@ -408,7 +424,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
ready::run(&patch_id, undo, &profile, &repository)?;
|
ready::run(&patch_id, undo, &profile, &repository)?;
|
||||||
}
|
}
|
||||||
Operation::Delete { patch_id } => {
|
Operation::Delete { patch_id } => {
|
||||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
|
||||||
delete::run(&patch_id, &profile, &repository)?;
|
delete::run(&patch_id, &profile, &repository)?;
|
||||||
}
|
}
|
||||||
Operation::Checkout { patch_id } => {
|
Operation::Checkout { patch_id } => {
|
||||||
|
|
@ -419,6 +435,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||||
edit::run(&patch_id, message, &profile, &repository)?;
|
edit::run(&patch_id, message, &profile, &repository)?;
|
||||||
}
|
}
|
||||||
|
Operation::Redact { revision_id } => {
|
||||||
|
redact::run(&revision_id, &profile, &repository)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
use radicle::cob::patch;
|
||||||
|
use radicle::prelude::*;
|
||||||
|
use radicle::storage::git::Repository;
|
||||||
|
|
||||||
|
use crate::git;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
revision_id: &git::Rev,
|
||||||
|
profile: &Profile,
|
||||||
|
repository: &Repository,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let signer = &term::signer(profile)?;
|
||||||
|
let mut patches = patch::Patches::open(repository)?;
|
||||||
|
|
||||||
|
let revision_id = revision_id.resolve::<patch::RevisionId>(&repository.backend)?;
|
||||||
|
let Some((patch_id, _, _)) = patches.find_by_revision(&revision_id)? else {
|
||||||
|
anyhow::bail!("patch revision `{revision_id}` not found");
|
||||||
|
};
|
||||||
|
let Ok(mut patch) = patches.get_mut(&patch_id) else {
|
||||||
|
anyhow::bail!("Patch `{patch_id}` not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
patch.redact(revision_id, signer)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,9 @@ pub type RevisionIx = usize;
|
||||||
/// Error applying an operation onto a state.
|
/// Error applying an operation onto a state.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// Error trying to delete the protected root revision.
|
||||||
|
#[error("refusing to delete root revision: {0}")]
|
||||||
|
RootRevision(RevisionId),
|
||||||
/// Causal dependency missing.
|
/// Causal dependency missing.
|
||||||
///
|
///
|
||||||
/// This error indicates that the operations are not being applied
|
/// This error indicates that the operations are not being applied
|
||||||
|
|
@ -862,6 +865,11 @@ impl store::Transaction<Patch> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redact the revision.
|
||||||
|
pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
|
||||||
|
self.push(Action::Redact { revision })
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a patch revision discussion.
|
/// Start a patch revision discussion.
|
||||||
pub fn thread<S: ToString>(
|
pub fn thread<S: ToString>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1011,11 +1019,23 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
description: String,
|
description: String,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<EntryId, Error> {
|
) -> Result<EntryId, Error> {
|
||||||
self.transaction("Edit Revision", signer, |tx| {
|
self.transaction("Edit revision", signer, |tx| {
|
||||||
tx.edit_revision(revision, description)
|
tx.edit_revision(revision, description)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redact a revision.
|
||||||
|
pub fn redact<G: Signer>(
|
||||||
|
&mut self,
|
||||||
|
revision: RevisionId,
|
||||||
|
signer: &G,
|
||||||
|
) -> Result<EntryId, Error> {
|
||||||
|
if revision == RevisionId::from(self.id) {
|
||||||
|
return Err(Error::RootRevision(revision));
|
||||||
|
}
|
||||||
|
self.transaction("Redact revision", signer, |tx| tx.redact(revision))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a thread on a patch revision.
|
/// Create a thread on a patch revision.
|
||||||
pub fn thread<G: Signer, S: ToString>(
|
pub fn thread<G: Signer, S: ToString>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1994,4 +2014,38 @@ mod test {
|
||||||
assert_eq!(revision.oid, update.oid);
|
assert_eq!(revision.oid, update.oid);
|
||||||
assert_eq!(revision.description(), "I've made changes.");
|
assert_eq!(revision.description(), "I've made changes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_patch_redact() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test::setup::Context::new(&tmp);
|
||||||
|
let signer = &ctx.signer;
|
||||||
|
let pr = ctx.branch_with(test::setup::initial_blobs());
|
||||||
|
let mut patches = Patches::open(&ctx.project).unwrap();
|
||||||
|
let mut patch = patches
|
||||||
|
.create(
|
||||||
|
"My first patch",
|
||||||
|
"Blah blah blah.",
|
||||||
|
MergeTarget::Delegates,
|
||||||
|
pr.base,
|
||||||
|
pr.oid,
|
||||||
|
&[],
|
||||||
|
signer,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let patch_id = patch.id;
|
||||||
|
|
||||||
|
let update = ctx.branch_with(test::setup::update_blobs());
|
||||||
|
let revision_id = patch
|
||||||
|
.update("I've made changes.", pr.base, update.oid, signer)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(patch.revisions().count(), 2);
|
||||||
|
|
||||||
|
patch.redact(revision_id, signer).unwrap();
|
||||||
|
assert_eq!(patch.latest().0, &RevisionId::from(patch_id));
|
||||||
|
assert_eq!(patch.revisions().count(), 1);
|
||||||
|
|
||||||
|
// The patch's root must always exist.
|
||||||
|
assert!(patch.redact(*patch.latest().0, signer).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue