summaryrefslogtreecommitdiff
path: root/server/src/git.rs
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-02-01 22:42:11 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-02-01 22:42:11 +0100
commitd780391408b9e6d443e5e4f907748cae484b79fb (patch)
treed961efd62478248081d1a327c818d6fa171f0a2d /server/src/git.rs
parent05b674190f26e2a58cc7b7288586c031552d50f3 (diff)
Use workspace instead of features
Having to include --feature=build-server in basically all commands that wasn't building eyeballs-githook got tiring quickly. Instead, use workspaces, with a separate project for building the githook. It means I also had to add a library common with code shared by both githook and server.
Diffstat (limited to 'server/src/git.rs')
-rw-r--r--server/src/git.rs537
1 files changed, 0 insertions, 537 deletions
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
- }
-}