JavaScript is disabled, refresh for a better experience. ambee/giterated

ambee/giterated

Git repository hosting, collaboration, and discovery for the Fediverse.

Change to new UTC chrono types

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨779637c

⁨giterated-models/src/repository/mod.rs⁩ - ⁨14934⁩ bytes
Raw
1 use std::fmt::{Display, Formatter};
2 use std::str::FromStr;
3
4 use chrono::{DateTime, Utc};
5 use serde::{Deserialize, Serialize};
6
7 use crate::object::GiteratedObject;
8
9 use super::{instance::Instance, user::User};
10
11 mod operations;
12 mod settings;
13 mod values;
14
15 pub use operations::*;
16 pub use settings::*;
17 pub use values::*;
18
19 /// A repository, defined by the instance it exists on along with
20 /// its owner and name.
21 ///
22 /// # Textual Format
23 /// A repository's textual reference is defined as:
24 ///
25 /// `{owner: User}/{name: String}@{instance: Instance}`
26 ///
27 /// # Examples
28 /// For the repository named `foo` owned by `barson:giterated.dev` on the instance
29 /// `giterated.dev`, the following [`Repository`] initialization would
30 /// be valid:
31 ///
32 /// ```
33 //# use giterated_models::model::repository::Repository;
34 //# use giterated_models::model::instance::Instance;
35 //# use giterated_models::model::user::User;
36 /// let repository = Repository {
37 /// owner: User::from_str("barson:giterated.dev").unwrap(),
38 /// name: String::from("foo"),
39 /// instance: Instance::from_str("giterated.dev").unwrap()
40 /// };
41 ///
42 /// // This is correct
43 /// assert_eq!(Repository::from_str("barson:giterated.dev/[email protected]").unwrap(), repository);
44 /// ```
45 #[derive(Hash, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46 pub struct Repository {
47 pub owner: User,
48 pub name: String,
49 /// Instance the repository is on
50 pub instance: Instance,
51 }
52
53 impl Display for Repository {
54 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55 f.write_str(&format!("{}/{}@{}", self.owner, self.name, self.instance))
56 }
57 }
58
59 impl GiteratedObject for Repository {
60 fn object_name() -> &'static str {
61 "repository"
62 }
63
64 fn from_object_str(object_str: &str) -> Result<Self, anyhow::Error> {
65 Ok(Repository::from_str(object_str)?)
66 }
67
68 fn home_uri(&self) -> String {
69 self.instance.home_uri()
70 }
71 }
72
73 impl TryFrom<String> for Repository {
74 type Error = RepositoryParseError;
75
76 fn try_from(value: String) -> Result<Self, Self::Error> {
77 Self::from_str(&value)
78 }
79 }
80
81 impl FromStr for Repository {
82 type Err = RepositoryParseError;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 let mut by_ampersand = s.split('@');
86 let mut path_split = by_ampersand.next().ok_or(RepositoryParseError)?.split('/');
87
88 let instance = Instance::from_str(by_ampersand.next().ok_or(RepositoryParseError)?)
89 .map_err(|_| RepositoryParseError)?;
90 let owner = User::from_str(path_split.next().ok_or(RepositoryParseError)?)
91 .map_err(|_| RepositoryParseError)?;
92 let name = path_split.next().ok_or(RepositoryParseError)?.to_string();
93
94 Ok(Self {
95 instance,
96 owner,
97 name,
98 })
99 }
100 }
101
102 #[derive(Debug, thiserror::Error)]
103 #[error("no parse!")]
104 pub struct RepositoryParseError;
105
106 /// Visibility of the repository to the general eye
107 #[derive(PartialEq, Eq, Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)]
108 #[sqlx(type_name = "visibility", rename_all = "lowercase")]
109 pub enum RepositoryVisibility {
110 Public,
111 Unlisted,
112 Private,
113 }
114
115 /// Implements [`Display`] for [`RepositoryVisiblity`] using [`Debug`]
116 impl Display for RepositoryVisibility {
117 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{:?}", self)
119 }
120 }
121
122 #[derive(Clone, Debug, Serialize, Deserialize)]
123 pub struct RepositoryView {
124 /// Name of the repository
125 ///
126 /// This is different than the [`Repository`] name,
127 /// which may be a path.
128 pub name: String,
129 /// Owner of the Repository
130 pub owner: User,
131 /// Repository description
132 pub description: Option<Description>,
133 /// Repository visibility
134 pub visibility: Visibility,
135 /// Default branch of the repository
136 pub default_branch: DefaultBranch,
137 /// Last commit made to the repository
138 pub latest_commit: Option<LatestCommit>,
139 /// Repository statistics
140 pub stats: RepositoryStatistics,
141 /// Revision of the displayed tree
142 pub tree_rev: Option<String>,
143 /// Repository tree
144 pub tree: Vec<RepositoryTreeEntry>,
145 }
146
147 /// Generic repository statistics
148 #[derive(Clone, Debug, Serialize, Deserialize, Default)]
149 pub struct RepositoryStatistics {
150 /// Amount of commits made to this branch in the repository
151 pub commits: usize,
152 /// Amount of branches the repository has
153 pub branches: usize,
154 /// Amount of tags the repository has
155 pub tags: usize,
156 }
157
158 /// Repository branch
159 #[derive(Clone, Debug, Serialize, Deserialize)]
160 pub struct RepositoryBranch {
161 /// Full reference name
162 pub name: String,
163 /// Whether the branch is stale or not
164 pub stale: bool,
165 /// The last commit made to the branch
166 pub last_commit: Option<Commit>,
167 /// How many commits this branch is ahead or behind from the default branch
168 pub ahead_behind_default: Option<(usize, usize)>,
169 }
170
171 /// Filter primarily used for branch requests
172 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
173 pub enum RepositoryBranchFilter {
174 None,
175 Active,
176 Stale,
177 }
178
179 /// Repository tag
180 #[derive(Clone, Debug, Serialize, Deserialize)]
181 pub struct RepositoryTag {
182 /// Git OID for the tag
183 pub id: String,
184 /// Full tag name
185 pub name: String,
186 /// First paragraph of the full message
187 pub summary: Option<String>,
188 /// Everything in the full message apart from the first paragraph
189 pub body: Option<String>,
190 /// Signature of the tag author
191 pub author: Option<CommitSignature>,
192 /// Time the tag or pointed commit was created
193 pub time: Option<DateTime<Utc>>,
194 /// Commit the tag is pointing to
195 pub commit: Option<Commit>,
196 }
197
198 #[derive(Clone, Debug, Serialize, Deserialize)]
199 pub struct RepositoryFile {
200 /// ID of the file
201 pub id: String,
202 /// Content of the file
203 pub content: Vec<u8>,
204 /// If the file is binary or not
205 pub binary: bool,
206 /// File size in bytes
207 pub size: usize,
208 }
209
210 #[derive(Clone, Debug, Serialize, Deserialize)]
211 pub struct RepositoryDiff {
212 /// "to" side of the diff commit
213 pub new_commit: Commit,
214 /// Total number of files changed
215 pub files_changed: usize,
216 /// Total number of insertions
217 pub insertions: usize,
218 /// Total number of deletions
219 pub deletions: usize,
220 /// List of changed files
221 pub files: Vec<RepositoryDiffFile>,
222 }
223
224 /// Represents the type of change made to a [`RepositoryDiffFile`]
225 #[derive(Clone, Debug, Serialize, Deserialize)]
226 pub enum RepositoryDiffFileStatus {
227 /// No changes
228 Unmodified,
229 Added,
230 Deleted,
231 /// Content changed between old and new
232 Modified,
233 /// Renamed between old and new
234 Renamed,
235 /// Copied from another old entry
236 Copied,
237 /// Ignored item in workdir
238 Ignored,
239 /// Untracked item in workdir
240 Untracked,
241 /// Type of file changed between old and new
242 Typechange,
243 /// File is unreadable
244 Unreadable,
245 /// File in the index is conflicted
246 Conflicted,
247 }
248
249 impl From<git2::Delta> for RepositoryDiffFileStatus {
250 fn from(status: git2::Delta) -> Self {
251 match status {
252 git2::Delta::Unmodified => Self::Unmodified,
253 git2::Delta::Added => Self::Added,
254 git2::Delta::Deleted => Self::Deleted,
255 git2::Delta::Modified => Self::Modified,
256 git2::Delta::Renamed => Self::Renamed,
257 git2::Delta::Copied => Self::Copied,
258 git2::Delta::Ignored => Self::Ignored,
259 git2::Delta::Untracked => Self::Untracked,
260 git2::Delta::Typechange => Self::Typechange,
261 git2::Delta::Unreadable => Self::Unreadable,
262 git2::Delta::Conflicted => Self::Conflicted,
263 }
264 }
265 }
266
267 /// Represents a single file of a diff
268 #[derive(Clone, Debug, Serialize, Deserialize)]
269 pub struct RepositoryDiffFile {
270 /// The type of change made to this file
271 pub status: RepositoryDiffFileStatus,
272 /// "From" side of the diff, can be nonexistent if file for example got added for the first time
273 pub old_file_info: Option<RepositoryDiffFileInfo>,
274 /// "To" side of the diff, can be nonexistent if file got removed
275 pub new_file_info: Option<RepositoryDiffFileInfo>,
276 /// Individual chunks of changes in this file
277 pub chunks: Vec<RepositoryDiffFileChunk>,
278 }
279
280 /// Represents one side of a file diff [`RepositoryDiffFile`]
281 #[derive(Clone, Debug, Serialize, Deserialize)]
282 pub struct RepositoryDiffFileInfo {
283 /// ID of the file
284 pub id: String,
285 /// Path of the entry relative to the working directory of the repository
286 pub path: String,
287 /// Size in bytes
288 pub size: u64,
289 /// If the file is binary or not
290 pub binary: bool,
291 }
292
293 /// Represents a single chunk of a file diff [`RepositoryDiffFile`]
294 #[derive(Clone, Debug, Serialize, Deserialize)]
295 pub struct RepositoryDiffFileChunk {
296 /// Header of the chunk
297 pub header: Option<String>,
298 /// Starting line number of the old file
299 pub old_start: u32,
300 /// Number of lines in "from" side of this chunk
301 pub old_lines: u32,
302 /// Starting line number of the new file
303 pub new_start: u32,
304 /// Number of lines in "to" side of this chunk
305 pub new_lines: u32,
306 /// Lines of the chunk
307 pub lines: Vec<RepositoryChunkLine>,
308 }
309
310 /// Represents the change type of the [`RepositoryChunkLine`], incomplete of what git actually provides.
311 #[derive(Clone, Debug, Serialize, Deserialize)]
312 pub enum RepositoryChunkLineType {
313 Context,
314 Addition,
315 Deletion,
316 }
317
318 impl From<git2::DiffLineType> for RepositoryChunkLineType {
319 fn from(line_type: git2::DiffLineType) -> Self {
320 match line_type {
321 git2::DiffLineType::Context => Self::Context,
322 git2::DiffLineType::Addition => Self::Addition,
323 git2::DiffLineType::Deletion => Self::Deletion,
324 _ => Self::Context,
325 }
326 }
327 }
328
329 /// Represents a single line of a [`RepositoryDiffFileChunk`]
330 #[derive(Clone, Debug, Serialize, Deserialize)]
331 pub struct RepositoryChunkLine {
332 /// Type of change the line is
333 pub change_type: RepositoryChunkLineType,
334 /// Content of the line
335 pub content: String,
336 /// Line number in old file
337 pub old_line_num: Option<u32>,
338 /// Line number in new file
339 pub new_line_num: Option<u32>,
340 }
341
342 #[derive(Debug, Clone, Serialize, Deserialize)]
343 pub enum RepositoryObjectType {
344 Tree,
345 Blob,
346 }
347
348 /// Stored info for our tree entries
349 #[derive(Debug, Clone, Serialize, Deserialize)]
350 pub struct RepositoryTreeEntry {
351 /// ID of the tree/blob
352 pub id: String,
353 /// Name of the tree/blob
354 pub name: String,
355 /// Type of the tree entry
356 pub object_type: RepositoryObjectType,
357 /// Git supplies us with the mode at all times, and people like it displayed.
358 pub mode: i32,
359 /// File size
360 pub size: Option<usize>,
361 /// Last commit made to the tree/blob
362 pub last_commit: Option<Commit>,
363 }
364
365 impl RepositoryTreeEntry {
366 // I love you Emilia <3
367 pub fn new(id: &str, name: &str, object_type: RepositoryObjectType, mode: i32) -> Self {
368 Self {
369 id: id.to_string(),
370 name: name.to_string(),
371 object_type,
372 mode,
373 size: None,
374 last_commit: None,
375 }
376 }
377 }
378
379 #[derive(Debug, Clone, Serialize, Deserialize)]
380 pub struct RepositoryTreeEntryWithCommit {
381 pub tree_entry: RepositoryTreeEntry,
382 pub commit: Commit,
383 }
384
385 /// Info about a git commit
386 #[derive(PartialEq, Hash, Eq, Debug, Clone, Serialize, Deserialize)]
387 pub struct Commit {
388 /// Unique commit ID
389 pub oid: String,
390 /// Shortened abbreviated OID
391 /// This starts at the git config's "core.abbrev" length (default 7 characters) and
392 /// iteratively extends to a longer string if that length is ambiguous. The
393 /// result will be unambiguous (at least until new objects are added to the repository).
394 pub short_oid: String,
395 /// First paragraph of the full message
396 pub summary: Option<String>,
397 /// Everything in the full message apart from the first paragraph
398 pub body: Option<String>,
399 /// All commit id's of the parents of this commit
400 pub parents: Vec<String>,
401 /// Who created the commit
402 pub author: CommitSignature,
403 /// Who committed the commit
404 pub committer: CommitSignature,
405 /// Time when the commit happened
406 pub time: chrono::DateTime<chrono::Utc>,
407 }
408
409 /// Gets all info from [`git2::Commit`] for easy use
410 impl From<git2::Commit<'_>> for Commit {
411 fn from(commit: git2::Commit<'_>) -> Self {
412 Self {
413 oid: commit.id().to_string(),
414 // This shouldn't ever fail, as we already know the object has an oid.
415 short_oid: commit
416 .as_object()
417 .short_id()
418 .unwrap()
419 .as_str()
420 .unwrap()
421 .to_string(),
422 summary: commit.summary().map(|summary| summary.to_string()),
423 body: commit.body().map(|body| body.to_string()),
424 parents: commit
425 .parents()
426 .map(|parent| parent.id().to_string())
427 .collect::<Vec<String>>(),
428 author: commit.author().into(),
429 committer: commit.committer().into(),
430 time: chrono::DateTime::from_timestamp(commit.time().seconds(), 0).unwrap(),
431 }
432 }
433 }
434
435 /// Git commit signature
436 #[derive(PartialEq, Hash, Eq, Debug, Clone, Serialize, Deserialize)]
437 pub struct CommitSignature {
438 pub name: Option<String>,
439 pub email: Option<String>,
440 pub time: chrono::DateTime<chrono::Utc>,
441 }
442
443 /// Converts the signature from git2 into something usable without explicit lifetimes.
444 impl From<git2::Signature<'_>> for CommitSignature {
445 fn from(signature: git2::Signature<'_>) -> Self {
446 Self {
447 name: signature.name().map(|name| name.to_string()),
448 email: signature.email().map(|email| email.to_string()),
449 time: chrono::DateTime::from_timestamp(signature.when().seconds(), 0).unwrap(),
450 }
451 }
452 }
453
454 /// The document type of a [`Commit`]'s body
455 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
456 pub enum CommitBodyType {
457 Plain,
458 Markdown,
459 }
460
461 impl Default for CommitBodyType {
462 fn default() -> Self {
463 Self::Plain
464 }
465 }
466
467 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
468 pub struct RepositorySummary {
469 pub repository: Repository,
470 pub owner: User,
471 pub visibility: RepositoryVisibility,
472 pub description: Option<String>,
473 pub last_commit: Option<Commit>,
474 }
475
476 #[derive(Clone, Debug, Serialize, Deserialize)]
477 pub struct IssueLabel {
478 pub name: String,
479 pub color: String,
480 }
481
482 #[derive(Clone, Debug, Serialize, Deserialize)]
483 pub struct RepositoryIssue {
484 pub author: User,
485 pub id: u64,
486 pub title: String,
487 pub contents: String,
488 pub labels: Vec<IssueLabel>,
489 }
490