-
Notifications
You must be signed in to change notification settings - Fork 356
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
base: main
Are you sure you want to change the base?
Prototype for jj api
#3601
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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))) | ||
} |
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) | ||
} | ||
} |
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; |
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really like the fact that the GRPC server is started in the jj_lib crate rather than the jj_cli crate. However, I anticipate that:
Happy to move this to jj_cli if we don't think that's the case. |
||
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(()) | ||
} |
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a lot of work to just get one field, which is why I'm considering a mechanism where we just have:
|
||
.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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect that we may be able to take advantage of caching to improve performance of functions like this. Seems like work for later though |
||
&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>>(), | ||
}) | ||
} | ||
} |
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()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly just to get around the fact this requires async functions, while I intend for the jj CLI to call the sync functions directly.