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

ambee/giterated

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

Unified stack `GetValue` implementation

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨325f5af

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