cli: alter use subcommands for patch
Make `rad patch` accept subcommands for specifying its operation in place of the '--command' notation. For instance `rad patch update` versus `rad patch --update`. Also make the default operation list active patches, replacing patch creation. Patch creation is now done via `rad patch open`. Make these changes in a way consistent with other commands like `rad delegate`. Add '--no-confirm' to patch to support this subcommand's documentation test. Signed-off-by: Slack Coder <slackcoder@server.ky>
This commit is contained in:
parent
6d420813f5
commit
a6d59b3c0e
|
|
@ -0,0 +1,65 @@
|
||||||
|
When contributing to another's project, it is common for the contribution to be
|
||||||
|
of many commits and involve a discussion with the project's maintainer. This is supported
|
||||||
|
via Radicle's patches.
|
||||||
|
|
||||||
|
Here we give a brief overview for using patches in our hypothetical car
|
||||||
|
scenario. It turns out instructions containing the power requirements were
|
||||||
|
missing from the project.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git checkout -b flux-capacitor-power
|
||||||
|
$ touch README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Here the instructions are added to the project's README for 1.21 gigawatts and
|
||||||
|
commit the changes to git.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git add README.md
|
||||||
|
$ git commit -m "Define power requirements"
|
||||||
|
[flux-capacitor-power 7939a9e] Define power requirements
|
||||||
|
1 file changed, 0 insertions(+), 0 deletions(-)
|
||||||
|
create mode 100644 README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the code is ready, we open (or create) a patch with our changes for the project.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rad patch open --message "define power requirements" --no-confirm
|
||||||
|
|
||||||
|
🌱 Creating patch for heartwood
|
||||||
|
|
||||||
|
ok Pushing HEAD to storage...
|
||||||
|
ok Analyzing remotes...
|
||||||
|
|
||||||
|
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master (cdf76ce) <- z6MknSL…StBU8Vi/flux-capacitor-power (7939a9e)
|
||||||
|
1 commit(s) ahead, 0 commit(s) behind
|
||||||
|
|
||||||
|
7939a9e Define power requirements
|
||||||
|
|
||||||
|
|
||||||
|
╭─ define power requirements ───────
|
||||||
|
|
||||||
|
No description provided.
|
||||||
|
|
||||||
|
╰───────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
ok Patch 3b1f58414e51266d7621203554a63eaee242b744 created 🌱
|
||||||
|
```
|
||||||
|
|
||||||
|
It will now be listed as one of the project's open patches.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rad patch
|
||||||
|
|
||||||
|
YOU PROPOSED
|
||||||
|
|
||||||
|
define power requirements 3b1f58414e5 R0 7939a9e (flux-capacitor-power) ahead 1, behind 0
|
||||||
|
└─ * opened by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
|
||||||
|
|
||||||
|
OTHERS PROPOSED
|
||||||
|
|
||||||
|
Nothing to show.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -24,11 +24,13 @@ pub const HELP: Help = Help {
|
||||||
usage: r#"
|
usage: r#"
|
||||||
Usage
|
Usage
|
||||||
|
|
||||||
rad patch [<option>...]
|
rad patch
|
||||||
|
rad patch open [<option>...]
|
||||||
|
rad patch update <id> [<option>...]
|
||||||
|
|
||||||
Create options
|
Create/Update options
|
||||||
|
|
||||||
-u, --update [<id>] Update an existing patch (default: no)
|
--[no-]confirm Don't ask for confirmation during clone
|
||||||
--[no-]sync Sync patch to seed (default: sync)
|
--[no-]sync Sync patch to seed (default: sync)
|
||||||
--[no-]push Push patch head to storage (default: true)
|
--[no-]push Push patch head to storage (default: true)
|
||||||
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
|
-m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
|
||||||
|
|
@ -36,32 +38,45 @@ Create options
|
||||||
|
|
||||||
Options
|
Options
|
||||||
|
|
||||||
-l, --list List all patches (default: false)
|
|
||||||
--help Print help
|
--help Print help
|
||||||
"#,
|
"#,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum Update {
|
pub enum OptPatch {
|
||||||
No,
|
#[default]
|
||||||
Any,
|
Any,
|
||||||
|
None,
|
||||||
Patch(PatchId),
|
Patch(PatchId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Update {
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
fn default() -> Self {
|
pub enum OperationName {
|
||||||
Self::No
|
Open,
|
||||||
}
|
Update,
|
||||||
|
#[default]
|
||||||
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
|
pub enum Operation {
|
||||||
|
Open {
|
||||||
|
message: Comment,
|
||||||
|
},
|
||||||
|
Update {
|
||||||
|
patch_id: OptPatch,
|
||||||
|
message: Comment,
|
||||||
|
},
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
pub list: bool,
|
pub op: Operation,
|
||||||
pub verbose: bool,
|
pub confirm: bool,
|
||||||
pub sync: bool,
|
pub sync: bool,
|
||||||
pub push: bool,
|
pub push: bool,
|
||||||
pub update: Update,
|
pub verbose: bool,
|
||||||
pub message: Comment,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args for Options {
|
impl Args for Options {
|
||||||
|
|
@ -69,34 +84,23 @@ impl Args for Options {
|
||||||
use lexopt::prelude::*;
|
use lexopt::prelude::*;
|
||||||
|
|
||||||
let mut parser = lexopt::Parser::from_args(args);
|
let mut parser = lexopt::Parser::from_args(args);
|
||||||
let mut list = false;
|
let mut confirm = true;
|
||||||
|
let mut op: Option<OperationName> = None;
|
||||||
let mut verbose = false;
|
let mut verbose = false;
|
||||||
let mut sync = true;
|
let mut sync = true;
|
||||||
|
let mut id = OptPatch::default();
|
||||||
let mut message = Comment::default();
|
let mut message = Comment::default();
|
||||||
let mut push = true;
|
let mut push = true;
|
||||||
let mut update = Update::default();
|
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
while let Some(arg) = parser.next()? {
|
||||||
match arg {
|
match arg {
|
||||||
// Operations.
|
|
||||||
Long("list") | Short('l') => {
|
|
||||||
list = true;
|
|
||||||
}
|
|
||||||
Long("update") | Short('u') => {
|
|
||||||
if let Ok(val) = parser.value() {
|
|
||||||
let val = val
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| anyhow!("patch id specified is not UTF-8"))?;
|
|
||||||
let id = PatchId::from_str(val)
|
|
||||||
.map_err(|_| anyhow!("invalid patch id '{}'", val))?;
|
|
||||||
|
|
||||||
update = Update::Patch(id);
|
|
||||||
} else {
|
|
||||||
update = Update::Any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options.
|
// Options.
|
||||||
|
Long("confirm") => {
|
||||||
|
confirm = true;
|
||||||
|
}
|
||||||
|
Long("no-confirm") => {
|
||||||
|
confirm = false;
|
||||||
|
}
|
||||||
Long("message") | Short('m') => {
|
Long("message") | Short('m') => {
|
||||||
if message != Comment::Blank {
|
if message != Comment::Blank {
|
||||||
// We skip this code when `no-message` is specified.
|
// We skip this code when `no-message` is specified.
|
||||||
|
|
@ -129,17 +133,43 @@ impl Args for Options {
|
||||||
Long("help") => {
|
Long("help") => {
|
||||||
return Err(Error::Help.into());
|
return Err(Error::Help.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
||||||
|
"l" | "list" => op = Some(OperationName::List),
|
||||||
|
"o" | "open" => op = Some(OperationName::Open),
|
||||||
|
"u" | "update" => op = Some(OperationName::Update),
|
||||||
|
|
||||||
|
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
||||||
|
},
|
||||||
|
Value(val) if op == Some(OperationName::Update) && id == OptPatch::Any => {
|
||||||
|
let val = val
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("patch id specified is not UTF-8"))?;
|
||||||
|
|
||||||
|
id = OptPatch::Patch(
|
||||||
|
PatchId::from_str(val)
|
||||||
|
.map_err(|_| anyhow!("invalid patch id '{}'", val))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => return Err(anyhow::anyhow!(arg.unexpected())),
|
_ => return Err(anyhow::anyhow!(arg.unexpected())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let op = match op.unwrap_or_default() {
|
||||||
|
OperationName::Open => Operation::Open { message },
|
||||||
|
OperationName::List => Operation::List,
|
||||||
|
OperationName::Update => Operation::Update {
|
||||||
|
patch_id: id,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Options {
|
Options {
|
||||||
list,
|
op,
|
||||||
|
confirm,
|
||||||
sync,
|
sync,
|
||||||
message,
|
|
||||||
push,
|
push,
|
||||||
update,
|
|
||||||
verbose,
|
verbose,
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
|
|
@ -154,10 +184,33 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let profile = ctx.profile()?;
|
let profile = ctx.profile()?;
|
||||||
let storage = profile.storage.repository(id)?;
|
let storage = profile.storage.repository(id)?;
|
||||||
|
|
||||||
if options.list {
|
match options.op {
|
||||||
list::run(&storage, &profile, Some(workdir), options)?;
|
Operation::Open { ref message } => {
|
||||||
} else {
|
create::run(
|
||||||
create::run(&storage, &profile, &workdir, options)?;
|
&storage,
|
||||||
|
&profile,
|
||||||
|
&workdir,
|
||||||
|
OptPatch::None,
|
||||||
|
message.clone(),
|
||||||
|
options,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Operation::List => {
|
||||||
|
list::run(&storage, &profile, Some(workdir), options)?;
|
||||||
|
}
|
||||||
|
Operation::Update {
|
||||||
|
ref patch_id,
|
||||||
|
ref message,
|
||||||
|
} => {
|
||||||
|
create::run(
|
||||||
|
&storage,
|
||||||
|
&profile,
|
||||||
|
&workdir,
|
||||||
|
*patch_id,
|
||||||
|
message.clone(),
|
||||||
|
options,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
|
@ -10,9 +11,10 @@ use radicle::storage::git::Repository;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::Error;
|
use crate::terminal::args::Error;
|
||||||
|
use crate::terminal::patch;
|
||||||
|
|
||||||
use super::common;
|
use super::common;
|
||||||
use super::{Options, Update};
|
use super::{OptPatch, Options};
|
||||||
|
|
||||||
const PATCH_MSG: &str = r#"
|
const PATCH_MSG: &str = r#"
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -33,11 +35,18 @@ blank is also okay.
|
||||||
-->
|
-->
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn confirm<D: fmt::Display>(prompt: D, options: &Options) -> bool {
|
||||||
|
!options.confirm || term::confirm(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run patch creation.
|
/// Run patch creation.
|
||||||
pub fn run(
|
pub fn run(
|
||||||
storage: &Repository,
|
storage: &Repository,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
workdir: &git::raw::Repository,
|
workdir: &git::raw::Repository,
|
||||||
|
patch_id: OptPatch,
|
||||||
|
message: patch::Comment,
|
||||||
options: Options,
|
options: Options,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let project = storage.project_of(&profile.public_key).context(format!(
|
let project = storage.project_of(&profile.public_key).context(format!(
|
||||||
|
|
@ -118,9 +127,9 @@ pub fn run(
|
||||||
let base_oid = workdir.merge_base((*target_oid).into(), head_oid)?;
|
let base_oid = workdir.merge_base((*target_oid).into(), head_oid)?;
|
||||||
let commits = common::patch_commits(workdir, &base_oid, &head_oid)?;
|
let commits = common::patch_commits(workdir, &base_oid, &head_oid)?;
|
||||||
|
|
||||||
let patch = match &options.update {
|
let patch = match &patch_id {
|
||||||
Update::No => None,
|
OptPatch::None => None,
|
||||||
Update::Any => {
|
OptPatch::Any => {
|
||||||
let mut spinner = term::spinner("Finding patches to update...");
|
let mut spinner = term::spinner("Finding patches to update...");
|
||||||
let mut result = common::find_unmerged_with_base(
|
let mut result = common::find_unmerged_with_base(
|
||||||
head_oid,
|
head_oid,
|
||||||
|
|
@ -152,7 +161,7 @@ pub fn run(
|
||||||
anyhow::bail!("No patches found that share a base, please create a new patch or specify the patch id manually");
|
anyhow::bail!("No patches found that share a base, please create a new patch or specify the patch id manually");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Update::Patch(id) => {
|
OptPatch::Patch(id) => {
|
||||||
if let Ok(patch) = patches.get_mut(id) {
|
if let Ok(patch) = patches.get_mut(id) {
|
||||||
Some((*id, patch))
|
Some((*id, patch))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -162,10 +171,12 @@ pub fn run(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((id, patch)) = patch {
|
if let Some((id, patch)) = patch {
|
||||||
if term::confirm("Update?") {
|
if confirm("Update?", &options) {
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
||||||
return update(patch, id, &base_oid, &head_oid, workdir, options, &signer);
|
return update(
|
||||||
|
patch, id, &base_oid, &head_oid, workdir, options, message, &signer,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!("Patch update aborted by user");
|
anyhow::bail!("Patch update aborted by user");
|
||||||
}
|
}
|
||||||
|
|
@ -194,14 +205,14 @@ pub fn run(
|
||||||
term::patch::list_commits(&commits)?;
|
term::patch::list_commits(&commits)?;
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
||||||
if !term::confirm("Continue?") {
|
if !confirm("Continue?", &options) {
|
||||||
anyhow::bail!("patch proposal aborted by user");
|
anyhow::bail!("patch proposal aborted by user");
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = head_commit
|
let commit_message = head_commit
|
||||||
.message()
|
.message()
|
||||||
.ok_or(anyhow!("commit summary is not valid UTF-8; aborting"))?;
|
.ok_or(anyhow!("commit summary is not valid UTF-8; aborting"))?;
|
||||||
let message = options.message.get(&format!("{}{}", message, PATCH_MSG));
|
let message = message.get(&format!("{}{}", commit_message, PATCH_MSG));
|
||||||
let (title, description) = message.split_once("\n\n").unwrap_or((&message, ""));
|
let (title, description) = message.split_once("\n\n").unwrap_or((&message, ""));
|
||||||
let (title, description) = (title.trim(), description.trim());
|
let (title, description) = (title.trim(), description.trim());
|
||||||
let description = description.replace(PATCH_MSG.trim(), ""); // Delete help message.
|
let description = description.replace(PATCH_MSG.trim(), ""); // Delete help message.
|
||||||
|
|
@ -229,7 +240,7 @@ pub fn run(
|
||||||
)));
|
)));
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
||||||
if !term::confirm("Create patch?") {
|
if !confirm("Create patch?", &options) {
|
||||||
anyhow::bail!("patch proposal aborted by user");
|
anyhow::bail!("patch proposal aborted by user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +272,7 @@ fn update<G: Signer>(
|
||||||
head: &Oid,
|
head: &Oid,
|
||||||
workdir: &git::raw::Repository,
|
workdir: &git::raw::Repository,
|
||||||
options: Options,
|
options: Options,
|
||||||
|
message: patch::Comment,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// TODO(cloudhead): Handle error.
|
// TODO(cloudhead): Handle error.
|
||||||
|
|
@ -280,13 +292,13 @@ fn update<G: Signer>(
|
||||||
term::format::dim(format!("R{}", current_version + 1)),
|
term::format::dim(format!("R{}", current_version + 1)),
|
||||||
term::format::secondary(term::format::oid(*head)),
|
term::format::secondary(term::format::oid(*head)),
|
||||||
);
|
);
|
||||||
let message = options.message.get(REVISION_MSG);
|
let message = message.get(REVISION_MSG);
|
||||||
|
|
||||||
// Difference between the two revisions.
|
// Difference between the two revisions.
|
||||||
term::patch::print_commits_ahead_behind(workdir, *head, *current_revision.oid)?;
|
term::patch::print_commits_ahead_behind(workdir, *head, *current_revision.oid)?;
|
||||||
term::blank();
|
term::blank();
|
||||||
|
|
||||||
if !term::confirm("Continue?") {
|
if !confirm("Continue?", &options) {
|
||||||
anyhow::bail!("patch update aborted by user");
|
anyhow::bail!("patch update aborted by user");
|
||||||
}
|
}
|
||||||
patch.update(message, *base, *head, signer)?;
|
patch.update(message, *base, *head, signer)?;
|
||||||
|
|
|
||||||
|
|
@ -84,3 +84,21 @@ fn rad_delegate() {
|
||||||
test("examples/rad-init.md", working.path(), Some(&profile)).unwrap();
|
test("examples/rad-init.md", working.path(), Some(&profile)).unwrap();
|
||||||
test("examples/rad-delegate.md", working.path(), Some(&profile)).unwrap();
|
test("examples/rad-delegate.md", working.path(), Some(&profile)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_patch() {
|
||||||
|
let home = tempfile::tempdir().unwrap();
|
||||||
|
let working = tempfile::tempdir().unwrap();
|
||||||
|
let profile = profile(home.path());
|
||||||
|
|
||||||
|
// Setup a test repository.
|
||||||
|
fixtures::repository(working.path());
|
||||||
|
// Set a fixed commit time.
|
||||||
|
env::set_var(radicle_cob::git::RAD_COMMIT_TIME, "1671125284");
|
||||||
|
env::set_var("GIT_COMMITTER_DATE", "1671125284");
|
||||||
|
env::set_var("GIT_AUTHOR_DATE", "1671125284");
|
||||||
|
|
||||||
|
test("examples/rad-init.md", working.path(), Some(&profile)).unwrap();
|
||||||
|
test("examples/rad-issue.md", working.path(), Some(&profile)).unwrap();
|
||||||
|
test("examples/rad-patch.md", working.path(), Some(&profile)).unwrap();
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue