rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood195bae992f9ac5adc8f5e0554443f0bda2489a48
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "0866819acc9414f83f2c57ff65917bc8b79bbfd2",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"title": "Add clap for parsing cli args",
"state": {
"status": "archived",
"conflicts": []
},
"before": "54aacc96197a48b79fcc260f94312d824f5e0a34",
"after": "195bae992f9ac5adc8f5e0554443f0bda2489a48",
"commits": [
"195bae992f9ac5adc8f5e0554443f0bda2489a48",
"229d8019b9f9dc1c3544cd90384bd5d4400d75ca",
"29a69bad9e9c61b94d18cd8f1a6594edd818a967",
"698652265a7a45959d8c2d3c7ddc1303361d3747",
"06ff636332fa846b750fdef56f247fcbf65f092d",
"56b64c9d3959f4ab5a364504b418c9b006479aba",
"ef2949832b1a684526bce50cfdfea5db260a7d28",
"92cbaa6d97ed7be579fc2e1da7ddff71595f117b"
],
"target": "7e1cb406bb935c3de008d2171e8961f57b4336ea",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "0866819acc9414f83f2c57ff65917bc8b79bbfd2",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Refactored the issue command using clap\n\n\nSet clap help_template\n\n\ncli: issue command shouldn't ask for passphrase on read-only operations\n\n\ncli: remove cli.rs and set default value for issue list --assigned\n\n\ncli: Add completion stubs for bash/zsh/fish\n\n\nPoint clap fork dep to github.com/icetan instead of local path\n\n\nBump nixpkgs to support latest version of clap",
"base": "54aacc96197a48b79fcc260f94312d824f5e0a34",
"oid": "195bae992f9ac5adc8f5e0554443f0bda2489a48",
"timestamp": 1710518037
},
{
"id": "31f6309a5f0f387bbd3e565371e729ab150b0b2f",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Changes:\n- Pin Nix back to release\n- WIP adding zsh completion to flake.nix\n- Some clean up",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "89de84f7491dab3b0066dccc62a730597e55e295",
"timestamp": 1716315598
},
{
"id": "391d2721520c45a93ad692e9c03dda505ce8efa7",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Changes:\n- reorganise args and commands into a module",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "8c8a2036b8aab7cc353f50b66b5fc6591ebe6477",
"timestamp": 1716453594
},
{
"id": "80b01629088d0dc2b1d0b7a226940d08345d3d88",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Changes:\n- Clean up on state transition argument\n- Show off the power of forcing documentation to ensure all fields are\n documented for the help output",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "15c4e1774946669877a781efd467db2f1e4b19b3",
"timestamp": 1716469647
},
{
"id": "6aef9c73473ca73f56f23bbd863b75ee732c23c0",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Rebase. Does not require icetan's fork anymore.",
"base": "01bed73a60ab429c3f90a90bdc31a138ed54c78e",
"oid": "62e4ff4e9ca9e0fa1641f72914bf50f59afcc628",
"timestamp": 1755447227
},
{
"id": "b58ccaba25524e4febd24f4bab5de5cbc5a6723f",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Here are some changes to the clap proposal.\n\nI used `git commit --fixup` for the smaller stuff and properly committed bigger changes.\n\nThe fixup commits are:\n\n* I rewrote a comment to be more accurate\n* I renamed the `Commands` enum to `Command`, because that's only ever one, not multiple\n\nFeel free to squash them away or, if you want them to be individual commits (be\nmy guest!) rewrite their commit message (or order me to do it)!",
"base": "01bed73a60ab429c3f90a90bdc31a138ed54c78e",
"oid": "3c581d9df5e24b183e0478f43e3e20f8928d5b14",
"timestamp": 1755452435
},
{
"id": "c0dcf6b4e19632014e147b7a59e8d2e735865886",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Squash in Matthias' improvements. Thanks!",
"base": "01bed73a60ab429c3f90a90bdc31a138ed54c78e",
"oid": "88b6728e8127044855311997334b337d7bdc7c77",
"timestamp": 1755454563
},
{
"id": "48e2bcf08c6a06053d9c0d8e693c5b54f699441d",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Rebase",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "8f5f7b596f22c68e6bebfd180beafb10eaa63a15",
"timestamp": 1756585509
},
{
"id": "6a1599d3cc78a825cce22b5893743bc0aff74ad0",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Some changes for converting the `rad patch` command to clap\n\nRight now this is in-flight. It does not even compile. But I am out of\nbrain-power now, so this will not be continued right now.\n\nI am publishing this in the state it is, though, so that it is visible that I\nam working on it, so we do not duplicate work here.",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "cb34d688533c6e42d0df840f86e823d5b9b3b03b",
"timestamp": 1757082445
},
{
"id": "377bce2a8474071107464235b1c0f6408e8b7b77",
"author": {
"id": "did:key:z6MkqRSjDSV28GMdvhuGFwPvsgPHvU8qUf1gM5RymRLqghpk",
"alias": null
},
"description": "Rebase",
"base": "646d4360e7d905f0a9ec87d32c6768c5390eabf3",
"oid": "0cb40e011d720424b86c882fdd739c8590ab9622",
"timestamp": 1757575566
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "57506ca9-4f75-472e-b3e4-095bcefbe7bb"
},
"info_url": "https://cci.rad.levitte.org//57506ca9-4f75-472e-b3e4-095bcefbe7bb.html"
}
Started at: 2026-09-09 00:02:50.755107+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/57506ca9-4f75-472e-b3e4-095bcefbe7bb/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 39 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 0866819acc9414f83f2c57ff65917bc8b79bbfd2
✓ Switched to branch patch/0866819 at revision 0866819
✓ Branch patch/0866819 setup to track rad/patches/0866819acc9414f83f2c57ff65917bc8b79bbfd2
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 195bae992f9ac5adc8f5e0554443f0bda2489a48
HEAD is now at 195bae99 Bump nixpkgs to support latest version of clap
Exit code: 0
$ rad patch show 0866819acc9414f83f2c57ff65917bc8b79bbfd2 -p
╭───────────────────────────────────────────────────────────────────────────────────╮
│ Title Add clap for parsing cli args │
│ Patch 0866819acc9414f83f2c57ff65917bc8b79bbfd2 │
│ Author z6MkqRS…RLqghpk │
│ Head 195bae992f9ac5adc8f5e0554443f0bda2489a48 │
│ Base 54aacc96197a48b79fcc260f94312d824f5e0a34 │
│ Target master │
│ Branches patch/0866819 │
│ Commits ahead 8, behind 1531 │
│ Status archived │
│ │
│ Refactored the issue command using clap │
│ │
│ │
│ Set clap help_template │
│ │
│ │
│ cli: issue command shouldn't ask for passphrase on read-only operations │
│ │
│ │
│ cli: remove cli.rs and set default value for issue list --assigned │
│ │
│ │
│ cli: Add completion stubs for bash/zsh/fish │
│ │
│ │
│ Point clap fork dep to github.com/icetan instead of local path │
│ │
│ │
│ Bump nixpkgs to support latest version of clap │
├───────────────────────────────────────────────────────────────────────────────────┤
│ 195bae9 Bump nixpkgs to support latest version of clap │
│ 229d801 Point clap fork dep to github.com/icetan instead of local path │
│ 29a69ba cli: Add completion stubs for bash/zsh/fish │
│ 6986522 cli: remove cli.rs and set default value for issue list --assigned │
│ 06ff636 cli: issue command shouldn't ask for passphrase on read-only operations │
│ 56b64c9 Set clap help_template │
│ ef29498 Refactored the issue command using clap │
│ 92cbaa6 Add clap for parsing cli args │
├───────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 0866819 @ 54aacc9..195bae9 by z6MkqRS…RLqghpk 2 years ago │
│ ↑ Revision 31f6309 @ 064ece3..89de84f by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 391d272 @ 064ece3..8c8a203 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 80b0162 @ 064ece3..15c4e17 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 6aef9c7 @ 01bed73..62e4ff4 by lorenz z6MkkPv…WX5sTEz 1 year ago │
│ ↑ Revision b58ccab @ 01bed73..3c581d9 by matthiasbeyer z6MkwcU…RDWq1kL 1 year ago │
│ ↑ Revision c0dcf6b @ 01bed73..88b6728 by lorenz z6MkkPv…WX5sTEz 1 year ago │
│ ↑ Revision 48e2bcf @ 55cdd88..8f5f7b5 by lorenz z6MkkPv…WX5sTEz 1 year ago │
│ ↑ Revision 6a1599d @ 55cdd88..cb34d68 by matthiasbeyer z6MkwcU…RDWq1kL 1 year ago │
│ ↑ Revision 377bce2 @ 646d436..0cb40e0 by lorenz z6MkkPv…WX5sTEz 11 months ago │
╰───────────────────────────────────────────────────────────────────────────────────╯
commit 195bae992f9ac5adc8f5e0554443f0bda2489a48
Author: Christopher Fredén <me@icetan.org>
Date: Fri Mar 15 16:19:10 2024 +0100
Bump nixpkgs to support latest version of clap
diff --git a/flake.lock b/flake.lock
index 9e7bcddec2..98fae1778b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -56,16 +56,15 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1701961404,
- "narHash": "sha256-ieWHyh6kJtabQYUam/dXXi22MgcgLQvl+f2x/95n+us=",
+ "lastModified": 1710513883,
+ "narHash": "sha256-MKYur1pNCrk8bU7agCpMRi3dCCEbv3GwH3pS0XuM1Ps=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "8fef9eee026f0d95c06b5880ef9c1af0f643aadf",
+ "rev": "bda93c2221bc4185056723795c62e1b4cc661c4b",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
diff --git a/flake.nix b/flake.nix
index ace294ee65..7554f56e61 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
description = "Radicle";
inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/release-23.11";
+ nixpkgs.url = "github:NixOS/nixpkgs";
crane = {
url = "github:ipetkov/crane";
commit 229d8019b9f9dc1c3544cd90384bd5d4400d75ca
Author: Christopher Fredén <me@icetan.org>
Date: Tue Mar 12 14:08:45 2024 +0100
Point clap fork dep to github.com/icetan instead of local path
diff --git a/Cargo.lock b/Cargo.lock
index f2ccb8750f..8c9b990ad1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -529,6 +529,7 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.1"
+source = "git+https://github.com/icetan/clap.git?branch=propagate-template#362f08485f2d548fb5212e1a0b9c9632bbfb3ec1"
dependencies = [
"clap_builder",
"clap_derive",
@@ -537,6 +538,7 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.1"
+source = "git+https://github.com/icetan/clap.git?branch=propagate-template#362f08485f2d548fb5212e1a0b9c9632bbfb3ec1"
dependencies = [
"anstream",
"anstyle",
@@ -547,6 +549,7 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.1"
+source = "git+https://github.com/icetan/clap.git?branch=propagate-template#362f08485f2d548fb5212e1a0b9c9632bbfb3ec1"
dependencies = [
"clap",
"clap_lex",
@@ -559,6 +562,7 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.0"
+source = "git+https://github.com/icetan/clap.git?branch=propagate-template#362f08485f2d548fb5212e1a0b9c9632bbfb3ec1"
dependencies = [
"heck",
"proc-macro2",
@@ -569,6 +573,7 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.0"
+source = "git+https://github.com/icetan/clap.git?branch=propagate-template#362f08485f2d548fb5212e1a0b9c9632bbfb3ec1"
[[package]]
name = "colorchoice"
diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml
index d6b4b5e09b..652fb014ef 100644
--- a/radicle-cli/Cargo.toml
+++ b/radicle-cli/Cargo.toml
@@ -45,8 +45,8 @@ tree-sitter-bash = { version = "0.20" }
tree-sitter-go = { version = "0.20.0" }
tree-sitter-md = { version = "0.1.5" }
zeroize = { version = "1.1" }
-clap = { path = "/home/icetan/src/radicle/clap", features = ["derive"] }
-clap_complete = { path = "/home/icetan/src/radicle/clap/clap_complete", features = ["unstable-dynamic"] }
+clap = { git = "https://github.com/icetan/clap.git", branch = "propagate-template", features = ["derive"] }
+clap_complete = { git = "https://github.com/icetan/clap.git", branch = "propagate-template", features = ["unstable-dynamic"] }
[dependencies.radicle]
commit 29a69bad9e9c61b94d18cd8f1a6594edd818a967
Author: Christopher Fredén <me@icetan.org>
Date: Tue Mar 5 10:58:28 2024 +0100
cli: Add completion stubs for bash/zsh/fish
diff --git a/radicle-cli/completion/_rad.zsh b/radicle-cli/completion/_rad.zsh
new file mode 100644
index 0000000000..8aefe8399f
--- /dev/null
+++ b/radicle-cli/completion/_rad.zsh
@@ -0,0 +1,23 @@
+#compdef rad
+# ZSH suggestion shim for Radicle CLI
+
+# DEBUG:
+# echo "\n\n\nstate:\t'$state'"
+# echo "line:\t'$line'"
+# echo "words:\t$words"
+
+export _CLAP_COMPLETE_INDEX=$((${#words[@]} - 1))
+# export _CLAP_COMPLETE_COMP_TYPE=9
+# export _CLAP_COMPLETE_SPACE=false
+
+local completions=($(rad complete --shell bash -- $words "" 2>/dev/null))
+
+if [ -z "$completions" ]
+then
+ # If the completion list is empty, just continue with filename selection
+ # _arguments '*::arguments: _normal'
+ return
+fi
+
+# This is not a variable assignment, don't remove spaces!
+_values = "${(ps:\v:)completions}"
diff --git a/radicle-cli/completion/bash/_rad_bash_complete.sh b/radicle-cli/completion/bash/_rad_bash_complete.sh
new file mode 100644
index 0000000000..303be5a8ed
--- /dev/null
+++ b/radicle-cli/completion/bash/_rad_bash_complete.sh
@@ -0,0 +1,19 @@
+
+_clap_complete_rad() {
+ export IFS=$'\013'
+ export _CLAP_COMPLETE_INDEX=${COMP_CWORD}
+ export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE}
+ if compopt +o nospace 2> /dev/null; then
+ export _CLAP_COMPLETE_SPACE=false
+ else
+ export _CLAP_COMPLETE_SPACE=true
+ fi
+ COMPREPLY=( $("rad" complete --shell bash -- "${COMP_WORDS[@]}") )
+ if [[ $? != 0 ]]; then
+ unset COMPREPLY
+ elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
+ compopt -o nospace
+ fi
+}
+complete -o nospace -o bashdefault -F _clap_complete_rad rad
+
diff --git a/radicle-cli/completion/fish/_rad b/radicle-cli/completion/fish/_rad
new file mode 100644
index 0000000000..6c483eacbe
--- /dev/null
+++ b/radicle-cli/completion/fish/_rad
@@ -0,0 +1 @@
+complete -x -c rad -a "("'rad'" complete --shell fish -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
diff --git a/radicle-cli/completion/zsh/_rad b/radicle-cli/completion/zsh/_rad
new file mode 100644
index 0000000000..8c139da99a
--- /dev/null
+++ b/radicle-cli/completion/zsh/_rad
@@ -0,0 +1,24 @@
+#compdef rad
+# ZSH suggestion shim for Radicle CLI
+
+# DEBUG:
+# echo "\n\n\nstate:\t'$state'"
+# echo "line:\t'$line'"
+# echo "words:\t$words"
+
+export IFS=$'\013'
+export _CLAP_COMPLETE_INDEX=$((${#words[@]} - 1))
+# export _CLAP_COMPLETE_COMP_TYPE=9
+# export _CLAP_COMPLETE_SPACE=false
+
+local completions=($(rad complete --shell bash -- $words "" 2>/dev/null))
+
+if [ -z "$completions" ]
+then
+ # If the completion list is empty, just continue with filename selection
+ # _arguments '*::arguments: _normal'
+ return
+fi
+
+# This is not a variable assignment, don't remove spaces!
+_values = "${(ps:\v:)completions}"
commit 698652265a7a45959d8c2d3c7ddc1303361d3747
Author: Christopher Fredén <me@icetan.org>
Date: Mon Mar 4 08:14:14 2024 +0100
cli: remove cli.rs and set default value for issue list --assigned
diff --git a/radicle-cli/src/cli.rs b/radicle-cli/src/cli.rs
deleted file mode 100644
index 1bf0dea866..0000000000
--- a/radicle-cli/src/cli.rs
+++ /dev/null
@@ -1,91 +0,0 @@
-use std::ffi::OsString;
-
-use clap::{Command, FromArgMatches, Subcommand};
-use clap_complete::dynamic::shells::CompleteCommand;
-use radicle::identity::Did;
-use radicle::issue::Issues;
-use radicle::storage::{ReadRepository, ReadStorage};
-
-pub fn get_assignee_did_hints(input: &str) -> Option<Vec<String>> {
- let (_, rid) = radicle::rad::cwd().ok()?;
- radicle::Profile::load()
- .ok()
- .and_then(|profile| profile.storage.repository(rid).ok())
- .and_then(|repo| {
- Issues::open(&repo).ok().and_then(|issues| {
- issues
- .all()
- .map(|issues| {
- issues
- .flat_map(|issue| {
- issue.map_or(vec![], |(_, issue)| {
- issue.assignees().cloned().collect::<Vec<_>>()
- })
- })
- .filter_map(|did| {
- let did = did.to_human();
- did.starts_with(input).then(|| String::from(did))
- })
- .collect::<Vec<_>>()
- })
- .ok()
- })
- })
-}
-
-pub fn get_issue_id_hints(input: &str) -> Option<Vec<String>> {
- let (_, rid) = radicle::rad::cwd().ok()?;
- radicle::Profile::load()
- .ok()
- .and_then(|profile| profile.storage.repository(rid).ok())
- .and_then(|repo| {
- Issues::open(&repo).ok().and_then(|issues| {
- issues
- .all()
- .map(|issues| {
- issues
- .filter_map(|issue| {
- if let Ok((id, _)) = issue {
- let id = id.to_string();
- if id.starts_with(input) {
- return Some(String::from(id.split_at(8).0));
- }
- }
- None
- })
- .collect::<Vec<_>>()
- })
- .ok()
- })
- })
-}
-
-pub fn get_did_hints<R: ReadRepository + radicle::cob::Store>(input: &str) -> Option<Vec<String>> {
- let (_, rid) = radicle::rad::cwd().ok()?;
- radicle::Profile::load()
- .ok()
- .and_then(|profile| profile.storage.repository(rid).ok())
- .and_then(|repo| {
- repo.remote_ids()
- .map(|issues| {
- issues
- .filter_map(|id| {
- let id = id.map(|id| Did::from(id).to_human()).ok()?;
- id.starts_with(input).then_some(id)
- })
- .collect::<Vec<_>>()
- })
- .ok()
- })
-}
-
-pub fn completer(cmd: Command, args: Vec<OsString>) -> () {
- let mut cmd = CompleteCommand::augment_subcommands(cmd);
- let matches = cmd.clone().get_matches();
-
- if let Ok(completions) = CompleteCommand::from_arg_matches(&matches) {
- completions.complete(&mut cmd);
- } else {
- dbg!("COMPLETER FAILED: args: {:?}, matches: {:?}", args, matches);
- }
-}
diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs
index ff3a915820..d395ce3df9 100644
--- a/radicle-cli/src/commands/issue.rs
+++ b/radicle-cli/src/commands/issue.rs
@@ -1,6 +1,8 @@
#[path = "issue/cache.rs"]
mod cache;
+use std::str::FromStr;
+
use anyhow::Context as _;
use clap::{ArgGroup, Parser, Subcommand, ValueHint};
@@ -9,15 +11,17 @@ use radicle::cob::issue;
use radicle::cob::issue::{CloseReason, State};
use radicle::cob::thread;
use radicle::crypto::Signer;
+use radicle::identity::did::DidError;
+use radicle::identity::RepoId;
use radicle::issue::cache::Issues as _;
-use radicle::prelude::{Did, RepoId};
+use radicle::issue::Issues;
+use radicle::prelude::Did;
use radicle::profile;
-use radicle::storage;
+use radicle::storage::{self, ReadStorage as _};
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
use radicle::Profile;
use radicle::{cob, Node};
-use crate::cli::{get_assignee_did_hints, get_issue_id_hints};
use crate::git::Rev;
use crate::node;
use crate::terminal as term;
@@ -76,7 +80,7 @@ Options
};
/// Command line Peer argument.
-#[derive(Default, Debug, PartialEq, Eq)]
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub enum Assigned {
#[default]
Me,
@@ -90,15 +94,18 @@ pub struct IssueArgs {
/// Don't print anything
#[arg(short, long)]
+ #[clap(global = true)]
quiet: bool,
/// Don't announce issue to peers
#[arg(long)]
#[arg(value_name = "no-announce")]
+ #[clap(global = true)]
no_announce: bool,
/// Show only the issue header, hiding the comments
#[arg(long)]
+ #[clap(global = true)]
header: bool,
#[arg(long, short)]
@@ -236,10 +243,13 @@ enum IssueCommands {
#[derive(Parser, Debug)]
struct ListArgs {
- /// List issues assigned to <did>
+ /// List issues assigned to <did> (default: me)
#[clap(value_hint = ValueHint::Dynamic(get_assignee_did_hints))]
#[arg(long, name = "did")]
- assigned: Option<Did>,
+ #[arg(default_missing_value = "me")]
+ #[arg(num_args = 0..=1)]
+ #[arg(require_equals = true)]
+ assigned: Option<Assigned>,
/// List all issues (default)
#[arg(long, group = "state")]
@@ -278,6 +288,18 @@ struct StateArgs {
solved: bool,
}
+impl FromStr for Assigned {
+ type Err = DidError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s == "me" {
+ Ok(Assigned::Me)
+ } else {
+ let value = s.parse::<Did>()?;
+ Ok(Assigned::Peer(value))
+ }
+ }
+}
fn to_state_filter(args: ListArgs) -> Option<State> {
if args.open {
@@ -312,6 +334,143 @@ fn to_state(args: StateArgs) -> State {
}
}
+pub fn get_assignee_did_hints(input: &str) -> Option<Vec<String>> {
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ Issues::open(&repo).ok().and_then(|issues| {
+ issues
+ .all()
+ .map(|issues| {
+ issues
+ .flat_map(|issue| {
+ issue.map_or(vec![], |(_, issue)| {
+ issue.assignees().cloned().collect::<Vec<_>>()
+ })
+ })
+ .filter_map(|did| {
+ let did = did.to_human();
+ did.starts_with(input).then(|| String::from(did))
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
+ })
+ })
+}
+
+pub fn get_issue_id_hints(input: &str) -> Option<Vec<String>> {
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ Issues::open(&repo).ok().and_then(|issues| {
+ issues
+ .all()
+ .map(|issues| {
+ issues
+ .filter_map(|issue| {
+ if let Ok((id, _)) = issue {
+ let id = id.to_string();
+ if id.starts_with(input) {
+ return Some(String::from(id.split_at(8).0));
+ }
+ }
+ None
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
+ })
+ })
+}
+
+pub fn get_did_hints<R: ReadRepository + radicle::cob::Store>(input: &str) -> Option<Vec<String>> {
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ repo.remote_ids()
+ .map(|issues| {
+ issues
+ .filter_map(|id| {
+ let id = id.map(|id| Did::from(id).to_human()).ok()?;
+ id.starts_with(input).then_some(id)
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
+ })
+}
+
+// pub fn get_assignee_did_hints(input: &str) -> Option<Vec<String>> {
+// let profile = term::profile().ok()?;
+// let (_, rid) = radicle::rad::cwd().ok()?;
+// let repo = profile.storage.repository_mut(rid).ok()?;
+// let issues = Issues::open(&repo).ok()?;
+
+// let completions = issues
+// .all()
+// .ok()?
+// .flat_map(|issue| {
+// issue.map_or(vec![], |(_, issue)| {
+// issue.assignees().cloned().collect::<Vec<_>>()
+// })
+// })
+// .filter_map(|did| {
+// let did = did.to_human();
+// did.starts_with(input).then(|| String::from(did))
+// })
+// .collect::<Vec<_>>();
+
+// Some(completions)
+// }
+
+// pub fn get_issue_id_hints(input: &str) -> Option<Vec<String>> {
+// let profile = term::profile().ok()?;
+
+// let (_, rid) = radicle::rad::cwd().ok()?;
+// let repo = profile.storage.repository_mut(rid).ok()?;
+// let issues = Issues::open(&repo).ok()?;
+
+// let completions = issues
+// .all()
+// .ok()?
+// .filter_map(|issue| {
+// if let Ok((id, _)) = issue {
+// let id = id.to_string();
+// if id.starts_with(input) {
+// return Some(String::from(id.split_at(8).0));
+// }
+// }
+// None
+// })
+// .collect::<Vec<_>>();
+
+// Some(completions)
+// }
+
+// pub fn get_did_hints(input: &str) -> Option<Vec<String>> {
+// let profile = term::profile().ok()?;
+// let (_, rid) = radicle::rad::cwd().ok()?;
+// let repo = profile.storage.repository_mut(rid).ok()?;
+
+// let ids = repo
+// .remote_ids()
+// .ok()?
+// .filter_map(|id| {
+// let id = id.map(|id| Did::from(id).to_human()).ok()?;
+// id.starts_with(input).then_some(id)
+// })
+// .collect::<Vec<_>>();
+
+// Some(ids)
+// }
+
pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let rid = if let Some(rid) = args.repo {
@@ -353,7 +512,7 @@ pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
}
}
IssueCommands::List(list_args) => {
- let assigned = list_args.assigned.map(Assigned::Peer);
+ let assigned = list_args.assigned.clone();
let state = to_state_filter(list_args);
list(issues, &assigned, &state, &profile)?;
}
diff --git a/radicle-cli/src/lib.rs b/radicle-cli/src/lib.rs
index f71f7c9cb3..32019151d9 100644
--- a/radicle-cli/src/lib.rs
+++ b/radicle-cli/src/lib.rs
@@ -7,4 +7,3 @@ pub mod node;
pub mod pager;
pub mod project;
pub mod terminal;
-pub mod cli;
diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs
index 23c75957c2..7a1bf9c952 100644
--- a/radicle-cli/src/main.rs
+++ b/radicle-cli/src/main.rs
@@ -5,10 +5,10 @@ use std::{io::ErrorKind, iter, process};
use anyhow::anyhow;
use clap::builder::styling::Style;
use clap::builder::Styles;
-use clap::{CommandFactory, Parser, Subcommand};
+use clap::{CommandFactory, FromArgMatches as _, Parser, Subcommand};
+use clap_complete::dynamic::shells::CompleteCommand;
use radicle::version;
-use radicle_cli::cli;
use radicle_cli::commands::rad_issue;
use radicle_cli::commands::*;
use radicle_cli::terminal as term;
@@ -45,7 +45,6 @@ Usage: {usage}
#[command(help_template = HELP_TEMPLATE)]
#[command(propagate_version = true)]
#[command(propagate_help_template = true)]
-// #[command(styles = Styles::styled().usage(AnsiColor::Blue.on_default()))]
#[command(styles = Styles::plain().literal(Style::new().bold()))]
struct CliArgs {
#[command(subcommand)]
@@ -265,6 +264,8 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
);
}
"issue" => {
+ // Use clap instead to parse all CLI args and ignore `args` passed
+ // to `run_other`.
let args_ = CliArgs::parse();
if let Some(command) = args_.command {
match command {
@@ -274,12 +275,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
.map_err(|e| anyhow!(e))?,
)?,
}
- // If clap parsed a command short circuit.
- // return Ok(());
}
}
+ // Used for dynamic shell completion (not user facing)
"complete" => {
- cli::completer(CliArgs::command(), args.to_vec());
+ run_completer(CliArgs::command());
}
"ls" => {
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
@@ -393,3 +393,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
}
Ok(())
}
+
+/// Output shell completions
+fn run_completer(cmd: clap::Command) -> () {
+ let mut cmd = CompleteCommand::augment_subcommands(cmd);
+ let matches = cmd.clone().get_matches();
+
+ if let Ok(completions) = CompleteCommand::from_arg_matches(&matches) {
+ completions.complete(&mut cmd);
+ }
+}
commit 06ff636332fa846b750fdef56f247fcbf65f092d
Author: Christopher Fredén <me@icetan.org>
Date: Sat Mar 2 21:51:00 2024 +0100
cli: issue command shouldn't ask for passphrase on read-only operations
diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs
index 70b9030d12..ff3a915820 100644
--- a/radicle-cli/src/commands/issue.rs
+++ b/radicle-cli/src/commands/issue.rs
@@ -421,6 +421,7 @@ pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
let signer = term::signer(&profile)?;
let id = id.resolve(&repo.backend)?;
if let Ok(mut issue) = issues.get_mut(&id) {
+ let signer = term::signer(&profile)?;
let comment_id = comment_id.unwrap_or_else(|| {
let (comment_id, _) = term::io::comment_select(&issue).unwrap();
*comment_id
commit 56b64c9d3959f4ab5a364504b418c9b006479aba
Author: Christopher Fredén <me@icetan.org>
Date: Fri Mar 1 14:56:17 2024 +0100
Set clap help_template
diff --git a/radicle-cli/src/cli.rs b/radicle-cli/src/cli.rs
index 153de9ae5f..1bf0dea866 100644
--- a/radicle-cli/src/cli.rs
+++ b/radicle-cli/src/cli.rs
@@ -89,85 +89,3 @@ pub fn completer(cmd: Command, args: Vec<OsString>) -> () {
dbg!("COMPLETER FAILED: args: {:?}, matches: {:?}", args, matches);
}
}
-
-// pub fn to_issue_options() -> anyhow::Result<rad_issue::Options> {
-// let args = CliArgs::parse();
-
-// // Default to List command.
-// // let command = args.command.unwrap_or(Commands::Issue(IssueArgs {
-// // command: Some(IssueCommands::List(ListArgs {
-// // assigned: None,
-// // filter: ListFilter::All,
-// // })),
-// // header: false,
-// // quiet: false,
-// // no_announce: false,
-// // }));
-
-// let options = match args {
-// CliArgs { command } => match command {
-// Some(Commands::Issue(IssueArgs {
-// command: subcommand,
-// quiet,
-// no_announce,
-// header,
-// })) => match subcommand {
-// Some(IssueCommands::List(args)) => Some(rad_issue::Options {
-// op: rad_issue::Operation::List {
-// assigned: args
-// .assigned
-// .map(|did| {
-// let did =
-// term::args::did(&OsString::from(format!("did:key:{did}")))?;
-// Ok::<Option<Assigned>, anyhow::Error>(Some(Assigned::Peer(did)))
-// })
-// .unwrap_or(Ok(None))?,
-// state: match args.filter {
-// ListFilter::All => None,
-// ListFilter::Open => Some(radicle::cob::issue::State::Open),
-// ListFilter::Closed => Some(State::Closed {
-// reason: CloseReason::Other,
-// }),
-// ListFilter::Solved => Some(State::Closed {
-// reason: CloseReason::Solved,
-// }),
-// },
-// },
-// announce: !no_announce,
-// quiet: quiet,
-// }),
-// Some(IssueCommands::Show(args)) => Some(rad_issue::Options {
-// op: rad_issue::Operation::Show {
-// id: Rev::from(args.id),
-// format: if header {
-// term::issue::Format::Header
-// } else {
-// term::issue::Format::Full
-// },
-// debug: args.debug,
-// },
-// announce: !no_announce,
-// quiet: quiet,
-// }),
-// // Default `issue` subcommand is `list`.
-// _ => Some(rad_issue::Options {
-// op: rad_issue::Operation::List {
-// assigned: None,
-// state: None,
-// },
-// announce: false,
-// quiet: false,
-// }),
-// },
-// _ => None,
-// },
-// };
-
-// let Some(option) = options else {
-// CliArgs::command().write_help(&mut std::io::stdout())?;
-// println!();
-// process::exit(0);
-// };
-
-// Ok(option)
-// }
diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs
index e716ce51d1..70b9030d12 100644
--- a/radicle-cli/src/commands/issue.rs
+++ b/radicle-cli/src/commands/issue.rs
@@ -2,7 +2,7 @@
mod cache;
use anyhow::Context as _;
-use clap::{Parser, Subcommand, ValueHint};
+use clap::{ArgGroup, Parser, Subcommand, ValueHint};
use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue;
@@ -107,14 +107,14 @@ pub struct IssueArgs {
#[derive(Subcommand, Debug)]
enum IssueCommands {
- /// Delete an issue.
+ /// Delete an issue
Delete {
#[arg(value_name = "issue-id")]
#[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
id: Rev,
},
- /// Edit an issue.
+ /// Edit an issue
Edit {
#[arg(value_name = "issue-id")]
#[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
@@ -127,15 +127,10 @@ enum IssueCommands {
description: Option<String>,
},
- List {
- #[clap(value_hint = ValueHint::Dynamic(get_assignee_did_hints))]
- #[arg(long, name = "did")]
- assigned: Option<Did>,
-
- #[clap(value_enum, default_value_t=StateFilter::All)]
- state: StateFilter,
- },
+ /// List and filter issues
+ List(ListArgs),
+ /// Create a new issue
Open {
#[arg(long, short)]
title: Option<String>,
@@ -150,6 +145,7 @@ enum IssueCommands {
assignees: Vec<Did>,
},
+ /// Add a reaction emoji to an issue or comment
React {
#[arg(value_name = "issue-id")]
#[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
@@ -184,7 +180,7 @@ enum IssueCommands {
delete: Vec<Did>,
},
- /// Update lables on an issue.
+ /// Update labels on an issue
Label {
/// The issue to label.
#[arg(value_name = "issue-id")]
@@ -219,6 +215,7 @@ enum IssueCommands {
reply_to: Option<Rev>,
},
+ /// Show a specific issue
Show {
#[arg(value_name = "issue-id")]
#[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
@@ -229,57 +226,89 @@ enum IssueCommands {
debug: bool,
},
- State {
- #[arg(value_name = "issue-id")]
- #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
- id: Rev,
-
- #[clap(value_enum)]
- state: StateChoice,
- },
Cache {
#[arg(value_name = "issue-id")]
id: Option<Rev>,
},
+
+ State(StateArgs),
+}
+
+#[derive(Parser, Debug)]
+struct ListArgs {
+ /// List issues assigned to <did>
+ #[clap(value_hint = ValueHint::Dynamic(get_assignee_did_hints))]
+ #[arg(long, name = "did")]
+ assigned: Option<Did>,
+
+ /// List all issues (default)
+ #[arg(long, group = "state")]
+ all: bool,
+
+ /// List only open issues
+ #[arg(long, group = "state")]
+ open: bool,
+
+ /// List only closed issues
+ #[arg(long, group = "state")]
+ closed: bool,
+
+ /// List only solved issues
+ #[arg(long, group = "state")]
+ solved: bool,
}
-#[derive(clap::ValueEnum, Clone, Debug)]
-enum StateFilter {
- All,
- Closed,
- Open,
- Solved,
+#[derive(Parser, Debug)]
+#[clap(group(ArgGroup::new("state").required(true)))]
+struct StateArgs {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ id: Rev,
+
+ /// Set issue state to open
+ #[arg(long, short, group = "state")]
+ open: bool,
+
+ /// Set issue state to closed
+ #[arg(long, short, group = "state")]
+ closed: bool,
+
+ /// Set issue state to solved
+ #[arg(long, short, group = "state")]
+ solved: bool,
}
-fn to_state_filter(list_state: StateFilter) -> Option<State> {
- match list_state {
- StateFilter::All => None,
- StateFilter::Open => Some(radicle::cob::issue::State::Open),
- StateFilter::Closed => Some(State::Closed {
+
+fn to_state_filter(args: ListArgs) -> Option<State> {
+ if args.open {
+ Some(radicle::cob::issue::State::Open)
+ } else if args.closed {
+ Some(State::Closed {
reason: CloseReason::Other,
- }),
- StateFilter::Solved => Some(State::Closed {
+ })
+ } else if args.solved {
+ Some(State::Closed {
reason: CloseReason::Solved,
- }),
+ })
+ } else {
+ None
}
}
-#[derive(clap::ValueEnum, Clone, Debug)]
-enum StateChoice {
- Closed,
- Open,
- Solved,
-}
-
-fn to_state_choice(choice: StateChoice) -> State {
- match choice {
- StateChoice::Open => radicle::cob::issue::State::Open,
- StateChoice::Closed => State::Closed {
+fn to_state(args: StateArgs) -> State {
+ if args.open {
+ radicle::cob::issue::State::Open
+ } else if args.closed {
+ State::Closed {
reason: CloseReason::Other,
- },
- StateChoice::Solved => State::Closed {
+ }
+ } else if args.solved {
+ State::Closed {
reason: CloseReason::Solved,
- },
+ }
+ } else {
+ // FIXME:
+ unreachable!("State flag needed");
}
}
@@ -323,9 +352,9 @@ pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
term::issue::show(&issue, issue.id(), Format::Header, &profile)?;
}
}
- IssueCommands::List { assigned, state } => {
- let assigned = assigned.map(Assigned::Peer);
- let state = to_state_filter(state);
+ IssueCommands::List(list_args) => {
+ let assigned = list_args.assigned.map(Assigned::Peer);
+ let state = to_state_filter(list_args);
list(issues, &assigned, &state, &profile)?;
}
IssueCommands::Show { id, debug } => {
@@ -346,11 +375,11 @@ pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
term::issue::show(&issue, &id, format, &profile)?;
}
}
- IssueCommands::State { id, state } => {
+ IssueCommands::State(state_args) => {
let signer = term::signer(&profile)?;
- let id = id.resolve(&repo.backend)?;
+ let id = state_args.id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&id)?;
- issue.lifecycle(to_state_choice(state), &signer)?;
+ issue.lifecycle(to_state(state_args), &signer)?;
}
IssueCommands::Assign { id, add, delete } => {
let signer = term::signer(&profile)?;
diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs
index 2a78ddaa15..23c75957c2 100644
--- a/radicle-cli/src/main.rs
+++ b/radicle-cli/src/main.rs
@@ -3,6 +3,8 @@ use std::io::{self, Write};
use std::{io::ErrorKind, iter, process};
use anyhow::anyhow;
+use clap::builder::styling::Style;
+use clap::builder::Styles;
use clap::{CommandFactory, Parser, Subcommand};
use radicle::version;
@@ -14,6 +16,7 @@ use radicle_cli::terminal as term;
pub const NAME: &str = "rad";
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DESCRIPTION: &str = "Radicle command line interface";
+pub const LONG_DESCRIPTION: &str = "Radicle is a distributed GIT forge.";
pub const GIT_HEAD: &str = env!("GIT_HEAD");
pub const TIMESTAMP: &str = env!("GIT_COMMIT_TIME");
pub const VERSION: Version = Version {
@@ -22,10 +25,28 @@ pub const VERSION: Version = Version {
commit: GIT_HEAD,
timestamp: TIMESTAMP,
};
+pub const LONG_VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HEAD"), ")");
+pub const HELP_TEMPLATE: &str = r#"
+{before-help}{bin} {version}
+{about-with-newline}
+Usage: {usage}
+{all-args}
+{after-help}
+"#;
+
+/// Radicle command line interface
+///
+/// Radicle is a distributed GIT forge.
#[derive(Parser, Debug)]
-#[command(name = "rad")]
-#[command(about = "The rad CLI", long_about = None)]
+#[command(name = NAME)]
+#[command(version = VERSION)]
+#[command(long_version = LONG_VERSION)]
+#[command(help_template = HELP_TEMPLATE)]
+#[command(propagate_version = true)]
+#[command(propagate_help_template = true)]
+// #[command(styles = Styles::styled().usage(AnsiColor::Blue.on_default()))]
+#[command(styles = Styles::plain().literal(Style::new().bold()))]
struct CliArgs {
#[command(subcommand)]
pub command: Option<Commands>,
@@ -33,6 +54,9 @@ struct CliArgs {
#[derive(Subcommand, Debug)]
enum Commands {
+ /// Manage issues
+ ///
+ /// With issues you can organize your project and use it to discuss bugs and improvements.
Issue(rad_issue::IssueArgs),
}
commit ef2949832b1a684526bce50cfdfea5db260a7d28
Author: Christopher Fredén <me@icetan.org>
Date: Fri Mar 1 02:13:55 2024 +0100
Refactored the issue command using clap
diff --git a/Cargo.lock b/Cargo.lock
index 794b29c476..f2ccb8750f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -528,7 +528,7 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.0"
+version = "4.5.1"
dependencies = [
"clap_builder",
"clap_derive",
@@ -536,7 +536,7 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.0"
+version = "4.5.1"
dependencies = [
"anstream",
"anstyle",
@@ -546,7 +546,7 @@ dependencies = [
[[package]]
name = "clap_complete"
-version = "4.5.0"
+version = "4.5.1"
dependencies = [
"clap",
"clap_lex",
diff --git a/radicle-cli/src/cli.rs b/radicle-cli/src/cli.rs
index 48660e9463..153de9ae5f 100644
--- a/radicle-cli/src/cli.rs
+++ b/radicle-cli/src/cli.rs
@@ -1,69 +1,85 @@
use std::ffi::OsString;
-use anyhow::anyhow;
-use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint};
+use clap::{Command, FromArgMatches, Subcommand};
use clap_complete::dynamic::shells::CompleteCommand;
+use radicle::identity::Did;
use radicle::issue::Issues;
-use radicle::storage::WriteStorage;
+use radicle::storage::{ReadRepository, ReadStorage};
-use crate::terminal as term;
-use crate::commands::rad_issue;
-use crate::git::Rev;
-
-#[derive(Parser, Debug)]
-#[command(name = "rad")]
-#[command(about = "The rad CLI", long_about = None)]
-struct CliArgs {
- #[command(subcommand)]
- command: Option<Commands>,
-}
-
-#[derive(Subcommand, Debug)]
-enum Commands {
- Issue(IssueArgs),
-}
-
-#[derive(Parser, Debug)]
-struct IssueArgs {
- #[command(subcommand)]
- command: Option<IssueCommands>,
+pub fn get_assignee_did_hints(input: &str) -> Option<Vec<String>> {
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ Issues::open(&repo).ok().and_then(|issues| {
+ issues
+ .all()
+ .map(|issues| {
+ issues
+ .flat_map(|issue| {
+ issue.map_or(vec![], |(_, issue)| {
+ issue.assignees().cloned().collect::<Vec<_>>()
+ })
+ })
+ .filter_map(|did| {
+ let did = did.to_human();
+ did.starts_with(input).then(|| String::from(did))
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
+ })
+ })
}
-#[derive(Subcommand, Debug)]
-enum IssueCommands {
- List,
- Show {
- #[clap(value_hint = ISSUE_ID_HINT)]
- id: String,
- },
+pub fn get_issue_id_hints(input: &str) -> Option<Vec<String>> {
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ Issues::open(&repo).ok().and_then(|issues| {
+ issues
+ .all()
+ .map(|issues| {
+ issues
+ .filter_map(|issue| {
+ if let Ok((id, _)) = issue {
+ let id = id.to_string();
+ if id.starts_with(input) {
+ return Some(String::from(id.split_at(8).0));
+ }
+ }
+ None
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
+ })
+ })
}
-const ISSUE_ID_HINT: ValueHint = ValueHint::Dynamic(get_issue_ids);
-
-fn get_issue_ids(input: &str) -> Option<Vec<String>> {
- let profile = term::profile().ok()?;
-
+pub fn get_did_hints<R: ReadRepository + radicle::cob::Store>(input: &str) -> Option<Vec<String>> {
let (_, rid) = radicle::rad::cwd().ok()?;
- let repo = profile.storage.repository_mut(rid).ok()?;
- let issues = Issues::open(&repo).ok()?;
-
- let completions = issues.all().ok()?
- .filter_map(|issue| {
- if let Ok((id, _)) = issue {
- let id = id.to_string();
- if id.starts_with(input) {
- return Some(String::from(id.split_at(8).0));
- }
- }
- None
+ radicle::Profile::load()
+ .ok()
+ .and_then(|profile| profile.storage.repository(rid).ok())
+ .and_then(|repo| {
+ repo.remote_ids()
+ .map(|issues| {
+ issues
+ .filter_map(|id| {
+ let id = id.map(|id| Did::from(id).to_human()).ok()?;
+ id.starts_with(input).then_some(id)
+ })
+ .collect::<Vec<_>>()
+ })
+ .ok()
})
- .collect::<Vec<_>>();
-
- Some(completions)
}
-pub fn completer(args: Vec<OsString>) -> () {
- let cmd = CliArgs::command();
+pub fn completer(cmd: Command, args: Vec<OsString>) -> () {
let mut cmd = CompleteCommand::augment_subcommands(cmd);
let matches = cmd.clone().get_matches();
@@ -74,36 +90,84 @@ pub fn completer(args: Vec<OsString>) -> () {
}
}
-pub fn to_issue_options() -> anyhow::Result<rad_issue::Options> {
- let args = CliArgs::parse();
-
- let options = match args {
- CliArgs { command } => match command {
- Some(Commands::Issue(IssueArgs {
- command: subcommand,
- })) => match subcommand {
- Some(IssueCommands::List) => Some(rad_issue::Options {
- op: rad_issue::Operation::List {
- assigned: None,
- state: None,
- },
- announce: true,
- quiet: false,
- }),
- Some(IssueCommands::Show { id }) => Some(rad_issue::Options {
- op: rad_issue::Operation::Show {
- id: Rev::from(id),
- format: term::issue::Format::Full,
- debug: false,
- },
- announce: true,
- quiet: false,
- }),
- _ => None,
- },
- _ => None,
- },
- };
-
- options.ok_or(anyhow!("Command not implemented FIXME!"))
-}
+// pub fn to_issue_options() -> anyhow::Result<rad_issue::Options> {
+// let args = CliArgs::parse();
+
+// // Default to List command.
+// // let command = args.command.unwrap_or(Commands::Issue(IssueArgs {
+// // command: Some(IssueCommands::List(ListArgs {
+// // assigned: None,
+// // filter: ListFilter::All,
+// // })),
+// // header: false,
+// // quiet: false,
+// // no_announce: false,
+// // }));
+
+// let options = match args {
+// CliArgs { command } => match command {
+// Some(Commands::Issue(IssueArgs {
+// command: subcommand,
+// quiet,
+// no_announce,
+// header,
+// })) => match subcommand {
+// Some(IssueCommands::List(args)) => Some(rad_issue::Options {
+// op: rad_issue::Operation::List {
+// assigned: args
+// .assigned
+// .map(|did| {
+// let did =
+// term::args::did(&OsString::from(format!("did:key:{did}")))?;
+// Ok::<Option<Assigned>, anyhow::Error>(Some(Assigned::Peer(did)))
+// })
+// .unwrap_or(Ok(None))?,
+// state: match args.filter {
+// ListFilter::All => None,
+// ListFilter::Open => Some(radicle::cob::issue::State::Open),
+// ListFilter::Closed => Some(State::Closed {
+// reason: CloseReason::Other,
+// }),
+// ListFilter::Solved => Some(State::Closed {
+// reason: CloseReason::Solved,
+// }),
+// },
+// },
+// announce: !no_announce,
+// quiet: quiet,
+// }),
+// Some(IssueCommands::Show(args)) => Some(rad_issue::Options {
+// op: rad_issue::Operation::Show {
+// id: Rev::from(args.id),
+// format: if header {
+// term::issue::Format::Header
+// } else {
+// term::issue::Format::Full
+// },
+// debug: args.debug,
+// },
+// announce: !no_announce,
+// quiet: quiet,
+// }),
+// // Default `issue` subcommand is `list`.
+// _ => Some(rad_issue::Options {
+// op: rad_issue::Operation::List {
+// assigned: None,
+// state: None,
+// },
+// announce: false,
+// quiet: false,
+// }),
+// },
+// _ => None,
+// },
+// };
+
+// let Some(option) = options else {
+// CliArgs::command().write_help(&mut std::io::stdout())?;
+// println!();
+// process::exit(0);
+// };
+
+// Ok(option)
+// }
diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs
index 84d14450ca..e716ce51d1 100644
--- a/radicle-cli/src/commands/issue.rs
+++ b/radicle-cli/src/commands/issue.rs
@@ -1,11 +1,8 @@
#[path = "issue/cache.rs"]
mod cache;
-use std::collections::BTreeSet;
-use std::ffi::OsString;
-use std::str::FromStr;
-
-use anyhow::{anyhow, Context as _};
+use anyhow::Context as _;
+use clap::{Parser, Subcommand, ValueHint};
use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue;
@@ -20,10 +17,11 @@ use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
use radicle::Profile;
use radicle::{cob, Node};
+use crate::cli::{get_assignee_did_hints, get_issue_id_hints};
use crate::git::Rev;
use crate::node;
use crate::terminal as term;
-use crate::terminal::args::{Args, Error, Help};
+use crate::terminal::args::Help;
use crate::terminal::format::Author;
use crate::terminal::issue::Format;
use crate::terminal::patch::Message;
@@ -77,22 +75,6 @@ Options
"#,
};
-#[derive(Default, Debug, PartialEq, Eq)]
-pub enum OperationName {
- Assign,
- Edit,
- Open,
- Comment,
- Delete,
- Label,
- #[default]
- List,
- React,
- Show,
- State,
- Cache,
-}
-
/// Command line Peer argument.
#[derive(Default, Debug, PartialEq, Eq)]
pub enum Assigned {
@@ -101,500 +83,368 @@ pub enum Assigned {
Peer(Did),
}
-#[derive(Debug, PartialEq, Eq)]
-pub enum Operation {
+#[derive(Parser, Debug)]
+pub struct IssueArgs {
+ #[command(subcommand)]
+ command: Option<IssueCommands>,
+
+ /// Don't print anything
+ #[arg(short, long)]
+ quiet: bool,
+
+ /// Don't announce issue to peers
+ #[arg(long)]
+ #[arg(value_name = "no-announce")]
+ no_announce: bool,
+
+ /// Show only the issue header, hiding the comments
+ #[arg(long)]
+ header: bool,
+
+ #[arg(long, short)]
+ repo: Option<RepoId>,
+}
+
+#[derive(Subcommand, Debug)]
+enum IssueCommands {
+ /// Delete an issue.
+ Delete {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ id: Rev,
+ },
+
+ /// Edit an issue.
Edit {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
id: Rev,
+
+ #[arg(long, short)]
title: Option<String>,
+
+ #[arg(long, short)]
description: Option<String>,
},
+
+ List {
+ #[clap(value_hint = ValueHint::Dynamic(get_assignee_did_hints))]
+ #[arg(long, name = "did")]
+ assigned: Option<Did>,
+
+ #[clap(value_enum, default_value_t=StateFilter::All)]
+ state: StateFilter,
+ },
+
Open {
+ #[arg(long, short)]
title: Option<String>,
+
+ #[arg(long, short)]
description: Option<String>,
+
+ #[arg(long)]
labels: Vec<Label>,
+
+ #[arg(long)]
assignees: Vec<Did>,
},
- Show {
- id: Rev,
- format: Format,
- debug: bool,
- },
- Comment {
- id: Rev,
- message: Message,
- reply_to: Option<Rev>,
- },
- State {
- id: Rev,
- state: State,
- },
- Delete {
- id: Rev,
- },
+
React {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
id: Rev,
+
+ #[arg(long = "emoji")]
+ #[arg(value_name = "char")]
reaction: Reaction,
+
+ #[arg(long = "to")]
+ #[arg(value_name = "comment")]
+ // TODO: Add dynamic hint for comment ids
comment_id: Option<thread::CommentId>,
},
+
+ /// Manage assignees of an issue
Assign {
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ #[arg(value_name = "issue-id")]
id: Rev,
- opts: AssignOptions,
+
+ /// Add an assignee to the issue (may be specified multiple times).
+ #[arg(long, short)]
+ #[arg(value_name = "did")]
+ #[arg(action = clap::ArgAction::Append)]
+ add: Vec<Did>,
+
+ /// Delete an assignee from the issue (may be specified multiple times).
+ #[arg(long, short)]
+ #[arg(value_name = "did")]
+ #[arg(action = clap::ArgAction::Append)]
+ delete: Vec<Did>,
},
+
+ /// Update lables on an issue.
Label {
+ /// The issue to label.
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
id: Rev,
- opts: LabelOptions,
+
+ /// Add an assignee to the issue (may be specified multiple times).
+ #[arg(long, short)]
+ #[arg(value_name = "label")]
+ #[arg(action = clap::ArgAction::Append)]
+ add: Vec<Label>,
+
+ /// Delete an assignee from the issue (may be specified multiple times).
+ #[arg(long, short)]
+ #[arg(value_name = "label")]
+ #[arg(action = clap::ArgAction::Append)]
+ delete: Vec<Label>,
},
- List {
- assigned: Option<Assigned>,
- state: Option<State>,
+
+ /// Add a comment to an issue.
+ Comment {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ id: Rev,
+
+ /// Message text.
+ #[arg(long, short)]
+ #[arg(value_name = "message")]
+ message: Message,
+
+ #[arg(long, name = "comment-id")]
+ reply_to: Option<Rev>,
+ },
+
+ Show {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ id: Rev,
+
+ /// Show the issue as Rust debug output
+ #[arg(long)]
+ debug: bool,
+ },
+
+ State {
+ #[arg(value_name = "issue-id")]
+ #[clap(value_hint = ValueHint::Dynamic(get_issue_id_hints))]
+ id: Rev,
+
+ #[clap(value_enum)]
+ state: StateChoice,
},
Cache {
+ #[arg(value_name = "issue-id")]
id: Option<Rev>,
},
}
-#[derive(Debug, Default, PartialEq, Eq)]
-pub struct AssignOptions {
- pub add: BTreeSet<Did>,
- pub delete: BTreeSet<Did>,
+#[derive(clap::ValueEnum, Clone, Debug)]
+enum StateFilter {
+ All,
+ Closed,
+ Open,
+ Solved,
}
-#[derive(Debug, Default, PartialEq, Eq)]
-pub struct LabelOptions {
- pub add: BTreeSet<Label>,
- pub delete: BTreeSet<Label>,
+fn to_state_filter(list_state: StateFilter) -> Option<State> {
+ match list_state {
+ StateFilter::All => None,
+ StateFilter::Open => Some(radicle::cob::issue::State::Open),
+ StateFilter::Closed => Some(State::Closed {
+ reason: CloseReason::Other,
+ }),
+ StateFilter::Solved => Some(State::Closed {
+ reason: CloseReason::Solved,
+ }),
+ }
}
-#[derive(Debug)]
-pub struct Options {
- pub op: Operation,
- pub repo: Option<RepoId>,
- pub announce: bool,
- pub quiet: bool,
+#[derive(clap::ValueEnum, Clone, Debug)]
+enum StateChoice {
+ Closed,
+ Open,
+ Solved,
}
-impl Args for Options {
- fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
- use lexopt::prelude::*;
-
- let mut parser = lexopt::Parser::from_args(args);
- let mut op: Option<OperationName> = None;
- let mut id: Option<Rev> = None;
- let mut assigned: Option<Assigned> = None;
- let mut title: Option<String> = None;
- let mut reaction: Option<Reaction> = None;
- let mut comment_id: Option<thread::CommentId> = None;
- let mut description: Option<String> = None;
- let mut state: Option<State> = Some(State::Open);
- let mut labels = Vec::new();
- let mut assignees = Vec::new();
- let mut format = Format::default();
- let mut message = Message::default();
- let mut reply_to = None;
- let mut announce = true;
- let mut quiet = false;
- let mut debug = false;
- let mut assign_opts = AssignOptions::default();
- let mut label_opts = LabelOptions::default();
- let mut repo = None;
-
- while let Some(arg) = parser.next()? {
- match arg {
- Long("help") | Short('h') => {
- return Err(Error::Help.into());
- }
-
- // List options.
- Long("all") if op.is_none() || op == Some(OperationName::List) => {
- state = None;
- }
- Long("closed") if op.is_none() || op == Some(OperationName::List) => {
- state = Some(State::Closed {
- reason: CloseReason::Other,
- });
- }
- Long("open") if op.is_none() || op == Some(OperationName::List) => {
- state = Some(State::Open);
- }
- Long("solved") if op.is_none() || op == Some(OperationName::List) => {
- state = Some(State::Closed {
- reason: CloseReason::Solved,
- });
- }
-
- // Open options.
- Long("title") if op == Some(OperationName::Open) => {
- title = Some(parser.value()?.to_string_lossy().into());
- }
- Short('l') | Long("label") if matches!(op, Some(OperationName::Open)) => {
- let val = parser.value()?;
- let name = term::args::string(&val);
- let label = Label::new(name)?;
-
- labels.push(label);
- }
- Long("assign") if op == Some(OperationName::Open) => {
- let val = parser.value()?;
- let did = term::args::did(&val)?;
-
- assignees.push(did);
- }
- Long("description") if op == Some(OperationName::Open) => {
- description = Some(parser.value()?.to_string_lossy().into());
- }
-
- // State options.
- Long("closed") if op == Some(OperationName::State) => {
- state = Some(State::Closed {
- reason: CloseReason::Other,
- });
- }
- Long("open") if op == Some(OperationName::State) => {
- state = Some(State::Open);
- }
- Long("solved") if op == Some(OperationName::State) => {
- state = Some(State::Closed {
- reason: CloseReason::Solved,
- });
- }
-
- // React options.
- Long("emoji") if op == Some(OperationName::React) => {
- if let Some(emoji) = parser.value()?.to_str() {
- reaction =
- Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
- }
- }
- Long("to") if op == Some(OperationName::React) => {
- let oid: String = parser.value()?.to_string_lossy().into();
- comment_id = Some(oid.parse()?);
- }
-
- // Show options.
- Long("format") if op == Some(OperationName::Show) => {
- let val = parser.value()?;
- let val = term::args::string(&val);
-
- match val.as_str() {
- "header" => format = Format::Header,
- "full" => format = Format::Full,
- _ => anyhow::bail!("unknown format '{val}'"),
- }
- }
- Long("debug") if op == Some(OperationName::Show) => {
- debug = true;
- }
-
- // Comment options.
- Long("message") | Short('m') if op == Some(OperationName::Comment) => {
- let val = parser.value()?;
- let txt = term::args::string(&val);
-
- message.append(&txt);
- }
- Long("reply-to") if op == Some(OperationName::Comment) => {
- let val = parser.value()?;
- let rev = term::args::rev(&val)?;
-
- reply_to = Some(rev);
- }
-
- // Assign options
- Short('a') | Long("add") if op == Some(OperationName::Assign) => {
- assign_opts.add.insert(term::args::did(&parser.value()?)?);
- }
- Short('d') | Long("delete") if op == Some(OperationName::Assign) => {
- assign_opts
- .delete
- .insert(term::args::did(&parser.value()?)?);
- }
- Long("assigned") | Short('a') if assigned.is_none() => {
- if let Ok(val) = parser.value() {
- let peer = term::args::did(&val)?;
- assigned = Some(Assigned::Peer(peer));
- } else {
- assigned = Some(Assigned::Me);
- }
- }
-
- // Label options
- Short('a') | Long("add") if matches!(op, Some(OperationName::Label)) => {
- let val = parser.value()?;
- let name = term::args::string(&val);
- let label = Label::new(name)?;
-
- label_opts.add.insert(label);
- }
- Short('d') | Long("delete") if matches!(op, Some(OperationName::Label)) => {
- let val = parser.value()?;
- let name = term::args::string(&val);
- let label = Label::new(name)?;
-
- label_opts.delete.insert(label);
- }
-
- // Options.
- Long("no-announce") => {
- announce = false;
- }
- Long("quiet") | Short('q') => {
- quiet = true;
- }
- Long("repo") => {
- let val = parser.value()?;
- let rid = term::args::rid(&val)?;
-
- repo = Some(rid);
- }
-
- Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
- "c" | "comment" => op = Some(OperationName::Comment),
- "w" | "show" => op = Some(OperationName::Show),
- "d" | "delete" => op = Some(OperationName::Delete),
- "e" | "edit" => op = Some(OperationName::Edit),
- "l" | "list" => op = Some(OperationName::List),
- "o" | "open" => op = Some(OperationName::Open),
- "r" | "react" => op = Some(OperationName::React),
- "s" | "state" => op = Some(OperationName::State),
- "assign" => op = Some(OperationName::Assign),
- "label" => op = Some(OperationName::Label),
- "cache" => op = Some(OperationName::Cache),
-
- unknown => anyhow::bail!("unknown operation '{}'", unknown),
- },
- Value(val) if op.is_some() => {
- let val = term::args::rev(&val)?;
- id = Some(val);
- }
- _ => {
- return Err(anyhow!(arg.unexpected()));
- }
- }
- }
-
- let op = match op.unwrap_or_default() {
- OperationName::Edit => Operation::Edit {
- id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
- title,
- description,
- },
- OperationName::Open => Operation::Open {
- title,
- description,
- labels,
- assignees,
- },
- OperationName::Comment => Operation::Comment {
- id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
- message,
- reply_to,
- },
- OperationName::Show => Operation::Show {
- id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
- format,
- debug,
- },
- OperationName::State => Operation::State {
- id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
- state: state.ok_or_else(|| anyhow!("a state operation must be provided"))?,
- },
- OperationName::React => Operation::React {
- id: id.ok_or_else(|| anyhow!("an issue must be provided"))?,
- reaction: reaction.ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
- comment_id,
- },
- OperationName::Delete => Operation::Delete {
- id: id.ok_or_else(|| anyhow!("an issue to remove must be provided"))?,
- },
- OperationName::Assign => Operation::Assign {
- id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
- opts: assign_opts,
- },
- OperationName::Label => Operation::Label {
- id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
- opts: label_opts,
- },
- OperationName::List => Operation::List { assigned, state },
- OperationName::Cache => Operation::Cache { id },
- };
-
- Ok((
- Options {
- op,
- repo,
- announce,
- quiet,
- },
- vec![],
- ))
+fn to_state_choice(choice: StateChoice) -> State {
+ match choice {
+ StateChoice::Open => radicle::cob::issue::State::Open,
+ StateChoice::Closed => State::Closed {
+ reason: CloseReason::Other,
+ },
+ StateChoice::Solved => State::Closed {
+ reason: CloseReason::Solved,
+ },
}
}
-pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+pub fn run(args: IssueArgs, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
- let rid = if let Some(rid) = options.repo {
+ let rid = if let Some(rid) = args.repo {
rid
} else {
radicle::rad::cwd().map(|(_, rid)| rid)?
};
let repo = profile.storage.repository_mut(rid)?;
- let announce = options.announce
- && matches!(
- &options.op,
- Operation::Open { .. }
- | Operation::React { .. }
- | Operation::State { .. }
- | Operation::Delete { .. }
- | Operation::Assign { .. }
- | Operation::Label { .. }
- );
let mut issues = profile.issues_mut(&repo)?;
- match options.op {
- Operation::Edit {
- id,
- title,
- description,
- } => {
- let signer = term::signer(&profile)?;
- let issue = edit(&mut issues, &repo, id, title, description, &signer)?;
- if !options.quiet {
- term::issue::show(&issue, issue.id(), Format::Header, &profile)?;
+ if let Some(command) = args.command {
+ let announce = !args.no_announce
+ && matches!(
+ &command,
+ IssueCommands::Open { .. }
+ | IssueCommands::React { .. }
+ | IssueCommands::State { .. }
+ | IssueCommands::Delete { .. }
+ | IssueCommands::Assign { .. }
+ | IssueCommands::Label { .. }
+ );
+
+ match command {
+ IssueCommands::Delete { id } => {
+ let signer = term::signer(&profile)?;
+ let id = id.resolve(&repo.backend)?;
+ issues.remove(&id, &signer)?;
}
- }
- Operation::Open {
- title: Some(title),
- description: Some(description),
- labels,
- assignees,
- } => {
- let signer = term::signer(&profile)?;
- let issue = issues.create(title, description, &labels, &assignees, [], &signer)?;
- if !options.quiet {
- term::issue::show(&issue, issue.id(), Format::Header, &profile)?;
+ IssueCommands::Edit {
+ id,
+ title,
+ description,
+ } => {
+ let signer = term::signer(&profile)?;
+ let issue = edit(&mut issues, &repo, id, title, description, &signer)?;
+ if !args.quiet {
+ term::issue::show(&issue, issue.id(), Format::Header, &profile)?;
+ }
}
- }
- Operation::Comment {
- id,
- message,
- reply_to,
- } => {
- let signer = term::signer(&profile)?;
- let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
- let mut issue = issues.get_mut(&issue_id)?;
- let (body, reply_to) = prompt_comment(message, reply_to, &issue, &repo)?;
- let comment_id = issue.comment(body, reply_to, vec![], &signer)?;
-
- if options.quiet {
- term::print(comment_id);
- } else {
- let comment = issue.thread().comment(&comment_id).unwrap();
- term::comment::widget(&comment_id, comment, &profile).print();
+ IssueCommands::List { assigned, state } => {
+ let assigned = assigned.map(Assigned::Peer);
+ let state = to_state_filter(state);
+ list(issues, &assigned, &state, &profile)?;
}
- }
- Operation::Show { id, format, debug } => {
- let id = id.resolve(&repo.backend)?;
- let issue = issues
- .get(&id)?
- .context("No issue with the given ID exists")?;
- if debug {
- println!("{:#?}", issue);
- } else {
- term::issue::show(&issue, &id, format, &profile)?;
+ IssueCommands::Show { id, debug } => {
+ let format = if args.header {
+ term::issue::Format::Header
+ } else {
+ term::issue::Format::Full
+ };
+
+ let id = id.resolve(&repo.backend)?;
+
+ let issue = issues
+ .get(&id)?
+ .context("No issue with the given ID exists")?;
+ if debug {
+ println!("{:#?}", issue);
+ } else {
+ term::issue::show(&issue, &id, format, &profile)?;
+ }
}
- }
- Operation::State { id, state } => {
- let signer = term::signer(&profile)?;
- let id = id.resolve(&repo.backend)?;
- let mut issue = issues.get_mut(&id)?;
- issue.lifecycle(state, &signer)?;
- }
- Operation::React {
- id,
- reaction,
- comment_id,
- } => {
- let id = id.resolve(&repo.backend)?;
- if let Ok(mut issue) = issues.get_mut(&id) {
+ IssueCommands::State { id, state } => {
let signer = term::signer(&profile)?;
- let comment_id = comment_id.unwrap_or_else(|| {
- let (comment_id, _) = term::io::comment_select(&issue).unwrap();
- *comment_id
- });
- issue.react(comment_id, reaction, true, &signer)?;
+ let id = id.resolve(&repo.backend)?;
+ let mut issue = issues.get_mut(&id)?;
+ issue.lifecycle(to_state_choice(state), &signer)?;
+ }
+ IssueCommands::Assign { id, add, delete } => {
+ let signer = term::signer(&profile)?;
+ let id = id.resolve(&repo.backend)?;
+ let Ok(mut issue) = issues.get_mut(&id) else {
+ anyhow::bail!("Issue `{id}` not found");
+ };
+ let assignees = issue
+ .assignees()
+ .filter(|did| !delete.contains(did))
+ .chain(add.iter())
+ .cloned()
+ .collect::<Vec<_>>();
+ issue.assign(assignees, &signer)?;
+ }
+ IssueCommands::Comment {
+ id,
+ message,
+ reply_to,
+ } => {
+ let signer = term::signer(&profile)?;
+ let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
+ let mut issue = issues.get_mut(&issue_id)?;
+ let (body, reply_to) = prompt_comment(message, reply_to, &issue, &repo)?;
+ let comment_id = issue.comment(body, reply_to, vec![], &signer)?;
+
+ if args.quiet {
+ term::print(comment_id);
+ } else {
+ let comment = issue.thread().comment(&comment_id).unwrap();
+ term::comment::widget(&comment_id, comment, &profile).print();
+ }
+ }
+ IssueCommands::React {
+ id,
+ comment_id,
+ reaction,
+ } => {
+ let signer = term::signer(&profile)?;
+ let id = id.resolve(&repo.backend)?;
+ if let Ok(mut issue) = issues.get_mut(&id) {
+ let comment_id = comment_id.unwrap_or_else(|| {
+ let (comment_id, _) = term::io::comment_select(&issue).unwrap();
+ *comment_id
+ });
+ issue.react(comment_id, reaction, true, &signer)?;
+ }
+ }
+ IssueCommands::Label { id, add, delete } => {
+ let signer = term::signer(&profile)?;
+ let id = id.resolve(&repo.backend)?;
+ let Ok(mut issue) = issues.get_mut(&id) else {
+ anyhow::bail!("Issue `{id}` not found");
+ };
+ let labels = issue
+ .labels()
+ .filter(|did| !delete.contains(did))
+ .chain(add.iter())
+ .cloned()
+ .collect::<Vec<_>>();
+ issue.label(labels, &signer)?;
+ }
+ IssueCommands::Open {
+ ref title,
+ ref description,
+ ref labels,
+ ref assignees,
+ } => {
+ let signer = term::signer(&profile)?;
+ open(
+ title.clone(),
+ description.clone(),
+ labels.to_vec(),
+ assignees.to_vec(),
+ args.quiet,
+ &mut issues,
+ &signer,
+ &profile,
+ )?;
+ }
+ IssueCommands::Cache { id } => {
+ let id = id.map(|id| id.resolve(&repo.backend)).transpose()?;
+ cache::run(id, &repo, &profile)?;
}
}
- Operation::Open {
- ref title,
- ref description,
- ref labels,
- ref assignees,
- } => {
- let signer = term::signer(&profile)?;
- open(
- title.clone(),
- description.clone(),
- labels.to_vec(),
- assignees.to_vec(),
- &options,
- &mut issues,
- &signer,
- &profile,
- )?;
- }
- Operation::Assign {
- id,
- opts: AssignOptions { add, delete },
- } => {
- let signer = term::signer(&profile)?;
- let id = id.resolve(&repo.backend)?;
- let Ok(mut issue) = issues.get_mut(&id) else {
- anyhow::bail!("Issue `{id}` not found");
- };
- let assignees = issue
- .assignees()
- .filter(|did| !delete.contains(did))
- .chain(add.iter())
- .cloned()
- .collect::<Vec<_>>();
- issue.assign(assignees, &signer)?;
- }
- Operation::Label {
- id,
- opts: LabelOptions { add, delete },
- } => {
- let signer = term::signer(&profile)?;
- let id = id.resolve(&repo.backend)?;
- let Ok(mut issue) = issues.get_mut(&id) else {
- anyhow::bail!("Issue `{id}` not found");
- };
- let labels = issue
- .labels()
- .filter(|did| !delete.contains(did))
- .chain(add.iter())
- .cloned()
- .collect::<Vec<_>>();
- issue.label(labels, &signer)?;
- }
- Operation::List { assigned, state } => {
- list(issues, &assigned, &state, &profile)?;
- }
- Operation::Delete { id } => {
- let signer = term::signer(&profile)?;
- let id = id.resolve(&repo.backend)?;
- issues.remove(&id, &signer)?;
- }
- Operation::Cache { id } => {
- let id = id.map(|id| id.resolve(&repo.backend)).transpose()?;
- cache::run(id, &repo, &profile)?;
- }
- }
- if announce {
- let mut node = Node::new(profile.socket());
- node::announce(rid, &mut node)?;
- }
+ if announce {
+ let mut node = Node::new(profile.socket());
+ node::announce(rid, &mut node)?;
+ }
+ } else {
+ // Default `issue` subcommand is `list`.
+ list(issues, &None, &None, &profile)?;
+ };
Ok(())
}
@@ -710,7 +560,7 @@ fn open<R, G>(
description: Option<String>,
labels: Vec<Label>,
assignees: Vec<Did>,
- options: &Options,
+ quiet: bool,
cache: &mut issue::Cache<issue::Issues<'_, R>, cob::cache::StoreWriter>,
signer: &G,
profile: &Profile,
@@ -735,7 +585,7 @@ where
signer,
)?;
- if !options.quiet {
+ if !quiet {
term::issue::show(&issue, issue.id(), Format::Header, profile)?;
}
Ok(())
diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs
index 04cb1bae0d..2a78ddaa15 100644
--- a/radicle-cli/src/main.rs
+++ b/radicle-cli/src/main.rs
@@ -3,11 +3,13 @@ use std::io::{self, Write};
use std::{io::ErrorKind, iter, process};
use anyhow::anyhow;
+use clap::{CommandFactory, Parser, Subcommand};
-use radicle::version::Version;
+use radicle::version;
+use radicle_cli::cli;
+use radicle_cli::commands::rad_issue;
use radicle_cli::commands::*;
use radicle_cli::terminal as term;
-use radicle_cli::cli;
pub const NAME: &str = "rad";
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -21,6 +23,19 @@ pub const VERSION: Version = Version {
timestamp: TIMESTAMP,
};
+#[derive(Parser, Debug)]
+#[command(name = "rad")]
+#[command(about = "The rad CLI", long_about = None)]
+struct CliArgs {
+ #[command(subcommand)]
+ pub command: Option<Commands>,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ Issue(rad_issue::IssueArgs),
+}
+
#[derive(Debug)]
enum Command {
Other(Vec<OsString>),
@@ -226,11 +241,21 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
);
}
"issue" => {
- let options = cli::to_issue_options()?;
- rad_issue::run(options, term::profile()?)?;
+ let args_ = CliArgs::parse();
+ if let Some(command) = args_.command {
+ match command {
+ Commands::Issue(args_) => rad_issue::run(
+ args_,
+ radicle::Profile::load()
+ .map_err(|e| anyhow!(e))?,
+ )?,
+ }
+ // If clap parsed a command short circuit.
+ // return Ok(());
+ }
}
"complete" => {
- cli::completer(args.to_vec());
+ cli::completer(CliArgs::command(), args.to_vec());
}
"ls" => {
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
diff --git a/radicle-cli/src/terminal/patch.rs b/radicle-cli/src/terminal/patch.rs
index cb2efa24bc..0009eaad51 100644
--- a/radicle-cli/src/terminal/patch.rs
+++ b/radicle-cli/src/terminal/patch.rs
@@ -108,6 +108,14 @@ impl Message {
}
}
+impl From<String> for Message {
+ fn from(value: String) -> Self {
+ let mut message = Message::default();
+ message.append(&value);
+ message
+ }
+}
+
pub const PATCH_MSG: &str = r#"
<!--
Please enter a patch message for your changes. An empty
commit 92cbaa6d97ed7be579fc2e1da7ddff71595f117b
Author: Christopher Fredén <me@icetan.org>
Date: Fri Feb 23 09:43:32 2024 +0100
Add clap for parsing cli args
diff --git a/Cargo.lock b/Cargo.lock
index cc59c204a4..794b29c476 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -526,6 +526,50 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "clap"
+version = "4.5.0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.5.0"
+dependencies = [
+ "clap",
+ "clap_lex",
+ "is_executable",
+ "pathdiff",
+ "shlex",
+ "unicode-xid",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+
[[package]]
name = "colorchoice"
version = "1.0.0"
@@ -1424,6 +1468,12 @@ dependencies = [
"allocator-api2",
]
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
[[package]]
name = "hermit-abi"
version = "0.3.4"
@@ -1614,6 +1664,15 @@ dependencies = [
"popol",
]
+[[package]]
+name = "is_executable"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "itoa"
version = "1.0.10"
@@ -2062,6 +2121,12 @@ dependencies = [
"windows-targets 0.48.5",
]
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -2317,6 +2382,8 @@ version = "0.8.0"
dependencies = [
"anyhow",
"chrono",
+ "clap",
+ "clap_complete",
"git-ref-format",
"lexopt",
"localtime",
@@ -3103,6 +3170,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
[[package]]
name = "subtle"
version = "2.5.0"
@@ -3610,6 +3683,12 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
[[package]]
name = "universal-hash"
version = "0.5.1"
diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml
index 1a9e8b3dc3..d6b4b5e09b 100644
--- a/radicle-cli/Cargo.toml
+++ b/radicle-cli/Cargo.toml
@@ -45,6 +45,9 @@ tree-sitter-bash = { version = "0.20" }
tree-sitter-go = { version = "0.20.0" }
tree-sitter-md = { version = "0.1.5" }
zeroize = { version = "1.1" }
+clap = { path = "/home/icetan/src/radicle/clap", features = ["derive"] }
+clap_complete = { path = "/home/icetan/src/radicle/clap/clap_complete", features = ["unstable-dynamic"] }
+
[dependencies.radicle]
version = "0"
diff --git a/radicle-cli/src/cli.rs b/radicle-cli/src/cli.rs
new file mode 100644
index 0000000000..48660e9463
--- /dev/null
+++ b/radicle-cli/src/cli.rs
@@ -0,0 +1,109 @@
+use std::ffi::OsString;
+
+use anyhow::anyhow;
+use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint};
+use clap_complete::dynamic::shells::CompleteCommand;
+use radicle::issue::Issues;
+use radicle::storage::WriteStorage;
+
+use crate::terminal as term;
+use crate::commands::rad_issue;
+use crate::git::Rev;
+
+#[derive(Parser, Debug)]
+#[command(name = "rad")]
+#[command(about = "The rad CLI", long_about = None)]
+struct CliArgs {
+ #[command(subcommand)]
+ command: Option<Commands>,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ Issue(IssueArgs),
+}
+
+#[derive(Parser, Debug)]
+struct IssueArgs {
+ #[command(subcommand)]
+ command: Option<IssueCommands>,
+}
+
+#[derive(Subcommand, Debug)]
+enum IssueCommands {
+ List,
+ Show {
+ #[clap(value_hint = ISSUE_ID_HINT)]
+ id: String,
+ },
+}
+
+const ISSUE_ID_HINT: ValueHint = ValueHint::Dynamic(get_issue_ids);
+
+fn get_issue_ids(input: &str) -> Option<Vec<String>> {
+ let profile = term::profile().ok()?;
+
+ let (_, rid) = radicle::rad::cwd().ok()?;
+ let repo = profile.storage.repository_mut(rid).ok()?;
+ let issues = Issues::open(&repo).ok()?;
+
+ let completions = issues.all().ok()?
+ .filter_map(|issue| {
+ if let Ok((id, _)) = issue {
+ let id = id.to_string();
+ if id.starts_with(input) {
+ return Some(String::from(id.split_at(8).0));
+ }
+ }
+ None
+ })
+ .collect::<Vec<_>>();
+
+ Some(completions)
+}
+
+pub fn completer(args: Vec<OsString>) -> () {
+ let cmd = CliArgs::command();
+ let mut cmd = CompleteCommand::augment_subcommands(cmd);
+ let matches = cmd.clone().get_matches();
+
+ if let Ok(completions) = CompleteCommand::from_arg_matches(&matches) {
+ completions.complete(&mut cmd);
+ } else {
+ dbg!("COMPLETER FAILED: args: {:?}, matches: {:?}", args, matches);
+ }
+}
+
+pub fn to_issue_options() -> anyhow::Result<rad_issue::Options> {
+ let args = CliArgs::parse();
+
+ let options = match args {
+ CliArgs { command } => match command {
+ Some(Commands::Issue(IssueArgs {
+ command: subcommand,
+ })) => match subcommand {
+ Some(IssueCommands::List) => Some(rad_issue::Options {
+ op: rad_issue::Operation::List {
+ assigned: None,
+ state: None,
+ },
+ announce: true,
+ quiet: false,
+ }),
+ Some(IssueCommands::Show { id }) => Some(rad_issue::Options {
+ op: rad_issue::Operation::Show {
+ id: Rev::from(id),
+ format: term::issue::Format::Full,
+ debug: false,
+ },
+ announce: true,
+ quiet: false,
+ }),
+ _ => None,
+ },
+ _ => None,
+ },
+ };
+
+ options.ok_or(anyhow!("Command not implemented FIXME!"))
+}
diff --git a/radicle-cli/src/lib.rs b/radicle-cli/src/lib.rs
index 32019151d9..f71f7c9cb3 100644
--- a/radicle-cli/src/lib.rs
+++ b/radicle-cli/src/lib.rs
@@ -7,3 +7,4 @@ pub mod node;
pub mod pager;
pub mod project;
pub mod terminal;
+pub mod cli;
diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs
index ec843acc19..04cb1bae0d 100644
--- a/radicle-cli/src/main.rs
+++ b/radicle-cli/src/main.rs
@@ -7,6 +7,7 @@ use anyhow::anyhow;
use radicle::version::Version;
use radicle_cli::commands::*;
use radicle_cli::terminal as term;
+use radicle_cli::cli;
pub const NAME: &str = "rad";
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -225,11 +226,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
);
}
"issue" => {
- term::run_command_args::<rad_issue::Options, _>(
- rad_issue::HELP,
- rad_issue::run,
- args.to_vec(),
- );
+ let options = cli::to_issue_options()?;
+ rad_issue::run(options, term::profile()?)?;
+ }
+ "complete" => {
+ cli::completer(args.to_vec());
}
"ls" => {
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 57506ca9-4f75-472e-b3e4-095bcefbe7bb -v /opt/radcis/ci.rad.levitte.org/cci/state/57506ca9-4f75-472e-b3e4-095bcefbe7bb/s:/57506ca9-4f75-472e-b3e4-095bcefbe7bb/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/57506ca9-4f75-472e-b3e4-095bcefbe7bb/w:/57506ca9-4f75-472e-b3e4-095bcefbe7bb/w -w /57506ca9-4f75-472e-b3e4-095bcefbe7bb/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /57506ca9-4f75-472e-b3e4-095bcefbe7bb/s/script.sh
+ cargo --version
info: syncing channel updates for '1.74-x86_64-unknown-linux-gnu'
info: latest update on 2023-12-07, rust version 1.74.1 (a28077b28 2023-12-04)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.74.1 (ecb9851af 2023-10-18)
+ rustc --version
rustc 1.74.1 (a28077b28 2023-12-04)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.74-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.74-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}