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