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

ambee/giterated

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

Make old operations use new types

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨b22dd12

⁨giterated-daemon/src/backend/git/mod.rs⁩ - ⁨25827⁩ bytes
Raw
1 use anyhow::Error;
2 use async_trait::async_trait;
3
4 use git2::BranchType;
5 use giterated_models::instance::{Instance, RepositoryCreateRequest};
6
7 use giterated_models::issue::{IssueInfoBasic, IssueTag};
8 use giterated_models::object::Object;
9 use giterated_models::repository::{
10 AccessList, Commit, DefaultBranch, Description, Repository, RepositoryBranch,
11 RepositoryBranchRequest, RepositoryBranchesRequest, RepositoryCommitBeforeRequest,
12 RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffPatchRequest,
13 RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest,
14 RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssueTagsRequest,
15 RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest,
16 RepositoryStatistics, RepositoryStatisticsRequest, RepositoryTag, RepositoryTagRequest,
17 RepositoryTagsRequest, RepositoryTreeEntry, RepositoryVisibility, Visibility,
18 };
19
20 use giterated_models::user::User;
21
22 use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState};
23
24 use sqlx::PgPool;
25 use std::ops::Deref;
26 use std::{path::PathBuf, sync::Arc};
27 use thiserror::Error;
28 use tokio::sync::OnceCell;
29
30 pub mod branches;
31 pub mod commit;
32 pub mod diff;
33 pub mod file;
34 pub mod tags;
35
36 use super::{IssuesBackend, RepositoryBackend};
37
38 //region database structures
39
40 /// Repository in the database
41 #[derive(Debug, sqlx::FromRow)]
42 pub struct GitRepository {
43 #[sqlx(try_from = "String")]
44 pub owner_user: User,
45 pub name: String,
46 pub description: Option<String>,
47 pub visibility: RepositoryVisibility,
48 pub default_branch: String,
49 }
50
51 impl GitRepository {
52 // Separate function because "Private" will be expanded later
53 /// Checks if the user is allowed to view this repository
54 pub async fn can_user_view_repository(
55 &self,
56 our_instance: &Instance,
57 user: &Option<AuthenticatedUser>,
58 stack: &GiteratedStack,
59 ) -> bool {
60 if matches!(self.visibility, RepositoryVisibility::Public) {
61 return true;
62 }
63
64 // User must exist for any further checks to pass
65 let user = match user {
66 Some(user) => user,
67 None => return false,
68 };
69
70 if *user.deref() == self.owner_user {
71 // owner can always view
72 return true;
73 }
74
75 if matches!(self.visibility, RepositoryVisibility::Private) {
76 // Check if the user can view\
77 let access_list = stack
78 .new_get_setting::<_, AccessList>(&Repository {
79 owner: self.owner_user.clone(),
80 name: self.name.clone(),
81 instance: our_instance.clone(),
82 })
83 .await
84 .unwrap();
85
86 access_list
87 .0
88 .iter()
89 .any(|access_list_user| access_list_user == user.deref())
90 } else {
91 false
92 }
93 }
94
95 // This is in it's own function because I assume I'll have to add logic to this later
96 pub fn open_git2_repository(
97 &self,
98 repository_directory: &str,
99 ) -> Result<git2::Repository, GitBackendError> {
100 match git2::Repository::open(format!(
101 "{}/{}/{}/{}",
102 repository_directory, self.owner_user.instance, self.owner_user.username, self.name
103 )) {
104 Ok(repository) => Ok(repository),
105 Err(err) => {
106 let err = GitBackendError::FailedOpeningFromDisk(err);
107 error!("Couldn't open a repository, this is bad! {:?}", err);
108
109 Err(err)
110 }
111 }
112 }
113 }
114
115 //endregion
116
117 #[derive(Error, Debug)]
118 pub enum GitBackendError {
119 #[error("Failed creating repository")]
120 FailedCreatingRepository(git2::Error),
121 #[error("Failed inserting into the database")]
122 FailedInsertingIntoDatabase(sqlx::Error),
123 #[error("Failed finding repository {owner_user:?}/{name:?}")]
124 RepositoryNotFound { owner_user: String, name: String },
125 #[error("Repository {owner_user:?}/{name:?} already exists")]
126 RepositoryAlreadyExists { owner_user: String, name: String },
127 #[error("Repository couldn't be deleted from the disk")]
128 CouldNotDeleteFromDisk(std::io::Error),
129 #[error("Failed deleting repository from database")]
130 FailedDeletingFromDatabase(sqlx::Error),
131 #[error("Failed opening repository on disk")]
132 FailedOpeningFromDisk(git2::Error),
133 #[error("Couldn't find ref with name `{0}`")]
134 RefNotFound(String),
135 #[error("Couldn't find repository head")]
136 HeadNotFound,
137 #[error("Couldn't find default repository branch")]
138 DefaultNotFound,
139 #[error("Couldn't find tag with name `{0}`")]
140 TagNotFound(String),
141 #[error("Couldn't find path in repository `{0}`")]
142 PathNotFound(String),
143 #[error("Couldn't find branch with name `{0}`")]
144 BranchNotFound(String),
145 #[error("Couldn't find commit for path `{0}`")]
146 LastCommitNotFound(String),
147 #[error("Object ID `{0}` is invalid")]
148 InvalidObjectId(String),
149 #[error("Blob with ID `{0}` not found")]
150 BlobNotFound(String),
151 #[error("Tree with ID `{0}` not found")]
152 TreeNotFound(String),
153 #[error("Commit with ID `{0}` not found")]
154 CommitNotFound(String),
155 #[error("Parent for commit with ID `{0}` not found")]
156 CommitParentNotFound(String),
157 #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")]
158 FailedDiffing(String, String),
159 }
160
161 /// Parses and trims a git message (commit, tag) into a summary and body.
162 pub fn parse_trim_git_message(message: &str) -> (Option<String>, Option<String>) {
163 // Iterate over the lines
164 let mut lines = message
165 .lines()
166 .filter_map(|line| {
167 if line.is_empty() {
168 None
169 } else {
170 // Trim the whitespace for every line
171 let mut whitespace_removed = String::with_capacity(line.len());
172
173 line.split_whitespace().for_each(|word| {
174 if !whitespace_removed.is_empty() {
175 whitespace_removed.push(' ');
176 }
177
178 whitespace_removed.push_str(word);
179 });
180
181 Some(whitespace_removed)
182 }
183 })
184 .collect::<Vec<String>>();
185
186 let summary = Some(lines.remove(0));
187 let body = if lines.is_empty() {
188 None
189 } else {
190 Some(lines.join("\n"))
191 };
192
193 (summary, body)
194 }
195
196 pub struct GitBackend {
197 pg_pool: PgPool,
198 repository_folder: String,
199 instance: Instance,
200 stack: Arc<OnceCell<GiteratedStack>>,
201 }
202
203 impl GitBackend {
204 pub fn new(
205 pg_pool: &PgPool,
206 repository_folder: &str,
207 instance: impl ToOwned<Owned = Instance>,
208 stack: Arc<OnceCell<GiteratedStack>>,
209 ) -> Self {
210 let instance = instance.to_owned();
211
212 Self {
213 pg_pool: pg_pool.clone(),
214 // We make sure there's no end slash
215 repository_folder: repository_folder.trim_end_matches(&['/', '\\']).to_string(),
216 instance,
217 stack,
218 }
219 }
220
221 pub async fn find_by_owner_user_name(
222 &self,
223 user: &User,
224 repository_name: &str,
225 ) -> Result<GitRepository, GitBackendError> {
226 // TODO: Patch for use with new GetValue system
227 if let Ok(repository) = sqlx::query_as!(GitRepository,
228 r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#,
229 user.to_string(), repository_name)
230 .fetch_one(&self.pg_pool.clone())
231 .await {
232 Ok(repository)
233 } else {
234 Err(GitBackendError::RepositoryNotFound {
235 owner_user: user.to_string(),
236 name: repository_name.to_string(),
237 })
238 }
239 }
240
241 pub async fn delete_by_owner_user_name(
242 &self,
243 user: &User,
244 repository_name: &str,
245 ) -> Result<u64, GitBackendError> {
246 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
247 "{}/{}/{}/{}",
248 self.repository_folder, user.instance, user.username, repository_name
249 ))) {
250 let err = GitBackendError::CouldNotDeleteFromDisk(err);
251 error!(
252 "Couldn't delete repository from disk, this is bad! {:?}",
253 err
254 );
255
256 return Err(err);
257 }
258
259 // Delete the repository from the database
260 self.delete_from_database(user, repository_name).await
261 }
262
263 /// Deletes the repository from the database
264 pub async fn delete_from_database(
265 &self,
266 user: &User,
267 repository_name: &str,
268 ) -> Result<u64, GitBackendError> {
269 match sqlx::query!(
270 "DELETE FROM repositories WHERE owner_user = $1 AND name = $2",
271 user.to_string(),
272 repository_name
273 )
274 .execute(&self.pg_pool.clone())
275 .await
276 {
277 Ok(deleted) => Ok(deleted.rows_affected()),
278 Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)),
279 }
280 }
281
282 pub async fn open_repository_and_check_permissions(
283 &self,
284 owner: &User,
285 name: &str,
286 requester: &Option<AuthenticatedUser>,
287 ) -> Result<git2::Repository, GitBackendError> {
288 let repository = match self
289 .find_by_owner_user_name(
290 // &request.owner.instance.url,
291 owner, name,
292 )
293 .await
294 {
295 Ok(repository) => repository,
296 Err(err) => return Err(err),
297 };
298
299 if let Some(requester) = requester {
300 if !repository
301 .can_user_view_repository(
302 &self.instance,
303 &Some(requester.clone()),
304 self.stack.get().unwrap(),
305 )
306 .await
307 {
308 return Err(GitBackendError::RepositoryNotFound {
309 owner_user: repository.owner_user.to_string(),
310 name: repository.name.clone(),
311 });
312 }
313 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
314 // Unauthenticated users can never view private repositories
315
316 return Err(GitBackendError::RepositoryNotFound {
317 owner_user: repository.owner_user.to_string(),
318 name: repository.name.clone(),
319 });
320 }
321
322 match repository.open_git2_repository(&self.repository_folder) {
323 Ok(git) => Ok(git),
324 Err(err) => Err(err),
325 }
326 }
327
328 /// Attempts to get the oid in this order:
329 /// 1. Full refname (refname_to_id)
330 /// 2. Short branch name (find_branch)
331 /// 3. Other (revparse_single)
332 pub fn get_oid_from_reference(
333 git: &git2::Repository,
334 rev: Option<&str>,
335 default_branch: &DefaultBranch,
336 ) -> Result<git2::Oid, GitBackendError> {
337 // If the rev is None try and get the default branch instead
338 let rev = rev.unwrap_or(default_branch.0.as_str());
339
340 // TODO: This is far from ideal or speedy and would love for a better way to check this in the same order, but I can't find proper methods to do any of this.
341 trace!("Attempting to get ref with name {}", rev);
342
343 // Try getting it as a refname (refs/heads/name)
344 if let Ok(oid) = git.refname_to_id(rev) {
345 Ok(oid)
346 // Try finding it as a short branch name
347 } else if let Ok(branch) = git.find_branch(rev, BranchType::Local) {
348 // SHOULD be safe to unwrap
349 Ok(branch.get().target().unwrap())
350 // As last resort, try revparsing (will catch short oid and tags)
351 } else if let Ok(object) = git.revparse_single(rev) {
352 Ok(match object.kind() {
353 Some(git2::ObjectType::Tag) => {
354 if let Ok(commit) = object.peel_to_commit() {
355 commit.id()
356 } else {
357 object.id()
358 }
359 }
360 _ => object.id(),
361 })
362 } else {
363 Err(GitBackendError::RefNotFound(rev.to_string()))
364 }
365 }
366 }
367
368 #[async_trait(?Send)]
369 impl RepositoryBackend for GitBackend {
370 async fn create_repository(
371 &mut self,
372 _user: &AuthenticatedUser,
373 request: &RepositoryCreateRequest,
374 ) -> Result<Repository, GitBackendError> {
375 // Check if repository already exists in the database
376 if let Ok(repository) = self
377 .find_by_owner_user_name(&request.owner, &request.name)
378 .await
379 {
380 let err = GitBackendError::RepositoryAlreadyExists {
381 owner_user: repository.owner_user.to_string(),
382 name: repository.name,
383 };
384 error!("{:?}", err);
385
386 return Err(err);
387 }
388
389 // Insert the repository into the database
390 let _ = match sqlx::query_as!(GitRepository,
391 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
392 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
393 .fetch_one(&self.pg_pool.clone())
394 .await {
395 Ok(repository) => repository,
396 Err(err) => {
397 let err = GitBackendError::FailedInsertingIntoDatabase(err);
398 error!("Failed inserting into the database! {:?}", err);
399
400 return Err(err);
401 }
402 };
403
404 // Create bare (server side) repository on disk
405 match git2::Repository::init_bare(PathBuf::from(format!(
406 "{}/{}/{}/{}",
407 self.repository_folder, request.owner.instance, request.owner.username, request.name
408 ))) {
409 Ok(_) => {
410 debug!(
411 "Created new repository with the name {}/{}/{}",
412 request.owner.instance, request.owner.username, request.name
413 );
414
415 let stack = self.stack.get().unwrap();
416
417 let repository = Repository {
418 owner: request.owner.clone(),
419 name: request.name.clone(),
420 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
421 };
422
423 stack
424 .write_setting(
425 &repository,
426 Description(request.description.clone().unwrap_or_default()),
427 )
428 .await
429 .unwrap();
430
431 stack
432 .write_setting(&repository, Visibility(request.visibility.clone()))
433 .await
434 .unwrap();
435
436 stack
437 .write_setting(&repository, DefaultBranch(request.default_branch.clone()))
438 .await
439 .unwrap();
440
441 Ok(repository)
442 }
443 Err(err) => {
444 let err = GitBackendError::FailedCreatingRepository(err);
445 error!("Failed creating repository on disk {:?}", err);
446
447 // Delete repository from database
448 self.delete_from_database(&request.owner, request.name.as_str())
449 .await?;
450
451 // ???
452 Err(err)
453 }
454 }
455 }
456
457 /// If the OID can't be found because there's no repository head, this will return an empty `Vec`.
458 async fn repository_file_inspect(
459 &mut self,
460 requester: &Option<AuthenticatedUser>,
461 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
462 OperationState(operation_state): OperationState<StackOperationState>,
463 request: &RepositoryFileInspectRequest,
464 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
465 self.handle_repository_file_inspect(
466 requester,
467 repository_object,
468 OperationState(operation_state),
469 request,
470 )
471 .await
472 }
473
474 async fn repository_file_from_id(
475 &mut self,
476 requester: &Option<AuthenticatedUser>,
477 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
478 OperationState(operation_state): OperationState<StackOperationState>,
479 request: &RepositoryFileFromIdRequest,
480 ) -> Result<RepositoryFile, Error> {
481 self.handle_repository_file_from_id(
482 requester,
483 repository_object,
484 OperationState(operation_state),
485 request,
486 )
487 .await
488 }
489
490 async fn repository_file_from_path(
491 &mut self,
492 requester: &Option<AuthenticatedUser>,
493 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
494 OperationState(operation_state): OperationState<StackOperationState>,
495 request: &RepositoryFileFromPathRequest,
496 ) -> Result<(RepositoryFile, String), Error> {
497 self.handle_repository_file_from_path(
498 requester,
499 repository_object,
500 OperationState(operation_state),
501 request,
502 )
503 .await
504 }
505
506 async fn repository_commit_from_id(
507 &mut self,
508 requester: &Option<AuthenticatedUser>,
509 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
510 OperationState(operation_state): OperationState<StackOperationState>,
511 request: &RepositoryCommitFromIdRequest,
512 ) -> Result<Commit, Error> {
513 self.handle_repository_commit_from_id(
514 requester,
515 repository_object,
516 OperationState(operation_state),
517 request,
518 )
519 .await
520 }
521
522 async fn repository_last_commit_of_file(
523 &mut self,
524 requester: &Option<AuthenticatedUser>,
525 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
526 OperationState(operation_state): OperationState<StackOperationState>,
527 request: &RepositoryLastCommitOfFileRequest,
528 ) -> Result<Commit, Error> {
529 self.handle_repository_last_commit_of_file(
530 requester,
531 repository_object,
532 OperationState(operation_state),
533 request,
534 )
535 .await
536 }
537
538 async fn repository_diff(
539 &mut self,
540 requester: &Option<AuthenticatedUser>,
541 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
542 OperationState(operation_state): OperationState<StackOperationState>,
543 request: &RepositoryDiffRequest,
544 ) -> Result<RepositoryDiff, Error> {
545 self.handle_repository_diff(
546 requester,
547 repository_object,
548 OperationState(operation_state),
549 request,
550 )
551 .await
552 }
553
554 async fn repository_diff_patch(
555 &mut self,
556 requester: &Option<AuthenticatedUser>,
557 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
558 OperationState(operation_state): OperationState<StackOperationState>,
559 request: &RepositoryDiffPatchRequest,
560 ) -> Result<String, Error> {
561 self.handle_repository_diff_patch(
562 requester,
563 repository_object,
564 OperationState(operation_state),
565 request,
566 )
567 .await
568 }
569
570 async fn repository_commit_before(
571 &mut self,
572 requester: &Option<AuthenticatedUser>,
573 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
574 OperationState(operation_state): OperationState<StackOperationState>,
575 request: &RepositoryCommitBeforeRequest,
576 ) -> Result<Commit, Error> {
577 self.handle_repository_commit_before(
578 requester,
579 repository_object,
580 OperationState(operation_state),
581 request,
582 )
583 .await
584 }
585
586 // TODO: See where this would need to go in terms of being split up into a different file
587 /// Returns zero for all statistics if an OID wasn't found
588 async fn repository_get_statistics(
589 &mut self,
590 requester: &Option<AuthenticatedUser>,
591 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
592 OperationState(operation_state): OperationState<StackOperationState>,
593 request: &RepositoryStatisticsRequest,
594 ) -> Result<RepositoryStatistics, Error> {
595 let repository = repository_object.object();
596 let git = self
597 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
598 .await?;
599
600 let default_branch = repository_object
601 .get::<DefaultBranch>(&operation_state)
602 .await?;
603 let tree_id =
604 match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) {
605 Ok(oid) => oid,
606 Err(_) => return Ok(RepositoryStatistics::default()),
607 };
608
609 // Count the amount of branches and tags
610 let mut branches = 0;
611 let mut tags = 0;
612 if let Ok(references) = git.references() {
613 for reference in references.flatten() {
614 if reference.is_branch() {
615 branches += 1;
616 } else if reference.is_tag() {
617 tags += 1;
618 }
619 }
620 }
621
622 // Attempt to get the commit from the oid
623 let commits = if let Ok(commit) = git.find_commit(tree_id) {
624 // Get the total commit count if we found the tree oid commit
625 GitBackend::get_total_commit_count(&git, &commit)?
626 } else {
627 0
628 };
629
630 Ok(RepositoryStatistics {
631 commits,
632 branches,
633 tags,
634 })
635 }
636
637 /// .0: List of branches filtering by passed requirements.
638 /// .1: Total amount of branches after being filtered
639 async fn repository_get_branches(
640 &mut self,
641 requester: &Option<AuthenticatedUser>,
642 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
643 OperationState(operation_state): OperationState<StackOperationState>,
644 request: &RepositoryBranchesRequest,
645 ) -> Result<(Vec<RepositoryBranch>, usize), Error> {
646 self.handle_repository_get_branches(
647 requester,
648 repository_object,
649 OperationState(operation_state),
650 request,
651 )
652 .await
653 }
654
655 async fn repository_get_branch(
656 &mut self,
657 requester: &Option<AuthenticatedUser>,
658 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
659 OperationState(operation_state): OperationState<StackOperationState>,
660 request: &RepositoryBranchRequest,
661 ) -> Result<RepositoryBranch, Error> {
662 self.handle_repository_get_branch(
663 requester,
664 repository_object,
665 OperationState(operation_state),
666 request,
667 )
668 .await
669 }
670
671 /// .0: List of tags in passed range
672 /// .1: Total amount of tags
673 async fn repository_get_tags(
674 &mut self,
675 requester: &Option<AuthenticatedUser>,
676 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
677 OperationState(operation_state): OperationState<StackOperationState>,
678 request: &RepositoryTagsRequest,
679 ) -> Result<(Vec<RepositoryTag>, usize), Error> {
680 self.handle_repository_get_tags(
681 requester,
682 repository_object,
683 OperationState(operation_state),
684 request,
685 )
686 .await
687 }
688
689 async fn repository_get_tag(
690 &mut self,
691 requester: &Option<AuthenticatedUser>,
692 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
693 OperationState(operation_state): OperationState<StackOperationState>,
694 request: &RepositoryTagRequest,
695 ) -> Result<RepositoryTag, Error> {
696 self.handle_repository_get_tag(
697 requester,
698 repository_object,
699 OperationState(operation_state),
700 request,
701 )
702 .await
703 }
704
705 async fn exists(
706 &mut self,
707 requester: &Option<AuthenticatedUser>,
708 repository: &Repository,
709 ) -> Result<bool, Error> {
710 if let Ok(repository) = self
711 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
712 .await
713 {
714 Ok(repository
715 .can_user_view_repository(&self.instance, requester, self.stack.get().unwrap())
716 .await)
717 } else {
718 Ok(false)
719 }
720 }
721 }
722
723 impl IssuesBackend for GitBackend {
724 fn issues_count(
725 &mut self,
726 _requester: &Option<AuthenticatedUser>,
727 _request: &RepositoryIssuesCountRequest,
728 ) -> Result<u64, Error> {
729 todo!()
730 }
731
732 fn issue_tags(
733 &mut self,
734 _requester: &Option<AuthenticatedUser>,
735 _request: &RepositoryIssueTagsRequest,
736 ) -> Result<Vec<IssueTag>, Error> {
737 todo!()
738 }
739
740 fn issues(
741 &mut self,
742 _requester: &Option<AuthenticatedUser>,
743 _request: &RepositoryIssuesRequest,
744 ) -> Result<Vec<IssueInfoBasic>, Error> {
745 todo!()
746 }
747 }
748
749 #[allow(unused)]
750 #[derive(Debug, sqlx::FromRow)]
751 struct RepositoryMetadata {
752 pub repository: String,
753 pub name: String,
754 pub value: String,
755 }
756