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

ambee/giterated

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

Register GetSetting as an operation

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨6a85805

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