diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-02-20 22:53:27 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-02-20 22:53:27 +0100 |
| commit | e940d84f69e3fd627731d5d3f698d6f838797862 (patch) | |
| tree | 779eefcde993e22c0a69c18a3cb6e1cb9d17aad3 /server/src/git_root.rs | |
| parent | bf025b4977543a371df9dbdddfe9cc2f02f2a8d0 (diff) | |
WIPWIP
Diffstat (limited to 'server/src/git_root.rs')
| -rw-r--r-- | server/src/git_root.rs | 128 |
1 files changed, 116 insertions, 12 deletions
diff --git a/server/src/git_root.rs b/server/src/git_root.rs index f818495..1df831c 100644 --- a/server/src/git_root.rs +++ b/server/src/git_root.rs @@ -1,17 +1,22 @@ use futures::{future::TryFutureExt, stream::TryStreamExt}; +use log::{trace, error}; use rmp_serde::{decode, Serializer}; use rocket::fairing::{self, AdHoc}; -use rocket::serde::ser::Serialize; use rocket::serde::Deserialize; +use rocket::serde::ser::Serialize; use rocket::{Build, Rocket}; use rocket_db_pools::{sqlx, Database, Pool}; use std::borrow::Cow; use std::collections::HashMap; +use std::fs::Permissions; use std::ops::Deref; -use std::path::PathBuf; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use tokio::fs; use tokio::net::{UnixListener, UnixStream}; use tokio::task; +use url::Url; use crate::api_model; use crate::fs_utils; @@ -43,14 +48,16 @@ impl Roots { db: &Db, project_id: &str, remote: &str, + remote_key: &str, main_branch: &str, - ) -> Result<(), git::Error> { + ) -> Result<(), anyhow::Error> { let project_id = project_id.to_string(); let repo = setup_project_root( config, db, &project_id, remote.to_string(), + remote_key.to_string(), main_branch.to_string(), ) .await?; @@ -115,13 +122,17 @@ async fn git_process_prehook( let mut errors: Vec<String> = Vec::new(); for row in receive { + trace!("prehook {} {} {}", row.old_value, row.new_value, row.reference); + if !valid_branch_name(row.reference.as_str()) { if row.reference.starts_with("refs/heads/") { + error!("{}: Bad branch name", row.reference); errors.push(format!( "{}: Bad branch name", row.reference.strip_prefix("refs/heads/").unwrap() )); } else { + error!("{}: Only branches are allowed", row.reference); errors.push(format!("{}: Only branches are allowed", row.reference)); } continue; @@ -141,11 +152,13 @@ async fn git_process_prehook( { Ok(_) => {} Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } }, None => { + error!("{branch}: Missing commiter"); errors.push(format!("{branch}: Missing commiter")); continue; } @@ -177,6 +190,7 @@ async fn git_process_prehook( // All branches should be connected to a branch, but in case of errors this might // be relevant. if result.is_ok() { + error!("{branch}: Not allowed to delete branch, delete review instead."); errors.push(format!( "{branch}: Not allowed to delete branch, delete review instead." )); @@ -187,6 +201,7 @@ async fn git_process_prehook( let (state, rewrite) = match result { Ok(data) => data, Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } @@ -194,10 +209,12 @@ async fn git_process_prehook( match state { api_model::ReviewState::Dropped => { + error!("{branch}: Review is dropped, no pushes allowed"); errors.push(format!("{branch}: Review is dropped, no pushes allowed")); continue; } api_model::ReviewState::Closed => { + error!("{branch}: Review is closed, no pushes allowed"); errors.push(format!("{branch}: Review is closed, no pushes allowed")); continue; } @@ -223,10 +240,12 @@ async fn git_process_prehook( if equal_content { continue; } + error!("{branch}: History rewrite not allowed"); errors.push(format!("{}: History rewrite not allowed as final result does not match. Please check locally with `git diff {} {}`", branch, row.old_value, row.new_value)); } api_model::Rewrite::Rebase => {} api_model::Rewrite::Disabled => { + error!("{}: Non fast-forward not allowed", row.reference); errors.push(format!( "Non fast-forward not allowed for review: {}", row.reference @@ -257,12 +276,15 @@ async fn git_process_posthook( let mut updated: Vec<u64> = Vec::new(); for row in receive { + trace!("posthook {} {} {}", row.old_value, row.new_value, row.reference); + let branch = row.reference.strip_prefix("refs/heads/").unwrap(); if row.old_value == git::EMPTY { let commiter = match repo.get_commiter(row.reference.as_str()).await { Ok(user) => user, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: {e}")); continue; } @@ -288,6 +310,7 @@ async fn git_process_posthook( )); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } }; @@ -319,6 +342,7 @@ async fn git_process_posthook( updated.push(id); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -328,6 +352,7 @@ async fn git_process_posthook( } }, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -402,38 +427,114 @@ async fn git_socket_listen( } } +fn get_host(url: &str) -> String { + match Url::parse(url) { + Ok(u) => match u.host_str() { + Some(h) => h.to_string(), + None => String::new(), + }, + Err(_) => String::new(), + } +} + +async fn write_private_file<P: AsRef<Path>, D: AsRef<[u8]>>(path: P, data: D) -> anyhow::Result<()> { + fs::write(&path, data).await?; + let permissions = Permissions::from_mode(0o600); + fs::set_permissions(&path, permissions).await?; + Ok(()) +} + +async fn write_ssh_config(ssh_config: &Path, host: &str, remote_key: &str) -> anyhow::Result<()> { + let host_pattern = if host.is_empty() { + "*" + } else { + host + }; + let basedir = ssh_config.parent().unwrap(); + let identity_file = basedir.join("ssh_identity"); + let known_hosts = basedir.join("ssh_known_hosts"); + + let mut identity_data = String::from("-----BEGIN OPENSSH PRIVATE KEY-----\n"); + { + let mut left = remote_key; + while left.len() > 70 { + let (a, b) = left.split_at(70); + identity_data.push_str(a); + identity_data.push('\n'); + left = b; + } + identity_data.push_str(left); + identity_data.push('\n'); + } + identity_data.push_str("-----END OPENSSH PRIVATE KEY-----\n"); + + let config_data = format!("Host {host_pattern} +IdentityFile ./{} +PasswordAuthentication no +StrictHostKeyChecking accept-new +UpdateHostKeys yes +UserKnownHostsFile ./{} +", identity_file.file_name().unwrap().to_str().unwrap(), known_hosts.file_name().unwrap().to_str().unwrap()); + + let (a, b, c) = tokio::join!( + write_private_file(identity_file, identity_data), + fs::write(known_hosts, "\n"), + fs::write(ssh_config, config_data), + ); + a?; + b?; + c?; + Ok(()) +} + async fn setup_project_root( config: &Config<'_>, db: &Db, project_id: &String, remote: String, + remote_key: String, main_branch: String, -) -> Result<Arc<git::Repository>, git::Error> { +) -> Result<Arc<git::Repository>, anyhow::Error> { let mut path = PathBuf::from(config.git_server_root.to_string()); path.push(project_id); + info!("{project_id}: Setup repo at {path:?}"); let githook = PathBuf::from(config.git_hook.to_string()); + let ssh_config = path.join("ssh_config"); let repo = Arc::new(git::Repository::new( path, true, Some(remote), Some(project_id), Some(githook), + Some(ssh_config), )); repo.setup().await?; + write_ssh_config(repo.ssh_config().unwrap(), + get_host(repo.remote().unwrap()).as_str(), + remote_key.as_str()).await?; + if !repo.remote().unwrap().is_empty() && !main_branch.is_empty() { let bg_repo = repo.clone(); - tokio::spawn(async move { bg_repo.fetch(main_branch).await }); + let bg_project_id = project_id.clone(); + tokio::spawn(async move { + match bg_repo.fetch(&main_branch).await { + Ok(()) => {} + Err(e) => { + error!("{bg_project_id}: fetch {main_branch} returned {e:?}"); + } + } + }); } let socket_repo = repo.clone(); let socket_db = db.deref().clone(); + let bg_project_id = project_id.clone(); tokio::spawn(async move { match git_socket_listen(socket_repo, socket_db).await { Ok(()) => {} Err(e) => { - // TODO: Log - print!("git_socket_listen returned {:?}", e) + error!("{bg_project_id}: git_socket_listen returned {e:?}"); } } }); @@ -444,17 +545,17 @@ async fn setup_project_root( async fn setup_projects_roots(roots: &Roots, config: &Config<'_>, db: &Db) -> anyhow::Result<()> { fs_utils::create_dir_allow_existing(PathBuf::from(config.git_server_root.to_string())).await?; - let projects = sqlx::query!("SELECT id,remote,main_branch FROM projects") + let projects = sqlx::query!("SELECT id,remote,remote_key,main_branch FROM projects") .fetch(&**db) - .map_ok(|r| (r.id, r.remote, r.main_branch)) + .map_ok(|r| (r.id, r.remote, r.remote_key, r.main_branch)) .try_collect::<Vec<_>>() .await .unwrap(); let mut project_repo: HashMap<String, Arc<git::Repository>> = HashMap::new(); - for (id, remote, main_branch) in projects { - let repo = setup_project_root(config, db, &id, remote, main_branch).await?; + for (id, remote, remote_key, main_branch) in projects { + let repo = setup_project_root(config, db, &id, remote, remote_key, main_branch).await?; project_repo.insert(id, repo); } @@ -472,7 +573,10 @@ async fn setup_projects(rocket: Rocket<Build>) -> fairing::Result { Some(roots) => match Db::fetch(&rocket) { Some(db) => match setup_projects_roots(roots, config, db).await { Ok(_) => Ok(rocket), - Err(_) => Err(rocket), + Err(e) => { + error!("{e:?}"); + Err(rocket) + }, }, None => Err(rocket), }, |
