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

ambee/giterated

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

Fix rev

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨088af7f

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