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

ambee/giterated

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

Unified stack `GetValue` implementation

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨325f5af

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