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

ambee/giterated

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

Giterated Stack `ObjectValue` and `Setting` refactor.

This refactor adds value and setting update events, as well as value getters. Additionally, the stack is now the owner of the ability to write settings into storage. This is accomplished with the `MetadataProvider` trait. This sets up the ground work for push federation, cache, and basically everything else. commit 7befc583cb3e0c6719506c550ed66ac76293413c Author: Amber <[email protected]> Date: Fri Sep 29 15:46:48 2023 -0500 Finish value and settings refactor in the stack. commit 3ac09994a0caafd1a0b95d9a781c7f202f20e75b Author: Amber <[email protected]> Date: Fri Sep 29 09:46:32 2023 -0500 Add set_setting handling back in commit 84fd31e3eae85d98fa68a28b333dbb32cde3bdb8 Author: Amber <[email protected]> Date: Wed Sep 27 06:36:31 2023 -0500 Remove some allocations from meta types commit 16c310ce3680c4a14ed35083b6a230aaecd43152 Author: Amber <[email protected]> Date: Wed Sep 27 05:35:03 2023 -0500 Add cargo metadata commit eb2520a20001bac7b21c6c3d34f62db32f0ada80 Author: Amber <[email protected]> Date: Wed Sep 27 05:26:27 2023 -0500 Refactor setting and value management to use the unified stack. Allows for tight management, inspection, and eventing of setting and value management. commit 901fe103da0fce4b40f33b0a8b64404049ae03cf Author: Amber <[email protected]> Date: Wed Sep 27 02:38:33 2023 -0500 Set up ground work for value / settings refactor

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨c377e4d

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