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

ambee/giterated

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

Repository commit before request and handler

Type: Feature

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨144b159

⁨giterated-daemon/src/backend/git.rs⁩ - ⁨33363⁩ bytes
Raw
1 use anyhow::Error;
2 use async_trait::async_trait;
3 use futures_util::StreamExt;
4
5 use giterated_models::instance::{Instance, RepositoryCreateRequest};
6
7 use giterated_models::repository::{
8 Commit, DefaultBranch, Description, IssueLabel, LatestCommit, Repository,
9 RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest,
10 RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryObjectType,
11 RepositoryTreeEntry, RepositoryVisibility, Visibility, RepositoryFile, RepositoryFileFromIdRequest, RepositoryDiffRequest, RepositoryDiff, RepositoryCommitBeforeRequest,
12 };
13 use giterated_models::settings::{AnySetting, Setting};
14 use giterated_models::user::{User, UserParseError};
15 use giterated_models::value::{AnyValue, GiteratedObjectValue};
16 use serde_json::Value;
17 use sqlx::PgPool;
18 use std::{
19 path::{Path, PathBuf},
20 sync::Arc,
21 };
22 use thiserror::Error;
23 use tokio::sync::Mutex;
24
25 use super::{IssuesBackend, MetadataBackend, RepositoryBackend};
26
27 // TODO: Handle this
28 //region database structures
29
30 /// Repository in the database
31 #[derive(Debug, sqlx::FromRow)]
32 pub struct GitRepository {
33 #[sqlx(try_from = "String")]
34 pub owner_user: User,
35 pub name: String,
36 pub description: Option<String>,
37 pub visibility: RepositoryVisibility,
38 pub default_branch: String,
39 }
40
41 impl GitRepository {
42 // Separate function because "Private" will be expanded later
43 /// Checks if the user is allowed to view this repository
44 pub fn can_user_view_repository(&self, user: Option<&User>) -> bool {
45 !matches!(self.visibility, RepositoryVisibility::Private)
46 || (matches!(self.visibility, RepositoryVisibility::Private)
47 && Some(&self.owner_user) == user)
48 }
49
50 // This is in it's own function because I assume I'll have to add logic to this later
51 pub fn open_git2_repository(
52 &self,
53 repository_directory: &str,
54 ) -> Result<git2::Repository, GitBackendError> {
55 match git2::Repository::open(format!(
56 "{}/{}/{}/{}",
57 repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name
58 )) {
59 Ok(repository) => Ok(repository),
60 Err(err) => {
61 let err = GitBackendError::FailedOpeningFromDisk(err);
62 error!("Couldn't open a repository, this is bad! {:?}", err);
63
64 Err(err)
65 }
66 }
67 }
68 }
69
70 //endregion
71
72 #[derive(Error, Debug)]
73 pub enum GitBackendError {
74 #[error("Failed creating repository")]
75 FailedCreatingRepository(git2::Error),
76 #[error("Failed inserting into the database")]
77 FailedInsertingIntoDatabase(sqlx::Error),
78 #[error("Failed finding repository {owner_user:?}/{name:?}")]
79 RepositoryNotFound { owner_user: String, name: String },
80 #[error("Repository {owner_user:?}/{name:?} already exists")]
81 RepositoryAlreadyExists { owner_user: String, name: String },
82 #[error("Repository couldn't be deleted from the disk")]
83 CouldNotDeleteFromDisk(std::io::Error),
84 #[error("Failed deleting repository from database")]
85 FailedDeletingFromDatabase(sqlx::Error),
86 #[error("Failed opening repository on disk")]
87 FailedOpeningFromDisk(git2::Error),
88 #[error("Couldn't find ref with name `{0}`")]
89 RefNotFound(String),
90 #[error("Couldn't find path in repository `{0}`")]
91 PathNotFound(String),
92 #[error("Couldn't find commit for path `{0}`")]
93 LastCommitNotFound(String),
94 #[error("Object ID `{0}` is invalid")]
95 InvalidObjectId(String),
96 #[error("Blob with ID `{0}` not found")]
97 BlobNotFound(String),
98 #[error("Tree with ID `{0}` not found")]
99 TreeNotFound(String),
100 #[error("Commit with ID `{0}` not found")]
101 CommitNotFound(String),
102 #[error("Parent for commit with ID `{0}` not found")]
103 CommitParentNotFound(String),
104 #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")]
105 FailedDiffing(String, String),
106 }
107
108 pub struct GitBackend {
109 pub pg_pool: PgPool,
110 pub repository_folder: String,
111 pub instance: Instance,
112 pub settings_provider: Arc<Mutex<dyn MetadataBackend + Send>>,
113 }
114
115 impl GitBackend {
116 pub fn new(
117 pg_pool: &PgPool,
118 repository_folder: &str,
119 instance: impl ToOwned<Owned = Instance>,
120 settings_provider: Arc<Mutex<dyn MetadataBackend + Send>>,
121 ) -> Self {
122 Self {
123 pg_pool: pg_pool.clone(),
124 repository_folder: repository_folder.to_string(),
125 instance: instance.to_owned(),
126 settings_provider,
127 }
128 }
129
130 pub async fn find_by_owner_user_name(
131 &self,
132 user: &User,
133 repository_name: &str,
134 ) -> Result<GitRepository, GitBackendError> {
135 if let Ok(repository) = sqlx::query_as!(GitRepository,
136 r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#,
137 user.to_string(), repository_name)
138 .fetch_one(&self.pg_pool.clone())
139 .await {
140 Ok(repository)
141 } else {
142 Err(GitBackendError::RepositoryNotFound {
143 owner_user: user.to_string(),
144 name: repository_name.to_string(),
145 })
146 }
147 }
148
149 pub async fn delete_by_owner_user_name(
150 &self,
151 user: &User,
152 repository_name: &str,
153 ) -> Result<u64, GitBackendError> {
154 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
155 "{}/{}/{}/{}",
156 self.repository_folder, user.instance.url, user.username, repository_name
157 ))) {
158 let err = GitBackendError::CouldNotDeleteFromDisk(err);
159 error!(
160 "Couldn't delete repository from disk, this is bad! {:?}",
161 err
162 );
163
164 return Err(err);
165 }
166
167 // Delete the repository from the database
168 match sqlx::query!(
169 "DELETE FROM repositories WHERE owner_user = $1 AND name = $2",
170 user.to_string(),
171 repository_name
172 )
173 .execute(&self.pg_pool.clone())
174 .await
175 {
176 Ok(deleted) => Ok(deleted.rows_affected()),
177 Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)),
178 }
179 }
180
181 pub async fn open_repository_and_check_permissions(&self, owner: &User, name: &str, requester: Option<&User>) -> Result<git2::Repository, GitBackendError> {
182 let repository = match self
183 .find_by_owner_user_name(
184 // &request.owner.instance.url,
185 owner,
186 name,
187 )
188 .await
189 {
190 Ok(repository) => repository,
191 Err(err) => return Err(err),
192 };
193
194 if let Some(requester) = requester {
195 if !repository.can_user_view_repository(Some(requester)) {
196 return Err(GitBackendError::RepositoryNotFound {
197 owner_user: repository.owner_user.to_string(),
198 name: repository.name.clone(),
199 });
200 }
201 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
202 info!("Unauthenticated");
203 // Unauthenticated users can never view private repositories
204
205 return Err(GitBackendError::RepositoryNotFound {
206 owner_user: repository.owner_user.to_string(),
207 name: repository.name.clone(),
208 });
209 }
210
211 match repository.open_git2_repository(&self.repository_folder) {
212 Ok(git) => Ok(git),
213 Err(err) => return Err(err),
214 }
215 }
216
217 // TODO: Find where this fits
218 // TODO: Cache this and general repository tree and invalidate select files on push
219 // TODO: Find better and faster technique for this
220 pub fn get_last_commit_of_file(
221 path: &str,
222 git: &git2::Repository,
223 start_commit: &git2::Commit,
224 ) -> anyhow::Result<Commit> {
225 let mut revwalk = git.revwalk()?;
226 revwalk.set_sorting(git2::Sort::TIME)?;
227 revwalk.push(start_commit.id())?;
228
229 for oid in revwalk {
230 let oid = oid?;
231 let commit = git.find_commit(oid)?;
232
233 // Merge commits have 2 or more parents
234 // Commits with 0 parents are handled different because we can't diff against them
235 if commit.parent_count() == 0 {
236 return Ok(commit.into());
237 } else if commit.parent_count() == 1 {
238 let tree = commit.tree()?;
239 let last_tree = commit.parent(0)?.tree()?;
240
241 // Get the diff between the current tree and the last one
242 let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?;
243
244 for dd in diff.deltas() {
245 // Get the path of the current file we're diffing against
246 let current_path = dd.new_file().path().unwrap();
247
248 // Path or directory
249 if current_path.eq(Path::new(&path)) || current_path.starts_with(path) {
250 return Ok(commit.into());
251 }
252 }
253 }
254 }
255
256 Err(GitBackendError::LastCommitNotFound(path.to_string()))?
257 }
258 }
259
260 #[async_trait]
261 impl RepositoryBackend for GitBackend {
262 async fn exists(&mut self, repository: &Repository) -> Result<bool, Error> {
263 if let Ok(_repository) = self
264 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
265 .await
266 {
267 Ok(true)
268 } else {
269 Ok(false)
270 }
271 }
272
273 async fn create_repository(
274 &mut self,
275 _user: &User,
276 request: &RepositoryCreateRequest,
277 ) -> Result<Repository, GitBackendError> {
278 // Check if repository already exists in the database
279 if let Ok(repository) = self
280 .find_by_owner_user_name(&request.owner, &request.name)
281 .await
282 {
283 let err = GitBackendError::RepositoryAlreadyExists {
284 owner_user: repository.owner_user.to_string(),
285 name: repository.name,
286 };
287 error!("{:?}", err);
288
289 return Err(err);
290 }
291
292 // Insert the repository into the database
293 let _ = match sqlx::query_as!(GitRepository,
294 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
295 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
296 .fetch_one(&self.pg_pool.clone())
297 .await {
298 Ok(repository) => repository,
299 Err(err) => {
300 let err = GitBackendError::FailedInsertingIntoDatabase(err);
301 error!("Failed inserting into the database! {:?}", err);
302
303 return Err(err);
304 }
305 };
306
307 // Create bare (server side) repository on disk
308 match git2::Repository::init_bare(PathBuf::from(format!(
309 "{}/{}/{}/{}",
310 self.repository_folder,
311 request.owner.instance.url,
312 request.owner.username,
313 request.name
314 ))) {
315 Ok(_) => {
316 debug!(
317 "Created new repository with the name {}/{}/{}",
318 request.owner.instance.url, request.owner.username, request.name
319 );
320 Ok(Repository {
321 owner: request.owner.clone(),
322 name: request.name.clone(),
323 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
324 })
325 }
326 Err(err) => {
327 let err = GitBackendError::FailedCreatingRepository(err);
328 error!("Failed creating repository on disk!? {:?}", err);
329
330 // Delete repository from database
331 self.delete_by_owner_user_name(&request.owner, request.name.as_str())
332 .await?;
333
334 // ???
335 Err(err)
336 }
337 }
338 }
339
340 async fn get_value(
341 &mut self,
342 repository: &Repository,
343 name: &str,
344 ) -> Result<AnyValue<Repository>, Error> {
345 Ok(unsafe {
346 if name == Description::value_name() {
347 AnyValue::from_raw(self.get_setting(repository, Description::name()).await?.0)
348 } else if name == Visibility::value_name() {
349 AnyValue::from_raw(self.get_setting(repository, Visibility::name()).await?.0)
350 } else if name == DefaultBranch::value_name() {
351 AnyValue::from_raw(self.get_setting(repository, DefaultBranch::name()).await?.0)
352 } else if name == LatestCommit::value_name() {
353 AnyValue::from_raw(serde_json::to_value(LatestCommit(None)).unwrap())
354 } else {
355 return Err(UserParseError.into());
356 }
357 })
358 }
359
360 async fn get_setting(
361 &mut self,
362 repository: &Repository,
363 name: &str,
364 ) -> Result<AnySetting, Error> {
365 let mut provider = self.settings_provider.lock().await;
366
367 Ok(provider.repository_get(repository, name).await?)
368 }
369
370 async fn write_setting(
371 &mut self,
372 repository: &Repository,
373 name: &str,
374 setting: &Value,
375 ) -> Result<(), Error> {
376 let mut provider = self.settings_provider.lock().await;
377
378 provider
379 .repository_write(repository, name, AnySetting(setting.clone()))
380 .await
381 }
382
383 // async fn repository_info(
384 // &mut self,
385 // requester: Option<&User>,
386 // request: &RepositoryInfoRequest,
387 // ) -> Result<RepositoryView, Error> {
388 // let repository = match self
389 // .find_by_owner_user_name(
390 // // &request.owner.instance.url,
391 // &request.repository.owner,
392 // &request.repository.name,
393 // )
394 // .await
395 // {
396 // Ok(repository) => repository,
397 // Err(err) => return Err(Box::new(err).into()),
398 // };
399
400 // if let Some(requester) = requester {
401 // if !repository.can_user_view_repository(Some(&requester)) {
402 // return Err(Box::new(GitBackendError::RepositoryNotFound {
403 // owner_user: request.repository.owner.to_string(),
404 // name: request.repository.name.clone(),
405 // })
406 // .into());
407 // }
408 // } else if matches!(repository.visibility, RepositoryVisibility::Private) {
409 // info!("Unauthenticated");
410 // // Unauthenticated users can never view private repositories
411
412 // return Err(Box::new(GitBackendError::RepositoryNotFound {
413 // owner_user: request.repository.owner.to_string(),
414 // name: request.repository.name.clone(),
415 // })
416 // .into());
417 // }
418
419 // let git = match repository.open_git2_repository(&self.repository_folder) {
420 // Ok(git) => git,
421 // Err(err) => return Err(Box::new(err).into()),
422 // };
423
424 // let rev_name = match &request.rev {
425 // None => {
426 // if let Ok(head) = git.head() {
427 // head.name().unwrap().to_string()
428 // } else {
429 // // Nothing in database, render empty tree.
430 // return Ok(RepositoryView {
431 // name: repository.name,
432 // owner: request.repository.owner.clone(),
433 // description: repository.description,
434 // visibility: repository.visibility,
435 // default_branch: repository.default_branch,
436 // latest_commit: None,
437 // tree_rev: None,
438 // tree: vec![],
439 // });
440 // }
441 // }
442 // Some(rev_name) => {
443 // // Find the reference, otherwise return GitBackendError
444 // match git
445 // .find_reference(format!("refs/heads/{}", rev_name).as_str())
446 // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
447 // {
448 // Ok(reference) => reference.name().unwrap().to_string(),
449 // Err(err) => return Err(Box::new(err).into()),
450 // }
451 // }
452 // };
453
454 // // Get the git object as a commit
455 // let rev = match git
456 // .revparse_single(rev_name.as_str())
457 // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
458 // {
459 // Ok(rev) => rev,
460 // Err(err) => return Err(Box::new(err).into()),
461 // };
462 // let commit = rev.as_commit().unwrap();
463
464 // // this is stupid
465 // let mut current_path = rev_name.replace("refs/heads/", "");
466
467 // // Get the commit tree
468 // let git_tree = if let Some(path) = &request.path {
469 // // Add it to our full path string
470 // current_path.push_str(format!("/{}", path).as_str());
471 // // Get the specified path, return an error if it wasn't found.
472 // let entry = match commit
473 // .tree()
474 // .unwrap()
475 // .get_path(&PathBuf::from(path))
476 // .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
477 // {
478 // Ok(entry) => entry,
479 // Err(err) => return Err(Box::new(err).into()),
480 // };
481 // // Turn the entry into a git tree
482 // entry.to_object(&git).unwrap().as_tree().unwrap().clone()
483 // } else {
484 // commit.tree().unwrap()
485 // };
486
487 // // Iterate over the git tree and collect it into our own tree types
488 // let mut tree = git_tree
489 // .iter()
490 // .map(|entry| {
491 // let object_type = match entry.kind().unwrap() {
492 // ObjectType::Tree => RepositoryObjectType::Tree,
493 // ObjectType::Blob => RepositoryObjectType::Blob,
494 // _ => unreachable!(),
495 // };
496 // let mut tree_entry =
497 // RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode());
498
499 // if request.extra_metadata {
500 // // Get the file size if It's a blob
501 // let object = entry.to_object(&git).unwrap();
502 // if let Some(blob) = object.as_blob() {
503 // tree_entry.size = Some(blob.size());
504 // }
505
506 // // Could possibly be done better
507 // let path = if let Some(path) = current_path.split_once('/') {
508 // format!("{}/{}", path.1, entry.name().unwrap())
509 // } else {
510 // entry.name().unwrap().to_string()
511 // };
512
513 // // Get the last commit made to the entry
514 // if let Ok(last_commit) =
515 // GitBackend::get_last_commit_of_file(&path, &git, commit)
516 // {
517 // tree_entry.last_commit = Some(last_commit);
518 // }
519 // }
520
521 // tree_entry
522 // })
523 // .collect::<Vec<RepositoryTreeEntry>>();
524
525 // // Sort the tree alphabetically and with tree first
526 // tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
527 // tree.sort_unstable_by_key(|entry| {
528 // std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
529 // });
530
531 // Ok(RepositoryView {
532 // name: repository.name,
533 // owner: request.repository.owner.clone(),
534 // description: repository.description,
535 // visibility: repository.visibility,
536 // default_branch: repository.default_branch,
537 // latest_commit: Some(Commit::from(commit.clone())),
538 // tree_rev: Some(rev_name),
539 // tree,
540 // })
541 // }
542
543 async fn repository_file_inspect(
544 &mut self,
545 requester: Option<&User>,
546 repository: &Repository,
547 request: &RepositoryFileInspectRequest,
548 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
549 let repository = match self
550 .find_by_owner_user_name(
551 // &request.owner.instance.url,
552 &repository.owner,
553 &repository.name,
554 )
555 .await
556 {
557 Ok(repository) => repository,
558 Err(err) => return Err(Box::new(err).into()),
559 };
560
561 if let Some(requester) = requester {
562 if !repository.can_user_view_repository(Some(requester)) {
563 return Err(Box::new(GitBackendError::RepositoryNotFound {
564 owner_user: repository.owner_user.to_string(),
565 name: repository.name.clone(),
566 })
567 .into());
568 }
569 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
570 info!("Unauthenticated");
571 // Unauthenticated users can never view private repositories
572
573 return Err(Box::new(GitBackendError::RepositoryNotFound {
574 owner_user: repository.owner_user.to_string(),
575 name: repository.name.clone(),
576 })
577 .into());
578 }
579
580 let git = match repository.open_git2_repository(&self.repository_folder) {
581 Ok(git) => git,
582 Err(err) => return Err(Box::new(err).into()),
583 };
584
585 // Try and parse the input as a reference and get the object ID
586 let mut tree_id = match &request.rev {
587 None => {
588 if let Ok(head) = git.head() {
589 // TODO: Fix for symbolic references
590 head.target()
591 } else {
592 // Nothing in database, render empty tree.
593 return Ok(vec![]);
594 }
595 }
596 Some(rev_name) => {
597 // Find the reference, otherwise return GitBackendError
598 git.refname_to_id(rev_name).ok()
599 }
600 };
601
602 // If the reference wasn't found, try parsing it as a commit ID and otherwise return error
603 if tree_id.is_none() {
604 match git2::Oid::from_str(request.rev.as_ref().unwrap()) {
605 Ok(oid) => tree_id = Some(oid),
606 Err(_) => return Err(Box::new(GitBackendError::RefNotFound(request.rev.clone().unwrap())).into()),
607 }
608 }
609
610 // unwrap might be dangerous?
611 // Get the commit from the oid
612 let commit = git.find_commit(tree_id.unwrap()).unwrap();
613
614 // this is stupid
615 let mut current_path = request.rev.clone().unwrap();
616
617 // Get the commit tree
618 let git_tree = if let Some(path) = &request.path {
619 // Add it to our full path string
620 current_path.push_str(format!("/{}", path).as_str());
621 // Get the specified path, return an error if it wasn't found.
622 let entry = match commit
623 .tree()
624 .unwrap()
625 .get_path(&PathBuf::from(path))
626 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
627 {
628 Ok(entry) => entry,
629 Err(err) => return Err(Box::new(err).into()),
630 };
631 // Turn the entry into a git tree
632 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
633 } else {
634 commit.tree().unwrap()
635 };
636
637 // Iterate over the git tree and collect it into our own tree types
638 let mut tree = git_tree
639 .iter()
640 .map(|entry| {
641 let object_type = match entry.kind().unwrap() {
642 git2::ObjectType::Tree => RepositoryObjectType::Tree,
643 git2::ObjectType::Blob => RepositoryObjectType::Blob,
644 _ => unreachable!(),
645 };
646 let mut tree_entry =
647 RepositoryTreeEntry::new(entry.id().to_string().as_str(), entry.name().unwrap(), object_type, entry.filemode());
648
649 if request.extra_metadata {
650 // Get the file size if It's a blob
651 let object = entry.to_object(&git).unwrap();
652 if let Some(blob) = object.as_blob() {
653 tree_entry.size = Some(blob.size());
654 }
655
656 // Could possibly be done better
657 let path = if let Some(path) = current_path.split_once('/') {
658 format!("{}/{}", path.1, entry.name().unwrap())
659 } else {
660 entry.name().unwrap().to_string()
661 };
662
663 // Get the last commit made to the entry
664 if let Ok(last_commit) =
665 GitBackend::get_last_commit_of_file(&path, &git, &commit)
666 {
667 tree_entry.last_commit = Some(last_commit);
668 }
669 }
670
671 tree_entry
672 })
673 .collect::<Vec<RepositoryTreeEntry>>();
674
675 // Sort the tree alphabetically and with tree first
676 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
677 tree.sort_unstable_by_key(|entry| {
678 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
679 });
680
681 Ok(tree)
682 }
683
684 async fn repository_file_from_id(
685 &mut self,
686 requester: Option<&User>,
687 repository: &Repository,
688 request: &RepositoryFileFromIdRequest,
689 ) -> Result<RepositoryFile, Error> {
690 // TODO: Get rid of all this duplicate code
691 let repository = match self
692 .find_by_owner_user_name(
693 // &request.owner.instance.url,
694 &repository.owner,
695 &repository.name,
696 )
697 .await
698 {
699 Ok(repository) => repository,
700 Err(err) => return Err(Box::new(err).into()),
701 };
702
703 if let Some(requester) = requester {
704 if !repository.can_user_view_repository(Some(requester)) {
705 return Err(Box::new(GitBackendError::RepositoryNotFound {
706 owner_user: repository.owner_user.to_string(),
707 name: repository.name.clone(),
708 })
709 .into());
710 }
711 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
712 info!("Unauthenticated");
713 // Unauthenticated users can never view private repositories
714
715 return Err(Box::new(GitBackendError::RepositoryNotFound {
716 owner_user: repository.owner_user.to_string(),
717 name: repository.name.clone(),
718 })
719 .into());
720 }
721
722 let git = match repository.open_git2_repository(&self.repository_folder) {
723 Ok(git) => git,
724 Err(err) => return Err(Box::new(err).into()),
725 };
726
727 // Parse the passed object id
728 let oid = match git2::Oid::from_str(request.0.as_str()) {
729 Ok(oid) => oid,
730 Err(_) => return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()),
731 };
732
733 // Find the file and turn it into our own struct
734 let file = match git.find_blob(oid) {
735 Ok(blob) => RepositoryFile { content: blob.content().to_vec(), binary: blob.is_binary(), size: blob.size() },
736 Err(_) => return Err(Box::new(GitBackendError::BlobNotFound(oid.to_string())).into()),
737 };
738
739 Ok(file)
740 }
741
742 async fn repository_diff(
743 &mut self,
744 requester: Option<&User>,
745 repository: &Repository,
746 request: &RepositoryDiffRequest,
747 ) -> Result<RepositoryDiff, Error> {
748 // TODO: Get rid of all this duplicate code
749 let repository = match self
750 .find_by_owner_user_name(
751 // &request.owner.instance.url,
752 &repository.owner,
753 &repository.name,
754 )
755 .await
756 {
757 Ok(repository) => repository,
758 Err(err) => return Err(Box::new(err).into()),
759 };
760
761 if let Some(requester) = requester {
762 if !repository.can_user_view_repository(Some(requester)) {
763 return Err(Box::new(GitBackendError::RepositoryNotFound {
764 owner_user: repository.owner_user.to_string(),
765 name: repository.name.clone(),
766 })
767 .into());
768 }
769 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
770 info!("Unauthenticated");
771 // Unauthenticated users can never view private repositories
772
773 return Err(Box::new(GitBackendError::RepositoryNotFound {
774 owner_user: repository.owner_user.to_string(),
775 name: repository.name.clone(),
776 })
777 .into());
778 }
779
780 let git = match repository.open_git2_repository(&self.repository_folder) {
781 Ok(git) => git,
782 Err(err) => return Err(Box::new(err).into()),
783 };
784
785 // Parse the passed object ids
786 let oid_old = match git2::Oid::from_str(request.old_id.as_str()) {
787 Ok(oid) => oid,
788 Err(_) => return Err(Box::new(GitBackendError::InvalidObjectId(request.old_id.clone())).into()),
789 };
790 let oid_new = match git2::Oid::from_str(request.new_id.as_str()) {
791 Ok(oid) => oid,
792 Err(_) => return Err(Box::new(GitBackendError::InvalidObjectId(request.new_id.clone())).into()),
793 };
794
795 // Get the trees of those object ids
796 let tree_old = match git.find_tree(oid_old) {
797 Ok(tree) => tree,
798 Err(_) => return Err(Box::new(GitBackendError::TreeNotFound(oid_old.to_string())).into()),
799 };
800 let tree_new = match git.find_tree(oid_new) {
801 Ok(tree) => tree,
802 Err(_) => return Err(Box::new(GitBackendError::TreeNotFound(oid_new.to_string())).into()),
803 };
804
805 // Diff the two trees against each other
806 let diff = match git.diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) {
807 Ok(diff) => diff,
808 Err(_) => return Err(Box::new(GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string())).into()),
809 };
810
811 // Should be safe to unwrap?
812 let stats = diff.stats().unwrap();
813
814 // Honestly not quite sure what is going on here, could not find documentation.
815 // Print the entire patch
816 let mut patch = String::new();
817
818 diff.print(git2::DiffFormat::Patch, |_, _, line| {
819 patch.push_str(std::str::from_utf8(line.content()).unwrap());
820 true
821 }).unwrap();
822
823 Ok(RepositoryDiff {
824 files_changed: stats.files_changed(),
825 insertions: stats.insertions(),
826 deletions: stats.deletions(),
827 patch,
828 })
829 }
830
831 async fn repository_commit_before(
832 &mut self,
833 requester: Option<&User>,
834 repository: &Repository,
835 request: &RepositoryCommitBeforeRequest,
836 ) -> Result<Commit, Error> {
837 let git = self.open_repository_and_check_permissions(&repository.owner, &repository.name, requester).await?;
838
839 // Parse the passed object id
840 let oid = match git2::Oid::from_str(request.0.as_str()) {
841 Ok(oid) => oid,
842 Err(_) => return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()),
843 };
844
845 // Find the commit using the parsed oid
846 let commit = match git.find_commit(oid) {
847 Ok(commit) => commit,
848 Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()),
849 };
850
851 // Get the first parent it has
852 let parent = match commit.parent(0) {
853 Ok(parent) => Commit::from(parent),
854 Err(_) => return Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into()),
855 };
856
857 Ok(parent)
858 }
859 }
860
861 impl IssuesBackend for GitBackend {
862 fn issues_count(
863 &mut self,
864 _requester: Option<&User>,
865 _request: &RepositoryIssuesCountRequest,
866 ) -> Result<u64, Error> {
867 todo!()
868 }
869
870 fn issue_labels(
871 &mut self,
872 _requester: Option<&User>,
873 _request: &RepositoryIssueLabelsRequest,
874 ) -> Result<Vec<IssueLabel>, Error> {
875 todo!()
876 }
877
878 fn issues(
879 &mut self,
880 _requester: Option<&User>,
881 _request: &RepositoryIssuesRequest,
882 ) -> Result<Vec<RepositoryIssue>, Error> {
883 todo!()
884 }
885 }
886
887 #[derive(Debug, sqlx::FromRow)]
888 struct RepositoryMetadata {
889 pub repository: String,
890 pub name: String,
891 pub value: String,
892 }
893