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

ambee/giterated

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

Improve oid_from_reference tag id fetching and pass full Repository Object to every function

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨6d7e35e

⁨giterated-daemon/src/backend/git.rs⁩ - ⁨52238⁩ 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 default_branch: &DefaultBranch,
356 ) -> Result<git2::Oid, GitBackendError> {
357 // If the rev is None try and get the default branch instead
358 let rev = rev.unwrap_or(default_branch.0.as_str());
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 trace!("Attempting to get ref with name {}", rev);
362
363 // Try getting it as a refname (refs/heads/name)
364 if let Ok(oid) = git.refname_to_id(rev) {
365 Ok(oid)
366 // Try finding it as a short branch name
367 } else if let Ok(branch) = git.find_branch(rev, BranchType::Local) {
368 // SHOULD be safe to unwrap
369 Ok(branch.get().target().unwrap())
370 // As last resort, try revparsing (will catch short oid and tags)
371 } else if let Ok(object) = git.revparse_single(rev) {
372 Ok(match object.kind() {
373 Some(git2::ObjectType::Tag) => {
374 if let Ok(commit) = object.peel_to_commit() {
375 commit.id()
376 } else {
377 object.id()
378 }
379 }
380 _ => object.id(),
381 })
382 } else {
383 Err(GitBackendError::RefNotFound(rev.to_string()))
384 }
385 }
386
387 /// Gets the last commit in a rev
388 pub fn get_last_commit_in_rev(
389 git: &git2::Repository,
390 rev: &str,
391 default_branch: &DefaultBranch,
392 ) -> anyhow::Result<Commit> {
393 let oid = Self::get_oid_from_reference(git, Some(rev), default_branch)?;
394
395 // Walk through the repository commit graph starting at our rev
396 let mut revwalk = git.revwalk()?;
397 revwalk.set_sorting(git2::Sort::TIME)?;
398 revwalk.push(oid)?;
399
400 if let Some(Ok(commit_oid)) = revwalk.next() {
401 if let Ok(commit) = git
402 .find_commit(commit_oid)
403 .map_err(|_| GitBackendError::CommitNotFound(commit_oid.to_string()))
404 {
405 return Ok(Commit::from(commit));
406 }
407 }
408
409 Err(GitBackendError::RefNotFound(oid.to_string()).into())
410 }
411 }
412
413 #[async_trait(?Send)]
414 impl RepositoryBackend for GitBackend {
415 async fn exists(
416 &mut self,
417 requester: &Option<AuthenticatedUser>,
418 repository: &Repository,
419 ) -> Result<bool, Error> {
420 if let Ok(repository) = self
421 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
422 .await
423 {
424 Ok(repository
425 .can_user_view_repository(&self.instance, requester, self.stack.get().unwrap())
426 .await)
427 } else {
428 Ok(false)
429 }
430 }
431
432 async fn create_repository(
433 &mut self,
434 _user: &AuthenticatedUser,
435 request: &RepositoryCreateRequest,
436 ) -> Result<Repository, GitBackendError> {
437 // Check if repository already exists in the database
438 if let Ok(repository) = self
439 .find_by_owner_user_name(&request.owner, &request.name)
440 .await
441 {
442 let err = GitBackendError::RepositoryAlreadyExists {
443 owner_user: repository.owner_user.to_string(),
444 name: repository.name,
445 };
446 error!("{:?}", err);
447
448 return Err(err);
449 }
450
451 // Insert the repository into the database
452 let _ = match sqlx::query_as!(GitRepository,
453 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
454 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
455 .fetch_one(&self.pg_pool.clone())
456 .await {
457 Ok(repository) => repository,
458 Err(err) => {
459 let err = GitBackendError::FailedInsertingIntoDatabase(err);
460 error!("Failed inserting into the database! {:?}", err);
461
462 return Err(err);
463 }
464 };
465
466 // Create bare (server side) repository on disk
467 match git2::Repository::init_bare(PathBuf::from(format!(
468 "{}/{}/{}/{}",
469 self.repository_folder, request.owner.instance, request.owner.username, request.name
470 ))) {
471 Ok(_) => {
472 debug!(
473 "Created new repository with the name {}/{}/{}",
474 request.owner.instance, request.owner.username, request.name
475 );
476
477 let stack = self.stack.get().unwrap();
478
479 let repository = Repository {
480 owner: request.owner.clone(),
481 name: request.name.clone(),
482 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
483 };
484
485 stack
486 .write_setting(
487 &repository,
488 Description(request.description.clone().unwrap_or_default()),
489 )
490 .await
491 .unwrap();
492
493 stack
494 .write_setting(&repository, Visibility(request.visibility.clone()))
495 .await
496 .unwrap();
497
498 stack
499 .write_setting(&repository, DefaultBranch(request.default_branch.clone()))
500 .await
501 .unwrap();
502
503 Ok(repository)
504 }
505 Err(err) => {
506 let err = GitBackendError::FailedCreatingRepository(err);
507 error!("Failed creating repository on disk {:?}", err);
508
509 // Delete repository from database
510 self.delete_from_database(&request.owner, request.name.as_str())
511 .await?;
512
513 // ???
514 Err(err)
515 }
516 }
517 }
518
519 /// If the OID can't be found because there's no repository head, this will return an empty `Vec`.
520 async fn repository_file_inspect(
521 &mut self,
522 requester: &Option<AuthenticatedUser>,
523 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
524 OperationState(operation_state): OperationState<StackOperationState>,
525 request: &RepositoryFileInspectRequest,
526 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
527 let repository = repository_object.object();
528 let git = self
529 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
530 .await?;
531
532 let default_branch = repository_object
533 .get::<DefaultBranch>(&operation_state)
534 .await?;
535 // Try and find the tree_id/branch
536 let tree_id =
537 match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) {
538 Ok(oid) => oid,
539 Err(GitBackendError::HeadNotFound) => return Ok(vec![]),
540 Err(err) => return Err(err.into()),
541 };
542
543 // Get the commit from the oid
544 let commit = match git.find_commit(tree_id) {
545 Ok(commit) => commit,
546 // If the commit isn't found, it's generally safe to assume the tree is empty.
547 Err(_) => return Ok(vec![]),
548 };
549
550 // this is stupid
551 let rev = request.rev.clone().unwrap_or_else(|| "master".to_string());
552 let mut current_path = rev.clone();
553
554 // Get the commit tree
555 let git_tree = if let Some(path) = &request.path {
556 // Add it to our full path string
557 current_path.push_str(format!("/{}", path).as_str());
558 // Get the specified path, return an error if it wasn't found.
559 let entry = match commit
560 .tree()
561 .unwrap()
562 .get_path(&PathBuf::from(path))
563 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
564 {
565 Ok(entry) => entry,
566 Err(err) => return Err(Box::new(err).into()),
567 };
568 // Turn the entry into a git tree
569 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
570 } else {
571 commit.tree().unwrap()
572 };
573
574 // Iterate over the git tree and collect it into our own tree types
575 let mut tree = git_tree
576 .iter()
577 .map(|entry| {
578 let object_type = match entry.kind().unwrap() {
579 git2::ObjectType::Tree => RepositoryObjectType::Tree,
580 git2::ObjectType::Blob => RepositoryObjectType::Blob,
581 _ => unreachable!(),
582 };
583 let mut tree_entry = RepositoryTreeEntry::new(
584 entry.id().to_string().as_str(),
585 entry.name().unwrap(),
586 object_type,
587 entry.filemode(),
588 );
589
590 if request.extra_metadata {
591 // Get the file size if It's a blob
592 let object = entry.to_object(&git).unwrap();
593 if let Some(blob) = object.as_blob() {
594 tree_entry.size = Some(blob.size());
595 }
596
597 // Get the path to the folder the file is in by removing the rev from current_path
598 let mut path = current_path.replace(&rev, "");
599 if path.starts_with('/') {
600 path.remove(0);
601 }
602
603 // Format it as the path + file name
604 let full_path = if path.is_empty() {
605 entry.name().unwrap().to_string()
606 } else {
607 format!("{}/{}", path, entry.name().unwrap())
608 };
609
610 // Get the last commit made to the entry
611 if let Ok(last_commit) =
612 GitBackend::get_last_commit_of_file(&full_path, &git, &commit)
613 {
614 tree_entry.last_commit = Some(last_commit);
615 }
616 }
617
618 tree_entry
619 })
620 .collect::<Vec<RepositoryTreeEntry>>();
621
622 // Sort the tree alphabetically and with tree first
623 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
624 tree.sort_unstable_by_key(|entry| {
625 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
626 });
627
628 Ok(tree)
629 }
630
631 async fn repository_file_from_id(
632 &mut self,
633 requester: &Option<AuthenticatedUser>,
634 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
635 OperationState(_operation_state): OperationState<StackOperationState>,
636 request: &RepositoryFileFromIdRequest,
637 ) -> Result<RepositoryFile, Error> {
638 let repository = repository_object.object();
639 let git = self
640 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
641 .await?;
642
643 // Parse the passed object id
644 let oid = match git2::Oid::from_str(request.0.as_str()) {
645 Ok(oid) => oid,
646 Err(_) => {
647 return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into())
648 }
649 };
650
651 // Find the file and turn it into our own struct
652 let file = match git.find_blob(oid) {
653 Ok(blob) => RepositoryFile {
654 id: blob.id().to_string(),
655 content: blob.content().to_vec(),
656 binary: blob.is_binary(),
657 size: blob.size(),
658 },
659 Err(_) => return Err(Box::new(GitBackendError::BlobNotFound(oid.to_string())).into()),
660 };
661
662 Ok(file)
663 }
664
665 async fn repository_file_from_path(
666 &mut self,
667 requester: &Option<AuthenticatedUser>,
668 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
669 OperationState(operation_state): OperationState<StackOperationState>,
670 request: &RepositoryFileFromPathRequest,
671 ) -> Result<(RepositoryFile, String), Error> {
672 let repository = repository_object.object();
673 let git = self
674 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
675 .await?;
676
677 let default_branch = repository_object
678 .get::<DefaultBranch>(&operation_state)
679 .await?;
680 let tree_id = Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch)?;
681
682 // unwrap might be dangerous?
683 // Get the commit from the oid
684 let commit = git.find_commit(tree_id).unwrap();
685
686 // this is stupid
687 let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string());
688
689 // Add it to our full path string
690 current_path.push_str(format!("/{}", request.path).as_str());
691 // Get the specified path, return an error if it wasn't found.
692 let entry = match commit
693 .tree()
694 .unwrap()
695 .get_path(&PathBuf::from(request.path.clone()))
696 .map_err(|_| GitBackendError::PathNotFound(request.path.to_string()))
697 {
698 Ok(entry) => entry,
699 Err(err) => return Err(Box::new(err).into()),
700 };
701
702 // Find the file and turn it into our own struct
703 let file = match git.find_blob(entry.id()) {
704 Ok(blob) => RepositoryFile {
705 id: blob.id().to_string(),
706 content: blob.content().to_vec(),
707 binary: blob.is_binary(),
708 size: blob.size(),
709 },
710 Err(_) => {
711 return Err(Box::new(GitBackendError::BlobNotFound(entry.id().to_string())).into())
712 }
713 };
714
715 Ok((file, commit.id().to_string()))
716 }
717
718 async fn repository_commit_from_id(
719 &mut self,
720 requester: &Option<AuthenticatedUser>,
721 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
722 OperationState(_operation_state): OperationState<StackOperationState>,
723 request: &RepositoryCommitFromIdRequest,
724 ) -> Result<Commit, Error> {
725 let repository = repository_object.object();
726 let git = self
727 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
728 .await?;
729
730 // Parse the passed object ids
731 let oid = git2::Oid::from_str(request.0.as_str())
732 .map_err(|_| GitBackendError::InvalidObjectId(request.0.clone()))?;
733
734 // Get the commit from the oid
735 let commit = git
736 .find_commit(oid)
737 .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?;
738
739 Ok(Commit::from(commit))
740 }
741
742 async fn repository_last_commit_of_file(
743 &mut self,
744 requester: &Option<AuthenticatedUser>,
745 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
746 OperationState(_operation_state): OperationState<StackOperationState>,
747 request: &RepositoryLastCommitOfFileRequest,
748 ) -> Result<Commit, Error> {
749 let repository = repository_object.object();
750 let git = self
751 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
752 .await?;
753
754 // Parse the passed object ids
755 let oid = git2::Oid::from_str(&request.start_commit)
756 .map_err(|_| GitBackendError::InvalidObjectId(request.start_commit.clone()))?;
757
758 // Get the commit from the oid
759 let commit = git
760 .find_commit(oid)
761 .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?;
762
763 // Find the last commit of the file
764 let commit = GitBackend::get_last_commit_of_file(request.path.as_str(), &git, &commit)?;
765
766 Ok(commit)
767 }
768
769 /// Returns zero for all statistics if an OID wasn't found
770 async fn repository_get_statistics(
771 &mut self,
772 requester: &Option<AuthenticatedUser>,
773 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
774 OperationState(operation_state): OperationState<StackOperationState>,
775 request: &RepositoryStatisticsRequest,
776 ) -> Result<RepositoryStatistics, Error> {
777 let repository = repository_object.object();
778 let git = self
779 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
780 .await?;
781
782 let default_branch = repository_object
783 .get::<DefaultBranch>(&operation_state)
784 .await?;
785 let tree_id =
786 match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) {
787 Ok(oid) => oid,
788 Err(_) => return Ok(RepositoryStatistics::default()),
789 };
790
791 // Count the amount of branches and tags
792 let mut branches = 0;
793 let mut tags = 0;
794 if let Ok(references) = git.references() {
795 for reference in references.flatten() {
796 if reference.is_branch() {
797 branches += 1;
798 } else if reference.is_tag() {
799 tags += 1;
800 }
801 }
802 }
803
804 // Attempt to get the commit from the oid
805 let commits = if let Ok(commit) = git.find_commit(tree_id) {
806 // Get the total commit count if we found the tree oid commit
807 GitBackend::get_total_commit_count(&git, &commit)?
808 } else {
809 0
810 };
811
812 Ok(RepositoryStatistics {
813 commits,
814 branches,
815 tags,
816 })
817 }
818
819 /// .0: List of branches filtering by passed requirements.
820 /// .1: Total amount of branches after being filtered
821 async fn repository_get_branches(
822 &mut self,
823 requester: &Option<AuthenticatedUser>,
824 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
825 OperationState(operation_state): OperationState<StackOperationState>,
826 request: &RepositoryBranchesRequest,
827 ) -> Result<(Vec<RepositoryBranch>, usize), Error> {
828 let repository = repository_object.object();
829 let git = self
830 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
831 .await?;
832
833 let default_branch_name = repository_object
834 .get::<DefaultBranch>(&operation_state)
835 .await?;
836 let default_branch = git
837 .find_branch(&default_branch_name.0, BranchType::Local)
838 .map_err(|_| GitBackendError::DefaultNotFound)?;
839
840 // Get the stale(after X seconds) setting
841 let stale_after = repository_object
842 .get::<BranchStaleAfter>(&operation_state)
843 .await
844 .unwrap_or_default()
845 .0;
846
847 // Could be done better with the RepositoryBranchFilter::None check done beforehand.
848 let mut filtered_branches = git
849 .branches(None)?
850 .filter_map(|branch| {
851 let branch = branch.ok()?.0;
852
853 let Some(name) = branch.name().ok().flatten() else {
854 return None;
855 };
856
857 // TODO: Non UTF-8?
858 let Some(commit) = GitBackend::get_last_commit_in_rev(
859 &git,
860 branch.get().name().unwrap(),
861 &default_branch_name,
862 )
863 .ok() else {
864 return None;
865 };
866
867 let stale = chrono::Utc::now()
868 .naive_utc()
869 .signed_duration_since(commit.time)
870 .num_seconds()
871 > stale_after.into();
872
873 // Filter based on if the branch is stale or not
874 if request.filter != RepositoryBranchFilter::None {
875 #[allow(clippy::if_same_then_else)]
876 if stale && request.filter == RepositoryBranchFilter::Active {
877 return None;
878 } else if !stale && request.filter == RepositoryBranchFilter::Stale {
879 return None;
880 }
881 }
882
883 Some((name.to_string(), branch, stale, commit))
884 })
885 .collect::<Vec<_>>();
886
887 // Get the total amount of filtered branches
888 let branch_count = filtered_branches.len();
889
890 if let Some(search) = &request.search {
891 // TODO: Caching
892 // Search by sorting using a simple fuzzy search algorithm
893 filtered_branches.sort_by(|(n1, _, _, _), (n2, _, _, _)| {
894 strsim::damerau_levenshtein(search, n1)
895 .cmp(&strsim::damerau_levenshtein(search, n2))
896 });
897 } else {
898 // Sort the branches by commit date
899 filtered_branches.sort_by(|(_, _, _, c1), (_, _, _, c2)| c2.time.cmp(&c1.time));
900 }
901
902 // Go to the requested position
903 let mut filtered_branches = filtered_branches.iter().skip(request.range.0);
904
905 let mut branches = vec![];
906
907 // Iterate through the filtered branches using the passed range
908 for _ in request.range.0..request.range.1 {
909 let Some((name, branch, stale, commit)) = filtered_branches.next() else {
910 break;
911 };
912
913 // Get how many commits are ahead of and behind of the head
914 let ahead_behind_default =
915 if default_branch.get().target().is_some() && branch.get().target().is_some() {
916 git.graph_ahead_behind(
917 branch.get().target().unwrap(),
918 default_branch.get().target().unwrap(),
919 )
920 .ok()
921 } else {
922 None
923 };
924
925 branches.push(RepositoryBranch {
926 name: name.to_string(),
927 stale: *stale,
928 last_commit: Some(commit.clone()),
929 ahead_behind_default,
930 })
931 }
932
933 Ok((branches, branch_count))
934 }
935
936 async fn repository_get_branch(
937 &mut self,
938 requester: &Option<AuthenticatedUser>,
939 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
940 OperationState(operation_state): OperationState<StackOperationState>,
941 request: &RepositoryBranchRequest,
942 ) -> Result<RepositoryBranch, Error> {
943 let repository = repository_object.object();
944 let git = self
945 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
946 .await?;
947
948 // TODO: Don't duplicate search when the default branch and the requested one are the same
949 // Get the default branch to compare against
950 let default_branch_name = repository_object
951 .get::<DefaultBranch>(&operation_state)
952 .await?;
953 let default_branch = git
954 .find_branch(&default_branch_name.0, BranchType::Local)
955 .map_err(|_| GitBackendError::DefaultNotFound)?;
956
957 // Find the requested branch
958 let branch = git
959 .find_branch(&request.name, BranchType::Local)
960 .map_err(|_| GitBackendError::BranchNotFound(request.name.clone()))?;
961
962 // Get the stale(after X seconds) setting
963 let stale_after = repository_object
964 .get::<BranchStaleAfter>(&operation_state)
965 .await
966 .unwrap_or_default()
967 .0;
968
969 // TODO: Non UTF-8?
970 let last_commit = GitBackend::get_last_commit_in_rev(
971 &git,
972 branch.get().name().unwrap(),
973 &default_branch_name,
974 )
975 .ok();
976
977 let stale = if let Some(ref last_commit) = last_commit {
978 chrono::Utc::now()
979 .naive_utc()
980 .signed_duration_since(last_commit.time)
981 .num_seconds()
982 > stale_after.into()
983 } else {
984 // TODO: Make sure it's acceptable to return false here
985 false
986 };
987
988 // Get how many commits are ahead of and behind of the head
989 let ahead_behind_default =
990 if default_branch.get().target().is_some() && branch.get().target().is_some() {
991 git.graph_ahead_behind(
992 branch.get().target().unwrap(),
993 default_branch.get().target().unwrap(),
994 )
995 .ok()
996 } else {
997 None
998 };
999
1000 Ok(RepositoryBranch {
1001 name: request.name.clone(),
1002 stale,
1003 last_commit,
1004 ahead_behind_default,
1005 })
1006 }
1007
1008 /// .0: List of tags in passed range
1009 /// .1: Total amount of tags
1010 async fn repository_get_tags(
1011 &mut self,
1012 requester: &Option<AuthenticatedUser>,
1013 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
1014 OperationState(_operation_state): OperationState<StackOperationState>,
1015 request: &RepositoryTagsRequest,
1016 ) -> Result<(Vec<RepositoryTag>, usize), Error> {
1017 let repository = repository_object.object();
1018 let git = self
1019 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
1020 .await?;
1021
1022 let mut tags = vec![];
1023
1024 // Iterate over each tag
1025 let _ = git.tag_foreach(|id, name| {
1026 // Get the name in utf8
1027 let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1);
1028
1029 // Find the tag so we can get the messages attached if any
1030 if let Ok(tag) = git.find_tag(id) {
1031 // Get the tag message and split it into a summary and body
1032 let (summary, body) = if let Some(message) = tag.message() {
1033 // Iterate over the lines
1034 let mut lines = message
1035 .lines()
1036 .map(|line| {
1037 // Trim the whitespace for every line
1038 let mut whitespace_removed = String::with_capacity(line.len());
1039
1040 line.split_whitespace().for_each(|word| {
1041 if !whitespace_removed.is_empty() {
1042 whitespace_removed.push(' ');
1043 }
1044
1045 whitespace_removed.push_str(word);
1046 });
1047
1048 whitespace_removed
1049 })
1050 .collect::<Vec<String>>();
1051
1052 let summary = Some(lines.remove(0));
1053 let body = if lines.is_empty() {
1054 None
1055 } else {
1056 Some(lines.join("\n"))
1057 };
1058
1059 (summary, body)
1060 } else {
1061 (None, None)
1062 };
1063
1064 // Get the commit the tag is (possibly) pointing to
1065 let commit = tag
1066 .peel()
1067 .map(|obj| obj.into_commit().ok())
1068 .ok()
1069 .flatten()
1070 .map(|c| Commit::from(c));
1071 // Get the author of the tag
1072 let author: Option<CommitSignature> = tag.tagger().map(|s| s.into());
1073 // Get the time the tag or pointed commit was created
1074 let time = if let Some(ref author) = author {
1075 Some(author.time)
1076 } else {
1077 // Get possible commit time if the tag has no author time
1078 commit.as_ref().map(|c| c.time.clone())
1079 };
1080
1081 tags.push(RepositoryTag {
1082 id: id.to_string(),
1083 name: name.to_string(),
1084 summary,
1085 body,
1086 author,
1087 time,
1088 commit,
1089 });
1090 } else {
1091 // Lightweight commit, we try and find the commit it's pointing to
1092 let commit = git.find_commit(id).ok().map(|c| Commit::from(c));
1093
1094 tags.push(RepositoryTag {
1095 id: id.to_string(),
1096 name: name.to_string(),
1097 summary: None,
1098 body: None,
1099 author: None,
1100 time: commit.as_ref().map(|c| c.time.clone()),
1101 commit,
1102 });
1103 };
1104
1105 true
1106 });
1107
1108 // Get the total amount of tags
1109 let tag_count = tags.len();
1110
1111 if let Some(search) = &request.search {
1112 // TODO: Caching
1113 // Search by sorting using a simple fuzzy search algorithm
1114 tags.sort_by(|n1, n2| {
1115 strsim::damerau_levenshtein(search, &n1.name)
1116 .cmp(&strsim::damerau_levenshtein(search, &n2.name))
1117 });
1118 } else {
1119 // Sort the tags using their creation or pointer date
1120 tags.sort_by(|t1, t2| t2.time.cmp(&t1.time));
1121 }
1122
1123 // Get the requested range of tags
1124 let tags = tags
1125 .into_iter()
1126 .skip(request.range.0)
1127 .take(request.range.1.saturating_sub(request.range.0))
1128 .collect::<Vec<RepositoryTag>>();
1129
1130 Ok((tags, tag_count))
1131 }
1132
1133 async fn repository_diff(
1134 &mut self,
1135 requester: &Option<AuthenticatedUser>,
1136 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
1137 OperationState(_operation_state): OperationState<StackOperationState>,
1138 request: &RepositoryDiffRequest,
1139 ) -> Result<RepositoryDiff, Error> {
1140 let repository = repository_object.object();
1141 let git = self
1142 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
1143 .await?;
1144
1145 // Parse the passed object ids
1146 let oid_old = git2::Oid::from_str(request.old_id.as_str())
1147 .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?;
1148 let oid_new = git2::Oid::from_str(request.new_id.as_str())
1149 .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?;
1150
1151 // Get the ids associates commits
1152 let commit_old = git
1153 .find_commit(oid_old)
1154 .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?;
1155 let commit_new = git
1156 .find_commit(oid_new)
1157 .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?;
1158
1159 // Get the commit trees
1160 let tree_old = commit_old
1161 .tree()
1162 .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?;
1163 let tree_new = commit_new
1164 .tree()
1165 .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?;
1166
1167 // Diff the two trees against each other
1168 let diff = git
1169 .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None)
1170 .map_err(|_| {
1171 GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string())
1172 })?;
1173
1174 // Should be safe to unwrap?
1175 let stats = diff.stats().unwrap();
1176 let mut files: Vec<RepositoryDiffFile> = vec![];
1177
1178 diff.deltas().enumerate().for_each(|(i, delta)| {
1179 // Parse the old file info from the delta
1180 let old_file_info = match delta.old_file().exists() {
1181 true => Some(RepositoryDiffFileInfo {
1182 id: delta.old_file().id().to_string(),
1183 path: delta
1184 .old_file()
1185 .path()
1186 .unwrap()
1187 .to_str()
1188 .unwrap()
1189 .to_string(),
1190 size: delta.old_file().size(),
1191 binary: delta.old_file().is_binary(),
1192 }),
1193 false => None,
1194 };
1195 // Parse the new file info from the delta
1196 let new_file_info = match delta.new_file().exists() {
1197 true => Some(RepositoryDiffFileInfo {
1198 id: delta.new_file().id().to_string(),
1199 path: delta
1200 .new_file()
1201 .path()
1202 .unwrap()
1203 .to_str()
1204 .unwrap()
1205 .to_string(),
1206 size: delta.new_file().size(),
1207 binary: delta.new_file().is_binary(),
1208 }),
1209 false => None,
1210 };
1211
1212 let mut chunks: Vec<RepositoryDiffFileChunk> = vec![];
1213 if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() {
1214 for chunk_num in 0..patch.num_hunks() {
1215 if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) {
1216 let mut lines: Vec<RepositoryChunkLine> = vec![];
1217
1218 for line_num in 0..chunk_num_lines {
1219 if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) {
1220 if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) {
1221 lines.push(RepositoryChunkLine {
1222 change_type: line.origin_value().into(),
1223 content: line_utf8,
1224 old_line_num: line.old_lineno(),
1225 new_line_num: line.new_lineno(),
1226 });
1227 }
1228
1229 continue;
1230 }
1231 }
1232
1233 chunks.push(RepositoryDiffFileChunk {
1234 header: String::from_utf8(chunk.header().to_vec()).ok(),
1235 old_start: chunk.old_start(),
1236 old_lines: chunk.old_lines(),
1237 new_start: chunk.new_start(),
1238 new_lines: chunk.new_lines(),
1239 lines,
1240 });
1241 }
1242 }
1243 };
1244
1245 let file = RepositoryDiffFile {
1246 status: RepositoryDiffFileStatus::from(delta.status()),
1247 old_file_info,
1248 new_file_info,
1249 chunks,
1250 };
1251
1252 files.push(file);
1253 });
1254
1255 Ok(RepositoryDiff {
1256 new_commit: Commit::from(commit_new),
1257 files_changed: stats.files_changed(),
1258 insertions: stats.insertions(),
1259 deletions: stats.deletions(),
1260 files,
1261 })
1262 }
1263
1264 async fn repository_diff_patch(
1265 &mut self,
1266 requester: &Option<AuthenticatedUser>,
1267 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
1268 OperationState(_operation_state): OperationState<StackOperationState>,
1269 request: &RepositoryDiffPatchRequest,
1270 ) -> Result<String, Error> {
1271 let repository = repository_object.object();
1272 let git = self
1273 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
1274 .await?;
1275
1276 // Parse the passed object ids
1277 let oid_old = git2::Oid::from_str(request.old_id.as_str())
1278 .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?;
1279 let oid_new = git2::Oid::from_str(request.new_id.as_str())
1280 .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?;
1281
1282 // Get the ids associates commits
1283 let commit_old = git
1284 .find_commit(oid_old)
1285 .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?;
1286 let commit_new = git
1287 .find_commit(oid_new)
1288 .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?;
1289
1290 // Get the commit trees
1291 let tree_old = commit_old
1292 .tree()
1293 .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?;
1294 let tree_new = commit_new
1295 .tree()
1296 .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?;
1297
1298 // Diff the two trees against each other
1299 let diff = git
1300 .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None)
1301 .map_err(|_| {
1302 GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string())
1303 })?;
1304
1305 // Print the entire patch
1306 let mut patch = String::new();
1307
1308 diff.print(git2::DiffFormat::Patch, |_, _, line| {
1309 match line.origin() {
1310 '+' | '-' | ' ' => patch.push(line.origin()),
1311 _ => {}
1312 }
1313 patch.push_str(std::str::from_utf8(line.content()).unwrap());
1314 true
1315 })
1316 .unwrap();
1317
1318 Ok(patch)
1319 }
1320
1321 async fn repository_commit_before(
1322 &mut self,
1323 requester: &Option<AuthenticatedUser>,
1324 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
1325 OperationState(_operation_state): OperationState<StackOperationState>,
1326 request: &RepositoryCommitBeforeRequest,
1327 ) -> Result<Commit, Error> {
1328 let repository = repository_object.object();
1329 let git = self
1330 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
1331 .await?;
1332
1333 // Parse the passed object id
1334 let oid = match git2::Oid::from_str(request.0.as_str()) {
1335 Ok(oid) => oid,
1336 Err(_) => {
1337 return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into())
1338 }
1339 };
1340
1341 // Find the commit using the parsed oid
1342 let commit = match git.find_commit(oid) {
1343 Ok(commit) => commit,
1344 Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()),
1345 };
1346
1347 // Get the first parent it has
1348 let parent = commit.parent(0);
1349 if let Ok(parent) = parent {
1350 return Ok(Commit::from(parent));
1351 } else {
1352 // TODO: See if can be done better
1353 // Walk through the repository commit graph starting at our current commit
1354 let mut revwalk = git.revwalk()?;
1355 revwalk.set_sorting(git2::Sort::TIME)?;
1356 revwalk.push(commit.id())?;
1357
1358 if let Some(Ok(before_commit_oid)) = revwalk.next() {
1359 // Find the commit using the parsed oid
1360 if let Ok(before_commit) = git.find_commit(before_commit_oid) {
1361 return Ok(Commit::from(before_commit));
1362 }
1363 }
1364
1365 Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into())
1366 }
1367 }
1368 }
1369
1370 impl IssuesBackend for GitBackend {
1371 fn issues_count(
1372 &mut self,
1373 _requester: &Option<AuthenticatedUser>,
1374 _request: &RepositoryIssuesCountRequest,
1375 ) -> Result<u64, Error> {
1376 todo!()
1377 }
1378
1379 fn issue_labels(
1380 &mut self,
1381 _requester: &Option<AuthenticatedUser>,
1382 _request: &RepositoryIssueLabelsRequest,
1383 ) -> Result<Vec<IssueLabel>, Error> {
1384 todo!()
1385 }
1386
1387 fn issues(
1388 &mut self,
1389 _requester: &Option<AuthenticatedUser>,
1390 _request: &RepositoryIssuesRequest,
1391 ) -> Result<Vec<RepositoryIssue>, Error> {
1392 todo!()
1393 }
1394 }
1395
1396 #[allow(unused)]
1397 #[derive(Debug, sqlx::FromRow)]
1398 struct RepositoryMetadata {
1399 pub repository: String,
1400 pub name: String,
1401 pub value: String,
1402 }
1403