Skip to content

Commit 2110801

Browse files
committed
Serve sever from but CLI
1 parent 3c108d4 commit 2110801

File tree

7 files changed

+310
-299
lines changed

7 files changed

+310
-299
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/buzz/dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ async function main() {
163163
log('\n📦 Starting but-server server...', colors.yellow);
164164

165165
// Start the but-server server
166-
butProcess = spawnProcess('cargo', ['run', '-p', 'but-server'], rootDir);
166+
butProcess = spawnProcess('cargo', ['run', '-p', 'but', '--', 'serve'], rootDir);
167167

168168
butProcess.on('close', (code) => {
169169
if (code !== 0 && code !== null) {

crates/but-server/src/lib.rs

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
use std::sync::Arc;
2+
3+
use axum::{Json, Router, routing::get};
4+
use but_settings::AppSettingsWithDiskSync;
5+
use serde::{Deserialize, Serialize};
6+
use serde_json::json;
7+
use tokio::sync::Mutex;
8+
use tower::ServiceBuilder;
9+
use tower_http::cors::{Any, CorsLayer};
10+
11+
use crate::projects::ActiveProjects;
12+
13+
mod action;
14+
mod askpass;
15+
mod cli;
16+
mod commands;
17+
mod config;
18+
mod diff;
19+
mod forge;
20+
mod github;
21+
mod menu;
22+
mod modes;
23+
mod open;
24+
mod projects;
25+
mod remotes;
26+
mod repo;
27+
mod rules;
28+
mod secret;
29+
mod settings;
30+
mod stack;
31+
mod undo;
32+
mod users;
33+
mod virtual_branches;
34+
mod workspace;
35+
mod zip;
36+
37+
pub(crate) struct RequestContext {
38+
app_settings: Arc<AppSettingsWithDiskSync>,
39+
user_controller: Arc<gitbutler_user::Controller>,
40+
project_controller: Arc<gitbutler_project::Controller>,
41+
active_projects: Arc<Mutex<ActiveProjects>>,
42+
}
43+
44+
#[derive(Serialize, Deserialize)]
45+
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
46+
enum Response {
47+
Success(serde_json::Value),
48+
Error(serde_json::Value),
49+
}
50+
51+
#[derive(Serialize, Deserialize)]
52+
#[serde(rename_all = "camelCase")]
53+
pub(crate) struct Request {
54+
command: String,
55+
params: serde_json::Value,
56+
}
57+
58+
pub async fn run() {
59+
let cors = CorsLayer::new()
60+
.allow_methods(Any)
61+
.allow_origin(Any)
62+
.allow_headers(Any);
63+
64+
let config_dir = dirs::config_dir()
65+
.expect("missing config dir")
66+
.join("gitbutler");
67+
68+
// TODO: This should probably be a real com.gitbutler.whatever directory
69+
let app_data_dir = dirs::config_dir()
70+
.expect("missing config dir")
71+
.join("gitbutler-server");
72+
73+
let app_settings = Arc::new(
74+
AppSettingsWithDiskSync::new(config_dir.clone()).expect("failed to create app settings"),
75+
);
76+
let user_controller = Arc::new(gitbutler_user::Controller::from_path(&app_data_dir));
77+
let project_controller = Arc::new(gitbutler_project::Controller::from_path(&app_data_dir));
78+
let active_projects = Arc::new(Mutex::new(ActiveProjects::new()));
79+
80+
// build our application with a single route
81+
let app = Router::new()
82+
.route(
83+
"/",
84+
get(|| async { "Hello, World!" }).post(move |req| {
85+
let ctx = RequestContext {
86+
app_settings: Arc::clone(&app_settings),
87+
user_controller: Arc::clone(&user_controller),
88+
project_controller: Arc::clone(&project_controller),
89+
active_projects: Arc::clone(&active_projects),
90+
};
91+
handle_command(req, ctx)
92+
}),
93+
)
94+
.layer(ServiceBuilder::new().layer(cors));
95+
96+
// run our app with hyper, listening globally on port 6978
97+
let listener = tokio::net::TcpListener::bind("0.0.0.0:6978").await.unwrap();
98+
println!("Running at 0.0.0.0:6978");
99+
axum::serve(listener, app).await.unwrap();
100+
}
101+
102+
async fn handle_command(
103+
Json(request): Json<Request>,
104+
ctx: RequestContext,
105+
) -> Json<serde_json::Value> {
106+
let command: &str = &request.command;
107+
let result = match command {
108+
// App settings
109+
"get_app_settings" => settings::get_app_settings(&ctx),
110+
"update_onboarding_complete" => settings::update_onboarding_complete(&ctx, request.params),
111+
"update_telemetry" => settings::update_telemetry(&ctx, request.params),
112+
"update_telemetry_distinct_id" => {
113+
settings::update_telemetry_distinct_id(&ctx, request.params)
114+
}
115+
"update_feature_flags" => settings::update_feature_flags(&ctx, request.params),
116+
// Secret management
117+
"secret_get_global" => secret::secret_get_global(&ctx, request.params),
118+
"secret_set_global" => secret::secret_set_global(&ctx, request.params),
119+
// User management
120+
"get_user" => users::get_user(&ctx),
121+
"set_user" => users::set_user(&ctx, request.params),
122+
"delete_user" => users::delete_user(&ctx, request.params),
123+
// Project management
124+
"update_project" => projects::update_project(&ctx, request.params),
125+
"add_project" => projects::add_project(&ctx, request.params),
126+
"get_project" => projects::get_project(&ctx, request.params),
127+
"list_projects" => projects::list_projects(&ctx).await,
128+
"delete_project" => projects::delete_project(&ctx, request.params),
129+
"set_project_active" => projects::set_project_active(&ctx, request.params).await,
130+
// Virtual branches commands
131+
"normalize_branch_name" => virtual_branches::normalize_branch_name(request.params),
132+
"create_virtual_branch" => virtual_branches::create_virtual_branch(&ctx, request.params),
133+
"delete_local_branch" => virtual_branches::delete_local_branch(&ctx, request.params),
134+
"create_virtual_branch_from_branch" => {
135+
virtual_branches::create_virtual_branch_from_branch(&ctx, request.params)
136+
}
137+
"integrate_upstream_commits" => {
138+
virtual_branches::integrate_upstream_commits(&ctx, request.params)
139+
}
140+
"get_base_branch_data" => virtual_branches::get_base_branch_data(&ctx, request.params),
141+
"set_base_branch" => virtual_branches::set_base_branch(&ctx, request.params),
142+
"push_base_branch" => virtual_branches::push_base_branch(&ctx, request.params),
143+
"update_stack_order" => virtual_branches::update_stack_order(&ctx, request.params),
144+
"unapply_stack" => virtual_branches::unapply_stack(&ctx, request.params).await,
145+
"can_apply_remote_branch" => {
146+
virtual_branches::can_apply_remote_branch(&ctx, request.params)
147+
}
148+
"list_commit_files" => virtual_branches::list_commit_files(&ctx, request.params),
149+
"amend_virtual_branch" => virtual_branches::amend_virtual_branch(&ctx, request.params),
150+
"move_commit_file" => virtual_branches::move_commit_file(&ctx, request.params),
151+
"undo_commit" => virtual_branches::undo_commit(&ctx, request.params),
152+
"insert_blank_commit" => virtual_branches::insert_blank_commit(&ctx, request.params),
153+
"reorder_stack" => virtual_branches::reorder_stack(&ctx, request.params),
154+
"find_git_branches" => virtual_branches::find_git_branches(&ctx, request.params),
155+
"list_branches" => virtual_branches::list_branches(&ctx, request.params),
156+
"get_branch_listing_details" => {
157+
virtual_branches::get_branch_listing_details(&ctx, request.params)
158+
}
159+
"squash_commits" => virtual_branches::squash_commits(&ctx, request.params),
160+
"fetch_from_remotes" => virtual_branches::fetch_from_remotes(&ctx, request.params),
161+
"move_commit" => virtual_branches::move_commit(&ctx, request.params),
162+
"update_commit_message" => virtual_branches::update_commit_message(&ctx, request.params),
163+
"find_commit" => virtual_branches::find_commit(&ctx, request.params),
164+
"upstream_integration_statuses" => {
165+
virtual_branches::upstream_integration_statuses(&ctx, request.params)
166+
}
167+
"integrate_upstream" => virtual_branches::integrate_upstream(&ctx, request.params),
168+
"resolve_upstream_integration" => {
169+
virtual_branches::resolve_upstream_integration(&ctx, request.params)
170+
}
171+
// General commands
172+
"git_remote_branches" => commands::git_remote_branches(&ctx, request.params),
173+
"git_test_push" => commands::git_test_push(&ctx, request.params),
174+
"git_test_fetch" => commands::git_test_fetch(&ctx, request.params),
175+
"git_index_size" => commands::git_index_size(&ctx, request.params),
176+
"git_head" => commands::git_head(&ctx, request.params),
177+
"delete_all_data" => commands::delete_all_data(&ctx, request.params),
178+
"git_set_global_config" => commands::git_set_global_config(&ctx, request.params),
179+
"git_remove_global_config" => commands::git_remove_global_config(&ctx, request.params),
180+
"git_get_global_config" => commands::git_get_global_config(&ctx, request.params),
181+
// Operating modes commands
182+
"operating_mode" => modes::operating_mode(&ctx, request.params),
183+
"enter_edit_mode" => modes::enter_edit_mode(&ctx, request.params),
184+
"abort_edit_and_return_to_workspace" => {
185+
modes::abort_edit_and_return_to_workspace(&ctx, request.params)
186+
}
187+
"save_edit_and_return_to_workspace" => {
188+
modes::save_edit_and_return_to_workspace(&ctx, request.params)
189+
}
190+
"edit_initial_index_state" => modes::edit_initial_index_state(&ctx, request.params),
191+
// Stack commands
192+
"create_branch" => stack::create_branch(&ctx, request.params),
193+
"remove_branch" => stack::remove_branch(&ctx, request.params),
194+
"update_branch_name" => stack::update_branch_name(&ctx, request.params),
195+
"update_branch_description" => stack::update_branch_description(&ctx, request.params),
196+
"update_branch_pr_number" => stack::update_branch_pr_number(&ctx, request.params),
197+
"push_stack" => stack::push_stack(&ctx, request.params),
198+
"push_stack_to_review" => stack::push_stack_to_review(&ctx, request.params),
199+
// Workspace commands
200+
"stacks" => workspace::stacks(&ctx, request.params),
201+
#[cfg(unix)]
202+
"show_graph_svg" => workspace::show_graph_svg(&ctx, request.params),
203+
"stack_details" => workspace::stack_details(&ctx, request.params),
204+
"branch_details" => workspace::branch_details(&ctx, request.params),
205+
"create_commit_from_worktree_changes" => {
206+
workspace::create_commit_from_worktree_changes(&ctx, request.params)
207+
}
208+
"amend_commit_from_worktree_changes" => {
209+
workspace::amend_commit_from_worktree_changes(&ctx, request.params)
210+
}
211+
"discard_worktree_changes" => workspace::discard_worktree_changes(&ctx, request.params),
212+
"move_changes_between_commits" => {
213+
workspace::move_changes_between_commits(&ctx, request.params)
214+
}
215+
"split_branch" => workspace::split_branch(&ctx, request.params),
216+
"split_branch_into_dependent_branch" => {
217+
workspace::split_branch_into_dependent_branch(&ctx, request.params)
218+
}
219+
"uncommit_changes" => workspace::uncommit_changes(&ctx, request.params),
220+
"stash_into_branch" => workspace::stash_into_branch(&ctx, request.params),
221+
"canned_branch_name" => workspace::canned_branch_name(&ctx, request.params),
222+
"target_commits" => workspace::target_commits(&ctx, request.params),
223+
// Diff commands
224+
"tree_change_diffs" => diff::tree_change_diffs(&ctx, request.params),
225+
"commit_details" => diff::commit_details(&ctx, request.params),
226+
"changes_in_branch" => diff::changes_in_branch(&ctx, request.params),
227+
"changes_in_worktree" => diff::changes_in_worktree(&ctx, request.params),
228+
"assign_hunk" => diff::assign_hunk(&ctx, request.params),
229+
// Archive/Zip commands
230+
"get_logs_archive_path" => zip::get_logs_archive_path(&ctx, request.params),
231+
"get_project_archive_path" => zip::get_project_archive_path(&ctx, request.params),
232+
// Repository commands
233+
"git_get_local_config" => repo::git_get_local_config(&ctx, request.params),
234+
"git_set_local_config" => repo::git_set_local_config(&ctx, request.params),
235+
"check_signing_settings" => repo::check_signing_settings(&ctx, request.params),
236+
"git_clone_repository" => repo::git_clone_repository(&ctx, request.params),
237+
"get_uncommited_files" => repo::get_uncommited_files(&ctx, request.params),
238+
"get_commit_file" => repo::get_commit_file(&ctx, request.params),
239+
"get_workspace_file" => repo::get_workspace_file(&ctx, request.params),
240+
"pre_commit_hook" => repo::pre_commit_hook(&ctx, request.params),
241+
"pre_commit_hook_diffspecs" => repo::pre_commit_hook_diffspecs(&ctx, request.params),
242+
"post_commit_hook" => repo::post_commit_hook(&ctx, request.params),
243+
"message_hook" => repo::message_hook(&ctx, request.params),
244+
// Undo/Snapshot commands
245+
"list_snapshots" => undo::list_snapshots(&ctx, request.params),
246+
"restore_snapshot" => undo::restore_snapshot(&ctx, request.params),
247+
"snapshot_diff" => undo::snapshot_diff(&ctx, request.params),
248+
"oplog_diff_worktrees" => undo::oplog_diff_worktrees(&ctx, request.params),
249+
// Config management commands
250+
"get_gb_config" => config::get_gb_config(&ctx, request.params),
251+
"set_gb_config" => config::set_gb_config(&ctx, request.params),
252+
// Remotes management commands
253+
"list_remotes" => remotes::list_remotes(&ctx, request.params),
254+
"add_remote" => remotes::add_remote(&ctx, request.params),
255+
// Rules/Workspace rules commands
256+
"create_workspace_rule" => rules::create_workspace_rule(&ctx, request.params),
257+
"delete_workspace_rule" => rules::delete_workspace_rule(&ctx, request.params),
258+
"update_workspace_rule" => rules::update_workspace_rule(&ctx, request.params),
259+
"list_workspace_rules" => rules::list_workspace_rules(&ctx, request.params),
260+
// Action/Workflow commands
261+
"list_actions" => action::list_actions(&ctx, request.params),
262+
"handle_changes" => action::handle_changes(&ctx, request.params),
263+
"list_workflows" => action::list_workflows(&ctx, request.params),
264+
// GitHub OAuth commands (async)
265+
"init_device_oauth" => github::init_device_oauth(&ctx, request.params).await,
266+
"check_auth_status" => github::check_auth_status(&ctx, request.params).await,
267+
// Forge commands
268+
"get_available_review_templates" => {
269+
forge::get_available_review_templates(&ctx, request.params)
270+
}
271+
"get_review_template_contents" => forge::get_review_template_contents(&ctx, request.params),
272+
// Menu commands (limited - no menu_item_set_enabled as it's Tauri-specific)
273+
"get_editor_link_scheme" => menu::get_editor_link_scheme(&ctx, request.params),
274+
// CLI commands
275+
"install_cli" => cli::install_cli(&ctx, request.params),
276+
"cli_path" => cli::cli_path(&ctx, request.params),
277+
// Askpass commands (async)
278+
"submit_prompt_response" => askpass::submit_prompt_response(&ctx, request.params).await,
279+
// Open/System commands (limited - no open_project_in_window as it's Tauri-specific)
280+
"open_url" => open::open_url(&ctx, request.params),
281+
282+
// TODO: Tauri-specific commands that cannot be ported to HTTP API:
283+
//
284+
// AI-Integrated Action Commands (require Tauri AppHandle for real-time UI updates):
285+
// - "auto_commit" => action::auto_commit() // Needs AppHandle for real-time AI progress updates
286+
// - "auto_branch_changes" => action::auto_branch_changes() // Needs AppHandle for real-time AI progress updates
287+
// - "absorb" => action::absorb() // Needs AppHandle for real-time AI progress updates
288+
// - "freestyle" => action::freestyle() // Needs AppHandle for real-time AI progress updates
289+
//
290+
// UI Management Commands (require Tauri window/menu system):
291+
// - "menu_item_set_enabled" => menu::menu_item_set_enabled() // Requires Tauri menu management
292+
// - "open_project_in_window" => projects::open_project_in_window() // Requires Tauri window creation
293+
_ => Err(anyhow::anyhow!("Command {} not found!", command)),
294+
};
295+
296+
match result {
297+
Ok(value) => Json(json!(Response::Success(value))),
298+
Err(e) => Json(json!(Response::Error(json!(e.to_string())))),
299+
}
300+
}

0 commit comments

Comments
 (0)