From 3a965ac399d57d924138bdf5a8e6fe0252cf82ac Mon Sep 17 00:00:00 2001 From: jchrist Date: Tue, 24 Dec 2024 16:37:33 +0200 Subject: [PATCH] feat: add support for missing CLI functions through native FFI calls to radicle crate Signed-off-by: jchrist --- .github/workflows/build.yml | 25 + .github/workflows/end-to-end-tests.yml | 6 +- .gitignore | 4 +- build.gradle.kts | 3 + jrad/Cargo.lock | 1937 +++++++++++++++++ jrad/Cargo.toml | 21 + jrad/src/lib.rs | 476 ++++ .../actions/rad/RadCobShow.java | 4 +- .../config/RadicleSettingsView.java | 2 +- .../dialog/clone/CloneUtil.java | 6 +- .../issues/IssueListPanel.java | 14 +- .../issues/IssuePanel.java | 13 +- .../issues/IssueSearchPanelViewModel.java | 11 +- .../issues/overview/IssueComponent.java | 45 +- .../models/RadAuthor.java | 34 +- .../models/RadDiscussion.java | 9 +- .../models/RadPatch.java | 31 +- .../patches/PatchListPanel.java | 16 +- .../patches/PatchTabController.java | 4 +- .../PatchDiffEditorComponentsFactory.java | 5 +- .../PatchReviewThreadComponentFactory.java | 78 +- .../patches/timeline/TimelineComponent.java | 21 +- .../timeline/TimelineComponentFactory.java | 56 +- .../services/FileService.java | 9 +- .../services/RadicleCliService.java | 95 +- .../services/RadicleNativeService.java | 372 ++++ .../services/RadicleProjectService.java | 16 +- .../services/auth/AuthService.java | 2 +- .../toolwindow/DragAndDropField.java | 4 +- .../toolwindow/EmojiPanel.java | 23 +- .../toolwindow/MarkDownEditorPaneFactory.java | 89 +- .../toolwindow/SearchViewModelBase.java | 3 + .../toolwindow/Utils.java | 37 +- src/main/resources/META-INF/plugin.xml | 1 + .../messages/RadicleBundle.properties | 9 +- .../radiclejetbrainsplugin/AbstractIT.java | 106 +- .../radiclejetbrainsplugin/ActionsTest.java | 8 +- .../radiclejetbrainsplugin/RadStub.java | 38 +- .../config/RadicleSettingsViewTest.java | 6 +- .../dialog/clone/RepositoryTest.java | 2 +- .../issues/IssueListPanelTest.java | 69 +- .../issues/OverviewTest.java | 144 +- .../models/RadPatchTest.java | 124 ++ .../patches/PatchListPanelTest.java | 87 +- .../patches/PatchProposalPanelTest.java | 13 +- .../patches/TimelineTest.java | 220 +- 46 files changed, 3704 insertions(+), 594 deletions(-) create mode 100644 jrad/Cargo.lock create mode 100644 jrad/Cargo.toml create mode 100644 jrad/src/lib.rs create mode 100644 src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleNativeService.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18563310..fa3b95fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,3 +76,28 @@ jobs: with: name: tests-result path: ${{ github.workspace }}/build/reports/tests + buildNativeLib: + name: BuildNativeLib + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + - os: macos-latest + # radicle cannot be compiled on Windows + # - os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - name: Fetch Sources + uses: actions/checkout@v4 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + - name: Lint + working-directory: ./jrad + run: | + cargo fmt --all --check + cargo clippy --all --tests + - name: Build + working-directory: ./jrad + run: | + cargo build --release \ No newline at end of file diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index a88be183..3f34be91 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -144,7 +144,11 @@ jobs: - name: Copy logs if: ${{ failure() }} working-directory: ${{ github.workspace }}/radicle-jetbrains-plugin - run: mv build/idea-sandbox/system/log/ build/reports + run: | + # make a loop to iterate over the wildcard in the directory, as we don't want to hardcode the ij version + for f in ./build/idea-sandbox/*/log/idea.log; do + cp "$f" ./build/reports/idea.log + done - name: Save fails report if: ${{ failure() }} uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 325946d5..4ccb2cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ build video /out/ -.intellijPlatform/ \ No newline at end of file +.intellijPlatform/ +*.iml +target/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c05996d2..d0cfec61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,6 +60,9 @@ dependencies { implementation("org.apache.tika:tika-core:${libs.versions.tika.get()}") implementation("com.automation-remarks:video-recorder-junit5:2.+") implementation("com.sshtools:maverick-synergy-client:${libs.versions.sshTools.get()}") + + // java -> rust + implementation("com.github.jnr:jnr-ffi:2.2.17") } checkstyle { diff --git a/jrad/Cargo.lock b/jrad/Cargo.lock new file mode 100644 index 00000000..04dd2147 --- /dev/null +++ b/jrad/Cargo.lock @@ -0,0 +1,1937 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "amplify" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "448cf0c3afc71439b5f837aac5399a1ef2b223f5f38324dbfb4343deec3b80cc" +dependencies = [ + "amplify_derive", + "amplify_num", + "ascii", + "wasm-bindgen", +] + +[[package]] +name = "amplify_derive" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a6309e6b8d89b36b9f959b7a8fa093583b94922a0f6438a24fb08936de4d428" +dependencies = [ + "amplify_syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "amplify_num" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99bcb75a2982047f733547042fc3968c0f460dfcf7d90b90dea3b2744580e9ad" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "amplify_syn" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ct-codecs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cypheraddr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5c54d2ad4ab9941383519471b75d12abc1a7b4779265e233168f2703a730d9" +dependencies = [ + "amplify", + "base32", + "cyphergraphy", + "sha3", +] + +[[package]] +name = "cyphergraphy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67c16c8ef5ddcdab57aab83fd8e770540ea3682ccdae09642c63575b0da2184" +dependencies = [ + "amplify", + "ec25519", +] + +[[package]] +name = "cyphernet" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac949369884a7a1d802cc669821269c707be8cec4d65043382e253733d2e62e1" +dependencies = [ + "cypheraddr", + "cyphergraphy", + "socks5-client", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "data-encoding-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "ec25519" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfd533a2fc01178c738c99412ae1f7e1ad2cb37c2e14bfd87e9d4618171c825" +dependencies = [ + "ct-codecs", + "ed25519", + "getrandom", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "git-ref-format" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7428e0d6e549a9a613d6f019b839a0f5142c331295b79e119ca8f4faac145da1" +dependencies = [ + "git-ref-format-core", + "git-ref-format-macro", +] + +[[package]] +name = "git-ref-format-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaeb9672a55e9e32cb6d3ef781e7526b25ab97d499fae71615649340b143424" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "git-ref-format-macro" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6ca5353accc201f6324dff744ba4660099546d4daf187ba868f07562e36ca4" +dependencies = [ + "git-ref-format-core", + "proc-macro-error", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jrad" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "radicle", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "localtime" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016a009e0bb8ba6e3229fb74bf11a8fe6ef24542cc6ef35ef38863ac13f96d87" +dependencies = [ + "serde", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "nonempty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995defdca0a589acfdd1bd2e8e3b896b4d4f7675a31fd14c32611440c7f608e6" +dependencies = [ + "serde", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qcheck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b439bd4242da51d62d18c95e6a6add749346756b0d1a587dfd0cc22fa6b5f3f0" +dependencies = [ + "rand", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radicle" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd823aeed3ffe73eb82a213e62cb3811f9bdf453844d6e0b14684e0757fb389b" +dependencies = [ + "amplify", + "base64 0.21.7", + "crossbeam-channel", + "cyphernet", + "fastrand", + "git2", + "libc", + "localtime", + "log", + "multibase", + "nonempty", + "once_cell", + "qcheck", + "radicle-cob", + "radicle-crypto", + "radicle-git-ext", + "radicle-ssh", + "serde", + "serde_json", + "siphasher", + "sqlite", + "tempfile", + "thiserror", + "unicode-normalization", +] + +[[package]] +name = "radicle-cob" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90581a9508ccc310998e991d7acf139d2991297d3fb37d30de07536e10256afb" +dependencies = [ + "fastrand", + "git2", + "log", + "nonempty", + "once_cell", + "radicle-crypto", + "radicle-dag", + "radicle-git-ext", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "radicle-crypto" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d6a67969719841ad06049597006368eb4238ca63a02d20207654dfd1d2d6ad" +dependencies = [ + "amplify", + "cyphernet", + "ec25519", + "fastrand", + "multibase", + "qcheck", + "radicle-git-ext", + "radicle-ssh", + "serde", + "sqlite", + "ssh-key", + "thiserror", + "zeroize", +] + +[[package]] +name = "radicle-dag" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb41c7e10ada3a4df960190a96bfb4af56d33ada890f917acc8e3b122b614875" +dependencies = [ + "fastrand", +] + +[[package]] +name = "radicle-git-ext" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b78c26e67d1712ad5a0c602ae3b236609461372ac04e200bda359fe4a1c6650" +dependencies = [ + "git-ref-format", + "git2", + "percent-encoding", + "radicle-std-ext", + "serde", + "thiserror", +] + +[[package]] +name = "radicle-ssh" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee758010fb64482be4b18591fbeb3cbc15b16450d143edf4edb5484c7366c6" +dependencies = [ + "byteorder", + "log", + "thiserror", + "zeroize", +] + +[[package]] +name = "radicle-std-ext" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db20136bbc9ae63f3fec8e5a6c369f4902fac2244501b5dfc6d668e43475aaa4" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature 2.2.0", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socks5-client" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc7dcf6fab1d65d82d633006a4cc658d76ce436e01cf1a7c71873c0eeba324c" +dependencies = [ + "amplify", + "cypheraddr", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03801c10193857d6a4a71ec46cee198a15cbc659622aabe1db0d0bdbefbcf8e6" +dependencies = [ + "libc", + "sqlite3-sys", +] + +[[package]] +name = "sqlite3-src" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc95a51a1ee38839599371685b9d4a926abb51791f0bc3bf8c3bb7867e6e454" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "sqlite3-sys" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2752c669433e40ebb08fde824146f50d9628aa0b66a3b7fc6be34db82a8063b" +dependencies = [ + "libc", + "sqlite3-src", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "bcrypt-pbkdf", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha2", + "signature 2.2.0", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.91", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] diff --git a/jrad/Cargo.toml b/jrad/Cargo.toml new file mode 100644 index 00000000..7fbac36a --- /dev/null +++ b/jrad/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "jrad" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +radicle = "^0.14.0" +anyhow = { version = "^1.0.95" } +serde = { version = "1.0.217", features = ["derive"] } +serde_json = { version = "^1.0.135", features = ["preserve_order"] } +base64 = { version = "^0.22.1" } +#jni = "^0.21.1" +#j4rs = "^0.21.0" +#j4rs_derive = "^0.1.1" + +[dev-dependencies] +radicle = { version = "^0.14.0", features = ["test"] } +tempfile = { version = "^3.15.0" } \ No newline at end of file diff --git a/jrad/src/lib.rs b/jrad/src/lib.rs new file mode 100644 index 00000000..d83594b1 --- /dev/null +++ b/jrad/src/lib.rs @@ -0,0 +1,476 @@ +use anyhow::anyhow; +use base64::Engine; +use radicle::cob::thread::CommentId; +use radicle::cob::{CodeLocation, DataUri, Embed, Reaction, Uri}; +use radicle::git::raw::IntoCString; +use radicle::git::Oid; +use radicle::node::AliasStore; +use radicle::patch::RevisionId; +use radicle::prelude::RepoId; +use radicle::storage::git::Repository; +use radicle::storage::ReadStorage; +use radicle::Profile; +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::HashMap; +use std::ffi::c_char; +use std::str::FromStr; + +#[no_mangle] +pub extern "system" fn radHome(_inp: *const c_char) -> *const c_char { + // let input_res = read_input(inp); + let p = Profile::load().unwrap(); + let result = p.home.path().to_str().unwrap(); + construct_result(String::from(result)) +} + +#[no_mangle] +pub extern "system" fn changeIssueTitleDescription(inp: *const c_char) -> *const c_char { + let result = handle_change_issue_title_description(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn editIssueComment(inp: *const c_char) -> *const c_char { + let result = handle_edit_issue_comment(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn getEmbeds(inp: *const c_char) -> *const c_char { + let result = handle_get_embeds(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn getAlias(inp: *const c_char) -> *const c_char { + let result = handle_get_alias(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn createPatchComment(inp: *const c_char) -> *const c_char { + let result = handle_create_patch_comment(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn editPatchComment(inp: *const c_char) -> *const c_char { + let result = handle_edit_patch_comment(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn deletePatchComment(inp: *const c_char) -> *const c_char { + let result = handle_delete_patch_comment(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn patchCommentReact(inp: *const c_char) -> *const c_char { + let result = handle_patch_comment_react(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[no_mangle] +pub extern "system" fn issueCommentReact(inp: *const c_char) -> *const c_char { + let result = handle_issue_comment_react(inp); + match result { + Ok(st) => construct_result(st), + Err(e) => construct_error_result(e), + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct ChangeIssueTitleDesc { + repo_id: RepoId, + issue_id: Oid, + title: String, + description: String, + embeds: Vec, +} +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ContentEmbed { + pub oid: Oid, + pub name: String, + pub content: String, +} +pub fn handle_change_issue_title_description(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: ChangeIssueTitleDesc = serde_json::from_str(&input)?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let signer = p.signer()?; + + let mut issues = p.issues_mut(&repo)?; + let mut issue = issues.get_mut(&req.issue_id.into())?; + + let _er = issue.edit(req.title, &signer)?; + if !req.description.is_empty() { + let embeds = resolve_embeds(&repo, req.embeds); + let _er2 = issue.edit_description(req.description, embeds, &signer)?; + } + + Ok(String::from("{\"ok\": true}")) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct EmbedReq { + pub repo_id: RepoId, + pub oids: Vec, +} +pub fn handle_get_embeds(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: EmbedReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut res_map = HashMap::::new(); + for oid in req.oids { + let blob = repo.backend.find_blob(oid.into()); + if blob.is_err() { + continue; + } + let blob = blob?; + let bytes = blob.content(); + let enc = base64::engine::general_purpose::STANDARD.encode(bytes); + res_map.insert(oid.to_string(), enc); + } + let res_map_json = serde_json::to_string(&res_map)?; + let result = format!("{{\"ok\": true, \"result\":{res_map_json}}}"); + Ok(result) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AliasReq { + pub ids: Vec, +} +pub fn handle_get_alias(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: AliasReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + + let mut map = HashMap::::new(); + for mut id in req.ids { + if id.starts_with("did:key:") { + id = id.replace("did:key:", ""); + } + let nid = id.clone().as_str().parse(); + if nid.is_err() { + continue; + } + let alias = p.aliases().alias(&nid?); + if alias.is_some() { + let alias = alias.unwrap(); + map.insert(id, alias.to_string()); + } + } + let res_map_json = serde_json::to_string(&map)?; + let result = format!("{{\"ok\": true, \"result\":{res_map_json}}}"); + Ok(result) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PatchCommentReq { + pub repo_id: RepoId, + pub patch_id: Oid, + pub revision_id: RevisionId, + #[serde(deserialize_with = "ok_or_none")] + #[serde(default)] + pub comment_id: Option, // required when editing + pub comment: String, + #[serde(deserialize_with = "ok_or_none")] + #[serde(default)] + pub reply_to: Option, + #[serde(deserialize_with = "ok_or_none")] + #[serde(default)] + pub location: Option, + pub embeds: Vec, +} +pub fn handle_create_patch_comment(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: PatchCommentReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut patches = p.patches_mut(&repo)?; + let mut patch = patches.get_mut(&req.patch_id.into())?; + let embeds = resolve_embeds(&repo, req.embeds); + let cid = patch.comment( + req.revision_id, + req.comment, + req.reply_to, + req.location, + embeds, + &p.signer()?, + )?; + let result = format!("{{\"ok\": true, \"result\":\"{cid}\"}}"); + Ok(result) +} +pub fn handle_edit_patch_comment(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: PatchCommentReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut patches = p.patches_mut(&repo)?; + let mut patch = patches.get_mut(&req.patch_id.into())?; + let embeds = resolve_embeds(&repo, req.embeds); + let comment_id = req + .comment_id + .ok_or_else(|| anyhow!("missing comment id"))?; + let cid = patch.comment_edit( + req.revision_id, + comment_id, + req.comment, + embeds, + &p.signer()?, + )?; + let result = format!("{{\"ok\": true, \"result\":\"{cid}\"}}"); + Ok(result) +} +pub fn handle_delete_patch_comment(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: PatchCommentReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut patches = p.patches_mut(&repo)?; + let mut patch = patches.get_mut(&req.patch_id.into())?; + let comment_id = req + .comment_id + .ok_or_else(|| anyhow!("missing comment id"))?; + let cid = patch.comment_redact(req.revision_id, comment_id, &p.signer()?)?; + let result = format!("{{\"ok\": true, \"result\":\"{cid}\"}}"); + Ok(result) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IssueCommentReq { + pub repo_id: RepoId, + pub issue_id: Oid, + #[serde(deserialize_with = "ok_or_none")] + #[serde(default)] + pub comment_id: Option, + #[serde(deserialize_with = "ok_or_none")] + #[serde(default)] + pub reply_to: Option, + pub comment: String, + pub embeds: Vec, +} +pub fn handle_edit_issue_comment(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: IssueCommentReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut issues = p.issues_mut(&repo)?; + let mut issue = issues.get_mut(&req.issue_id.into())?; + let comment_id = req + .comment_id + .ok_or_else(|| anyhow!("missing comment id"))?; + let embeds = resolve_embeds(&repo, req.embeds); + let id = issue.edit_comment(comment_id, req.comment, embeds, &p.signer()?)?; + let result = format!("{{\"ok\": true, \"result\":\"{id}\"}}"); + Ok(result) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PatchCommentReactReq { + pub repo_id: RepoId, + pub patch_id: Oid, + pub revision_id: RevisionId, + pub comment_id: CommentId, + pub reaction: Reaction, + pub active: bool, +} +pub fn handle_patch_comment_react(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: PatchCommentReactReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut patches = p.patches_mut(&repo)?; + let mut patch = patches.get_mut(&req.patch_id.into())?; + let eid = patch.comment_react( + req.revision_id, + req.comment_id, + req.reaction, + req.active, + &p.signer()?, + )?; + let result = format!("{{\"ok\": true, \"result\":\"{eid}\"}}"); + Ok(result) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct IssueCommentReactReq { + pub repo_id: RepoId, + pub issue_id: Oid, + pub comment_id: CommentId, + pub reaction: Reaction, + pub active: bool, +} +pub fn handle_issue_comment_react(inp: *const c_char) -> Result { + let input = read_input(inp)?; + let req: IssueCommentReactReq = serde_json::from_str(input.as_str())?; + let p = Profile::load()?; + let repo = p.storage.repository(req.repo_id)?; + let mut issues = p.issues_mut(&repo)?; + let mut issue = issues.get_mut(&req.issue_id.into())?; + let id = issue.react(req.comment_id, req.reaction, req.active, &p.signer()?)?; + let result = format!("{{\"ok\": true, \"result\":\"{id}\"}}"); + Ok(result) +} + +pub fn resolve_embeds(repo: &Repository, embeds: Vec) -> Vec> { + embeds + .into_iter() + .filter_map(|embed| resolve_embed(repo, embed).ok()) + .collect() +} + +pub fn resolve_embed(repo: &Repository, embed: ContentEmbed) -> Result, anyhow::Error> { + let res = repo.backend.find_blob(embed.oid.into()); + if res.is_err() { + let content_uri = Uri::from_str(embed.content.as_str()).map_err(|e| anyhow!("{}", e))?; + let content = DataUri::try_from(&content_uri).map_err(|e| anyhow!("{}", e))?; + let content_vec = Vec::from(content); + let embedded = Embed::::store(&embed.name, content_vec.as_slice(), &repo.backend)?; + let res: Embed = Embed:: { + name: embed.name, + content: embedded.oid().into(), + }; + Ok(res) + } else { + let res: Embed = Embed:: { + name: embed.name, + content: embed.oid.into(), + }; + Ok(res) + } +} + +fn read_input(input: *const c_char) -> Result { + let input_str = unsafe { std::ffi::CStr::from_ptr(input) }; + let input_string = input_str.to_str()?; + Ok(String::from(input_string)) +} + +fn construct_result(input: String) -> *const c_char { + input.to_string().into_c_string().unwrap().into_raw() +} + +fn construct_error_result(e: anyhow::Error) -> *const c_char { + construct_result(format!("{{\"ok\": false, \"msg\": \"{e}\"}}")) +} + +fn ok_or_none<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let v = serde_json::Value::deserialize(deserializer)?; + Ok(T::deserialize(v).ok()) +} + +#[cfg(test)] +pub mod tests { + use crate::changeIssueTitleDescription; + use radicle::cob::migrate; + use radicle::crypto::ssh::Keystore; + use radicle::crypto::test::signer::MockSigner; + use radicle::crypto::{KeyPair, Seed}; + use radicle::node::config::Network; + use radicle::node::Alias; + use radicle::profile::{Config, Home}; + use radicle::test::setup::{Node, NodeRepo}; + use radicle::Profile; + use std::ffi::{CStr, CString}; + use std::path::PathBuf; + use tempfile::tempdir; + + #[test] + fn change_issue_with_embeds() { + let t = generate_test_data(); + + let mut issues = t.profile.issues_mut(&t.repo.repo).unwrap(); + let issue = issues + .create( + "test_issue_1", + "test_description_1", + &[], + &[], + [], + &t.node.signer, + ) + .unwrap(); + let iid = issue.id().to_string(); + let rid = t.repo.id; + + // prepare input + let json_input = format!("{{\"repo_id\": \"{rid}\", \"issue_id\":\"{iid}\",\"title\":\"new title 2\",\"description\":\"new description 2\",\"embeds\":[]}}"); + let input_cstr = CString::new(json_input.as_bytes()).unwrap(); + let input_ptr = input_cstr.as_ptr(); + + let result_ptr = changeIssueTitleDescription(input_ptr); + let result = unsafe { CStr::from_ptr(result_ptr) }.to_str().unwrap(); + assert_eq!(result, "{\"ok\": true}"); + } + + pub fn generate_test_data() -> TestData { + let alias = "tester"; + let node = Node::new(tempdir().unwrap(), MockSigner::from_seed([!0; 32]), alias); + let repo = node.project(); + let home_path = node.root.join("home"); + let home = Home::new(home_path.clone()).unwrap(); + let keystore = Keystore::new(&home.keys()); + let keypair = KeyPair::from_seed(Seed::from([!0; 32])); + keystore.store(keypair.clone(), alias, None).unwrap(); + + // create config as well + let mut cfg = Config::new(Alias::new(alias)); + cfg.node.network = Network::Test; + cfg.write(&home.config()).unwrap(); + + // set correct home + std::env::set_var("RAD_HOME", home_path.to_str().unwrap()); + + // load profile and migrate db + let p = Profile::load().unwrap(); + p.cobs_db_mut().unwrap().migrate(migrate::ignore).unwrap(); + + TestData { + node, + repo, + home_path, + home, + profile: p, + } + } + + pub struct TestData { + pub node: Node, + pub repo: NodeRepo, + pub home_path: PathBuf, + pub home: Home, + pub profile: Profile, + } +} diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/actions/rad/RadCobShow.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/actions/rad/RadCobShow.java index d2c24539..6c05701b 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/actions/rad/RadCobShow.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/actions/rad/RadCobShow.java @@ -39,7 +39,7 @@ public RadIssue getIssue() { RadIssue issue = null; try { issue = RadicleCliService.MAPPER.readValue(json, new TypeReference<>() { }); - var firstDiscussion = issue.discussion.get(0); + var firstDiscussion = issue.discussion.getFirst(); issue.author = firstDiscussion.author; issue.project = repo.getProject(); issue.repo = repo; @@ -63,7 +63,7 @@ public RadPatch getPatch() { patch = RadicleCliService.MAPPER.readValue(json, new TypeReference<>() { }); return patch; } catch (Exception e) { - logger.error("Unable to deserialize patch from json: " + json, e); + logger.warn("Unable to deserialize patch from json: " + json, e); } return patch; } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/config/RadicleSettingsView.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/config/RadicleSettingsView.java index fd5e0117..e74dae52 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/config/RadicleSettingsView.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/config/RadicleSettingsView.java @@ -156,7 +156,7 @@ private void updateRadVersionLabel() { if (!Strings.isNullOrEmpty(version)) { msg = RadicleBundle.message("radVersion") + " " + version; } - String finalMsg = msg; + final var finalMsg = msg; ApplicationManager.getApplication().invokeLater(() -> { radVersionLabel.setText(finalMsg); showHideEnforceLabel(version); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/dialog/clone/CloneUtil.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/dialog/clone/CloneUtil.java index 4c4233e4..b4678b7f 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/dialog/clone/CloneUtil.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/dialog/clone/CloneUtil.java @@ -32,7 +32,7 @@ public static void doClone(@NotNull CheckoutProvider.Listener listener, @NotNull if (destinationValidation != null) { RadAction.showErrorNotification(project, RadicleBundle.message("cloneFailed"), RadicleBundle.message("directoryError")); - logger.error("Clone Failed. Unable to create destination directory"); + logger.warn("Clone Failed. Unable to create destination directory"); return; } @@ -44,7 +44,7 @@ public static void doClone(@NotNull CheckoutProvider.Listener listener, @NotNull if (destinationParent == null) { RadAction.showErrorNotification(project, RadicleBundle.message("cloneFailed"), RadicleBundle.message("destinationDoesntExist")); - logger.error("Clone Failed. Destination doesn't exist"); + logger.warn("Clone Failed. Destination doesn't exist"); return; } @@ -54,7 +54,7 @@ public static void doClone(@NotNull CheckoutProvider.Listener listener, @NotNull } catch (Exception e) { RadAction.showErrorNotification(project, RadicleBundle.message("cloneFailed"), RadicleBundle.message("tempDirError")); - logger.error("Unable to create temp directory"); + logger.warn("Unable to create temp directory"); return; } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueListPanel.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueListPanel.java index b7096637..b4490be3 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueListPanel.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueListPanel.java @@ -33,9 +33,11 @@ public class IssueListPanel extends ListPanel issueListCellRenderer = new IssueListCellRenderer(); private final IssueTabController cntrl; protected IssueListSearchValue issueListSearchValue; + protected RadicleCliService rad; public IssueListPanel(IssueTabController controller, Project project) { super(controller, project); + rad = project.getService(RadicleCliService.class); this.cntrl = controller; this.issueListSearchValue = getEmptySearchValueModel(); this.issueListSearchValue.state = RadIssue.State.OPEN.label; @@ -87,15 +89,14 @@ public void filterList(IssueListSearchValue searchValue) { var assigneeFilter = searchValue.assignee; var loadedRadIssues = loadedData; List filteredPatches = loadedRadIssues.stream() - .filter(p -> Strings.isNullOrEmpty(searchFilter) || p.author.generateLabelText().contains(searchFilter) || - p.title.contains(searchFilter)) + .filter(p -> Strings.isNullOrEmpty(searchFilter) || p.author.contains(rad, searchFilter) || + p.title.contains(searchFilter)) .filter(p -> Strings.isNullOrEmpty(projectFilter) || p.repo.getRoot().getName().equals(projectFilter)) - .filter(p -> Strings.isNullOrEmpty(peerAuthorFilter) || Strings.nullToEmpty(p.author.alias).equals(peerAuthorFilter) || - p.author.id.equals(peerAuthorFilter)) + .filter(p -> Strings.isNullOrEmpty(peerAuthorFilter) || p.author.contains(rad, peerAuthorFilter)) .filter(p -> Strings.isNullOrEmpty(stateFilter) || (p.state != null && p.state.label.equals(stateFilter))) .filter(p -> Strings.isNullOrEmpty(labelFilter) || p.labels.stream().anyMatch(label -> label.equals(labelFilter))) .filter(p -> Strings.isNullOrEmpty(assigneeFilter) || p.assignees.stream().anyMatch(assignee -> - assignee.generateLabelText().equals(assigneeFilter))) + assignee.contains(rad, assigneeFilter))) .collect(Collectors.toList()); model.addAll(filteredPatches); } @@ -128,6 +129,7 @@ public static class Cell extends JPanel { public Cell(int index, RadIssue issue) { this.index = index; this.issue = issue; + var rad = issue.project.getService(RadicleCliService.class); var gapAfter = JBUI.scale(5); var issuePanel = new JPanel(); @@ -150,7 +152,7 @@ public Cell(int index, RadIssue issue) { var firstDiscussion = issue.discussion.get(0); var formattedDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(firstDiscussion.timestamp.atZone(ZoneId.systemDefault())); var info = new JLabel(RadicleBundle.message("created") + ": " + formattedDate + " " + - RadicleBundle.message("by") + " " + issue.author.generateLabelText()); + RadicleBundle.message("by") + " " + issue.author.generateLabelText(rad)); info.setForeground(JBColor.GRAY); if (!issue.labels.isEmpty()) { var labels = new JLabel(RadicleBundle.message("labels") + ": " + String.join(", ", issue.labels)); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssuePanel.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssuePanel.java index d92cd8d5..8f8d5f81 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssuePanel.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssuePanel.java @@ -21,7 +21,6 @@ import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadIssueAssignee; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadIssueLabel; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadIssueState; -import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadAuthor; import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadIssue; import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleCliService; import network.radicle.jetbrains.radiclejetbrainsplugin.toolwindow.LabeledListPanelHandle; @@ -107,7 +106,7 @@ private JComponent getIssueInfo() { detailsSection.add(issueIdAndCopyButton, new CC().gapBottom(String.valueOf(UI.scale(4)))); - var issueAuthor = getLabelPanel(RadicleBundle.message("issueAuthor", Strings.nullToEmpty(issue.author.generateLabelText()))); + var issueAuthor = getLabelPanel(RadicleBundle.message("issueAuthor", Strings.nullToEmpty(issue.author.generateLabelText(cli)))); detailsSection.add(issueAuthor, new CC().gapBottom(String.valueOf(UI.scale(4)))); if (!issue.labels.isEmpty()) { @@ -117,7 +116,7 @@ private JComponent getIssueInfo() { if (!issue.assignees.isEmpty()) { var issueAssignees = getLabelPanel(RadicleBundle.message("issueAssignees", - issue.assignees.stream().map(RadAuthor::generateLabelText).collect(Collectors.joining(",")))); + issue.assignees.stream().map(a -> a.generateLabelText(cli)).collect(Collectors.joining(",")))); detailsSection.add(issueAssignees, new CC().gapBottom(String.valueOf(UI.scale(4)))); } @@ -405,8 +404,9 @@ public CompletableFuture>(); for (var delegate : projectInfo.delegates) { - final Assignee assignee = new AssigneesSelect.Assignee(delegate.id, delegate.generateLabelText()); - final boolean isSelected = issue.assignees.stream().anyMatch(as -> as.id.contains(delegate.id)); + delegate.tryResolveAlias(cli); + final Assignee assignee = new AssigneesSelect.Assignee(delegate.id, delegate.alias); + final boolean isSelected = issue.assignees.stream().anyMatch(as -> as.contains(cli, delegate.id)); var selectableWrapper = new SelectionListCellRenderer.SelectableWrapper<>(assignee, isSelected); assignees.add(selectableWrapper); } @@ -416,7 +416,8 @@ public CompletableFuture(assignee, true); assignees.add(selectableWrapper); } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueSearchPanelViewModel.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueSearchPanelViewModel.java index 1801973a..8820ef41 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueSearchPanelViewModel.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/IssueSearchPanelViewModel.java @@ -15,7 +15,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -79,11 +81,16 @@ public CompletableFuture> getStateLabels() { public CompletableFuture> getAssignees() { return CompletableFuture.supplyAsync(() -> { List assignees = new ArrayList<>(); + Set seen = new HashSet<>(); var filteredList = filterListByProject(); for (var issue : filteredList) { for (var assignee : issue.assignees) { - if (!assignees.contains(assignee.id) && !assignees.contains(assignee.alias)) { - assignees.add(assignee.generateLabelText()); + if (!seen.contains(assignee.id) && !seen.contains(assignee.alias)) { + var label = assignee.generateLabelText(rad); + assignees.add(label); + seen.add(assignee.id); + seen.add(Strings.nullToEmpty(assignee.alias)); + seen.add(label); } } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/overview/IssueComponent.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/overview/IssueComponent.java index c64b09ec..04325c70 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/overview/IssueComponent.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/issues/overview/IssueComponent.java @@ -80,7 +80,7 @@ public JComponent create() { ApplicationManager.getApplication().invokeLater(() -> { commentSection = createCommentSection(radIssue.discussion); issueContainer.add(commentSection); - this.commentFieldPanel = createTimeLineItem(getCommentField().panel, horizontalPanel, radDetails.did, null); + this.commentFieldPanel = createTimeLineItem(getCommentField().panel, horizontalPanel, radDetails.toRadAuthor().generateLabelText(), null); issueContainer.add(commentFieldPanel); }, ModalityState.any()); } @@ -105,7 +105,7 @@ public static String findMessage(String replyTo, List discussionL public JComponent createCommentSection(List discussionList) { var mainPanel = getVerticalPanel(0); /* The first discussion is the description of the issue */ - if (discussionList.size() == 1) { + if (discussionList.size() <= 1) { return mainPanel; } for (var i = 1; i < discussionList.size(); i++) { @@ -129,23 +129,21 @@ public JComponent createCommentSection(List discussionList) { verticalPanel.add(replyPanel); panel.addToBottom(verticalPanel); var panelHandle = new EditablePanelHandler.PanelBuilder(radIssue.project, panel, - RadicleBundle.message("save"), new SingleValueModel<>(com.body), f -> true).build(); - // TODO: disabling editing issue comment - /*(field) -> { - var edited = api.editIssueComment(radIssue, field.getText(), com.id, field.getEmbedList()); + RadicleBundle.message("save"), new SingleValueModel<>(com.body), (field) -> { + var edited = cli.editIssueComment(radIssue, com.id, field.getText(), field.getEmbedList()); final boolean success = edited != null; if (success) { issueModel.setValue(radIssue); } return success; - }*/ + }).build(); var actionsPanel = CollaborationToolsUIUtilKt.HorizontalListPanel(CodeReviewCommentUIUtil.Actions.HORIZONTAL_GAP); - /* actionsPanel.add(CodeReviewCommentUIUtil.INSTANCE.createEditButton(e -> { + actionsPanel.add(CodeReviewCommentUIUtil.INSTANCE.createEditButton(e -> { panelHandle.showAndFocusEditor(); return null; - })); */ + })); var contentPanel = panelHandle.panel; - mainPanel.add(createTimeLineItem(contentPanel, actionsPanel, com.author.generateLabelText(), com.timestamp)); + mainPanel.add(createTimeLineItem(contentPanel, actionsPanel, com.author.generateLabelText(cli), com.timestamp)); } return mainPanel; } @@ -176,7 +174,18 @@ public boolean createComment(DragAndDropField field) { private JComponent getDescription() { var bodyIssue = radIssue.getDescription(); var editorPane = new MarkDownEditorPaneFactory(bodyIssue, radIssue.project, radIssue.projectId, file); - descPanel = Utils.descriptionPanel(editorPane, radIssue.project); + descPanel = Utils.descriptionPanel(editorPane, radIssue.project, "issue.change.description", f -> { + var newDesc = f.getText(); + if (Strings.isNullOrEmpty(newDesc)) { + return false; + } + var edited = cli.changeIssueTitleDescription(radIssue, radIssue.title, newDesc, f.getEmbedList()); + final boolean success = edited != null; + if (success) { + issueModel.setValue(edited); + } + return success; + }); return descPanel; } @@ -191,8 +200,7 @@ private JComponent getHeader() { var panelHandle = new EditablePanelHandler.PanelBuilder(radIssue.repo.getProject(), headerTitle, RadicleBundle.message("issue.change.title"), new SingleValueModel<>(radIssue.title), (field) -> { - // TODO: this will not work - var edited = cli.changeIssueTitleDescription(radIssue, field.getText(), radIssue.getDescription()); + var edited = cli.changeIssueTitleDescription(radIssue, field.getText(), radIssue.getDescription(), field.getEmbedList()); final boolean success = edited != null; if (success) { issueModel.setValue(edited); @@ -257,18 +265,17 @@ public boolean addReply(String comment, List embedList, String replyToId) public class IssueEmojiPanel extends EmojiPanel { protected IssueEmojiPanel(SingleValueModel model, List reactions, String discussionId, RadDetails radDetails) { - super(model, reactions, discussionId, radDetails); + super(radIssue.project, model, reactions, discussionId, radDetails); } @Override - public RadIssue addEmoji(Emoji emoji, String discussionId) { - return cli.issueCommentReact(radIssue, discussionId, emoji.unicode(), true); + public RadIssue addEmoji(Emoji emoji, String commentId) { + return cli.issueCommentReact(radIssue, commentId, emoji.unicode(), true); } @Override - public RadIssue removeEmoji(String emojiUnicode, String discussionId) { - //return api.issueCommentReact(radIssue, discussionId, emojiUnicode, false); - return cli.issueCommentReact(radIssue, discussionId, emojiUnicode, false); + public RadIssue removeEmoji(String emojiUnicode, String commentId) { + return cli.issueCommentReact(radIssue, commentId, emojiUnicode, false); } @Override diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadAuthor.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadAuthor.java index 8f3dc655..37a02726 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadAuthor.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadAuthor.java @@ -1,19 +1,22 @@ package network.radicle.jetbrains.radiclejetbrainsplugin.models; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Strings; +import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleCliService; +import network.radicle.jetbrains.radiclejetbrainsplugin.toolwindow.Utils; public class RadAuthor { public String id; public String alias; - public RadAuthor() { + @JsonCreator // needed when author is represented only by a string (id), e.g. inside rad patch edits + protected RadAuthor(String id) { + this(id, null); } - public RadAuthor(String id) { - this.id = id; - } - - public RadAuthor(String id, String alias) { + @JsonCreator + public RadAuthor(@JsonProperty("id") String id, @JsonProperty("alias") String alias) { this.id = id; this.alias = alias; } @@ -22,11 +25,28 @@ public String generateLabelText() { if (!Strings.isNullOrEmpty(alias)) { return alias; } - return id; + return Utils.formatDid(id); + } + + public String generateLabelText(RadicleCliService rad) { + tryResolveAlias(rad); + return generateLabelText(); } @Override public String toString() { return "{\"id\": " + id + "\", \"alias\": \"" + alias + "\"}"; } + + public void tryResolveAlias(RadicleCliService rad) { + if (Strings.isNullOrEmpty(alias) && rad != null) { + alias = Strings.nullToEmpty(rad.getAlias(id)); + } + } + + public boolean contains(RadicleCliService rad, String query) { + tryResolveAlias(rad); + final var cq = Strings.nullToEmpty(query).toLowerCase(); + return Strings.nullToEmpty(id).toLowerCase().contains(cq) || Strings.nullToEmpty(alias).toLowerCase().contains(cq); + } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadDiscussion.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadDiscussion.java index de923f0b..e0e1939e 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadDiscussion.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadDiscussion.java @@ -60,7 +60,7 @@ public Reaction findReaction(String emojiUnicode) { @Override public Instant getTimestamp() { - return this.timestamp; + return this.timestamp == null ? Instant.now() : this.timestamp; } public static class Location { @@ -89,6 +89,9 @@ public Map getMapObject() { @JsonProperty("new") private void unpackNewObject(Map line) { + if (line == null) { + return; + } var range = (HashMap) line.get("range"); type = (String) line.get("type"); start = range.get("start"); @@ -136,10 +139,10 @@ public List deserialize(JsonParser jsonParser, DeserializationContext .orElse(null); if (reaction == null) { List authors = new ArrayList<>(); - authors.add(new RadAuthor(id)); + authors.add(new RadAuthor(id, null)); reactions.add(new Reaction(emoji, authors)); } else { - reaction.authors().add(new RadAuthor(id)); + reaction.authors().add(new RadAuthor(id, null)); } } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadPatch.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadPatch.java index 5719710b..4d3741ef 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadPatch.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/models/RadPatch.java @@ -27,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -128,7 +129,7 @@ public Revision getLatestRevision() { if (myRevisions == null || myRevisions.isEmpty()) { return null; } - return myRevisions.get(myRevisions.size() - 1); + return myRevisions.getLast(); } @JsonIgnore @@ -150,14 +151,19 @@ public List getRevisionList() { } return this.revisions.keySet().stream() .map(revId -> this.revisions.get(revId)) - .sorted(Comparator.comparing(rev -> rev.timestamp)) - .collect(Collectors.toCollection(ArrayList::new)); + .filter(Objects::nonNull) + .filter(r -> r.timestamp != null) + .sorted(Comparator.comparing(Revision::getTimestamp)) + .collect(Collectors.toList()); } @JsonIgnore public List getTimelineEvents() { var list = new ArrayList(); for (var revision : getRevisionList()) { + if (revision.timestamp == null) { + continue; + } list.add(revision); list.addAll(revision.getReviewList()); list.addAll(revision.getDiscussions()); @@ -212,7 +218,7 @@ public Map deserialize(JsonParser jsonParser, DeserializationCon Review review = RadicleCliService.MAPPER.treeToValue(rev, Review.class); reviews.put(fn, review); } catch (Exception e) { - logger.error("error reading Review from tree: {}", rev, e); + logger.warn("error reading Review from tree: {}", rev, e); } }); @@ -231,7 +237,7 @@ public record Revision( Map reviews) implements TimelineEvent { @Override public Instant getTimestamp() { - return timestamp; + return timestamp == null ? Instant.now() : timestamp; } public List getReviewList() { @@ -239,9 +245,11 @@ public List getReviewList() { var myReviews = new ArrayList(); for (var reviewId : reviews.keySet()) { var review = reviews.get(reviewId); - myReviews.add(review); + if (review.timestamp != null) { + myReviews.add(review); + } } - myReviews.sort(Comparator.comparing(Review::timestamp).reversed()); + myReviews.sort(Comparator.comparing(Review::getTimestamp).reversed()); // Remove the duplicates reviews. We can only have 1 review per author per revision myReviews.removeIf(e -> !seen.add(e.author.id)); return myReviews; @@ -252,17 +260,18 @@ public List getDiscussions() { return new ArrayList<>(); } return discussion.comments.keySet().stream().map(discussion.comments::get) - .collect(Collectors.toCollection(ArrayList::new)); + .filter(c -> c != null && c.timestamp != null) + .collect(Collectors.toList()); } public List getReviewComments(String filePath) { - return getDiscussions().stream().filter(disc -> disc.isReviewComment() && + return getDiscussions().stream().filter(disc -> disc != null && disc.isReviewComment() && disc.location.path.equals(filePath)) .collect(Collectors.toList()); } public RadDiscussion findDiscussion(String commentId) { - return getDiscussions().stream().filter(disc -> disc.id.equals(commentId)).findFirst().orElse(null); + return getDiscussions().stream().filter(disc -> disc != null && disc.id.equals(commentId)).findFirst().orElse(null); } @JsonIgnore @@ -283,7 +292,7 @@ public record Review(String id, RadAuthor author, Verdict verdict, String summar DiscussionObj comments, Instant timestamp) implements TimelineEvent { @Override public Instant getTimestamp() { - return timestamp; + return timestamp == null ? Instant.now() : timestamp; } @JsonFormat(shape = JsonFormat.Shape.STRING) diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchListPanel.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchListPanel.java index c315f043..e53f58f0 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchListPanel.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchListPanel.java @@ -34,9 +34,11 @@ public class PatchListPanel extends ListPanel patchListCellRenderer = new PatchListCellRenderer(); protected PatchListSearchValue patchListSearchValue; + protected RadicleCliService cli; public PatchListPanel(PatchTabController ctrl, Project project) { super(ctrl, project); + this.cli = project.getService(RadicleCliService.class); this.controller = ctrl; this.patchListSearchValue = getEmptySearchValueModel(); this.patchListSearchValue.state = RadPatch.State.OPEN.label; @@ -92,8 +94,7 @@ public void filterList(PatchListSearchValue plsv) { final var search = plsv.searchQuery.toLowerCase().trim(); patchesStream = patchesStream.filter(p -> p.id.toLowerCase().contains(search) || - Strings.nullToEmpty(p.author.id).toLowerCase().contains(search) || - Strings.nullToEmpty(p.author.alias).toLowerCase().contains(search) || + (p.author != null && p.author.contains(cli, search)) || Strings.nullToEmpty(p.title).toLowerCase().contains(searchFilter.toLowerCase().trim()) || Strings.nullToEmpty(p.getLatestNonEmptyRevisionDescription()).toLowerCase().contains(search)); } @@ -101,8 +102,7 @@ public void filterList(PatchListSearchValue plsv) { patchesStream = patchesStream.filter(p -> p.repo.getRoot().getName().equals(projectFilter)); } if (!Strings.isNullOrEmpty(peerAuthorFilter)) { - patchesStream = patchesStream.filter(p -> Strings.nullToEmpty(p.author.alias).equals(peerAuthorFilter) || - p.author.id.equals(peerAuthorFilter)); + patchesStream = patchesStream.filter(p -> p.author != null && p.author.contains(cli, peerAuthorFilter)); } if (!Strings.isNullOrEmpty(stateFilter)) { patchesStream = patchesStream.filter(p -> p.state != null && p.state.label.equals(stateFilter)); @@ -132,7 +132,7 @@ public Component getListCellRendererComponent( cell.setBackground(ListUiUtil.WithTallRow.INSTANCE.background(list, isSelected, list.hasFocus())); cell.title.setForeground(ListUiUtil.WithTallRow.INSTANCE.foreground(isSelected, list.hasFocus())); cell.setToolTipText("

" + RadicleBundle.message("patchId") + ": " + cell.patch.id + "

" + - "

" + RadicleBundle.message("created") + " " + RadicleBundle.message("by") + ": " + cell.patch.author.id + "

"); + "

" + RadicleBundle.message("created") + " " + RadicleBundle.message("by") + ": " + cell.patch.author.generateLabelText() + "

"); return cell; } @@ -140,10 +140,12 @@ public static class Cell extends JPanel { public final int index; public final JLabel title; public final RadPatch patch; + public final RadicleCliService rad; public Cell(int index, RadPatch patch) { this.index = index; this.patch = patch; + rad = patch.project.getService(RadicleCliService.class); var gapAfter = JBUI.scale(5); var patchPanel = new JPanel(); @@ -169,7 +171,7 @@ public Cell(int index, RadPatch patch) { info.setForeground(JBColor.GRAY); infoPanel.add(info); - var authorLabel = new JLabel(Strings.isNullOrEmpty(patch.author.alias) ? patch.author.generateLabelText() : patch.author.alias); + var authorLabel = new JLabel(patch.author.generateLabelText(rad)); // TODO cannot enable tooltip specifically on author authorLabel.setForeground(JBColor.GRAY); infoPanel.add(authorLabel); @@ -184,7 +186,7 @@ public Cell(int index, RadPatch patch) { @Override public AccessibleContext getAccessibleContext() { var ac = super.getAccessibleContext(); - ac.setAccessibleName(patch.repo.getRoot().getName() + " - " + patch.author.generateLabelText()); + ac.setAccessibleName(patch.repo.getRoot().getName() + " - " + patch.author.generateLabelText(rad)); return ac; } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchTabController.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchTabController.java index 653e87aa..38889826 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchTabController.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/PatchTabController.java @@ -23,9 +23,11 @@ public class PatchTabController extends TabController patchModel; private PatchProposalPanel patchProposalPanel; private JComponent patchProposalJPanel; + private RadicleCliService rad; public PatchTabController(Content tab, Project project) { super(project, tab); + rad = project.getService(RadicleCliService.class); patchListPanel = new PatchListPanel(this, project); } @@ -54,7 +56,7 @@ public void createNewPatchPanel(List gitRepos) { } protected void createInternalPatchProposalPanel(SingleValueModel patch, JComponent mainPanel) { - tab.setDisplayName(RadicleBundle.message("patchProposalFrom") + ": " + patch.getValue().author.generateLabelText()); + tab.setDisplayName(RadicleBundle.message("patchProposalFrom") + ": " + patch.getValue().author.generateLabelText(rad)); patchProposalPanel = new PatchProposalPanel(this, patch); patchProposalJPanel = patchProposalPanel.createViewPatchProposalPanel(); mainPanel.removeAll(); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchDiffEditorComponentsFactory.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchDiffEditorComponentsFactory.java index a5dee736..e3d9c808 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchDiffEditorComponentsFactory.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchDiffEditorComponentsFactory.java @@ -41,14 +41,13 @@ public JComponent createSingleCommentComponent(PatchDiffEditorGutterIconFactory. new SingleValueModel<>(""), field -> { var location = new RadDiscussion.Location(observableThreadModel.getFilePath(), "ranges", observableThreadModel.getCommitHash(), editorLine, editorLine); - var res = this.cli.createPatchComment(patch.repo, patch.getLatestRevision().id(), field.getText(), null, location, field.getEmbedList()); - boolean success = res != null; + boolean success = this.cli.createPatchComment(patch, patch.getLatestRevision().id(), field.getText(), null, location, field.getEmbedList()); if (success) { observableThreadModel.update(patch); ApplicationManager.getApplication().invokeLater(() -> hideComponent.hide(editorLine)); } return success; - }).enableDragAndDrop(false).hideCancelAction(true).build(); + }).enableDragAndDrop(true).hideCancelAction(true).build(); panelHandle.showAndFocusEditor(); var builder = new CodeReviewChatItemUIUtil.Builder(CodeReviewChatItemUIUtil.ComponentType.COMPACT, integer -> new SingleValueModel<>(RadicleIcons.DEFAULT_AVATAR), panelHandle.panel); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchReviewThreadComponentFactory.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchReviewThreadComponentFactory.java index 64a9d036..0491f3e1 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchReviewThreadComponentFactory.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/review/PatchReviewThreadComponentFactory.java @@ -1,6 +1,5 @@ package network.radicle.jetbrains.radiclejetbrainsplugin.patches.review; -import com.google.common.base.Strings; import com.google.protobuf.Any; import com.intellij.collaboration.ui.CollaborationToolsUIUtilKt; import com.intellij.collaboration.ui.SingleValueModel; @@ -28,15 +27,14 @@ import network.radicle.jetbrains.radiclejetbrainsplugin.models.ThreadModel; import network.radicle.jetbrains.radiclejetbrainsplugin.patches.timeline.EditablePanelHandler; import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleCliService; -import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleProjectService; import network.radicle.jetbrains.radiclejetbrainsplugin.toolwindow.EmojiPanel; import network.radicle.jetbrains.radiclejetbrainsplugin.toolwindow.MarkDownEditorPaneFactory; import network.radicle.jetbrains.radiclejetbrainsplugin.toolwindow.Utils; -import javax.swing.JComponent; -import javax.swing.JPanel; import javax.swing.BorderFactory; +import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JPanel; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -52,7 +50,6 @@ public class PatchReviewThreadComponentFactory { private static final int PADDING_TOP_BOTTOM = 5; private static final String JPANEL_PREFIX_NAME = "COMMENT_"; - private final RadicleProjectService rad; private final RadicleCliService cli; private final RadPatch patch; private final ObservableThreadModel threadsModel; @@ -60,7 +57,6 @@ public class PatchReviewThreadComponentFactory { private RadDetails radDetails; public PatchReviewThreadComponentFactory(RadPatch patch, ObservableThreadModel threadsModel, Editor editor) { - this.rad = patch.project.getService(RadicleProjectService.class); this.cli = patch.project.getService(RadicleCliService.class); this.radDetails = cli.getCurrentIdentity(); this.threadsModel = threadsModel; @@ -82,10 +78,9 @@ public JComponent createThread(ThreadModel threadModel) { } public boolean deleteComment(RadDiscussion disc) { - // var revisionId = patch.findRevisionId(disc.id); - // var res = api.deleteRevisionComment(patch, revisionId, disc.id); - // return res != null; - return false; + var revisionId = patch.findRevisionId(disc.id); + var res = cli.deletePatchComment(patch, revisionId, disc.id); + return res != null; } public JComponent createUncollapsedThreadActionsComponent(ThreadModel threadModel) { @@ -97,13 +92,12 @@ public JComponent createUncollapsedThreadActionsComponent(ThreadModel threadMode var location = new RadDiscussion.Location(threadsModel.getFilePath(), "ranges", threadsModel.getCommitHash(), line, line); var revision = patch.findRevisionId(latestRev.id); - var res = this.cli.createPatchComment(patch.repo, revision, field.getText(), latestRev.id, location, field.getEmbedList()); - boolean success = res != null; + boolean success = this.cli.createPatchComment(patch, revision, field.getText(), latestRev.id, location, field.getEmbedList()); if (success) { threadsModel.update(patch); } return success; - }).enableDragAndDrop(false).hideCancelAction(true).build(); + }).enableDragAndDrop(true).hideCancelAction(true).build(); panelHandle.showAndFocusEditor(); var builder = new CodeReviewChatItemUIUtil.Builder(CodeReviewChatItemUIUtil.ComponentType.COMPACT, integer -> new SingleValueModel<>(RadicleIcons.DEFAULT_AVATAR), panelHandle.panel); @@ -126,9 +120,7 @@ public void doClick() { public JComponent getThreadActionsComponent(ThreadModel myThreadsModel) { var toggleModel = new SingleValueModel<>(false); return ToggleableContainer.INSTANCE.create(toggleModel, () -> { - ReplyAction rep = () -> { - toggleModel.setValue(true); - }; + ReplyAction rep = () -> toggleModel.setValue(true); return createCollapsedThreadActionComponent(rep); }, () -> createUncollapsedThreadActionsComponent(myThreadsModel)); } @@ -140,40 +132,38 @@ private JComponent createComponent(RadDiscussion disc) { var panelHandle = new EditablePanelHandler.PanelBuilder(patch.project, editorPane.htmlEditorPane(), RadicleBundle.message("review.edit.comment"), new SingleValueModel<>(disc.body), (field) -> { - RadPatch res = null; // this.api.changePatchComment(revisionId, disc.id, field.getText(), patch, List.of()); + RadPatch res = this.cli.editPatchComment(patch, revisionId, disc.id, field.getText(), field.getEmbedList()); boolean success = res != null; if (success) { threadsModel.update(patch); } return success; - }).enableDragAndDrop(false).build(); - var editButton = CodeReviewCommentUIUtil.INSTANCE.createEditButton(actionEvent -> { - panelHandle.showAndFocusEditor(); - return Unit.INSTANCE; - }); - var deleteButton = CodeReviewCommentUIUtil.INSTANCE.createDeleteCommentIconButton(actionEvent -> { - ApplicationManager.getApplication().executeOnPooledThread(() -> { - var success = false; // this.deleteComment(disc); - if (success) { - threadsModel.update(patch); - } - }); - return Unit.INSTANCE; - }); + }).enableDragAndDrop(true).build(); var actionsPanel = CollaborationToolsUIUtilKt.HorizontalListPanel(CodeReviewCommentUIUtil.Actions.HORIZONTAL_GAP); radDetails = cli.getCurrentIdentity(); - var self = radDetails != null && radDetails.did.equals(disc.author.id); - // TODO: removing functionality to edit/delete comment, not supported on CLI - /*if (self) { + var self = radDetails != null && disc.author.id.contains(radDetails.nodeId); + if (self) { + var editButton = CodeReviewCommentUIUtil.INSTANCE.createEditButton(e -> { + panelHandle.showAndFocusEditor(); + return Unit.INSTANCE; + }); + var deleteButton = CodeReviewCommentUIUtil.INSTANCE.createDeleteCommentIconButton(e -> { + ApplicationManager.getApplication().executeOnPooledThread(() -> { + var success = this.deleteComment(disc); + if (success) { + threadsModel.update(patch); + } + }); + return Unit.INSTANCE; + }); actionsPanel.add(editButton); actionsPanel.add(deleteButton); - }*/ + } var emojiPanel = new MyEmojiPanel(new SingleValueModel<>(patch), disc.reactions, disc.id, radDetails); var builder = new CodeReviewChatItemUIUtil.Builder(CodeReviewChatItemUIUtil.ComponentType.COMPACT, integer -> new SingleValueModel<>(RadicleIcons.DEFAULT_AVATAR), panelHandle.panel); - var author = !Strings.isNullOrEmpty(disc.author.alias) ? disc.author.alias : Utils.formatDid(disc.author.id); - var authorLink = HtmlChunk.link("#", - author).wrapWith(HtmlChunk.font(ColorUtil.toHtmlColor(UIUtil.getLabelForeground()))).bold(); + var author = disc.author.generateLabelText(cli); + var authorLink = HtmlChunk.link("#", author).wrapWith(HtmlChunk.font(ColorUtil.toHtmlColor(UIUtil.getLabelForeground()))).bold(); var titleText = new HtmlBuilder().append(authorLink) .append(HtmlChunk.nbsp()) .append(disc.timestamp != null ? DATE_TIME_FORMATTER.format(disc.timestamp) : ""); @@ -195,23 +185,17 @@ private class MyEmojiPanel extends EmojiPanel { protected MyEmojiPanel(SingleValueModel model, List reactions, String discussionId, RadDetails radDetails) { - super(model, reactions, discussionId, radDetails); + super(patch.project, model, reactions, discussionId, radDetails); } @Override public RadPatch addEmoji(Emoji emoji, String commentId) { - // TODO: disabling patch comment reactions, not supported from CLI - // var revisionId = patch.findRevisionId(commentId); - // return api.patchCommentReact(patch, commentId, revisionId, emoji.unicode(), true); - return null; + return cli.patchCommentReact(patch, commentId, emoji.unicode(), true); } @Override public RadPatch removeEmoji(String emojiUnicode, String commentId) { - // TODO: disabling patch comment reactions, not supported from CLI - // var revisionId = patch.findRevisionId(commentId); - // return api.patchCommentReact(patch, commentId, revisionId, emojiUnicode, false); - return null; + return cli.patchCommentReact(patch, commentId, emojiUnicode, false); } @Override diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponent.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponent.java index 9a4366bf..94a9b290 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponent.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponent.java @@ -18,11 +18,7 @@ import com.intellij.util.ui.JBUI; import com.intellij.util.ui.NamedColorUtil; import network.radicle.jetbrains.radiclejetbrainsplugin.RadicleBundle; -import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadAction; -import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadSelf; import network.radicle.jetbrains.radiclejetbrainsplugin.icons.RadicleIcons; -import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadAuthor; -import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadDetails; import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadPatch; import network.radicle.jetbrains.radiclejetbrainsplugin.patches.PatchProposalPanel; import network.radicle.jetbrains.radiclejetbrainsplugin.patches.timeline.editor.PatchVirtualFile; @@ -70,10 +66,10 @@ public JComponent create() { var horizontalPanel = Utils.getHorizontalPanel(8); ApplicationManager.getApplication().executeOnPooledThread(() -> { - var radDetails = getCurrentRadDetails(); + var radDetails = cli.getCurrentIdentity(); if (radDetails != null) { ApplicationManager.getApplication().invokeLater(() -> { - var selfAuthor = new RadAuthor(radDetails.nodeId, radDetails.alias); + var selfAuthor = radDetails.toRadAuthor(); var self = selfAuthor.generateLabelText(); var commentSection = createTimeLineItem(getCommentField().panel, horizontalPanel, self, null); commentPanel = commentSection; @@ -91,22 +87,11 @@ public JComponent create() { return mainPanel; } - private RadDetails getCurrentRadDetails() { - var radSelf = new RadSelf(radPatch.project); - radSelf.askForIdentity(false); - var output = radSelf.perform(); - if (RadAction.isSuccess(output)) { - return new RadDetails(output.getStdoutLines(true)); - } - return null; - } - public boolean createComment(DragAndDropField field) { if (Strings.isNullOrEmpty(field.getText())) { return false; } - var output = cli.createPatchComment(radPatch.repo, radPatch.getLatestRevision().id(), field.getText(), null); - var ok = RadAction.isSuccess(output); + boolean ok = cli.createPatchComment(radPatch, radPatch.getLatestRevision().id(), field.getText(), null, null, field.getEmbedList()); if (ok) { radPatchModel.setValue(radPatch); return true; diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponentFactory.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponentFactory.java index 3c43cfe0..811d6d33 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponentFactory.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/patches/timeline/TimelineComponentFactory.java @@ -17,6 +17,7 @@ import com.intellij.ui.ColorUtil; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.components.BorderLayoutPanel; +import kotlin.Unit; import network.radicle.jetbrains.radiclejetbrainsplugin.RadicleBundle; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadAction; import network.radicle.jetbrains.radiclejetbrainsplugin.icons.RadicleIcons; @@ -79,7 +80,7 @@ public JComponent createDescSection() { description = RadicleBundle.message("noDescription"); } var editorPane = new MarkDownEditorPaneFactory(description, patch.project, patch.radProject.id, file); - descSection = Utils.descriptionPanel(editorPane, patch.project, true, "patch.proposal.change.description", f -> { + descSection = Utils.descriptionPanel(editorPane, patch.project, "patch.proposal.change.description", f -> { var newDesc = f.getText(); if (Strings.isNullOrEmpty(newDesc)) { return false; @@ -144,9 +145,8 @@ private JComponent createRevisionComponent(RadPatch.Revision rev) { contentPanel.setOpaque(false); var horizontalPanel = getHorizontalPanel(8); horizontalPanel.setOpaque(false); - var revAuthor = !Strings.isNullOrEmpty(rev.author().alias) ? rev.author().alias : rev.author().id; - return createTimeLineItem(contentPanel, horizontalPanel, RadicleBundle.message("revisionPublish", rev.id(), revAuthor), - rev.timestamp()); + var revAuthor = rev.author().generateLabelText(cli); + return createTimeLineItem(contentPanel, horizontalPanel, RadicleBundle.message("revisionPublish", rev.id(), revAuthor), rev.timestamp()); } private JComponent createReviewComponent(RadPatch.Review review) { @@ -169,7 +169,7 @@ private JComponent createReviewComponent(RadPatch.Review review) { RadicleBundle.message("save"), new SingleValueModel<>(message), (field) -> true).enableDragAndDrop(false).build(); var contentPanel = panelHandle.panel; var actionsPanel = CollaborationToolsUIUtilKt.HorizontalListPanel(CodeReviewCommentUIUtil.Actions.HORIZONTAL_GAP); - var item = createTimeLineItem(contentPanel, actionsPanel, review.author().generateLabelText(), review.timestamp()); + var item = createTimeLineItem(contentPanel, actionsPanel, review.author().generateLabelText(cli), review.timestamp()); reviewPanel.add(item); return reviewPanel; } @@ -207,21 +207,35 @@ private JComponent createCommentComponent(RadDiscussion com) { } var panelHandle = new EditablePanelHandler.PanelBuilder(patch.project, panel, RadicleBundle.message("save"), new SingleValueModel<>(com.body), (field) -> { - RadPatch edited = null; // api.changePatchComment(patch.findRevisionId(com.id), com.id, field.getText(), patch, field.getEmbedList()); + var edited = cli.editPatchComment(patch, patch.findRevisionId(com.id), com.id, field.getText(), field.getEmbedList()); final boolean success = edited != null; if (success) { patchModel.setValue(patch); } return success; - }).enableDragAndDrop(false).build(); + }).enableDragAndDrop(true).build(); var contentPanel = panelHandle.panel; var actionsPanel = CollaborationToolsUIUtilKt.HorizontalListPanel(CodeReviewCommentUIUtil.Actions.HORIZONTAL_GAP); - // TODO: disable editing patch comments, not supported from CLI - /* actionsPanel.add(CodeReviewCommentUIUtil.INSTANCE.createEditButton(e -> { - panelHandle.showAndFocusEditor(); - return null; - })); */ - commentPanel = createTimeLineItem(contentPanel, actionsPanel, com.author.generateLabelText(), com.timestamp); + var self = cli.getCurrentIdentity(); + if (self != null && com.author.id.contains(self.nodeId)) { + final var editButton = CodeReviewCommentUIUtil.INSTANCE.createEditButton(e -> { + panelHandle.showAndFocusEditor(); + return null; + }); + final var deleteButton = CodeReviewCommentUIUtil.INSTANCE.createDeleteCommentIconButton(e -> { + ApplicationManager.getApplication().executeOnPooledThread(() -> { + var revisionId = patch.findRevisionId(com.id); + var success = cli.deletePatchComment(patch, revisionId, com.id); + if (success != null) { + patchModel.setValue(patch); + } + }); + return Unit.INSTANCE; + }); + actionsPanel.add(editButton); + actionsPanel.add(deleteButton); + } + commentPanel = createTimeLineItem(contentPanel, actionsPanel, com.author.generateLabelText(cli), com.timestamp); myMainPanel.add(commentPanel); myMainPanel.setName(JPANEL_PREFIX_NAME + com.id); return myMainPanel; @@ -268,31 +282,23 @@ public MyReplyPanel(Project project, RadDiscussion radDiscussion, SingleValueMod @Override public boolean addReply(String comment, List list, String replyToId) { - var output = cli.createPatchComment(patch.repo, patch.getLatestRevision().id(), comment, replyToId); - return RadAction.isSuccess(output); + return cli.createPatchComment(patch, patch.getLatestRevision().id(), comment, replyToId, null, list); } } private class PatchEmojiPanel extends EmojiPanel { - public PatchEmojiPanel(SingleValueModel model, List reactions, String discussionId, RadDetails radDetails) { - super(model, reactions, discussionId, radDetails); + super(patch.project, model, reactions, discussionId, radDetails); } @Override public RadPatch addEmoji(Emoji emoji, String commentId) { - // TODO: disabling patch comment reactions, not supported from CLI - // var revisionId = patch.findRevisionId(commentId); - // return api.patchCommentReact(patch, commentId, revisionId, emoji.unicode(), true); - return null; + return cli.patchCommentReact(patch, commentId, emoji.unicode(), true); } @Override public RadPatch removeEmoji(String emojiUnicode, String commentId) { - // TODO: disabling patch comment reactions, not supported from CLI - // var revisionId = patch.findRevisionId(commentId); - // return api.patchCommentReact(patch, commentId, revisionId, emojiUnicode, false); - return null; + return cli.patchCommentReact(patch, commentId, emojiUnicode, false); } @Override diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/FileService.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/FileService.java index 36f00a8b..df02fd7d 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/FileService.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/FileService.java @@ -7,7 +7,6 @@ import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; @@ -26,7 +25,7 @@ public String calculateGitObjectId(String base64) { var base64Payload = base64Parts.length > 1 ? base64Parts[1] : null; var base64PayloadBytes = base64Payload != null ? Base64.getDecoder().decode(base64Payload) : null; if (base64PayloadBytes == null) { - logger.error("Empty base64 payload for {}", base64); + logger.warn("Empty base64 payload for {}", base64); return null; } @@ -57,8 +56,8 @@ public String calculateGitObjectId(String base64) { hexString.append(hex); } return hexString.toString(); - } catch (NoSuchAlgorithmException e) { - logger.error("Exception caught for {}", base64, e); + } catch (Exception e) { + logger.warn("Exception caught for {}", base64, e); return null; } } @@ -67,7 +66,7 @@ private String getMimeType(byte[] fileBytes) { try (var is = new ByteArrayInputStream(fileBytes)) { return new Tika().detect(is); } catch (Exception e) { - logger.error("Exception upon detecting mime-type", e); + logger.warn("Exception upon detecting mime-type", e); return null; } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleCliService.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleCliService.java index e741198d..911665a8 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleCliService.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleCliService.java @@ -13,11 +13,11 @@ import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadAction; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadCobList; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadCobShow; +import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadComment; +import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadIssueCreate; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadPatchCreate; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadPatchLabel; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadPatchReview; -import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadComment; -import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadIssueCreate; import network.radicle.jetbrains.radiclejetbrainsplugin.actions.rad.RadSelf; import network.radicle.jetbrains.radiclejetbrainsplugin.config.RadicleProjectSettingsHandler; import network.radicle.jetbrains.radiclejetbrainsplugin.models.Embed; @@ -33,6 +33,7 @@ import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -45,15 +46,17 @@ public class RadicleCliService { public static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()) .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES); - private final Project project; - private final Map radRepoIds; - private final RadicleProjectService rad; - private RadDetails identity; + protected final Project project; + protected final Map radRepoIds; + protected RadicleProjectService rad; + protected RadicleNativeService jrad; + protected RadDetails identity; public RadicleCliService(Project project) { this.project = project; radRepoIds = new HashMap<>(); this.rad = project.getService(RadicleProjectService.class); + this.jrad = project.getService(RadicleNativeService.class); } public ProcessOutput createIssue(GitRepository repo, String title, String description, List assignees, List labels) { @@ -72,7 +75,7 @@ public String createPatch(GitRepository repo, String title, String description, if (lines.isEmpty()) { return ""; } - var firstLine = lines.get(0); + var firstLine = lines.getFirst(); var parts = firstLine.split(" "); String patchId = null; if (parts.length > 2) { @@ -85,8 +88,7 @@ public String createPatch(GitRepository repo, String title, String description, return patchId; } - public ProcessOutput createPatchLabels(GitRepository repo, String patchId, - List addedLabels, List deletedLabels) { + public ProcessOutput createPatchLabels(GitRepository repo, String patchId, List addedLabels, List deletedLabels) { var radPatchLabel = new RadPatchLabel(repo, patchId, addedLabels, deletedLabels); return radPatchLabel.perform(); } @@ -104,33 +106,56 @@ public List getIssues(GitRepository repo, String projectId) { return List.of(); } var issueIds = listOutput.getStdoutLines(); - var issues = new ArrayList(); + List issues = new ArrayList<>(); for (var objectId : issueIds) { var issue = getIssue(repo, projectId, objectId); if (issue == null) { continue; } + issues.add(issue); } - issues.sort(Comparator.comparing(issue -> ((RadIssue) issue).discussion.get(0).timestamp).reversed()); + issues.sort(Comparator.comparing((RadIssue issue) -> issue.discussion == null || issue.discussion.isEmpty() ? Instant.now() : + issue.discussion.getFirst().timestamp).reversed()); return issues; } - public RadIssue issueCommentReact(RadIssue issue, String discussionId, String reaction, boolean active) { + public RadIssue editIssueComment(RadIssue issue, String commentId, String comment, List embeds) { + boolean ok = jrad.editIssueComment(issue.projectId, issue.id, commentId, comment, embeds); + return ok ? issue : null; + } + + public RadPatch patchCommentReact(RadPatch patch, String commentId, String reaction, boolean active) { + try { + var revId = patch.findRevisionId(commentId); + var res = jrad.patchCommentReact(patch.radProject.id, patch.id, revId, commentId, reaction, active); + if (!res) { + logger.warn("received invalid result for reacting to patch:{} comment:{}", patch.id, commentId); + return null; + } + return patch; + } catch (Exception e) { + logger.warn("error reacting to patch:{} comment: {}", patch.id, commentId, e); + } + + return null; + } + + public RadIssue issueCommentReact(RadIssue issue, String commentId, String reaction, boolean active) { if (!active) { - logger.error("not implemented yet from the CLI!"); - return null; + boolean ok = jrad.issueCommentReact(issue.projectId, issue.id, commentId, reaction, false); + return ok ? issue : null; } try { - var res = rad.reactToIssueComment(issue.repo, issue.id, discussionId, reaction, active); + var res = rad.reactToIssueComment(issue.repo, issue.id, commentId, reaction, true); if (!RadAction.isSuccess(res)) { logger.warn("received invalid command output:{} for reacting to issue:{} comment:{}. out:{} err:{}", - res.getExitCode(), issue.id, discussionId, res.getStdout(), res.getStderr()); + res.getExitCode(), issue.id, commentId, res.getStdout(), res.getStderr()); return null; } return issue; } catch (Exception e) { - logger.warn("error reacting to discussion: {}", discussionId, e); + logger.warn("error reacting to discussion: {}", commentId, e); } return null; @@ -178,6 +203,10 @@ public List getPatches(GitRepository repo, String projectId) { return patches; } + public String getAlias(String did) { + return jrad.getAlias(did); + } + public RadDetails getCurrentIdentity() { if (identity != null) { return identity; @@ -192,14 +221,24 @@ public void resetIdentity() { this.identity = null; } - public ProcessOutput createPatchComment(GitRepository repo, String revisionId, String comment, String replyTo) { - return createPatchComment(repo, revisionId, comment, replyTo, null, null); + public boolean createPatchComment( + RadPatch patch, String revisionId, String comment, String replyTo, RadDiscussion.Location location, List embedList) { + if (location == null && (embedList == null || embedList.isEmpty())) { + var res = createComment(patch.repo, revisionId, comment, replyTo, RadComment.Type.PATCH); + return RadAction.isSuccess(res); + } + return jrad.createPatchComment(patch.radProject.id, patch.id, revisionId, comment, replyTo, location, embedList); + } + + public RadPatch editPatchComment( + RadPatch patch, String revisionId, String commentId, String comment, List embedList) { + boolean ok = jrad.editPatchComment(patch.radProject.id, patch.id, revisionId, commentId, comment, embedList); + return ok ? patch : null; } - public ProcessOutput createPatchComment( - GitRepository repo, String revisionId, String comment, String replyTo, RadDiscussion.Location location, List embedList) { - //TODO: location and embeds are not supported by the CLI - return createComment(repo, revisionId, comment, replyTo, RadComment.Type.PATCH); + public RadPatch deletePatchComment(RadPatch patch, String revisionId, String commentId) { + boolean ok = jrad.deletePatchComment(patch.radProject.id, patch.id, revisionId, commentId); + return ok ? patch : null; } public ProcessOutput createIssueComment(GitRepository repo, String issueId, String comment, String replyTo) { @@ -223,12 +262,12 @@ public RadPatch changePatchTitleDescription(RadPatch patch, String newTitle, Str return null; } - public RadIssue changeIssueTitleDescription(RadIssue issue, String newTitle, String newDescription) { + public RadIssue changeIssueTitleDescription(RadIssue issue, String newTitle, String newDescription, List embeds) { try { - var res = rad.editIssueTitleDescription(issue.repo, issue.id, newTitle, newDescription); - if (!RadAction.isSuccess(res)) { - logger.warn("received invalid command output for changing issue message (title/description): {} - {} - {}", - res.getExitCode(), res.getStdout(), res.getStderr()); + var resp = jrad.editIssueTitleDescription(issue.projectId, issue.id, newTitle, newDescription, embeds); + if (!resp.ok()) { + logger.warn("received invalid native response for changing issue(title/description): repoId:{} issueId:{} title:{} description:{} resp:{}", + issue.projectId, issue.id, newTitle, newDescription, resp); return null; } // return issue as-is, it will trigger a re-fetch anyway diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleNativeService.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleNativeService.java new file mode 100644 index 00000000..2512f4e2 --- /dev/null +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleNativeService.java @@ -0,0 +1,372 @@ +package network.radicle.jetbrains.radiclejetbrainsplugin.services; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Strings; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import jnr.ffi.LibraryLoader; +import network.radicle.jetbrains.radiclejetbrainsplugin.models.Embed; +import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadDiscussion; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class RadicleNativeService { + private static final Logger logger = Logger.getInstance(RadicleNativeService.class); + + public static JavaRad javaRad; + public static boolean loadError = false; + public static Path libFile; + public static Map aliases; + + private final Project project; + + public RadicleNativeService(Project project) { + this.project = project; + aliases = new HashMap<>(); + load(); + } + + public String radHome() { + if (javaRad == null) { + return null; + } + return javaRad.radHome("jchrist"); + } + + public JRadResponse editIssueTitleDescription(String repoId, String issueId, String title, String description, List embeds) { + if (javaRad == null) { + return new JRadResponse(false, "native service not loaded"); + } + + try { + var json = RadicleCliService.MAPPER.writeValueAsString( + Map.of("repo_id", repoId, + "issue_id", issueId, + "title", Strings.nullToEmpty(title), + "description", Strings.nullToEmpty(description), + "embeds", embeds == null ? List.of() : embeds)); + var res = javaRad.changeIssueTitleDescription(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("error response from native service: " + resp); + if (resp == null) { + resp = new JRadResponse(false, "no resp"); + } + } + return resp; + } catch (Throwable e) { + logger.warn("Error changing issue title", e); + return new JRadResponse(false, e.getMessage()); + } + } + + public Map getEmbeds(String repoId, List oids) { + if (javaRad == null) { + return null; + } + + try { + var json = RadicleCliService.MAPPER.writeValueAsString(Map.of("repo_id", repoId, "oids", oids)); + var res = javaRad.getEmbeds(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("error response from native service: " + resp); + return null; + } + var tree = RadicleCliService.MAPPER.readTree(res); + var jsonMap = tree.get("result"); + return RadicleCliService.MAPPER.convertValue(jsonMap, new TypeReference<>() { }); + } catch (Throwable e) { + logger.warn("Error getting embeds", e); + return null; + } + } + + public String getAlias(String nid) { + nid = normalizeNid(nid); + if (aliases.containsKey(nid)) { + return aliases.get(nid); + } + if (javaRad == null) { + return null; + } + var result = getAlias(List.of(nid)); + return result.get(nid); + } + + public Map getAlias(List nids) { + nids = nids.stream().map(RadicleNativeService::normalizeNid).toList(); + Map result = new HashMap<>(); + Set missing = new HashSet<>(nids); + for (var nid : nids) { + if (aliases.containsKey(nid)) { + result.put(nid, aliases.get(nid)); + missing.remove(nid); + } + } + if (missing.isEmpty()) { + return result; + } + if (javaRad == null) { + return null; + } + try { + var json = RadicleCliService.MAPPER.writeValueAsString(Map.of("ids", missing)); + var res = javaRad.getAlias(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("error response from native service for aliases: " + resp); + // add a value to signal the error and not retry it + for (var nid : missing) { + aliases.put(nid, ""); + } + return result; + } + var tree = RadicleCliService.MAPPER.readTree(res); + var jsonMap = tree.get("result"); + var resolvedMap = RadicleCliService.MAPPER.convertValue(jsonMap, new TypeReference>() { }); + missing.removeAll(resolvedMap.keySet()); + aliases.putAll(resolvedMap); + result.putAll(resolvedMap); + if (!missing.isEmpty()) { + for (var nid : missing) { + aliases.put(nid, ""); + } + } + return result; + } catch (Throwable e) { + logger.warn("Error getting aliases for nids:" + nids, e); + for (var nid : missing) { + aliases.put(nid, ""); + } + return null; + } + } + + public boolean createPatchComment( + String repoId, String patchId, String revisionId, String comment, String replyTo, RadDiscussion.Location location, List embeds) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("patch_id", patchId); + jsonMap.put("revision_id", revisionId); + jsonMap.put("comment", comment); + jsonMap.put("reply_to", Strings.nullToEmpty(replyTo)); + jsonMap.put("location", location == null ? null : location.getMapObject()); + jsonMap.put("embeds", embeds == null ? List.of() : embeds); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.createPatchComment(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error creating patch comment:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error creating patch comment", e); + return false; + } + } + + public boolean editPatchComment( + String repoId, String patchId, String revisionId, String commentId, String comment, List embeds) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("patch_id", patchId); + jsonMap.put("revision_id", revisionId); + jsonMap.put("comment_id", commentId); + jsonMap.put("comment", comment); + jsonMap.put("embeds", embeds == null ? List.of() : embeds); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.editPatchComment(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error creating patch comment:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error creating patch comment", e); + return false; + } + } + + public boolean deletePatchComment(String repoId, String patchId, String revisionId, String commentId) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("patch_id", patchId); + jsonMap.put("revision_id", revisionId); + jsonMap.put("comment_id", commentId); + jsonMap.put("comment", ""); + jsonMap.put("embeds", List.of()); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.deletePatchComment(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error deleting patch comment:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error deleting patch comment", e); + return false; + } + } + + public boolean editIssueComment( + String repoId, String issueId, String commentId, String comment, List embeds) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("issue_id", issueId); + jsonMap.put("comment_id", commentId); + jsonMap.put("comment", comment); + jsonMap.put("embeds", embeds == null ? List.of() : embeds); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.editIssueComment(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error editing issue comment:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error editing issue comment", e); + return false; + } + } + + public boolean patchCommentReact( + String repoId, String patchId, String revisionId, String commentId, String reaction, boolean active) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("patch_id", patchId); + jsonMap.put("revision_id", revisionId); + jsonMap.put("comment_id", commentId); + jsonMap.put("reaction", reaction); + jsonMap.put("active", active); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.patchCommentReact(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error adding patch comment reaction:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error adding patch comment reaction", e); + return false; + } + } + + public boolean issueCommentReact( + String repoId, String issueId, String commentId, String reaction, boolean active) { + if (javaRad == null) { + return false; + } + try { + Map jsonMap = new HashMap<>(); + jsonMap.put("repo_id", repoId); + jsonMap.put("issue_id", issueId); + jsonMap.put("comment_id", commentId); + jsonMap.put("reaction", reaction); + jsonMap.put("active", active); + var json = RadicleCliService.MAPPER.writeValueAsString(jsonMap); + var res = javaRad.issueCommentReact(json); + var resp = RadicleCliService.MAPPER.readValue(res, JRadResponse.class); + if (resp == null || !resp.ok) { + logger.warn("Error adding issue comment reaction:" + resp); + return false; + } + return true; + } catch (Throwable e) { + logger.warn("Error adding issue comment reaction", e); + return false; + } + } + + public static String normalizeNid(String nid) { + nid = Strings.nullToEmpty(nid); + if (nid.startsWith("did:key:")) { + nid = nid.substring(8); + } + return nid; + } + + public static Path createTempFileFromJar(final String library) throws Exception { + // create temp file + var tempDir = Files.createTempDirectory("radicle-jetbrains-plugin-native-" + System.currentTimeMillis()); + var tempFile = Files.createFile(tempDir.resolve(library)); + try (var is = RadicleNativeService.class.getResourceAsStream("/META-INF/jrad/" + library)) { + Files.copy(Objects.requireNonNull(is), tempFile, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + logger.warn("error opening library resource stream: " + e); + } finally { + // set folder to be deleted after VM shuts down + tempFile.toFile().deleteOnExit(); + tempDir.toFile().deleteOnExit(); + } + return tempFile; + } + + public static void load() { + if (javaRad != null || loadError) { + return; + } + try { + var libName = System.mapLibraryName("jrad"); + libFile = createTempFileFromJar(libName); + javaRad = LibraryLoader.create(JavaRad.class) + .search(Paths.get("./jrad/target/release").toAbsolutePath().normalize().toString()) + .search(Paths.get("./jrad/target/debug").toAbsolutePath().normalize().toString()) + .search(libFile == null ? "." : libFile.toAbsolutePath().getParent().toString()) + .failImmediately() + .load("jrad"); + } catch (Throwable t) { + logger.warn("Error loading native library", t); + loadError = true; + } + } + + public record JRadResponse(boolean ok, String msg) { } + + public interface JavaRad { + String radHome(String input); + String changeIssueTitleDescription(String input); + String getEmbeds(String input); + String getAlias(String input); + String createPatchComment(String input); + String editPatchComment(String input); + String deletePatchComment(String input); + String editIssueComment(String input); + String patchCommentReact(String input); + String issueCommentReact(String input); + } +} diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleProjectService.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleProjectService.java index b141cc66..2b4d36fb 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleProjectService.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/RadicleProjectService.java @@ -55,10 +55,10 @@ public class RadicleProjectService { private static final Logger logger = LoggerFactory.getLogger(RadicleProjectService.class); private static final int TIMEOUT = 60_000; - private final RadicleProjectSettingsHandler projectSettingsHandler; - private RadDetails radDetails; - private String wslDistro; - private Project project; + protected final RadicleProjectSettingsHandler projectSettingsHandler; + protected RadDetails radDetails; + protected String wslDistro; + protected Project project; public RadicleProjectService(Project project) { this(new RadicleProjectSettingsHandler(project)); @@ -437,11 +437,11 @@ public ProcessOutput addReview(GitRepository repo, String verdict, String messag public ProcessOutput changePatchState(GitRepository repo, String patchId, String currState, String state) { if (Strings.isNullOrEmpty(currState) || Strings.isNullOrEmpty(state) || currState.equals(state)) { - logger.error("cannot change patch state with invalid curr:{}/new:{} states for patch:{}", currState, state, patchId); + logger.warn("cannot change patch state with invalid curr:{}/new:{} states for patch:{}", currState, state, patchId); return new ProcessOutput(-1); } if (RadPatch.State.MERGED.status.equals(currState) || RadPatch.State.MERGED.status.equals(state)) { - logger.error("cannot change patch state to/from merged for patch:{}", patchId); + logger.warn("cannot change patch state to/from merged for patch:{}", patchId); return new ProcessOutput(-1); } ProcessOutput res = null; @@ -547,8 +547,8 @@ public ProcessOutput runCommand(GeneralCommandLine cmdLine, GitRepository repo, } } return result; - } catch (ExecutionException ex) { - logger.error("unable to execute rad command", ex); + } catch (Exception ex) { + logger.warn("unable to execute rad command", ex); return new ProcessOutput(-1); } } diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/auth/AuthService.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/auth/AuthService.java index 1ba7ab5c..80e75fa2 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/auth/AuthService.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/services/auth/AuthService.java @@ -82,7 +82,7 @@ public IdentityDialog.IdentityDialogData showIdentityDialog(String title, boolea try { latch.await(); } catch (InterruptedException e) { - logger.error("error awaiting update latch!", e); + logger.warn("error awaiting update latch!", e); return null; } if (okButton.get()) { diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/DragAndDropField.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/DragAndDropField.java index 20dbc542..748221b5 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/DragAndDropField.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/DragAndDropField.java @@ -119,9 +119,7 @@ public void dragOver(DropTargetDragEvent dtde) { public void drop(DropTargetDropEvent evt) { try { evt.acceptDrop(DnDConstants.ACTION_COPY); - var droppedFiles = (List) evt - .getTransferable().getTransferData( - DataFlavor.javaFileListFlavor); + var droppedFiles = (List) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); for (var file : droppedFiles) { var fileName = file.getName(); var fileBytes = Files.readAllBytes(file.toPath()); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/EmojiPanel.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/EmojiPanel.java index fda81915..50c1133f 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/EmojiPanel.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/EmojiPanel.java @@ -3,6 +3,7 @@ import com.intellij.collaboration.ui.SingleValueModel; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupListener; import com.intellij.ui.AnimatedIcon; @@ -12,8 +13,9 @@ import network.radicle.jetbrains.radiclejetbrainsplugin.models.Emoji; import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadAuthor; import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadDetails; -import network.radicle.jetbrains.radiclejetbrainsplugin.models.RadPatch; import network.radicle.jetbrains.radiclejetbrainsplugin.models.Reaction; +import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleNativeService; +import org.assertj.core.util.Strings; import javax.swing.BorderFactory; import javax.swing.JLabel; @@ -41,16 +43,20 @@ public abstract class EmojiPanel { private final List reactions; private final String discussionId; private final RadDetails radDetails; + private final Project project; + private final RadicleNativeService rad; private JBPopup reactorsPopUp; private JBPopup emojisPopUp; private JBPopupListener popupListener; private CountDownLatch latch; - protected EmojiPanel(SingleValueModel model, List reactions, String discussionId, RadDetails radDetails) { + protected EmojiPanel(Project project, SingleValueModel model, List reactions, String discussionId, RadDetails radDetails) { + this.project = project; this.model = model; this.reactions = reactions; this.discussionId = discussionId; this.radDetails = radDetails; + this.rad = project.getService(RadicleNativeService.class); } private static class EmojiRender extends SelectionListCellRenderer { @@ -143,13 +149,18 @@ public void mouseClicked(MouseEvent e) { }); var borderPanel = new BorderLayoutPanel(); borderPanel.setOpaque(false); - if (!(this.model.getValue() instanceof RadPatch)) { - // TODO: disable reactions on patch comments, not supported from CLI - borderPanel.addToLeft(emojiButton); - } + borderPanel.addToLeft(emojiButton); + var horizontalPanel = getHorizontalPanel(10); horizontalPanel.setOpaque(false); horizontalPanel.add(progressLabel); + for (var r : reactions) { + for (var a : r.authors()) { + if (Strings.isNullOrEmpty(a.alias)) { + a.alias = rad.getAlias(a.id); + } + } + } var groupReactions = groupEmojis(reactions); for (var emojiUnicode : groupReactions.keySet()) { var reactorEmoji = new JLabel(emojiUnicode); diff --git a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/MarkDownEditorPaneFactory.java b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/MarkDownEditorPaneFactory.java index d6c36215..3d656e66 100644 --- a/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/MarkDownEditorPaneFactory.java +++ b/src/main/java/network/radicle/jetbrains/radiclejetbrainsplugin/toolwindow/MarkDownEditorPaneFactory.java @@ -13,32 +13,35 @@ import com.intellij.util.ui.JBFont; import com.intellij.util.ui.JBInsets; import com.intellij.util.ui.StyleSheetUtil; -import network.radicle.jetbrains.radiclejetbrainsplugin.config.RadicleProjectSettings; -import network.radicle.jetbrains.radiclejetbrainsplugin.config.RadicleProjectSettingsHandler; import network.radicle.jetbrains.radiclejetbrainsplugin.models.Embed; +import network.radicle.jetbrains.radiclejetbrainsplugin.services.RadicleNativeService; import org.apache.tika.Tika; import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil; import org.jetbrains.annotations.NotNull; -import javax.swing.JTextPane; import javax.swing.JEditorPane; +import javax.swing.JTextPane; import javax.swing.event.HyperlinkEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; public class MarkDownEditorPaneFactory { public static final String IMG_WIDTH = "450px"; - private final RadicleProjectSettings settings; + public static final String EMBED_REGEX = "\\[([^]]+)\\]\\(([^)]+)\\)"; + public static final Pattern EMBED_PATTERN = Pattern.compile(EMBED_REGEX); + public static final String IMG_REGEX = "]*>"; + public static final Pattern IMG_PATTERN = Pattern.compile(IMG_REGEX); private final String radProjectId; private final VirtualFile file; private final Project project; private final String rawContent; private String content; + private Map embedMap; public MarkDownEditorPaneFactory(String content, Project project, String radProjectId, VirtualFile file) { - this.settings = new RadicleProjectSettingsHandler(project).loadSettings(); this.content = content; this.radProjectId = radProjectId; this.file = file; @@ -79,10 +82,8 @@ public String getRawContent() { } private List getAllImgHtmlTags() { + var matcher = IMG_PATTERN.matcher(this.content); var imgTags = new ArrayList(); - var imgPattern = "]*>"; - var pattern = Pattern.compile(imgPattern); - var matcher = pattern.matcher(this.content); while (matcher.find()) { imgTags.add(matcher.group()); } @@ -91,9 +92,7 @@ private List getAllImgHtmlTags() { private List findEmbedList() { // Find all embeds from the content e.g [name.jpg](4f4ba) - String regex = "\\[([^]]+)\\]\\(([^)]+)\\)"; - var pattern = Pattern.compile(regex); - var matcher = pattern.matcher(this.rawContent); + var matcher = EMBED_PATTERN.matcher(this.rawContent); var embedList = new ArrayList(); while (matcher.find()) { String filename = matcher.group(1); @@ -103,22 +102,6 @@ private List findEmbedList() { return embedList; } - private boolean isExternalFile(String objectId) { - return objectId.contains("https://") || objectId.contains("http://"); - } - - private boolean isSvg(String mimeType) { - return mimeType.contains("svg"); - } - - private String findMimeType(Embed embed) { - var tika = new Tika(); - if (isExternalFile(embed.getOid())) { - return tika.detect(embed.getOid()); - } - return tika.detect(embed.getName()); - } - private void replaceHtmlTags() { var embedList = findEmbedList(); if (embedList.isEmpty()) { @@ -129,6 +112,16 @@ private void replaceHtmlTags() { return; } var map = new HashMap(); + var embedOids = embedList.stream().map(Embed::getOid).filter(oid -> !Strings.isNullOrEmpty(oid)).filter(oid -> !isExternalFile(oid)).toList(); + var radNative = project.getService(RadicleNativeService.class); + var repoId = radProjectId; + if (repoId.startsWith("rad:")) { + repoId = repoId.substring(4); + } + embedMap = radNative.getEmbeds(repoId, embedOids); + if (embedMap == null) { + embedMap = new HashMap<>(); + } for (var embed : embedList) { var objectId = embed.getOid(); if (Strings.isNullOrEmpty(objectId)) { @@ -161,19 +154,43 @@ private String getBlobUrl(String objectId, String mimeType) { if (isExternalFile(objectId)) { return objectId; } - var url = settings.getSeedNode() + "/raw/" + radProjectId + "/blobs/" + objectId; - if (!Strings.isNullOrEmpty(mimeType)) { - //In order to know the browser what type of file is and open it - url += "?mime=" + mimeType; + // return a data url + if (Strings.isNullOrEmpty(mimeType)) { + mimeType = "image/png"; } - return url; + + var b64 = embedMap.get(objectId); + if (Strings.isNullOrEmpty(b64)) { + return objectId; + } + return "data:" + mimeType + ";base64," + b64; } - private String getHrefTag(String url, String fileName) { + private void convertMarkdownToHtml() { + this.content = MarkdownUtil.INSTANCE.generateMarkdownHtml(file, this.content, project); + } + + public static boolean isExternalFile(String objectId) { + return objectId.contains("https://") || objectId.contains("http://"); + } + + public static boolean isSvg(String mimeType) { + return mimeType.contains("svg"); + } + + public static String findMimeType(Embed embed) { + var tika = new Tika(); + if (isExternalFile(embed.getOid())) { + return tika.detect(embed.getOid()); + } + return tika.detect(embed.getName()); + } + + public static String getHrefTag(String url, String fileName) { return "" + fileName + ""; } - private String getImgTag(String url) { + public static String getImgTag(String url) { return "
"; } @@ -181,8 +198,4 @@ public static String wrapHtml(String body) { return "