Add client-side wrappers for creating integration review requests and for listing repository branches, and wire native command imports so the app can call those integration endpoints. Update the review center UI to include a create-review dialog import and refine table and provider button layout for improved spacing and readability. - Add wrappers for create review requests and branch listing - Wire native integration imports and prepare the create-review dialog - Adjust table sizing and provider button layout for better spacing
467 lines
16 KiB
Rust
467 lines
16 KiB
Rust
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
mod badge;
|
|
mod external_tools;
|
|
mod git;
|
|
mod integrations;
|
|
mod telemetry;
|
|
|
|
use badge::set_sync_badge;
|
|
use external_tools::{
|
|
detect_external_tools, launch_external_diff, launch_external_merge, launch_external_tool,
|
|
};
|
|
use git::{
|
|
SearchCancellationState, add_remote, add_to_gitignore, add_worktree, amend_commit,
|
|
apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, cherry_pick_abort,
|
|
cherry_pick_commit, cherry_pick_continue, clone_repository, commit, commit_ai_generate,
|
|
commit_ai_review, commit_ai_split, compare_commits, compare_file_to_head,
|
|
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
|
|
delete_branch, delete_commit_note, delete_remote_branch, delete_remote_branches, delete_tag,
|
|
diff_file_against_working_tree, fetch, fetch_commit_notes, get_bisect_state, get_commit_note,
|
|
get_file_blame, get_file_patch, get_remote_url, get_status, git_lfs_install, git_lfs_prune,
|
|
git_lfs_pull, git_lfs_status, git_lfs_track, git_lfs_untrack, init_repository,
|
|
last_commit_message, list_branches, list_commits, list_file_history,
|
|
list_interactive_rebase_commits, list_reflog, list_remotes, list_repository_files,
|
|
list_stashes, list_tags, list_worktrees, lock_worktree, mark_bisect, merge_abort, merge_branch,
|
|
merge_continue, move_worktree, open_repo_in_explorer, open_repository, open_repository_bundle,
|
|
open_repository_file, prune_worktrees, pull, push, push_commit_notes, push_tag, read_conflict,
|
|
rebase_abort, rebase_branch, rebase_continue, remove_remote, remove_worktree, rename_branch,
|
|
rename_remote_branch, repair_worktree, reset_bisect, resolve_conflict, resolve_conflict_side,
|
|
restore_file_from_commit, restore_files, restore_reflog_entry, restore_to_commit,
|
|
revert_commit, run_sequence_editor_if_requested, search_code_introductions,
|
|
set_branch_upstream, set_commit_note, stage_files, start_bisect, start_interactive_rebase,
|
|
stash_apply, stash_drop, stash_pop, stash_push, undo_last_commit, unlock_worktree,
|
|
unstage_files, untrack_paths, update_remote,
|
|
};
|
|
use integrations::{
|
|
create_integration_review_request, list_integration_repository_branches,
|
|
add_integration_review_comment, get_integration_review_details, list_integration_repositories, list_integration_review_requests, open_in_browser,
|
|
run_integration_review_action, list_integration_issues, get_integration_board, list_integration_boards, move_integration_board_card, list_integration_issue_comments, add_integration_issue_comment, close_integration_issue,
|
|
};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Mutex;
|
|
use tauri::{Emitter, Manager};
|
|
use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled};
|
|
|
|
struct StartupRepository(Mutex<Option<String>>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct StartupCloneRequest {
|
|
remote_url: String,
|
|
parent_path: String,
|
|
directory_name: String,
|
|
}
|
|
|
|
struct StartupClone(Mutex<Option<StartupCloneRequest>>);
|
|
|
|
fn resolve_startup_path(path: &str, cwd: &Path) -> Option<PathBuf> {
|
|
let path = path.trim();
|
|
if path.is_empty() {
|
|
return None;
|
|
}
|
|
let path = PathBuf::from(path);
|
|
Some(if path.is_absolute() {
|
|
path
|
|
} else {
|
|
cwd.join(path)
|
|
})
|
|
}
|
|
|
|
fn clone_request_from_args(
|
|
args: impl IntoIterator<Item = String>,
|
|
cwd: &Path,
|
|
) -> Option<StartupCloneRequest> {
|
|
let mut args = args.into_iter();
|
|
|
|
while let Some(arg) = args.next() {
|
|
let remote_url = if arg == "clone" || arg == "--clone" {
|
|
args.next()
|
|
} else {
|
|
arg.strip_prefix("--clone=").map(ToString::to_string)
|
|
};
|
|
let Some(remote_url) = remote_url.filter(|value| !value.trim().is_empty()) else {
|
|
continue;
|
|
};
|
|
let target = resolve_startup_path(&args.next()?, cwd)?;
|
|
let directory_name = target.file_name()?.to_string_lossy().trim().to_string();
|
|
let parent_path = target.parent()?.to_string_lossy().into_owned();
|
|
if directory_name.is_empty() || parent_path.trim().is_empty() {
|
|
return None;
|
|
}
|
|
return Some(StartupCloneRequest {
|
|
remote_url,
|
|
parent_path,
|
|
directory_name,
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn repository_path_from_args(args: impl IntoIterator<Item = String>, cwd: &Path) -> Option<String> {
|
|
let mut args = args.into_iter();
|
|
let mut repository = None;
|
|
|
|
while let Some(arg) = args.next() {
|
|
if arg == "clone" || arg == "--clone" || arg.starts_with("--clone=") {
|
|
return None;
|
|
}
|
|
if arg == "--repo" {
|
|
repository = args.next();
|
|
break;
|
|
}
|
|
if let Some(path) = arg.strip_prefix("--repo=") {
|
|
repository = Some(path.to_string());
|
|
break;
|
|
}
|
|
if !arg.starts_with('-') {
|
|
repository = Some(arg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
repository
|
|
.and_then(|path| resolve_startup_path(&path, cwd))
|
|
.map(|path| path.to_string_lossy().into_owned())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod startup_repository_tests {
|
|
use super::{clone_request_from_args, repository_path_from_args};
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn accepts_direct_relative_repository_path() {
|
|
let path =
|
|
repository_path_from_args(["projects/repo".to_string()], Path::new("/home/user"));
|
|
assert_eq!(
|
|
path.map(|value| value.replace('\\', "/")),
|
|
Some("/home/user/projects/repo".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_repo_option() {
|
|
let path = repository_path_from_args(
|
|
["--repo".to_string(), "/projects/repo".to_string()],
|
|
Path::new("/home/user"),
|
|
);
|
|
assert_eq!(path.as_deref(), Some("/projects/repo"));
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_repo_equals_option() {
|
|
let path = repository_path_from_args(
|
|
["--repo=projects/repo".to_string()],
|
|
Path::new("/home/user"),
|
|
);
|
|
assert_eq!(
|
|
path.map(|value| value.replace('\\', "/")),
|
|
Some("/home/user/projects/repo".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_clone_command_with_exact_relative_target() {
|
|
let request = clone_request_from_args(
|
|
[
|
|
"clone".to_string(),
|
|
"https://example.com/team/project.git".to_string(),
|
|
"clones/local-copy".to_string(),
|
|
],
|
|
Path::new("/home/user"),
|
|
)
|
|
.expect("clone request should be parsed");
|
|
|
|
assert_eq!(request.remote_url, "https://example.com/team/project.git");
|
|
assert_eq!(request.directory_name, "local-copy");
|
|
assert_eq!(request.parent_path.replace('\\', "/"), "/home/user/clones");
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_clone_option_and_clone_equals_option() {
|
|
for args in [
|
|
vec![
|
|
"--clone".to_string(),
|
|
"git@example.com:team/project.git".to_string(),
|
|
"/projects/project".to_string(),
|
|
],
|
|
vec![
|
|
"--clone=git@example.com:team/project.git".to_string(),
|
|
"/projects/project".to_string(),
|
|
],
|
|
] {
|
|
let request = clone_request_from_args(args, Path::new("/home/user"))
|
|
.expect("clone option should be parsed");
|
|
assert_eq!(request.directory_name, "project");
|
|
assert_eq!(request.parent_path.replace('\\', "/"), "/projects");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repository_parser_does_not_treat_clone_values_as_repository_paths() {
|
|
let path = repository_path_from_args(
|
|
[
|
|
"--clone".to_string(),
|
|
"https://example.com/team/project.git".to_string(),
|
|
"clones/project".to_string(),
|
|
],
|
|
Path::new("/home/user"),
|
|
);
|
|
assert!(path.is_none());
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn take_startup_repository(state: tauri::State<'_, StartupRepository>) -> Option<String> {
|
|
state.0.lock().ok()?.take()
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn take_startup_clone(state: tauri::State<'_, StartupClone>) -> Option<StartupCloneRequest> {
|
|
state.0.lock().ok()?.take()
|
|
}
|
|
|
|
struct ConsoleLogger;
|
|
|
|
impl log::Log for ConsoleLogger {
|
|
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
|
|
metadata.level() <= log::Level::Info
|
|
}
|
|
|
|
fn log(&self, record: &log::Record<'_>) {
|
|
if self.enabled(record.metadata()) {
|
|
eprintln!(
|
|
"[{}] [{}] {}",
|
|
record.level(),
|
|
record.target(),
|
|
record.args()
|
|
);
|
|
}
|
|
}
|
|
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;
|
|
|
|
fn init_console_logging() {
|
|
if log::set_logger(&CONSOLE_LOGGER).is_ok() {
|
|
log::set_max_level(log::LevelFilter::Info);
|
|
log::info!(target: "gitty", "Rust console logging initialized");
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window("splashscreen") {
|
|
let _ = window.hide();
|
|
window
|
|
.destroy()
|
|
.map_err(|error| format!("failed to destroy splashscreen: {error}"))?;
|
|
}
|
|
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.maximize();
|
|
window
|
|
.show()
|
|
.map_err(|error| format!("failed to show main window: {error}"))?;
|
|
let _ = window.set_focus();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
init_console_logging();
|
|
telemetry::init();
|
|
if let Some(result) = run_sequence_editor_if_requested() {
|
|
if let Err(error) = result {
|
|
eprintln!("{error}");
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let startup_args: Vec<String> = std::env::args().skip(1).collect();
|
|
let startup_cwd = std::env::current_dir().unwrap_or_default();
|
|
let startup_clone = clone_request_from_args(startup_args.clone(), &startup_cwd);
|
|
let startup_repository = repository_path_from_args(startup_args, &startup_cwd);
|
|
|
|
let builder = tauri::Builder::default()
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
|
let args: Vec<String> = args.into_iter().skip(1).collect();
|
|
if let Some(request) = clone_request_from_args(args.clone(), Path::new(&cwd)) {
|
|
if let Ok(mut pending) = app.state::<StartupClone>().0.lock() {
|
|
*pending = Some(request);
|
|
}
|
|
let _ = app.emit("open-startup-repository", ());
|
|
} else if let Some(path) = repository_path_from_args(args, Path::new(&cwd)) {
|
|
if let Ok(mut pending) = app.state::<StartupRepository>().0.lock() {
|
|
*pending = Some(path);
|
|
}
|
|
let _ = app.emit("open-startup-repository", ());
|
|
}
|
|
#[cfg(desktop)]
|
|
let _ = app
|
|
.get_webview_window("main")
|
|
.expect("no main window")
|
|
.set_focus();
|
|
}))
|
|
.plugin(
|
|
tauri_plugin_aptabase::Builder::new("A-SH-1344793789")
|
|
.with_options(tauri_plugin_aptabase::InitOptions {
|
|
host: Some("https://aptabase.cbsk-tech.de".to_string()),
|
|
flush_interval: None,
|
|
})
|
|
.build(),
|
|
)
|
|
.manage(StartupRepository(Mutex::new(startup_repository)))
|
|
.manage(StartupClone(Mutex::new(startup_clone)))
|
|
.manage(SearchCancellationState::default())
|
|
.plugin(tauri_plugin_dialog::init());
|
|
|
|
// Linux installs are expected to come from the system package manager (see the
|
|
// PKGBUILD), which owns updates itself — the self-updater is only wired up for
|
|
// the platforms whose install method this app ships (NSIS/Windows, .app/macOS).
|
|
#[cfg(not(target_os = "linux"))]
|
|
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
|
|
|
builder
|
|
.invoke_handler(tauri::generate_handler![
|
|
open_repository,
|
|
init_repository,
|
|
clone_repository,
|
|
open_repo_in_explorer,
|
|
open_repository_file,
|
|
detect_external_tools,
|
|
launch_external_tool,
|
|
launch_external_diff,
|
|
launch_external_merge,
|
|
get_status,
|
|
git_lfs_status,
|
|
git_lfs_install,
|
|
git_lfs_track,
|
|
git_lfs_untrack,
|
|
git_lfs_pull,
|
|
git_lfs_prune,
|
|
list_branches,
|
|
list_remotes,
|
|
add_remote,
|
|
update_remote,
|
|
remove_remote,
|
|
set_branch_upstream,
|
|
delete_remote_branch,
|
|
delete_remote_branches,
|
|
list_stashes,
|
|
checkout_branch,
|
|
create_branch,
|
|
rename_branch,
|
|
rename_remote_branch,
|
|
delete_branch,
|
|
list_worktrees,
|
|
add_worktree,
|
|
remove_worktree,
|
|
move_worktree,
|
|
lock_worktree,
|
|
unlock_worktree,
|
|
prune_worktrees,
|
|
repair_worktree,
|
|
list_tags,
|
|
create_tag,
|
|
delete_tag,
|
|
push_tag,
|
|
cherry_pick_commit,
|
|
cherry_pick_continue,
|
|
cherry_pick_abort,
|
|
stage_files,
|
|
unstage_files,
|
|
add_to_gitignore,
|
|
untrack_paths,
|
|
stash_push,
|
|
stash_apply,
|
|
stash_pop,
|
|
stash_drop,
|
|
restore_files,
|
|
get_file_patch,
|
|
apply_file_patch,
|
|
commit,
|
|
amend_commit,
|
|
undo_last_commit,
|
|
last_commit_message,
|
|
commit_ai_generate,
|
|
commit_ai_review,
|
|
commit_ai_split,
|
|
pull,
|
|
push,
|
|
fetch,
|
|
list_commits,
|
|
get_commit_note,
|
|
set_commit_note,
|
|
delete_commit_note,
|
|
fetch_commit_notes,
|
|
push_commit_notes,
|
|
restore_to_commit,
|
|
restore_file_from_commit,
|
|
merge_branch,
|
|
merge_continue,
|
|
merge_abort,
|
|
revert_commit,
|
|
rebase_branch,
|
|
rebase_continue,
|
|
rebase_abort,
|
|
list_interactive_rebase_commits,
|
|
start_interactive_rebase,
|
|
list_reflog,
|
|
restore_reflog_entry,
|
|
get_bisect_state,
|
|
start_bisect,
|
|
mark_bisect,
|
|
reset_bisect,
|
|
list_repository_files,
|
|
open_repository_bundle,
|
|
list_file_history,
|
|
cancel_file_history,
|
|
get_file_blame,
|
|
compare_commits,
|
|
compare_file_to_head,
|
|
compare_file_to_parent,
|
|
diff_file_against_working_tree,
|
|
search_code_introductions,
|
|
cancel_code_search,
|
|
read_conflict,
|
|
resolve_conflict,
|
|
resolve_conflict_side,
|
|
get_remote_url,
|
|
cred_load,
|
|
cred_save,
|
|
cred_delete,
|
|
list_integration_repository_branches,
|
|
create_integration_review_request,
|
|
list_integration_repositories,
|
|
list_integration_review_requests,
|
|
list_integration_issues,
|
|
get_integration_board,
|
|
list_integration_boards,
|
|
move_integration_board_card,
|
|
list_integration_issue_comments,
|
|
add_integration_issue_comment,
|
|
close_integration_issue,
|
|
get_integration_review_details,
|
|
add_integration_review_comment,
|
|
run_integration_review_action,
|
|
open_in_browser,
|
|
set_sync_badge,
|
|
close_splashscreen,
|
|
set_telemetry_enabled,
|
|
emit_frontend_log,
|
|
emit_frontend_span,
|
|
take_startup_repository,
|
|
take_startup_clone
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|