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

ambee/giterated

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

Add ahead_behind comparison, filter and range to branches request.

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨3aca035

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