diff --git a/Cargo.lock b/Cargo.lock index 027395c8fb..5b97c43023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,15 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "api_client" +version = "0.16.0" +dependencies = [ + "jj-api", + "tokio", + "tonic", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -152,6 +161,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -169,6 +200,51 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes 1.6.0", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes 1.6.0", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backoff" version = "0.4.0" @@ -195,6 +271,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -1165,7 +1247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", - "hashbrown", + "hashbrown 0.14.3", "parking_lot", ] @@ -1187,7 +1269,7 @@ dependencies = [ "gix-object", "gix-traverse", "gix-utils", - "hashbrown", + "hashbrown 0.14.3", "itoa", "libc", "memmap2", @@ -1470,6 +1552,25 @@ dependencies = [ "regex-syntax 0.8.2", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes 1.6.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util 0.7.10", + "tracing", +] + [[package]] name = "half" version = "2.4.0" @@ -1480,6 +1581,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1523,6 +1630,82 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes 1.6.0", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes 1.6.0", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes 1.6.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1572,6 +1755,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1579,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -1663,6 +1856,19 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jj-api" +version = "0.16.0" +dependencies = [ + "hex", + "prost", + "prost-build", + "prost-types", + "protoc-bin-vendored", + "tonic", + "tonic-build", +] + [[package]] name = "jj-cli" version = "0.16.0" @@ -1687,10 +1893,11 @@ dependencies = [ "git2", "gix", "hex", - "indexmap", + "indexmap 2.2.6", "indoc", "insta", "itertools 0.12.1", + "jj-api", "jj-cli", "jj-lib", "libc", @@ -1743,6 +1950,7 @@ dependencies = [ "indoc", "insta", "itertools 0.12.1", + "jj-api", "jj-lib-proc-macros", "maplit", "num_cpus", @@ -1752,6 +1960,7 @@ dependencies = [ "pollster", "pretty_assertions", "prost", + "prost-types", "rand", "rand_chacha", "rayon", @@ -1767,6 +1976,8 @@ dependencies = [ "testutils", "thiserror", "tokio", + "tonic", + "tonic-web", "tracing", "version_check", "watchman_client", @@ -1889,9 +2100,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1918,6 +2129,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.1" @@ -1933,6 +2150,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2101,9 +2324,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -2192,7 +2415,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.2.6", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2374,6 +2617,56 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + [[package]] name = "quote" version = "1.0.36" @@ -2824,6 +3117,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" version = "3.10.1" @@ -3023,6 +3322,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.2.0" @@ -3034,6 +3343,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.10" @@ -3050,6 +3370,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes 1.6.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.11" @@ -3074,13 +3408,123 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", "winnow 0.5.40", ] +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes 1.6.0", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tonic-web" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3b0e1cedbf19fdfb78ef3d672cb9928e0a91a9cb4629cc0c916e8cff8aaaa1" +dependencies = [ + "base64", + "bytes 1.6.0", + "http", + "http-body", + "hyper", + "pin-project", + "tokio-stream", + "tonic", + "tower-http", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util 0.7.10", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.5.0", + "bytes 1.6.0", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -3153,6 +3597,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -3273,6 +3723,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3353,7 +3812,7 @@ dependencies = [ "serde_bser", "thiserror", "tokio", - "tokio-util", + "tokio-util 0.6.10", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index 6812e75680..9cfe4b5c91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = [] [workspace] resolver = "2" -members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] +members = [ "api", "api_client","cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] [workspace.package] version = "0.16.0" @@ -70,8 +70,10 @@ pest_derive = "2.7.9" pollster = "0.3.0" pretty_assertions = "1.4.0" proc-macro2 = "1.0.81" +protoc-bin-vendored = "3.0.0" prost = "0.12.3" prost-build = "0.12.3" +prost-types = "0.12.3" quote = "1.0.36" rand = "0.8.5" rand_chacha = "0.3.1" @@ -96,8 +98,12 @@ test-case = "3.3.1" textwrap = "0.16.1" thiserror = "1.0.59" timeago = { version = "0.4.2", default-features = false } -tokio = { version = "1.37.0" } +tokio = { version = "1.37.0", features = ["rt", "macros"] } toml_edit = { version = "0.19.15", features = ["serde"] } +tonic = "0.11.0" +tonic-build = "0.11.0" +tonic-reflection = "0.11.0" +tonic-web = "0.11.0" tracing = "0.1.40" tracing-chrome = "0.7.2" tracing-subscriber = { version = "0.3.18", default-features = false, features = [ @@ -117,6 +123,7 @@ zstd = "0.12.4" # their own (alphabetically sorted) block jj-lib = { path = "lib", version = "0.16.0" } +jj-api = { path = "api", version = "0.16.0" } jj-lib-proc-macros = { path = "lib/proc-macros", version = "0.16.0" } testutils = { path = "lib/testutils" } diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000000..ca1d972c65 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,24 @@ +# This crate contains the proto files that correspond to the API for jj. +[package] +name = "jj-api" +version.workspace = true +license.workspace = true +rust-version.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +hex.workspace = true +prost.workspace = true +prost-types.workspace = true +tonic.workspace = true + +[build-dependencies] +prost-build.workspace = true +protoc-bin-vendored.workspace = true +tonic-build.workspace = true \ No newline at end of file diff --git a/api/build.rs b/api/build.rs new file mode 100644 index 0000000000..2cb0eb16d5 --- /dev/null +++ b/api/build.rs @@ -0,0 +1,42 @@ +use std::io::Result; +use std::path::{Path, PathBuf}; + +fn list_files(dir: &Path) -> impl Iterator { + std::fs::read_dir(&dir) + .unwrap() + .into_iter() + .filter_map(|res| { + let res = res.unwrap(); + res.file_type().unwrap().is_file().then_some(res.path()) + }) +} + +fn main() -> Result<()> { + // Doesn't support all architectures (namely, M1 macs), so for now we can't just unwrap it. + if let Ok(protoc) = protoc_bin_vendored::protoc_bin_path() { + std::env::set_var("PROTOC", protoc); + } + + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let generated = crate_root.join("src/generated"); + let input_dir = crate_root.join("proto"); + let proto_files: Vec = list_files(&input_dir.join("rpc")) + .chain(list_files(&input_dir.join("objects"))) + .collect(); + let service_files: Vec = list_files(&input_dir.join("services")).collect(); + + prost_build::Config::new() + .out_dir(&generated) + .include_file(generated.join("mod.rs")) + .compile_protos(&proto_files, &[&input_dir]) + .unwrap(); + + tonic_build::configure() + .out_dir(&generated) + .build_client(true) + .build_server(true) + .compile(&service_files, &[&input_dir]) + .unwrap(); + + Ok(()) +} diff --git a/api/proto/objects/change.proto b/api/proto/objects/change.proto new file mode 100644 index 0000000000..306ec22ca0 --- /dev/null +++ b/api/proto/objects/change.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package jj_api.objects; + +message Change { + string change_id = 1; + string commit_id = 2; + // TODO: add more fields. +} \ No newline at end of file diff --git a/api/proto/objects/repo_options.proto b/api/proto/objects/repo_options.proto new file mode 100644 index 0000000000..6e9f3c74f8 --- /dev/null +++ b/api/proto/objects/repo_options.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package jj_api.objects; + +message RepoOptions { + string repo_path = 1; + string at_operation = 2; +} diff --git a/api/proto/objects/workspace.proto b/api/proto/objects/workspace.proto new file mode 100644 index 0000000000..9363f6af65 --- /dev/null +++ b/api/proto/objects/workspace.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +import "objects/change.proto"; + +package jj_api.objects; + + +message Workspace { + string workspace_id = 1; + + jj_api.objects.Change change = 2; +} diff --git a/api/proto/rpc/ListWorkspacesRequest.proto b/api/proto/rpc/ListWorkspacesRequest.proto new file mode 100644 index 0000000000..1339b493ec --- /dev/null +++ b/api/proto/rpc/ListWorkspacesRequest.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "objects/repo_options.proto"; +import "google/protobuf/field_mask.proto"; + +package jj_api.rpc; + +message ListWorkspacesRequest { + jj_api.objects.RepoOptions repo = 1; + google.protobuf.FieldMask field_mask = 2; +} \ No newline at end of file diff --git a/api/proto/rpc/ListWorkspacesResponse.proto b/api/proto/rpc/ListWorkspacesResponse.proto new file mode 100644 index 0000000000..2d1914db1a --- /dev/null +++ b/api/proto/rpc/ListWorkspacesResponse.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +import "objects/workspace.proto"; + +package jj_api.rpc; + +message ListWorkspacesResponse { + repeated jj_api.objects.Workspace workspace = 1; +} \ No newline at end of file diff --git a/api/proto/services/service.proto b/api/proto/services/service.proto new file mode 100644 index 0000000000..6bd30e2e57 --- /dev/null +++ b/api/proto/services/service.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "rpc/ListWorkspacesRequest.proto"; +import "rpc/ListWorkspacesResponse.proto"; + +package jj_api.services; + +// Handles things that operate on a given workspace. +// Most commands will be contained within here, but commands such as `jj init` +// will not. +service JjService { + rpc ListWorkspaces(jj_api.rpc.ListWorkspacesRequest) returns (jj_api.rpc.ListWorkspacesResponse) {} +} \ No newline at end of file diff --git a/api/src/from_proto.rs b/api/src/from_proto.rs new file mode 100644 index 0000000000..be3c6d07d2 --- /dev/null +++ b/api/src/from_proto.rs @@ -0,0 +1,25 @@ +use std::path::{Path, PathBuf}; +use tonic::Status; + +pub fn option_str(value: &str) -> Option<&str> { + if value == "" { + None + } else { + Some(&value) + } +} + +pub fn path(value: &str) -> Option<&Path> { + option_str(value).map(Path::new) +} + +pub fn pathbuf(value: &str) -> Option { + path(value).map(PathBuf::from) +} + +pub fn hex(value: &str) -> Result>, Status> { + option_str(value) + .map(hex::decode) + .transpose() + .map_err(|err| Status::invalid_argument(err.to_string())) +} diff --git a/api/src/generated/jj_api.objects.rs b/api/src/generated/jj_api.objects.rs new file mode 100644 index 0000000000..819dbcb7b7 --- /dev/null +++ b/api/src/generated/jj_api.objects.rs @@ -0,0 +1,26 @@ +// This file is @generated by prost-build. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RepoOptions { + #[prost(string, tag = "1")] + pub repo_path: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub at_operation: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Change { + #[prost(string, tag = "1")] + pub change_id: ::prost::alloc::string::String, + /// TODO: add more fields. + #[prost(string, tag = "2")] + pub commit_id: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Workspace { + #[prost(string, tag = "1")] + pub workspace_id: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub change: ::core::option::Option, +} diff --git a/api/src/generated/jj_api.rpc.rs b/api/src/generated/jj_api.rpc.rs new file mode 100644 index 0000000000..cd99f8214a --- /dev/null +++ b/api/src/generated/jj_api.rpc.rs @@ -0,0 +1,15 @@ +// This file is @generated by prost-build. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListWorkspacesRequest { + #[prost(message, optional, tag = "1")] + pub repo: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub field_mask: ::core::option::Option<::prost_types::FieldMask>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListWorkspacesResponse { + #[prost(message, repeated, tag = "1")] + pub workspace: ::prost::alloc::vec::Vec, +} diff --git a/api/src/generated/jj_api.services.rs b/api/src/generated/jj_api.services.rs new file mode 100644 index 0000000000..55a36ef5e5 --- /dev/null +++ b/api/src/generated/jj_api.services.rs @@ -0,0 +1,303 @@ +// This file is @generated by prost-build. +/// Generated client implementations. +pub mod jj_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Handles things that operate on a given workspace. + /// Most commands will be contained within here, but commands such as `jj init` + /// will not. + #[derive(Debug, Clone)] + pub struct JjServiceClient { + inner: tonic::client::Grpc, + } + impl JjServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl JjServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> JjServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + JjServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn list_workspaces( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/jj_api.services.JjService/ListWorkspaces", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("jj_api.services.JjService", "ListWorkspaces")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod jj_service_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with JjServiceServer. + #[async_trait] + pub trait JjService: Send + Sync + 'static { + async fn list_workspaces( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Handles things that operate on a given workspace. + /// Most commands will be contained within here, but commands such as `jj init` + /// will not. + #[derive(Debug)] + pub struct JjServiceServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl JjServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for JjServiceServer + where + T: JjService, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/jj_api.services.JjService/ListWorkspaces" => { + #[allow(non_camel_case_types)] + struct ListWorkspacesSvc(pub Arc); + impl< + T: JjService, + > tonic::server::UnaryService< + super::super::rpc::ListWorkspacesRequest, + > for ListWorkspacesSvc { + type Response = super::super::rpc::ListWorkspacesResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::rpc::ListWorkspacesRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_workspaces(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = ListWorkspacesSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for JjServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for JjServiceServer { + const NAME: &'static str = "jj_api.services.JjService"; + } +} diff --git a/api/src/generated/mod.rs b/api/src/generated/mod.rs new file mode 100644 index 0000000000..2517cc2c5f --- /dev/null +++ b/api/src/generated/mod.rs @@ -0,0 +1,9 @@ +// This file is @generated by prost-build. +pub mod jj_api { + pub mod objects { + include!("jj_api.objects.rs"); + } + pub mod rpc { + include!("jj_api.rpc.rs"); + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000000..a90f7d79ca --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,17 @@ +mod generated; + +// Because we declare all our .proto files under the package "jj_api", inside the crate jj_api, we +// need to un-nest them so that they don't appear as jj_api::jj_api::foo. +pub use crate::generated::jj_api::*; + +mod services { + include!("generated/jj_api.services.rs"); +} + +pub use services::jj_service_client as client; +pub use services::jj_service_server as server; + +mod to_proto; +pub use to_proto::ToProto; + +pub mod from_proto; diff --git a/api/src/to_proto.rs b/api/src/to_proto.rs new file mode 100644 index 0000000000..301bcd06f4 --- /dev/null +++ b/api/src/to_proto.rs @@ -0,0 +1,18 @@ +use std::path::{Path, PathBuf}; + +pub trait ToProto { + fn to_proto(&self) -> T; +} + +// For some reason I was getting errors with `for AsRef`. +impl ToProto for Path { + fn to_proto(&self) -> String { + self.to_string_lossy().to_string() + } +} + +impl ToProto for PathBuf { + fn to_proto(&self) -> String { + self.to_string_lossy().to_string() + } +} diff --git a/api_client/Cargo.toml b/api_client/Cargo.toml new file mode 100644 index 0000000000..64a13edb17 --- /dev/null +++ b/api_client/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "api_client" +version.workspace = true +license.workspace = true +rust-version.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tonic.workspace = true +jj-api.workspace = true +tokio = { workspace = true, features = ["full"] } \ No newline at end of file diff --git a/api_client/src/main.rs b/api_client/src/main.rs new file mode 100644 index 0000000000..e521b3441c --- /dev/null +++ b/api_client/src/main.rs @@ -0,0 +1,15 @@ +use jj_api::client::JjServiceClient; +use jj_api::rpc::ListWorkspacesRequest; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = JjServiceClient::connect("http://[::1]:8888").await?; + + let request = tonic::Request::new(ListWorkspacesRequest::default()); + + let response = client.list_workspaces(request).await?; + + println!("RESPONSE={:?}", response); + + Ok(()) +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8d2b41c514..bd08b7b9f0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -63,6 +63,7 @@ gix = { workspace = true } hex = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } +jj-api = { workspace = true } jj-lib = { workspace = true } maplit = { workspace = true } minus = { workspace = true } diff --git a/cli/src/commands/api.rs b/cli/src/commands/api.rs new file mode 100644 index 0000000000..9608d49f3d --- /dev/null +++ b/cli/src/commands/api.rs @@ -0,0 +1,62 @@ +// Copyright 2020 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::arg; +use jj_lib::api::server::{start_api, GrpcOptions, StartupOptions}; +use jj_lib::api::servicer::Servicer; +use std::fmt::Debug; + + + + +use tracing::instrument; + +use crate::command_error::{CommandError, CommandErrorKind}; +use crate::commands::CommandHelper; +use crate::ui::Ui; + +#[derive(clap::Subcommand, Clone, Debug)] +pub enum ApiCommand { + Grpc(GrpcArgs), +} + +#[derive(clap::Args, Clone, Debug)] +pub struct GrpcArgs { + #[arg(long)] + port: u16, + + #[arg(long)] + web: bool, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_api( + _ui: &mut Ui, + command: &CommandHelper, + subcommand: &ApiCommand, +) -> Result<(), CommandError> { + let startup_options = match subcommand { + ApiCommand::Grpc(args) => StartupOptions::Grpc(GrpcOptions { + port: args.port, + web: args.web, + }), + }; + // Running jj api from a non-jj repository is still valid, as the user can provide the repository path in each individual request. + let default_workspace_loader = command.workspace_loader().ok(); + start_api( + startup_options, + Servicer::new(default_workspace_loader.cloned()), + ) + .map_err(|e| CommandError::new(CommandErrorKind::Internal, e)) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 4007ca8259..ad6c1993f8 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod abandon; +mod api; mod backout; #[cfg(feature = "bench")] mod bench; @@ -69,6 +70,8 @@ use crate::ui::Ui; #[derive(clap::Parser, Clone, Debug)] enum Command { + #[command(subcommand)] + Api(api::ApiCommand), Abandon(abandon::AbandonArgs), Backout(backout::BackoutArgs), #[cfg(feature = "bench")] @@ -203,6 +206,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Branch(sub_args) => branch::cmd_branch(ui, command_helper, sub_args), Command::Undo(sub_args) => operation::cmd_op_undo(ui, command_helper, sub_args), Command::Operation(sub_args) => operation::cmd_operation(ui, command_helper, sub_args), + Command::Api(sub_args) => api::cmd_api(ui, command_helper, sub_args), Command::Workspace(sub_args) => workspace::cmd_workspace(ui, command_helper, sub_args), Command::Sparse(sub_args) => sparse::cmd_sparse(ui, command_helper, sub_args), Command::Tag(sub_args) => tag::cmd_tag(ui, command_helper, sub_args), diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d7a68f6a13..51e59fbda3 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -48,6 +48,7 @@ glob = { workspace = true } hex = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } +jj-api = { workspace = true } jj-lib-proc-macros = { workspace = true } maplit = { workspace = true } once_cell = { workspace = true } @@ -66,7 +67,10 @@ smallvec = { workspace = true } strsim = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, optional = true } +tokio = { workspace = true } +tonic = { workspace = true } +tonic-web = { workspace = true } +prost-types = { workspace = true } tracing = { workspace = true } watchman_client = { workspace = true, optional = true } whoami = { workspace = true } @@ -93,5 +97,5 @@ tokio = { workspace = true, features = ["full"] } [features] default = [] vendored-openssl = ["git2/vendored-openssl"] -watchman = ["dep:tokio", "dep:watchman_client"] +watchman = ["dep:watchman_client"] testing = [] diff --git a/lib/src/api/from_proto.rs b/lib/src/api/from_proto.rs new file mode 100644 index 0000000000..ecf5903892 --- /dev/null +++ b/lib/src/api/from_proto.rs @@ -0,0 +1,9 @@ +use crate::op_store::OperationId; +use jj_api::from_proto; +use tonic::Status; + +pub(crate) use jj_api::from_proto::*; + +pub(crate) fn operation_id(value: &str) -> Result, Status> { + Ok(from_proto::hex(value)?.map(|bytes| OperationId::from_bytes(&bytes))) +} diff --git a/lib/src/api/grpc_servicer.rs b/lib/src/api/grpc_servicer.rs new file mode 100644 index 0000000000..3c7565cd10 --- /dev/null +++ b/lib/src/api/grpc_servicer.rs @@ -0,0 +1,28 @@ +use crate::api::servicer::Servicer; +use jj_api::rpc::{ListWorkspacesRequest, ListWorkspacesResponse}; +use jj_api::server::JjService; +use tonic::{Request, Response, Status}; + +pub struct GrpcServicer { + servicer: Servicer, +} + +impl GrpcServicer { + pub fn new(servicer: Servicer) -> Self { + Self { servicer } + } +} + +#[tonic::async_trait] +impl JjService for GrpcServicer { + // TODO: this should be boilerplate. Maybe turn it into macros. + // eg. rpc!(list_workspaces, ListWorkspacesRequest, ListWorkspacesResponse) + async fn list_workspaces( + &self, + request: Request, + ) -> Result, Status> { + self.servicer + .list_workspaces(request.get_ref()) + .map(Response::new) + } +} diff --git a/lib/src/api/mod.rs b/lib/src/api/mod.rs new file mode 100644 index 0000000000..de6812ff2f --- /dev/null +++ b/lib/src/api/mod.rs @@ -0,0 +1,6 @@ +mod from_proto; +mod grpc_servicer; +pub mod servicer; +mod status; + +pub mod server; diff --git a/lib/src/api/server.rs b/lib/src/api/server.rs new file mode 100644 index 0000000000..37a6e187ae --- /dev/null +++ b/lib/src/api/server.rs @@ -0,0 +1,48 @@ +use crate::api::grpc_servicer::GrpcServicer; +use crate::api::servicer::Servicer; +use jj_api::server::JjServiceServer; +use tonic::transport::Server; + +pub enum StartupOptions { + Grpc(GrpcOptions), +} + +pub struct GrpcOptions { + pub port: u16, + pub web: bool, +} + +#[tokio::main(flavor = "current_thread")] +pub async fn start_api( + options: StartupOptions, + servicer: Servicer, +) -> Result<(), Box> { + match options { + StartupOptions::Grpc(options) => start_grpc(options, servicer), + } + .await +} + +pub async fn start_grpc( + options: GrpcOptions, + servicer: Servicer, +) -> Result<(), Box> { + let addr = format!("[::1]:{}", options.port).parse()?; + + let server = JjServiceServer::new(GrpcServicer::new(servicer)); + + let mut builder = Server::builder() + // The gRPC server is inherently async, but we want it to be synchronous. + .concurrency_limit_per_connection(1); + if options.web { + // GrpcWeb is over http1 so we must enable it. + builder + .accept_http1(true) + .add_service(tonic_web::enable(server)) + } else { + builder.add_service(server) + } + .serve(addr) + .await?; + Ok(()) +} diff --git a/lib/src/api/servicer.rs b/lib/src/api/servicer.rs new file mode 100644 index 0000000000..43c99f748d --- /dev/null +++ b/lib/src/api/servicer.rs @@ -0,0 +1,101 @@ +use crate::api::from_proto; +use crate::object_id::ObjectId; +use crate::repo::ReadonlyRepo; +use crate::settings::UserSettings; +use config::Config; +use itertools::Itertools; +use jj_api::objects::{Change as ChangeProto, Workspace as WorkspaceProto}; +use jj_api::rpc::{ListWorkspacesRequest, ListWorkspacesResponse}; +use jj_lib::op_store::OperationId; +use jj_lib::operation::Operation; +use jj_lib::repo::RepoLoader; +use jj_lib::workspace::WorkspaceLoader; + +use std::sync::Arc; +use tonic::Status; + +/// The servicer handles all requests going to jj-lib. Eventually, ideally, jj-cli +/// will interact with jj-lib purely through this class. +pub struct Servicer { + default_workspace_loader: Option, + user_settings: UserSettings, +} + +impl Servicer { + pub fn new(default_workspace_loader: Option) -> Self { + Self { + default_workspace_loader, + user_settings: UserSettings::from_config(Config::default()), + } + } + + fn workspace_loader( + &self, + opts: &Option, + ) -> Result { + opts.as_ref() + .map(|opts| from_proto::path(&opts.repo_path)) + .flatten() + .map(WorkspaceLoader::init) + .transpose()? + .or(self.default_workspace_loader.clone()) + .ok_or_else(|| { + Status::invalid_argument( + "No default workspace loader, and no repository.repo_path provided", + ) + }) + } + + fn repo( + &self, + opts: &Option, + ) -> Result, Status> { + let workspace_loader = self.workspace_loader(opts)?; + + let at_operation: Option = opts + .as_ref() + .map(|opts| from_proto::operation_id(&opts.at_operation)) + .transpose()? + .flatten(); + + let repo_loader = RepoLoader::init( + &self.user_settings, + &workspace_loader.repo_path(), + &Default::default(), + )?; + + Ok(match at_operation { + None => repo_loader.load_at_head(&self.user_settings), + Some(at_operation) => { + let op = repo_loader.op_store().read_operation(&at_operation)?; + repo_loader.load_at(&Operation::new( + repo_loader.op_store().clone(), + at_operation, + op, + )) + } + }?) + } + + pub fn list_workspaces( + &self, + request: &ListWorkspacesRequest, + ) -> Result { + let repo = self.repo(&request.repo)?; + Ok(ListWorkspacesResponse { + workspace: repo + .view() + .wc_commit_ids() + .iter() + .sorted() + .map(|(workspace_id, commit_id)| WorkspaceProto { + workspace_id: workspace_id.as_str().to_string(), + change: Some(ChangeProto { + commit_id: commit_id.hex(), + ..Default::default() + }), + }) + .collect::>(), + }) + } +} diff --git a/lib/src/api/status.rs b/lib/src/api/status.rs new file mode 100644 index 0000000000..7988751131 --- /dev/null +++ b/lib/src/api/status.rs @@ -0,0 +1,40 @@ +use crate::repo::{RepoLoaderError, StoreLoadError}; +use jj_lib::op_store::OpStoreError; +use jj_lib::workspace::WorkspaceLoadError; +use tonic::Status; + +impl From for Status { + fn from(value: StoreLoadError) -> Status { + Status::internal(value.to_string()) + } +} + +impl From for Status { + fn from(value: RepoLoaderError) -> Status { + (match value { + RepoLoaderError::OpHeadResolution { .. } => Status::not_found, + _ => Status::internal, + })(value.to_string()) + } +} + +impl From for Status { + fn from(value: OpStoreError) -> Status { + (match value { + OpStoreError::ObjectNotFound { .. } => Status::not_found, + _ => Status::internal, + })(value.to_string()) + } +} + +impl From for Status { + fn from(value: WorkspaceLoadError) -> Status { + (match value { + WorkspaceLoadError::RepoDoesNotExist(_) + | WorkspaceLoadError::NoWorkspaceHere(_) + | WorkspaceLoadError::NonUnicodePath + | WorkspaceLoadError::Path(_) => Status::invalid_argument, + _ => Status::internal, + })(value.to_string()) + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6db22c9f67..e8fe9ad790 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -28,6 +28,7 @@ extern crate self as jj_lib; #[macro_use] pub mod content_hash; +pub mod api; pub mod backend; pub mod commit; pub mod commit_builder;