Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype for jj api #3601

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
481 changes: 470 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 = [
Expand All @@ -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" }

Expand Down
24 changes: 24 additions & 0 deletions api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions api/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::io::Result;
use std::path::{Path, PathBuf};

fn list_files(dir: &Path) -> impl Iterator<Item = PathBuf> {
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<PathBuf> = list_files(&input_dir.join("rpc"))
.chain(list_files(&input_dir.join("objects")))
.collect();
let service_files: Vec<PathBuf> = 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(())
}
9 changes: 9 additions & 0 deletions api/proto/objects/change.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
syntax = "proto3";

package jj_api.objects;

message Change {
string change_id = 1;
string commit_id = 2;
// TODO: add more fields.
}
8 changes: 8 additions & 0 deletions api/proto/objects/repo_options.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
syntax = "proto3";

package jj_api.objects;

message RepoOptions {
string repo_path = 1;
string at_operation = 2;
}
12 changes: 12 additions & 0 deletions api/proto/objects/workspace.proto
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions api/proto/rpc/ListWorkspacesRequest.proto
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that field masks would be really handy because sometimes you only want change IDs, while sometimes you want full changes. Unfortunately, although the type exists, there's currently no way to actually do anything based on these field masks in rust.

}
9 changes: 9 additions & 0 deletions api/proto/rpc/ListWorkspacesResponse.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
syntax = "proto3";

import "objects/workspace.proto";

package jj_api.rpc;

message ListWorkspacesResponse {
repeated jj_api.objects.Workspace workspace = 1;
}
14 changes: 14 additions & 0 deletions api/proto/services/service.proto
Original file line number Diff line number Diff line change
@@ -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) {}
}
25 changes: 25 additions & 0 deletions api/src/from_proto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::path::{Path, PathBuf};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serialization and deserialization are a pain here. I wrote a bunch of wrappers because it gets to be a massive pain in the ass since protos are represented as a string, but in reality the type we usually want is Option<String>, Option<Path>, Option<ChangeID>, etc.

I'd like to see if we can think of a better way to do it, but I'm not hopeful.

Maybe we need to have every proto message have its own rust type, and implement

impl From<Change> for ChangeProto { ... }
impl TryFrom<ChangeProto> for Change { 
    type Error = tonic::Status,

    ....
}

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<PathBuf> {
path(value).map(PathBuf::from)
}

pub fn hex(value: &str) -> Result<Option<Vec<u8>>, Status> {
option_str(value)
.map(hex::decode)
.transpose()
.map_err(|err| Status::invalid_argument(err.to_string()))
}
26 changes: 26 additions & 0 deletions api/src/generated/jj_api.objects.rs
Original file line number Diff line number Diff line change
@@ -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<Change>,
}
15 changes: 15 additions & 0 deletions api/src/generated/jj_api.rpc.rs
Original file line number Diff line number Diff line change
@@ -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<super::objects::RepoOptions>,
#[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<super::objects::Workspace>,
}
Loading
Loading