use std::fmt::{Display, Formatter}; use std::str::FromStr; use serde::{Deserialize, Serialize}; use crate::object::GiteratedObject; use super::{instance::Instance, user::User}; mod operations; mod settings; mod values; pub use operations::*; pub use settings::*; pub use values::*; /// A repository, defined by the instance it exists on along with /// its owner and name. /// /// # Textual Format /// A repository's textual reference is defined as: /// /// `{owner: User}/{name: String}@{instance: Instance}` /// /// # Examples /// For the repository named `foo` owned by `barson:giterated.dev` on the instance /// `giterated.dev`, the following [`Repository`] initialization would /// be valid: /// /// ``` //# use giterated_models::model::repository::Repository; //# use giterated_models::model::instance::Instance; //# use giterated_models::model::user::User; /// let repository = Repository { /// owner: User::from_str("barson:giterated.dev").unwrap(), /// name: String::from("foo"), /// instance: Instance::from_str("giterated.dev").unwrap() /// }; /// /// // This is correct /// assert_eq!(Repository::from_str("barson:giterated.dev/foo@giterated.dev").unwrap(), repository); /// ``` #[derive(Hash, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Repository { pub owner: User, pub name: String, /// Instance the repository is on pub instance: Instance, } impl Display for Repository { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&format!("{}/{}@{}", self.owner, self.name, self.instance)) } } impl GiteratedObject for Repository { fn object_name() -> &'static str { "repository" } fn from_object_str(object_str: &str) -> Result { Ok(Repository::from_str(object_str)?) } } impl TryFrom for Repository { type Error = RepositoryParseError; fn try_from(value: String) -> Result { Self::from_str(&value) } } impl FromStr for Repository { type Err = RepositoryParseError; fn from_str(s: &str) -> Result { let mut by_ampersand = s.split('@'); let mut path_split = by_ampersand.next().ok_or(RepositoryParseError)?.split('/'); let instance = Instance::from_str(by_ampersand.next().ok_or(RepositoryParseError)?) .map_err(|_| RepositoryParseError)?; let owner = User::from_str(path_split.next().ok_or(RepositoryParseError)?) .map_err(|_| RepositoryParseError)?; let name = path_split.next().ok_or(RepositoryParseError)?.to_string(); Ok(Self { instance, owner, name, }) } } #[derive(Debug, thiserror::Error)] #[error("no parse!")] pub struct RepositoryParseError; /// Visibility of the repository to the general eye #[derive(PartialEq, Eq, Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] #[sqlx(type_name = "visibility", rename_all = "lowercase")] pub enum RepositoryVisibility { Public, Unlisted, Private, } /// Implements [`Display`] for [`RepositoryVisiblity`] using [`Debug`] impl Display for RepositoryVisibility { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryView { /// Name of the repository /// /// This is different than the [`Repository`] name, /// which may be a path. pub name: String, /// Owner of the Repository pub owner: User, /// Repository description pub description: Option, /// Repository visibility pub visibility: Visibility, /// Default branch of the repository pub default_branch: DefaultBranch, /// Last commit made to the repository pub latest_commit: Option, /// Repository statistics pub stats: RepositoryStatistics, /// Revision of the displayed tree pub tree_rev: Option, /// Repository tree pub tree: Vec, } /// Generic repository statistics #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryStatistics { /// Amount of commits made to this branch in the repository pub commits: usize, /// Amount of branches the repository has pub branches: usize, /// Amount of tags the repository has pub tags: usize, } /// Repository branch #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryBranch { /// Full reference name pub name: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryFile { /// ID of the file pub id: String, /// Content of the file pub content: Vec, /// If the file is binary or not pub binary: bool, /// File size in bytes pub size: usize, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryDiff { /// "to" side of the diff commit pub new_commit: Commit, /// Total number of files changed pub files_changed: usize, /// Total number of insertions pub insertions: usize, /// Total number of deletions pub deletions: usize, /// List of changed files pub files: Vec, } /// Represents the type of change made to a [`RepositoryDiffFile`] #[derive(Clone, Debug, Serialize, Deserialize)] pub enum RepositoryDiffFileStatus { /// No changes Unmodified, Added, Deleted, /// Content changed between old and new Modified, /// Renamed between old and new Renamed, /// Copied from another old entry Copied, /// Ignored item in workdir Ignored, /// Untracked item in workdir Untracked, /// Type of file changed between old and new Typechange, /// File is unreadable Unreadable, /// File in the index is conflicted Conflicted, } impl From for RepositoryDiffFileStatus { fn from(status: git2::Delta) -> Self { match status { git2::Delta::Unmodified => Self::Unmodified, git2::Delta::Added => Self::Added, git2::Delta::Deleted => Self::Deleted, git2::Delta::Modified => Self::Modified, git2::Delta::Renamed => Self::Renamed, git2::Delta::Copied => Self::Copied, git2::Delta::Ignored => Self::Ignored, git2::Delta::Untracked => Self::Untracked, git2::Delta::Typechange => Self::Typechange, git2::Delta::Unreadable => Self::Unreadable, git2::Delta::Conflicted => Self::Conflicted, } } } /// Represents a single file of a diff #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryDiffFile { /// The type of change made to this file pub status: RepositoryDiffFileStatus, /// "From" side of the diff, can be nonexistent if file for example got added for the first time pub old_file_info: Option, /// "To" side of the diff, can be nonexistent if file got removed pub new_file_info: Option, /// Individual chunks of changes in this file pub chunks: Vec, } /// Represents one side of a file diff [`RepositoryDiffFile`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryDiffFileInfo { /// ID of the file pub id: String, /// Path of the entry relative to the working directory of the repository pub path: String, /// Size in bytes pub size: u64, /// If the file is binary or not pub binary: bool, } /// Represents a single chunk of a file diff [`RepositoryDiffFile`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryDiffFileChunk { /// Header of the chunk pub header: Option, /// Starting line number of the old file pub old_start: u32, /// Number of lines in "from" side of this chunk pub old_lines: u32, /// Starting line number of the new file pub new_start: u32, /// Number of lines in "to" side of this chunk pub new_lines: u32, /// Lines of the chunk pub lines: Vec, } /// Represents the change type of the [`RepositoryChunkLine`], incomplete of what git actually provides. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum RepositoryChunkLineType { Context, Addition, Deletion, } impl From for RepositoryChunkLineType { fn from(line_type: git2::DiffLineType) -> Self { match line_type { git2::DiffLineType::Context => Self::Context, git2::DiffLineType::Addition => Self::Addition, git2::DiffLineType::Deletion => Self::Deletion, _ => Self::Context, } } } /// Represents a single line of a [`RepositoryDiffFileChunk`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryChunkLine { /// Type of change the line is pub change_type: RepositoryChunkLineType, /// Content of the line pub content: String, /// Line number in old file pub old_line_num: Option, /// Line number in new file pub new_line_num: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RepositoryObjectType { Tree, Blob, } /// Stored info for our tree entries #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RepositoryTreeEntry { /// ID of the tree/blob pub id: String, /// Name of the tree/blob pub name: String, /// Type of the tree entry pub object_type: RepositoryObjectType, /// Git supplies us with the mode at all times, and people like it displayed. pub mode: i32, /// File size pub size: Option, /// Last commit made to the tree/blob pub last_commit: Option, } impl RepositoryTreeEntry { // I love you Emilia <3 pub fn new(id: &str, name: &str, object_type: RepositoryObjectType, mode: i32) -> Self { Self { id: id.to_string(), name: name.to_string(), object_type, mode, size: None, last_commit: None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RepositoryTreeEntryWithCommit { pub tree_entry: RepositoryTreeEntry, pub commit: Commit, } /// Info about a git commit #[derive(PartialEq, Hash, Eq, Debug, Clone, Serialize, Deserialize)] pub struct Commit { /// Unique commit ID pub oid: String, /// Shortened abbreviated OID /// This starts at the git config's "core.abbrev" length (default 7 characters) and /// iteratively extends to a longer string if that length is ambiguous. The /// result will be unambiguous (at least until new objects are added to the repository). pub short_oid: String, /// First paragraph of the full message pub summary: Option, /// Everything in the full message apart from the first paragraph pub body: Option, /// All commit id's of the parents of this commit pub parents: Vec, /// Who created the commit pub author: CommitSignature, /// Who committed the commit pub committer: CommitSignature, /// Time when the commit happened pub time: chrono::NaiveDateTime, } /// Gets all info from [`git2::Commit`] for easy use impl From> for Commit { fn from(commit: git2::Commit<'_>) -> Self { Self { oid: commit.id().to_string(), // This shouldn't ever fail, as we already know the object has an oid. short_oid: commit .as_object() .short_id() .unwrap() .as_str() .unwrap() .to_string(), summary: commit.summary().map(|summary| summary.to_string()), body: commit.body().map(|body| body.to_string()), parents: commit .parents() .map(|parent| parent.id().to_string()) .collect::>(), author: commit.author().into(), committer: commit.committer().into(), time: chrono::NaiveDateTime::from_timestamp_opt(commit.time().seconds(), 0).unwrap(), } } } /// Git commit signature #[derive(PartialEq, Hash, Eq, Debug, Clone, Serialize, Deserialize)] pub struct CommitSignature { pub name: Option, pub email: Option, pub time: chrono::NaiveDateTime, } /// Converts the signature from git2 into something usable without explicit lifetimes. impl From> for CommitSignature { fn from(signature: git2::Signature<'_>) -> Self { Self { name: signature.name().map(|name| name.to_string()), email: signature.email().map(|email| email.to_string()), time: chrono::NaiveDateTime::from_timestamp_opt(signature.when().seconds(), 0).unwrap(), } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositorySummary { pub repository: Repository, pub owner: User, pub visibility: RepositoryVisibility, pub description: Option, pub last_commit: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct IssueLabel { pub name: String, pub color: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryIssue { pub author: User, pub id: u64, pub title: String, pub contents: String, pub labels: Vec, }