summaryrefslogtreecommitdiff
path: root/server/src/git_root.rs
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-02-20 22:53:27 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-02-20 22:53:27 +0100
commite940d84f69e3fd627731d5d3f698d6f838797862 (patch)
tree779eefcde993e22c0a69c18a3cb6e1cb9d17aad3 /server/src/git_root.rs
parentbf025b4977543a371df9dbdddfe9cc2f02f2a8d0 (diff)
WIPWIP
Diffstat (limited to 'server/src/git_root.rs')
-rw-r--r--server/src/git_root.rs128
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),
},