summaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/fs_utils.rs54
-rw-r--r--server/src/git.rs537
-rw-r--r--server/src/git_socket.rs24
-rw-r--r--server/src/githook.rs106
-rw-r--r--server/src/main.rs7
5 files changed, 4 insertions, 724 deletions
diff --git a/server/src/fs_utils.rs b/server/src/fs_utils.rs
deleted file mode 100644
index 7905d01..0000000
--- a/server/src/fs_utils.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-#![allow(dead_code)]
-
-use std::io;
-use std::path::Path;
-use tokio::fs;
-
-pub async fn create_dir_allow_existing(path: impl AsRef<Path>) -> io::Result<()> {
- match fs::create_dir(path).await {
- Ok(_) => Ok(()),
- Err(e) => {
- if e.kind() == io::ErrorKind::AlreadyExists {
- Ok(())
- } else {
- Err(e)
- }
- }
- }
-}
-
-pub async fn remove_file_allow_not_found(path: impl AsRef<Path>) -> io::Result<()> {
- match fs::remove_file(path).await {
- Ok(_) => Ok(()),
- Err(e) => {
- if e.kind() == io::ErrorKind::NotFound {
- Ok(())
- } else {
- Err(e)
- }
- }
- }
-}
-
-pub async fn symlink_update_existing(
- src: impl AsRef<Path>,
- dst: impl AsRef<Path>,
-) -> io::Result<()> {
- let src = src.as_ref();
- let dst = dst.as_ref();
- match fs::symlink(&src, &dst).await {
- Ok(_) => Ok(()),
- Err(e) => {
- if e.kind() == io::ErrorKind::AlreadyExists {
- let path = fs::read_link(&dst).await?;
- if path == src {
- return Ok(());
- }
- fs::remove_file(&dst).await?;
- fs::symlink(&src, &dst).await
- } else {
- Err(e)
- }
- }
- }
-}
diff --git a/server/src/git.rs b/server/src/git.rs
deleted file mode 100644
index a05c670..0000000
--- a/server/src/git.rs
+++ /dev/null
@@ -1,537 +0,0 @@
-use futures::future::TryFutureExt;
-use pathdiff::diff_paths;
-use std::collections::HashMap;
-use std::fmt;
-use std::path::{Path, PathBuf};
-use std::process::Stdio;
-use tokio::fs;
-use tokio::process::Command;
-use tokio::sync::{RwLock, Semaphore};
-
-use crate::fs_utils;
-
-#[derive(Debug)]
-pub struct Error {
- pub message: String,
-}
-
-impl Error {
- fn new(message: impl Into<String>) -> Self {
- Self {
- message: message.into(),
- }
- }
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}", self.message)
- }
-}
-
-impl std::error::Error for Error {}
-
-pub const EMPTY: &str = "0000000000000000000000000000000000000000";
-
-struct RepoData {
- // Only one fetch at a time, and they should be in queue
- fetch_semaphore: Semaphore,
- config_cache: HashMap<String, String>,
-}
-
-pub struct Repository {
- path: PathBuf,
- bare: bool,
-
- remote: Option<String>,
- project_id: Option<String>,
- socket: Option<PathBuf>,
- githook: Option<PathBuf>,
-
- // Lock for any repo task, 90% of all tasks are readers but there are some writers
- // where nothing else may be done.
- lock: RwLock<RepoData>,
-}
-
-#[allow(dead_code)]
-pub struct User {
- pub name: String,
- pub email: String,
- // Part before '@' in email
- pub username: String,
-}
-
-fn io_err(action: &str, e: std::io::Error) -> Error {
- Error::new(format!("{action}: {e}"))
-}
-
-fn parse_user(output: String) -> User {
- let mut lines = output.lines();
- let name = lines.next().unwrap_or("").to_string();
- let username = lines.next().unwrap_or("").to_string();
- let email = lines.next().unwrap_or("").to_string();
- User {
- name,
- email,
- username,
- }
-}
-
-impl RepoData {
- fn new() -> Self {
- Self {
- fetch_semaphore: Semaphore::new(1),
- config_cache: HashMap::new(),
- }
- }
-
- async fn fetch(&self, repo: &Repository, branch: String) -> Result<(), Error> {
- if repo.remote.is_none() {
- return Err(Error::new("No remote set"));
- }
-
- let _permit = self.fetch_semaphore.acquire().await;
-
- let mut cmd = self.git_cmd(repo);
- cmd.arg("fetch");
- // Use an atomic transaction to update local refs.
- cmd.arg("--atomic");
- // Print the output to standard output in an easy-to-parse format for scripts.
- cmd.arg("--porcelain");
- // This option disables this automatic tag following.
- cmd.arg("--no-tags");
- cmd.arg("origin");
- // <+ force update><remote branch>:<local branch>
- cmd.arg(format!("+{branch}:{branch}"));
-
- self.output(&mut cmd).await?;
-
- Ok(())
- }
-
- async fn init(&mut self, repo: &Repository) -> Result<(), Error> {
- fs_utils::create_dir_allow_existing(repo.path())
- .map_err(|e| Error::new(format!("{e}")))
- .await?;
-
- let mut cmd = self.git_cmd(repo);
- cmd.arg("init");
- if repo.is_bare() {
- cmd.arg("--bare");
- }
-
- self.run(&mut cmd).await?;
-
- Ok(())
- }
-
- async fn sync_config(&mut self, repo: &Repository) -> Result<(), Error> {
- self.config_fill_cache(repo).await?;
-
- if let Some(remote) = repo.remote() {
- self.config_set(repo, "remote.origin.url", remote).await?;
- }
- if let Some(socket) = repo.socket() {
- let relative = diff_paths(socket, repo.path()).unwrap();
- self.config_set(repo, "eyeballs.socket", relative.to_str().unwrap())
- .await?;
- }
-
- // Handled by pre-receive hook, allow fast forwards for reviews that expect it.
- self.config_set(repo, "receive.denyNonFastForwards", "false")
- .await?;
- // Handled by pre-receive hook, allow deletes for non-review branches
- self.config_set(repo, "receive.denyDeletes", "false")
- .await?;
-
- Ok(())
- }
-
- async fn sync_hooks(&mut self, repo: &Repository) -> Result<(), Error> {
- let hook = match repo.githook() {
- Some(path) => PathBuf::from(path),
- None => {
- let server_exe =
- std::env::current_exe().map_err(|e| io_err("unable to get current exe", e))?;
- server_exe.parent().unwrap().join("eyeballs-githook")
- }
- };
-
- let hooks = if repo.is_bare() {
- repo.path().join("hooks")
- } else {
- repo.path().join(".git/hooks")
- };
-
- fs_utils::create_dir_allow_existing(&hooks)
- .map_err(|e| io_err("unable to create hooks", e))
- .await?;
-
- let pre_receive = hooks.join("pre-receive");
- let update = hooks.join("update");
- let post_receive = hooks.join("post-receive");
-
- fs_utils::remove_file_allow_not_found(update)
- .map_err(|e| io_err("unable to remove update hook", e))
- .await?;
-
- // Must be hard links, symbolic links doesn't allow the hook
- // the lookup how it's called using std::env::current_exe().
- fs_utils::remove_file_allow_not_found(&pre_receive)
- .map_err(|e| io_err("unable to remove pre-receive hook", e))
- .await?;
- fs::hard_link(hook.as_path(), pre_receive)
- .map_err(|e| io_err("unable to link pre-receive hook", e))
- .await?;
- fs_utils::remove_file_allow_not_found(&post_receive)
- .map_err(|e| io_err("unable to remove post-receive hook", e))
- .await?;
- fs::hard_link(hook.as_path(), post_receive)
- .map_err(|e| io_err("unable to link post-receive hook", e))
- .await
- }
-
- async fn config_get(&self, repo: &Repository, name: &str) -> Result<String, Error> {
- if let Some(value) = self.config_cache.get(name) {
- return Ok(value.clone());
- }
-
- // Note, want to keep this method non-mutable so we can't update the cache here, should be
- // edge case to end up here anyway.
-
- let mut cmd = self.git_cmd(repo);
- cmd.arg("config")
- .arg("get")
- // End value with the null character and use newline as delimiter between key and value
- .arg("--null")
- .arg("--default=")
- .arg(name);
- let data = self.output(&mut cmd).await?;
- match data.as_str().split_once('\0') {
- Some((value, _)) => Ok(value.to_string()),
- None => Err(Error::new("Invalid output from git config get")),
- }
- }
-
- async fn config_fill_cache(&mut self, repo: &Repository) -> Result<(), Error> {
- self.config_cache.clear();
-
- let mut cmd = self.git_cmd(repo);
- cmd.arg("config")
- .arg("list")
- // read only from the repository .git/config,
- .arg("--local")
- // End value with the null character and use newline as delimiter between key and value
- .arg("--null");
- let data = self.output(&mut cmd).await?;
- for key_value in data.split_terminator('\0') {
- match key_value.split_once('\n') {
- Some((key, value)) => self.config_cache.insert(key.to_string(), value.to_string()),
- None => return Err(Error::new("Invalid output from git config list")),
- };
- }
- Ok(())
- }
-
- async fn config_set(
- &mut self,
- repo: &Repository,
- name: &str,
- value: &str,
- ) -> Result<(), Error> {
- if let Some(cached_value) = self.config_cache.get(name) {
- if cached_value == value {
- return Ok(());
- }
- }
-
- let mut cmd = self.git_cmd(repo);
- cmd.arg("config").arg("set").arg(name).arg(value);
- self.run(&mut cmd).await?;
-
- self.config_cache
- .insert(name.to_string(), value.to_string());
-
- Ok(())
- }
-
- async fn is_ancestor(
- &self,
- repo: &Repository,
- ancestor: &str,
- commit: &str,
- ) -> Result<bool, Error> {
- let mut cmd = self.git_cmd(repo);
- cmd.arg("merge-base")
- .arg("--is-ancestor")
- .arg(ancestor)
- .arg(commit);
- self.check(&mut cmd).await
- }
-
- async fn is_equal_content(
- &self,
- repo: &Repository,
- commit1: &str,
- commit2: &str,
- ) -> Result<bool, Error> {
- let mut cmd = self.git_cmd(repo);
- cmd.arg("diff")
- .arg("--quiet")
- .arg("--no-renames")
- .arg(commit1)
- .arg(commit2);
- self.check(&mut cmd).await
- }
-
- async fn get_author(&self, repo: &Repository, commit: &str) -> Result<User, Error> {
- self.get_log_format(repo, commit, "%an%n%al%n%ae")
- .map_ok(parse_user)
- .await
- }
-
- async fn get_commiter(&self, repo: &Repository, commit: &str) -> Result<User, Error> {
- self.get_log_format(repo, commit, "%cn%n%cl%n%ce")
- .map_ok(parse_user)
- .await
- }
-
- async fn get_log_format(
- &self,
- repo: &Repository,
- commit: &str,
- format: &str,
- ) -> Result<String, Error> {
- let mut cmd = self.git_cmd(repo);
- cmd.arg("log")
- .arg("-1")
- .arg("--no-decorate")
- .arg("--no-mailmap")
- .arg(format!("--pretty=format:{format}"))
- .arg(commit);
- self.output(&mut cmd).await
- }
-
- fn git_cmd(&self, repo: &Repository) -> Command {
- let mut cmd = Command::new("git");
- // Run as if git was started in <path> instead of the current working directory.
- cmd.arg("-C").arg(repo.path().to_str().unwrap());
- // Disable all advice hints from being printed.
- cmd.arg("--no-advice");
- // Do not pipe Git output into a pager.
- cmd.arg("--no-pager");
- // Do not perform optional operations that require locks.
- cmd.arg("--no-optional-locks");
-
- cmd
- }
-
- async fn run(&self, cmd: &mut Command) -> Result<(), Error> {
- cmd.stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::piped());
-
- let child = cmd
- .spawn()
- .map_err(|e| Error::new(format!("git command failed to start: {e}")))?;
-
- let output = child
- .wait_with_output()
- .map_err(|e| Error::new(format!("git command failed to execute: {e}")))
- .await?;
-
- if output.status.success() {
- Ok(())
- } else {
- Err(Error::new(format!(
- "git command failed with exitcode: {}\n{:?}\n{}",
- output.status,
- cmd.as_std().get_args(),
- std::str::from_utf8(output.stderr.as_slice()).unwrap_or(""),
- )))
- }
- }
-
- async fn check(&self, cmd: &mut Command) -> Result<bool, Error> {
- cmd.stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::piped());
-
- let child = cmd
- .spawn()
- .map_err(|e| Error::new(format!("git command failed to start: {e}")))?;
-
- let output = child
- .wait_with_output()
- .map_err(|e| Error::new(format!("git command failed to execute: {e}")))
- .await?;
-
- if output.status.success() {
- Ok(true)
- } else {
- match output.status.code() {
- Some(1) => Ok(false),
- _ => Err(Error::new(format!(
- "git command failed with exitcode: {}\n{:?}\n{}",
- output.status,
- cmd.as_std().get_args(),
- std::str::from_utf8(output.stderr.as_slice()).unwrap_or(""),
- ))),
- }
- }
- }
-
- async fn output(&self, cmd: &mut Command) -> Result<String, Error> {
- cmd.stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped());
-
- let child = cmd
- .spawn()
- .map_err(|e| Error::new(format!("git command failed to start: {e}")))?;
-
- let output = child
- .wait_with_output()
- .map_err(|e| Error::new(format!("git command failed to execute: {e}")))
- .await?;
-
- if output.status.success() {
- let output_utf8 = String::from_utf8(output.stdout)
- .map_err(|e| Error::new(format!("git command had invalid output: {e}")))?;
- Ok(output_utf8)
- } else {
- Err(Error::new(format!(
- "git command failed with exitcode: {}\n{:?}\n{}",
- output.status,
- cmd.as_std().get_args(),
- std::str::from_utf8(output.stderr.as_slice()).unwrap_or(""),
- )))
- }
- }
-}
-
-#[allow(dead_code)]
-impl Repository {
- pub fn new(
- path: impl Into<PathBuf>,
- bare: bool,
- remote: Option<impl Into<String>>,
- project_id: Option<impl Into<String>>,
- githook: Option<impl Into<PathBuf>>,
- ) -> Self {
- let path = path.into();
- let project_id = project_id.map(|x| x.into());
- let githook = githook.map(|x| x.into());
- let socket: Option<PathBuf>;
- if let Some(project_id) = &project_id {
- socket = Some(
- path.parent()
- .unwrap()
- .join(format!("{}.socket", project_id)),
- );
- } else {
- socket = None;
- }
-
- Self {
- remote: remote.map(|x| x.into()),
- project_id,
- path,
- socket,
- githook,
- bare,
- lock: RwLock::new(RepoData::new()),
- }
- }
-
- pub fn remote(&self) -> Option<&str> {
- self.remote.as_deref()
- }
-
- pub fn project_id(&self) -> Option<&str> {
- self.project_id.as_deref()
- }
-
- pub fn path(&self) -> &Path {
- self.path.as_path()
- }
-
- pub fn socket(&self) -> Option<&Path> {
- self.socket.as_deref()
- }
-
- fn githook(&self) -> Option<&Path> {
- self.githook.as_deref()
- }
-
- pub fn is_bare(&self) -> bool {
- self.bare
- }
-
- pub async fn setup(&self) -> Result<(), Error> {
- let mut data = self.lock.write().await;
-
- data.init(self).await?;
- data.sync_config(self).await?;
- if self.socket.is_some() {
- data.sync_hooks(self).await?;
- }
-
- Ok(())
- }
-
- pub async fn fetch(&self, branch: impl Into<String>) -> Result<(), Error> {
- let branch = branch.into();
- let data = self.lock.read().await;
-
- data.fetch(self, branch).await
- }
-
- pub async fn config_get(&self, name: impl Into<String>) -> Result<String, Error> {
- let name = name.into();
- let data = self.lock.read().await;
-
- data.config_get(self, name.as_str()).await
- }
-
- pub async fn is_ancestor(
- &self,
- ancestor: impl Into<String>,
- commit: impl Into<String>,
- ) -> Result<bool, Error> {
- let ancestor = ancestor.into();
- let commit = commit.into();
-
- let data = self.lock.read().await;
-
- data.is_ancestor(self, ancestor.as_str(), commit.as_str())
- .await
- }
-
- pub async fn is_equal_content(
- &self,
- commit1: impl Into<String>,
- commit2: impl Into<String>,
- ) -> Result<bool, Error> {
- let commit1 = commit1.into();
- let commit2 = commit2.into();
- let data = self.lock.read().await;
-
- data.is_equal_content(self, commit1.as_str(), commit2.as_str())
- .await
- }
-
- pub async fn get_author(&self, commit: impl Into<String>) -> Result<User, Error> {
- let commit = commit.into();
- let data = self.lock.read().await;
-
- data.get_author(self, commit.as_str()).await
- }
-
- pub async fn get_commiter(&self, commit: impl Into<String>) -> Result<User, Error> {
- let commit = commit.into();
- let data = self.lock.read().await;
-
- data.get_commiter(self, commit.as_str()).await
- }
-}
diff --git a/server/src/git_socket.rs b/server/src/git_socket.rs
deleted file mode 100644
index a4805be..0000000
--- a/server/src/git_socket.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-#[derive(Deserialize, Serialize)]
-pub struct GitReceive {
- pub old_value: String,
- pub new_value: String,
- pub reference: String,
- // Only set for pre hooks, because server can't read the objects the pre-hook has not yet
- // accepted, so to be able to validate the commiter, send them. Also only set if new_value
- // is not empty.
- pub commiter: Option<String>,
-}
-
-#[derive(Deserialize, Serialize)]
-pub struct GitHookRequest {
- pub pre: bool,
- pub receive: Vec<GitReceive>,
-}
-
-#[derive(Deserialize, Serialize)]
-pub struct GitHookResponse {
- pub ok: bool,
- pub message: String,
-}
diff --git a/server/src/githook.rs b/server/src/githook.rs
deleted file mode 100644
index 2e1de13..0000000
--- a/server/src/githook.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use rmp_serde::{decode, Serializer};
-use serde::ser::Serialize;
-use std::error::Error;
-use std::fmt;
-use std::os::unix::net::UnixStream;
-use std::path::PathBuf;
-use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
-use tokio::task;
-
-mod fs_utils;
-mod git;
-mod git_socket;
-
-#[derive(Debug)]
-struct IoError {
- message: String,
-}
-
-impl IoError {
- fn new(message: impl Into<String>) -> Self {
- Self {
- message: message.into(),
- }
- }
-}
-
-impl fmt::Display for IoError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}", self.message)
- }
-}
-
-impl Error for IoError {}
-
-#[tokio::main]
-async fn main() -> Result<(), Box<dyn Error>> {
- let pre = match std::env::current_exe()?
- .file_name()
- .and_then(|x| x.to_str())
- {
- Some("pre-receive") => true,
- Some("post-receive") => false,
- _ => return Err(Box::<dyn Error>::from("Invalid hook executable name")),
- };
-
- let input = io::stdin();
- let reader = BufReader::new(input);
- let mut lines = reader.lines();
-
- let mut request = git_socket::GitHookRequest {
- pre,
- receive: Vec::new(),
- };
-
- let repo = git::Repository::new(
- PathBuf::from("."),
- true,
- None::<String>,
- None::<String>,
- None::<PathBuf>,
- );
-
- while let Some(line) = lines.next_line().await? {
- let data: Vec<&str> = line.split(' ').collect();
-
- if data.len() == 3 {
- let mut commiter: Option<String> = None;
- if pre && data[1] != git::EMPTY {
- if let Ok(user) = repo.get_commiter(data[1]).await {
- commiter = Some(user.username);
- }
- }
-
- request.receive.push(git_socket::GitReceive {
- old_value: data[0].to_string(),
- new_value: data[1].to_string(),
- reference: data[2].to_string(),
- commiter,
- })
- }
- }
-
- let socket = PathBuf::from(repo.config_get("eyeballs.socket").await?);
-
- let response = task::spawn_blocking(move || {
- let stream = UnixStream::connect(socket).map_err(|e| IoError::new(e.to_string()))?;
- let mut serializer = Serializer::new(&stream);
- request
- .serialize(&mut serializer)
- .map_err(|e| IoError::new(e.to_string()))?;
- let result: Result<git_socket::GitHookResponse, IoError> =
- decode::from_read(stream).map_err(|e| IoError::new(e.to_string()));
- result
- })
- .await?
- .map_err(Box::<dyn Error>::from)?;
-
- let mut output = io::stdout();
- output.write_all(response.message.as_bytes()).await?;
-
- if response.ok {
- Ok(())
- } else {
- Err(Box::<dyn Error>::from("Hook failed"))
- }
-}
diff --git a/server/src/main.rs b/server/src/main.rs
index 3d6d0e6..b2974ac 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -14,6 +14,10 @@ use std::path::PathBuf;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
+use eyeballs_common::fs_utils;
+use eyeballs_common::git;
+use eyeballs_common::git_socket;
+
#[cfg(test)]
mod tests;
@@ -21,10 +25,7 @@ mod api_model;
mod auth;
mod authorized_keys;
mod db_utils;
-mod fs_utils;
-mod git;
mod git_root;
-mod git_socket;
use auth::AuthApiAddon;