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

ambee/giterated

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

Fix branch

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨ff70fd4

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