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

ambee/giterated

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

Basic tag search and range

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨112ca96

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