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

ambee/giterated

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

Add info to RepositoryBranch, fix unwrap and reduce duplicate code

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨b285b99

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