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

ambee/giterated

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

Add access list to private repos

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨8c17f89

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