Skip to content

Commit

Permalink
Add the API server.
Browse files Browse the repository at this point in the history
  • Loading branch information
matts1 committed Apr 30, 2024
1 parent 7e80d0e commit 701c7fd
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 7 deletions.
56 changes: 52 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ 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-web = "0.11.0"
tracing = "0.1.40"
tracing-chrome = "0.7.2"
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
Expand Down
8 changes: 6 additions & 2 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand All @@ -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 = []
9 changes: 9 additions & 0 deletions lib/src/api/from_proto.rs
Original file line number Diff line number Diff line change
@@ -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<Option<OperationId>, Status> {
Ok(from_proto::hex(value)?.map(|bytes| OperationId::from_bytes(&bytes)))
}
28 changes: 28 additions & 0 deletions lib/src/api/grpc_servicer.rs
Original file line number Diff line number Diff line change
@@ -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<ListWorkspacesRequest>,
) -> Result<Response<ListWorkspacesResponse>, Status> {
self.servicer
.list_workspaces(request.get_ref())
.map(Response::new)
}
}
6 changes: 6 additions & 0 deletions lib/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod from_proto;
mod grpc_servicer;
pub mod servicer;
mod status;

pub mod server;
48 changes: 48 additions & 0 deletions lib/src/api/server.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error + Send + Sync>> {
match options {
StartupOptions::Grpc(options) => start_grpc(options, servicer),
}
.await
}

pub async fn start_grpc(
options: GrpcOptions,
servicer: Servicer,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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(())
}
101 changes: 101 additions & 0 deletions lib/src/api/servicer.rs
Original file line number Diff line number Diff line change
@@ -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<WorkspaceLoader>,
user_settings: UserSettings,
}

impl Servicer {
pub fn new(default_workspace_loader: Option<WorkspaceLoader>) -> Self {
Self {
default_workspace_loader,
user_settings: UserSettings::from_config(Config::default()),
}
}

fn workspace_loader(
&self,
opts: &Option<jj_api::objects::RepoOptions>,
) -> Result<WorkspaceLoader, Status> {
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<jj_api::objects::RepoOptions>,
) -> Result<Arc<ReadonlyRepo>, Status> {
let workspace_loader = self.workspace_loader(opts)?;

let at_operation: Option<OperationId> = 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<ListWorkspacesResponse, Status> {
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::<Vec<WorkspaceProto>>(),
})
}
}
40 changes: 40 additions & 0 deletions lib/src/api/status.rs
Original file line number Diff line number Diff line change
@@ -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<StoreLoadError> for Status {
fn from(value: StoreLoadError) -> Status {
Status::internal(value.to_string())
}
}

impl From<RepoLoaderError> for Status {
fn from(value: RepoLoaderError) -> Status {
(match value {
RepoLoaderError::OpHeadResolution { .. } => Status::not_found,
_ => Status::internal,
})(value.to_string())
}
}

impl From<OpStoreError> for Status {
fn from(value: OpStoreError) -> Status {
(match value {
OpStoreError::ObjectNotFound { .. } => Status::not_found,
_ => Status::internal,
})(value.to_string())
}
}

impl From<WorkspaceLoadError> 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())
}
}
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 701c7fd

Please sign in to comment.