mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-15 16:00:38 +08:00
bin: Layout basic queue logic for patch review
This commit is contained in:
@@ -80,7 +80,6 @@ pub fn all(profile: &Profile, repository: &Repository) -> Result<Vec<(PatchId, P
|
||||
Ok(patches.flatten().collect())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn find(profile: &Profile, repository: &Repository, id: &PatchId) -> Result<Option<Patch>> {
|
||||
let cache = profile.patches(repository)?;
|
||||
Ok(cache.get(id)?)
|
||||
|
||||
+73
-17
@@ -9,16 +9,21 @@ use std::ffi::OsString;
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::identity::RepoId;
|
||||
use radicle::patch::Status;
|
||||
use radicle::storage::WriteRepository;
|
||||
|
||||
use radicle_cli::git::Rev;
|
||||
use radicle_cli::terminal;
|
||||
use radicle_cli::terminal::args::{Args, Error, Help};
|
||||
use radicle_cli::terminal::args::{string, Args, Error, Help};
|
||||
|
||||
use crate::cob::patch;
|
||||
use crate::cob::patch::Filter;
|
||||
use crate::commands::tui_patch::review::ReviewAction;
|
||||
|
||||
use crate::tui_patch::review::builder::{Brain, ReviewBuilder};
|
||||
|
||||
pub const HELP: Help = Help {
|
||||
name: "patch",
|
||||
description: "Terminal interfaces for patches",
|
||||
@@ -56,14 +61,14 @@ pub struct Options {
|
||||
}
|
||||
|
||||
pub enum Operation {
|
||||
Review,
|
||||
Select { opts: SelectOptions },
|
||||
Review { opts: ReviewOptions },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum OperationName {
|
||||
Review,
|
||||
Select,
|
||||
Review,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
@@ -72,6 +77,12 @@ pub struct SelectOptions {
|
||||
filter: patch::Filter,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReviewOptions {
|
||||
patch_id: Rev,
|
||||
revision_id: Option<Rev>,
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||
use lexopt::prelude::*;
|
||||
@@ -80,6 +91,8 @@ impl Args for Options {
|
||||
let mut op: Option<OperationName> = None;
|
||||
let mut repo = None;
|
||||
let mut select_opts = SelectOptions::default();
|
||||
let mut patch_id = None;
|
||||
let mut revision_id = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
@@ -121,19 +134,27 @@ impl Args for Options {
|
||||
.filter
|
||||
.with_author(terminal::args::did(&parser.value()?)?);
|
||||
}
|
||||
|
||||
Long("repo") => {
|
||||
let val = parser.value()?;
|
||||
let rid = terminal::args::rid(&val)?;
|
||||
|
||||
repo = Some(rid);
|
||||
}
|
||||
Long("revision") => {
|
||||
let val = parser.value()?;
|
||||
let rev_id = terminal::args::rev(&val)?;
|
||||
|
||||
revision_id = Some(rev_id);
|
||||
}
|
||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
||||
"select" => op = Some(OperationName::Select),
|
||||
"review" => op = Some(OperationName::Review),
|
||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||
},
|
||||
Value(val) if patch_id.is_none() => {
|
||||
let val = string(&val);
|
||||
patch_id = Some(Rev::from(val));
|
||||
}
|
||||
_ => return Err(anyhow!(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
@@ -143,7 +164,12 @@ impl Args for Options {
|
||||
}
|
||||
|
||||
let op = match op.ok_or_else(|| anyhow!("an operation must be provided"))? {
|
||||
OperationName::Review => Operation::Review,
|
||||
OperationName::Review => Operation::Review {
|
||||
opts: ReviewOptions {
|
||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||
revision_id: revision_id,
|
||||
},
|
||||
},
|
||||
OperationName::Select => Operation::Select { opts: select_opts },
|
||||
};
|
||||
Ok((Options { op, repo }, vec![]))
|
||||
@@ -185,38 +211,68 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
|
||||
|
||||
eprint!("{output}");
|
||||
}
|
||||
Operation::Review => {
|
||||
let profile = ctx.profile()?;
|
||||
let rid = options.repo.unwrap_or(rid);
|
||||
let repository = profile.storage.repository(rid).unwrap();
|
||||
|
||||
Operation::Review { opts } => {
|
||||
if let Err(err) = crate::log::enable() {
|
||||
println!("{}", err);
|
||||
}
|
||||
log::info!("Starting patch review interface in project {}..", rid);
|
||||
log::info!("Starting patch review interface in project {rid}..");
|
||||
|
||||
let mut queue = vec![0, 1, 2];
|
||||
let profile = ctx.profile()?;
|
||||
let signer = terminal::signer(&profile)?;
|
||||
let rid = options.repo.unwrap_or(rid);
|
||||
let repo = profile.storage.repository(rid).unwrap();
|
||||
|
||||
// Load patch
|
||||
let patch_id = opts.patch_id.resolve(&repo.backend)?;
|
||||
let patch = patch::find(&profile, &repo, &patch_id)?
|
||||
.ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?;
|
||||
|
||||
// Load revision
|
||||
let revision_id = opts
|
||||
.revision_id
|
||||
.map(|rev| rev.resolve::<radicle::git::Oid>(&repo.backend))
|
||||
.transpose()?
|
||||
.map(radicle::cob::patch::RevisionId::from);
|
||||
let (_revision_id, revision) = match revision_id {
|
||||
Some(id) => (
|
||||
id,
|
||||
patch
|
||||
.revision(&id)
|
||||
.ok_or_else(|| anyhow!("Patch revision `{id}` not found"))?,
|
||||
),
|
||||
None => patch.latest(),
|
||||
};
|
||||
|
||||
let brain = if let Ok(b) = Brain::load(patch_id, signer.public_key(), repo.raw()) {
|
||||
log::info!(
|
||||
"Loaded existing review {} for patch {}",
|
||||
b.head().id(),
|
||||
&patch_id
|
||||
);
|
||||
b
|
||||
} else {
|
||||
let base = repo.raw().find_commit((*revision.base()).into())?;
|
||||
Brain::new(patch_id, signer.public_key(), base, repo.raw())?
|
||||
};
|
||||
|
||||
let queue = ReviewBuilder::new(patch_id, signer, &repo).queue(&brain, &revision)?;
|
||||
|
||||
while !queue.is_empty() {
|
||||
let selection = review::Tui::new(&profile, &repository).run().await?;
|
||||
let selection = review::Tui::new(&profile, &repo, &queue).run().await?;
|
||||
log::info!("Received selection from TUI: {:?}", selection);
|
||||
|
||||
if let Some(selection) = selection.as_ref() {
|
||||
match ReviewAction::try_from(selection.action)? {
|
||||
ReviewAction::Accept => {
|
||||
// brain accept
|
||||
queue.pop();
|
||||
}
|
||||
ReviewAction::Ignore => {
|
||||
// next hunk
|
||||
queue.pop();
|
||||
}
|
||||
ReviewAction::Comment => {
|
||||
radicle_cli::terminal::Editor::new()
|
||||
.extension("diff")
|
||||
.edit(String::new())?;
|
||||
|
||||
queue.pop();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[path = "review/builder.rs"]
|
||||
mod builder;
|
||||
pub mod builder;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -18,6 +18,8 @@ use tui::ui::im::widget::{TextViewState, Window};
|
||||
use tui::ui::im::{Borders, Context, Show};
|
||||
use tui::{Channel, Exit};
|
||||
|
||||
use self::builder::ReviewQueue;
|
||||
|
||||
/// The actions that a user can carry out on a review item.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ReviewAction {
|
||||
@@ -62,13 +64,15 @@ pub struct Selection {
|
||||
pub struct Tui<'a> {
|
||||
pub _profile: &'a Profile,
|
||||
pub _repository: &'a Repository,
|
||||
pub queue: &'a ReviewQueue,
|
||||
}
|
||||
|
||||
impl<'a> Tui<'a> {
|
||||
pub fn new(profile: &'a Profile, repository: &'a Repository) -> Self {
|
||||
pub fn new(profile: &'a Profile, repository: &'a Repository, queue: &'a ReviewQueue) -> Self {
|
||||
Self {
|
||||
_profile: profile,
|
||||
_repository: repository,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ pub struct Brain<'a> {
|
||||
|
||||
impl<'a> Brain<'a> {
|
||||
/// Create a new brain in the repository.
|
||||
fn new(
|
||||
pub fn new(
|
||||
patch: PatchId,
|
||||
remote: &NodeId,
|
||||
base: git::raw::Commit,
|
||||
@@ -515,12 +515,12 @@ impl<'a> Brain<'a> {
|
||||
|
||||
/// Return the content identifier of this brain. This represents the state of the
|
||||
/// accepted hunks, ie. the git tree.
|
||||
fn cid(&self) -> Oid {
|
||||
pub fn cid(&self) -> Oid {
|
||||
self.accepted.id().into()
|
||||
}
|
||||
|
||||
/// Load an existing brain from the repository.
|
||||
fn load(
|
||||
pub fn load(
|
||||
patch: PatchId,
|
||||
remote: &NodeId,
|
||||
repo: &'a git::raw::Repository,
|
||||
@@ -539,7 +539,7 @@ impl<'a> Brain<'a> {
|
||||
}
|
||||
|
||||
/// Accept changes to the brain.
|
||||
fn accept(
|
||||
pub fn accept(
|
||||
&mut self,
|
||||
diff: git::raw::Diff,
|
||||
repo: &'a git::raw::Repository,
|
||||
@@ -563,9 +563,17 @@ impl<'a> Brain<'a> {
|
||||
}
|
||||
|
||||
/// Get the brain's refname given the patch and remote.
|
||||
fn refname(patch: &PatchId, remote: &NodeId) -> git::Namespaced<'a> {
|
||||
pub fn refname(patch: &PatchId, remote: &NodeId) -> git::Namespaced<'a> {
|
||||
git::refs::storage::draft::review(remote, patch)
|
||||
}
|
||||
|
||||
pub fn head(&self) -> &git::raw::Commit<'a> {
|
||||
&self.head
|
||||
}
|
||||
|
||||
pub fn accepted(&self) -> &git::raw::Tree<'a> {
|
||||
&self.accepted
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a patch review interactively, across multiple files.
|
||||
@@ -606,6 +614,32 @@ impl<'a, G: Signer> ReviewBuilder<'a, G> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Assemble the review for the given revision.
|
||||
pub fn queue(&self, brain: &'a Brain<'a>, revision: &Revision) -> anyhow::Result<ReviewQueue> {
|
||||
let repo = self.repo.raw();
|
||||
let signer = &self.signer;
|
||||
let patch_id = self.patch_id;
|
||||
let base = repo.find_commit((*revision.base()).into())?;
|
||||
let tree = {
|
||||
let commit = repo.find_commit(revision.head().into())?;
|
||||
commit.tree()?
|
||||
};
|
||||
|
||||
let mut opts = git::raw::DiffOptions::new();
|
||||
opts.patience(true).minimal(true).context_lines(3_u32);
|
||||
|
||||
let diff = self.diff(&brain.accepted(), &tree, repo, &mut opts)?;
|
||||
let drafts = DraftStore::new(*signer.public_key(), self.repo).with(
|
||||
signer.public_key(),
|
||||
&cob::patch::TYPENAME,
|
||||
&patch_id,
|
||||
)?;
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&patch_id)?;
|
||||
|
||||
Ok(ReviewQueue::from(diff))
|
||||
}
|
||||
|
||||
/// Run the review builder for the given revision.
|
||||
pub fn run(self, revision: &Revision, opts: &mut git::raw::DiffOptions) -> anyhow::Result<()> {
|
||||
let repo = self.repo.raw();
|
||||
@@ -774,7 +808,7 @@ impl<'a, G: Signer> ReviewBuilder<'a, G> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn diff(
|
||||
pub fn diff(
|
||||
&self,
|
||||
brain: &git::raw::Tree<'_>,
|
||||
tree: &git::raw::Tree<'_>,
|
||||
|
||||
Reference in New Issue
Block a user