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

ambee/giterated

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

Incomplete repository branches request

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨6872fbf

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