diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-02-01 22:42:11 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-02-01 22:42:11 +0100 |
| commit | d780391408b9e6d443e5e4f907748cae484b79fb (patch) | |
| tree | d961efd62478248081d1a327c818d6fa171f0a2d /server/src/git.rs | |
| parent | 05b674190f26e2a58cc7b7288586c031552d50f3 (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.rs | 537 |
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 - } -} |
