// Back - An extremely simple git bug visualization system. Inspired by TVL's // panettone. // // Copyright (C) 2024 Benedikt Peetz // Copyright (C) 2025 Benedikt Peetz // SPDX-License-Identifier: AGPL-3.0-or-later // // This file is part of Back. // // You should have received a copy of the License along with this program. // If not, see . use std::{ fs, path::{Path, PathBuf}, }; use git_bug::{entities::issue::Issue, replica::Replica}; use serde::Deserialize; use url::Url; use crate::error::{self, Error}; #[derive(Debug, Deserialize)] pub struct BackConfig { /// The url to the source code of back. This is needed, because back is licensed under the /// AGPL. pub source_code_repository_url: Url, /// The root url this instance of back is hosted on. /// For example: /// `issues.foss-syndicate.org` pub root_url: Url, /// The file that list all the projects. /// /// This is `cgit`'s `project-list` setting. pub project_list: PathBuf, /// The path that is the common parent of all the repositories. /// /// This is `cgit`'s `scan-path` setting. pub scan_path: PathBuf, } #[derive(Debug)] pub struct BackRepositories { repositories: Vec, } impl BackRepositories { #[must_use] pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter { self.into_iter() } } impl<'a> IntoIterator for &'a BackRepositories { type IntoIter = <&'a Vec as IntoIterator>::IntoIter; type Item = <&'a Vec as IntoIterator>::Item; fn into_iter(self) -> Self::IntoIter { self.repositories.iter() } } impl BackRepositories { /// Try to get the repository at path `path`. /// /// # Errors /// - If no repository was registered/found at `path` pub fn get(&self, path: &Path) -> Result<&BackRepository, Error> { self.repositories .iter() .find(|p| p.repo_path == path) .ok_or(Error::RepoFind { repository_path: path.to_owned(), }) } } #[derive(Debug)] pub struct BackRepository { repo_path: PathBuf, } impl BackRepository { /// Open this repository. /// /// # Errors /// /// This function will return an error if the repository could not be opened. pub fn open(&self, scan_path: &Path) -> Result { let path = { let base = scan_path.join(&self.repo_path); if base.is_dir() { base } else { PathBuf::from(base.display().to_string() + ".git") } }; let repo = Replica::from_path(path).map_err(|err| Error::RepoOpen { repository_path: self.repo_path.clone(), error: Box::new(err), })?; // We could also check that Identity data is in the Replica, // but Issue existent is paramount. if repo.contains::()? { Ok(repo) } else { Err(Error::NotGitBug { path: self.repo_path.clone(), }) } } #[must_use] pub fn path(&self) -> &Path { &self.repo_path } } impl BackConfig { /// Returns the repositories of this [`BackConfig`]. /// /// # Note /// This will always re-read the `projects.list` file, to pick up repositories that were added /// in the mean time. /// /// # Errors /// /// This function will return an error if the associated IO operations fail. pub fn repositories(&self) -> error::Result { let repositories = fs::read_to_string(&self.project_list) .map_err(|err| Error::ProjectListRead { error: err, file: self.project_list.clone(), })? .lines() .try_fold(vec![], |mut acc, path| { acc.push(BackRepository { repo_path: PathBuf::from(path), }); Ok::<_, Error>(acc) })?; Ok(BackRepositories { repositories }) } /// Construct this [`BackConfig`] from a config file. /// /// # Errors /// /// This function will return an error if the associated IO operations fail. pub fn from_config_file(path: &Path) -> error::Result { let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead { file: path.to_owned(), error: err, })?; serde_json::from_str(&value).map_err(|err| Error::ConfigParse { file: path.to_owned(), error: err, }) } }