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

ambee/giterated

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

Simple branch staleness implementation

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨efc3f02

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