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

ambee/giterated

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

Very simple branch search implementation

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨a4f9476

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