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

ambee/giterated

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

Repository diff request and handler

Type: Feature

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨502a11c

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