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

ambee/giterated

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

Implement `FromOperationState` for `AuthenticatedUser` and `AuthenticatedInstance`

Use `AuthenticatedUser` on repository requests so we can filter by privacy. Woohoo! Attempt to filter `UserRepositoriesRequest` responses by visibility to the requester.

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨75dcac3

⁨giterated-daemon/src/backend/git.rs⁩ - ⁨36467⁩ 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 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
336 #[async_trait]
337 impl RepositoryBackend for GitBackend {
338 async fn exists(
339 &mut self,
340 requester: &Option<AuthenticatedUser>,
341 repository: &Repository,
342 ) -> Result<bool, Error> {
343 if let Ok(repository) = self
344 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
345 .await
346 {
347 Ok(repository
348 .can_user_view_repository(&self.instance, requester, &self.settings_provider)
349 .await)
350 } else {
351 Ok(false)
352 }
353 }
354
355 async fn create_repository(
356 &mut self,
357 _user: &AuthenticatedUser,
358 request: &RepositoryCreateRequest,
359 ) -> Result<Repository, GitBackendError> {
360 // Check if repository already exists in the database
361 if let Ok(repository) = self
362 .find_by_owner_user_name(&request.owner, &request.name)
363 .await
364 {
365 let err = GitBackendError::RepositoryAlreadyExists {
366 owner_user: repository.owner_user.to_string(),
367 name: repository.name,
368 };
369 error!("{:?}", err);
370
371 return Err(err);
372 }
373
374 // Insert the repository into the database
375 let _ = match sqlx::query_as!(GitRepository,
376 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
377 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
378 .fetch_one(&self.pg_pool.clone())
379 .await {
380 Ok(repository) => repository,
381 Err(err) => {
382 let err = GitBackendError::FailedInsertingIntoDatabase(err);
383 error!("Failed inserting into the database! {:?}", err);
384
385 return Err(err);
386 }
387 };
388
389 // Create bare (server side) repository on disk
390 match git2::Repository::init_bare(PathBuf::from(format!(
391 "{}/{}/{}/{}",
392 self.repository_folder,
393 request.owner.instance.url,
394 request.owner.username,
395 request.name
396 ))) {
397 Ok(_) => {
398 debug!(
399 "Created new repository with the name {}/{}/{}",
400 request.owner.instance.url, request.owner.username, request.name
401 );
402
403 let repository = Repository {
404 owner: request.owner.clone(),
405 name: request.name.clone(),
406 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
407 };
408
409 let mut settings_backend = self.settings_provider.lock().await;
410 settings_backend
411 .repository_write(
412 &repository,
413 Description::name(),
414 AnySetting(
415 serde_json::to_value(Description(
416 request.description.clone().unwrap_or_default(),
417 ))
418 .unwrap(),
419 ),
420 )
421 .await
422 .unwrap();
423 settings_backend
424 .repository_write(
425 &repository,
426 Visibility::name(),
427 AnySetting(
428 serde_json::to_value(Visibility(request.visibility.clone())).unwrap(),
429 ),
430 )
431 .await
432 .unwrap();
433 settings_backend
434 .repository_write(
435 &repository,
436 DefaultBranch::name(),
437 AnySetting(
438 serde_json::to_value(DefaultBranch(request.default_branch.clone()))
439 .unwrap(),
440 ),
441 )
442 .await
443 .unwrap();
444
445 Ok(repository)
446 }
447 Err(err) => {
448 let err = GitBackendError::FailedCreatingRepository(err);
449 error!("Failed creating repository on disk!? {:?}", err);
450
451 // Delete repository from database
452 self.delete_by_owner_user_name(&request.owner, request.name.as_str())
453 .await?;
454
455 // ???
456 Err(err)
457 }
458 }
459 }
460
461 async fn get_value(
462 &mut self,
463 repository: &Repository,
464 name: &str,
465 ) -> Result<AnyValue<Repository>, Error> {
466 Ok(unsafe {
467 if name == Description::value_name() {
468 AnyValue::from_raw(self.get_setting(repository, Description::name()).await?.0)
469 } else if name == Visibility::value_name() {
470 AnyValue::from_raw(self.get_setting(repository, Visibility::name()).await?.0)
471 } else if name == DefaultBranch::value_name() {
472 AnyValue::from_raw(self.get_setting(repository, DefaultBranch::name()).await?.0)
473 } else if name == LatestCommit::value_name() {
474 AnyValue::from_raw(serde_json::to_value(LatestCommit(None)).unwrap())
475 } else {
476 return Err(UserParseError.into());
477 }
478 })
479 }
480
481 async fn get_setting(
482 &mut self,
483 repository: &Repository,
484 name: &str,
485 ) -> Result<AnySetting, Error> {
486 let mut provider = self.settings_provider.lock().await;
487
488 Ok(provider.repository_get(repository, name).await?)
489 }
490
491 async fn write_setting(
492 &mut self,
493 repository: &Repository,
494 name: &str,
495 setting: &Value,
496 ) -> Result<(), Error> {
497 let mut provider = self.settings_provider.lock().await;
498
499 provider
500 .repository_write(repository, name, AnySetting(setting.clone()))
501 .await
502 }
503
504 async fn repository_file_inspect(
505 &mut self,
506 requester: &Option<AuthenticatedUser>,
507 repository: &Repository,
508 request: &RepositoryFileInspectRequest,
509 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
510 let git = self
511 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
512 .await?;
513
514 // Try and parse the input as a reference and get the object ID
515 let mut tree_id = match &request.rev {
516 None => {
517 if let Ok(head) = git.head() {
518 // TODO: Fix for symbolic references
519 head.target()
520 } else {
521 // Nothing in database, render empty tree.
522 return Ok(vec![]);
523 }
524 }
525 Some(rev_name) => {
526 // Find the reference, otherwise return GitBackendError
527 git.refname_to_id(rev_name).ok()
528 }
529 };
530
531 // If the reference wasn't found, try parsing it as a commit ID
532 if tree_id.is_none() {
533 if let Ok(oid) = git2::Oid::from_str(request.rev.as_ref().unwrap()) {
534 tree_id = Some(oid)
535 }
536 }
537
538 // If the commit ID wasn't found, try parsing it as a branch and otherwise return error
539 if tree_id.is_none() {
540 match git.find_branch(request.rev.as_ref().unwrap(), BranchType::Local) {
541 Ok(branch) => tree_id = branch.get().target(),
542 Err(_) => {
543 return Err(Box::new(GitBackendError::RefNotFound(
544 request.rev.clone().unwrap(),
545 ))
546 .into())
547 }
548 }
549 }
550
551 // unwrap might be dangerous?
552 // Get the commit from the oid
553 let commit = git.find_commit(tree_id.unwrap()).unwrap();
554
555 // this is stupid
556 let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string());
557
558 // Get the commit tree
559 let git_tree = if let Some(path) = &request.path {
560 // Add it to our full path string
561 current_path.push_str(format!("/{}", path).as_str());
562 // Get the specified path, return an error if it wasn't found.
563 let entry = match commit
564 .tree()
565 .unwrap()
566 .get_path(&PathBuf::from(path))
567 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
568 {
569 Ok(entry) => entry,
570 Err(err) => return Err(Box::new(err).into()),
571 };
572 // Turn the entry into a git tree
573 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
574 } else {
575 commit.tree().unwrap()
576 };
577
578 // Iterate over the git tree and collect it into our own tree types
579 let mut tree = git_tree
580 .iter()
581 .map(|entry| {
582 let object_type = match entry.kind().unwrap() {
583 git2::ObjectType::Tree => RepositoryObjectType::Tree,
584 git2::ObjectType::Blob => RepositoryObjectType::Blob,
585 _ => unreachable!(),
586 };
587 let mut tree_entry = RepositoryTreeEntry::new(
588 entry.id().to_string().as_str(),
589 entry.name().unwrap(),
590 object_type,
591 entry.filemode(),
592 );
593
594 if request.extra_metadata {
595 // Get the file size if It's a blob
596 let object = entry.to_object(&git).unwrap();
597 if let Some(blob) = object.as_blob() {
598 tree_entry.size = Some(blob.size());
599 }
600
601 // Could possibly be done better
602 let path = if let Some(path) = current_path.split_once('/') {
603 format!("{}/{}", path.1, entry.name().unwrap())
604 } else {
605 entry.name().unwrap().to_string()
606 };
607
608 // Get the last commit made to the entry
609 if let Ok(last_commit) =
610 GitBackend::get_last_commit_of_file(&path, &git, &commit)
611 {
612 tree_entry.last_commit = Some(last_commit);
613 }
614 }
615
616 tree_entry
617 })
618 .collect::<Vec<RepositoryTreeEntry>>();
619
620 // Sort the tree alphabetically and with tree first
621 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
622 tree.sort_unstable_by_key(|entry| {
623 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
624 });
625
626 Ok(tree)
627 }
628
629 async fn repository_file_from_id(
630 &mut self,
631 requester: &Option<AuthenticatedUser>,
632 repository: &Repository,
633 request: &RepositoryFileFromIdRequest,
634 ) -> Result<RepositoryFile, Error> {
635 let git = self
636 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
637 .await?;
638
639 // Parse the passed object id
640 let oid = match git2::Oid::from_str(request.0.as_str()) {
641 Ok(oid) => oid,
642 Err(_) => {
643 return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into())
644 }
645 };
646
647 // Find the file and turn it into our own struct
648 let file = match git.find_blob(oid) {
649 Ok(blob) => RepositoryFile {
650 content: blob.content().to_vec(),
651 binary: blob.is_binary(),
652 size: blob.size(),
653 },
654 Err(_) => return Err(Box::new(GitBackendError::BlobNotFound(oid.to_string())).into()),
655 };
656
657 Ok(file)
658 }
659
660 async fn repository_file_from_path(
661 &mut self,
662 requester: &Option<AuthenticatedUser>,
663 repository: &Repository,
664 request: &RepositoryFileFromPathRequest,
665 ) -> Result<RepositoryFile, Error> {
666 let git = self
667 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
668 .await?;
669
670 // TODO: Remove duplicate code with repository_file_inspect, most of it is duplicate.
671 // Try and parse the input as a reference and get the object ID
672 let mut tree_id = match &request.rev {
673 None => {
674 if let Ok(head) = git.head() {
675 // TODO: Fix for symbolic references
676 head.target()
677 } else {
678 // Nothing in database, render empty tree.
679 return Err(Box::new(GitBackendError::RefNotFound(
680 request.rev.clone().unwrap(),
681 ))
682 .into());
683 }
684 }
685 Some(rev_name) => {
686 // Find the reference, otherwise return GitBackendError
687 git.refname_to_id(rev_name).ok()
688 }
689 };
690
691 // If the reference wasn't found, try parsing it as a commit ID
692 if tree_id.is_none() {
693 if let Ok(oid) = git2::Oid::from_str(request.rev.as_ref().unwrap()) {
694 tree_id = Some(oid)
695 }
696 }
697
698 // If the commit ID wasn't found, try parsing it as a branch and otherwise return error
699 if tree_id.is_none() {
700 match git.find_branch(request.rev.as_ref().unwrap(), BranchType::Local) {
701 Ok(branch) => tree_id = branch.get().target(),
702 Err(_) => {
703 return Err(Box::new(GitBackendError::RefNotFound(
704 request.rev.clone().unwrap(),
705 ))
706 .into())
707 }
708 }
709 }
710
711 // unwrap might be dangerous?
712 // Get the commit from the oid
713 let commit = git.find_commit(tree_id.unwrap()).unwrap();
714
715 // this is stupid
716 let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string());
717
718 // Add it to our full path string
719 current_path.push_str(format!("/{}", request.path).as_str());
720 // Get the specified path, return an error if it wasn't found.
721 let entry = match commit
722 .tree()
723 .unwrap()
724 .get_path(&PathBuf::from(request.path.clone()))
725 .map_err(|_| GitBackendError::PathNotFound(request.path.to_string()))
726 {
727 Ok(entry) => entry,
728 Err(err) => return Err(Box::new(err).into()),
729 };
730
731 // Find the file and turn it into our own struct
732 let file = match git.find_blob(entry.id()) {
733 Ok(blob) => RepositoryFile {
734 content: blob.content().to_vec(),
735 binary: blob.is_binary(),
736 size: blob.size(),
737 },
738 Err(_) => {
739 return Err(Box::new(GitBackendError::BlobNotFound(entry.id().to_string())).into())
740 }
741 };
742
743 Ok(file)
744 }
745
746 async fn repository_diff(
747 &mut self,
748 requester: &Option<AuthenticatedUser>,
749 repository: &Repository,
750 request: &RepositoryDiffRequest,
751 ) -> Result<RepositoryDiff, Error> {
752 let git = self
753 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
754 .await?;
755
756 // Parse the passed object ids
757 let oid_old = git2::Oid::from_str(request.old_id.as_str())
758 .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?;
759 let oid_new = git2::Oid::from_str(request.new_id.as_str())
760 .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?;
761
762 // Get the ids associates commits
763 let commit_old = git
764 .find_commit(oid_old)
765 .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?;
766 let commit_new = git
767 .find_commit(oid_new)
768 .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?;
769
770 // Get the commit trees
771 let tree_old = commit_old
772 .tree()
773 .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?;
774 let tree_new = commit_new
775 .tree()
776 .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?;
777
778 // Diff the two trees against each other
779 let diff = git
780 .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None)
781 .map_err(|_| {
782 GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string())
783 })?;
784
785 // Should be safe to unwrap?
786 let stats = diff.stats().unwrap();
787 let mut files: Vec<RepositoryDiffFile> = vec![];
788
789 diff.deltas().enumerate().for_each(|(i, delta)| {
790 // Parse the old file info from the delta
791 let old_file_info = match delta.old_file().exists() {
792 true => Some(RepositoryDiffFileInfo {
793 id: delta.old_file().id().to_string(),
794 path: delta
795 .old_file()
796 .path()
797 .unwrap()
798 .to_str()
799 .unwrap()
800 .to_string(),
801 size: delta.old_file().size(),
802 binary: delta.old_file().is_binary(),
803 }),
804 false => None,
805 };
806 // Parse the new file info from the delta
807 let new_file_info = match delta.new_file().exists() {
808 true => Some(RepositoryDiffFileInfo {
809 id: delta.new_file().id().to_string(),
810 path: delta
811 .new_file()
812 .path()
813 .unwrap()
814 .to_str()
815 .unwrap()
816 .to_string(),
817 size: delta.new_file().size(),
818 binary: delta.new_file().is_binary(),
819 }),
820 false => None,
821 };
822
823 let mut chunks: Vec<RepositoryDiffFileChunk> = vec![];
824 if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() {
825 for chunk_num in 0..patch.num_hunks() {
826 if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) {
827 let mut lines: Vec<RepositoryChunkLine> = vec![];
828
829 for line_num in 0..chunk_num_lines {
830 if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) {
831 if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) {
832 lines.push(RepositoryChunkLine {
833 change_type: line.origin_value().into(),
834 content: line_utf8,
835 old_line_num: line.old_lineno(),
836 new_line_num: line.new_lineno(),
837 });
838 }
839
840 continue;
841 }
842 }
843
844 chunks.push(RepositoryDiffFileChunk {
845 header: String::from_utf8(chunk.header().to_vec()).ok(),
846 old_start: chunk.old_start(),
847 old_lines: chunk.old_lines(),
848 new_start: chunk.new_start(),
849 new_lines: chunk.new_lines(),
850 lines,
851 });
852 }
853 }
854 };
855
856 let file = RepositoryDiffFile {
857 status: RepositoryDiffFileStatus::from(delta.status()),
858 old_file_info,
859 new_file_info,
860 chunks,
861 };
862
863 files.push(file);
864 });
865
866 Ok(RepositoryDiff {
867 new_commit: Commit::from(commit_new),
868 files_changed: stats.files_changed(),
869 insertions: stats.insertions(),
870 deletions: stats.deletions(),
871 files,
872 })
873 }
874
875 async fn repository_diff_patch(
876 &mut self,
877 requester: &Option<AuthenticatedUser>,
878 repository: &Repository,
879 request: &RepositoryDiffPatchRequest,
880 ) -> Result<String, Error> {
881 let git = self
882 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
883 .await?;
884
885 // Parse the passed object ids
886 let oid_old = git2::Oid::from_str(request.old_id.as_str())
887 .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?;
888 let oid_new = git2::Oid::from_str(request.new_id.as_str())
889 .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?;
890
891 // Get the ids associates commits
892 let commit_old = git
893 .find_commit(oid_old)
894 .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?;
895 let commit_new = git
896 .find_commit(oid_new)
897 .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?;
898
899 // Get the commit trees
900 let tree_old = commit_old
901 .tree()
902 .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?;
903 let tree_new = commit_new
904 .tree()
905 .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?;
906
907 // Diff the two trees against each other
908 let diff = git
909 .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None)
910 .map_err(|_| {
911 GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string())
912 })?;
913
914 // Print the entire patch
915 let mut patch = String::new();
916
917 diff.print(git2::DiffFormat::Patch, |_, _, line| {
918 match line.origin() {
919 '+' | '-' | ' ' => patch.push(line.origin()),
920 _ => {}
921 }
922 patch.push_str(std::str::from_utf8(line.content()).unwrap());
923 true
924 })
925 .unwrap();
926
927 Ok(patch)
928 }
929
930 async fn repository_commit_before(
931 &mut self,
932 requester: &Option<AuthenticatedUser>,
933 repository: &Repository,
934 request: &RepositoryCommitBeforeRequest,
935 ) -> Result<Commit, Error> {
936 let git = self
937 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
938 .await?;
939
940 // Parse the passed object id
941 let oid = match git2::Oid::from_str(request.0.as_str()) {
942 Ok(oid) => oid,
943 Err(_) => {
944 return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into())
945 }
946 };
947
948 // Find the commit using the parsed oid
949 let commit = match git.find_commit(oid) {
950 Ok(commit) => commit,
951 Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()),
952 };
953
954 // Get the first parent it has
955 let parent = commit.parent(0);
956 if let Ok(parent) = parent {
957 return Ok(Commit::from(parent));
958 } else {
959 // TODO: See if can be done better
960 // Walk through the repository commit graph starting at our current commit
961 let mut revwalk = git.revwalk()?;
962 revwalk.set_sorting(git2::Sort::TIME)?;
963 revwalk.push(commit.id())?;
964
965 if let Some(next) = revwalk.next() {
966 if let Ok(before_commit_oid) = next {
967 // Find the commit using the parsed oid
968 if let Ok(before_commit) = git.find_commit(before_commit_oid) {
969 return Ok(Commit::from(before_commit));
970 }
971 }
972 }
973
974 Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into())
975 }
976 }
977 }
978
979 impl IssuesBackend for GitBackend {
980 fn issues_count(
981 &mut self,
982 _requester: &Option<AuthenticatedUser>,
983 _request: &RepositoryIssuesCountRequest,
984 ) -> Result<u64, Error> {
985 todo!()
986 }
987
988 fn issue_labels(
989 &mut self,
990 _requester: &Option<AuthenticatedUser>,
991 _request: &RepositoryIssueLabelsRequest,
992 ) -> Result<Vec<IssueLabel>, Error> {
993 todo!()
994 }
995
996 fn issues(
997 &mut self,
998 _requester: &Option<AuthenticatedUser>,
999 _request: &RepositoryIssuesRequest,
1000 ) -> Result<Vec<RepositoryIssue>, Error> {
1001 todo!()
1002 }
1003 }
1004
1005 #[allow(unused)]
1006 #[derive(Debug, sqlx::FromRow)]
1007 struct RepositoryMetadata {
1008 pub repository: String,
1009 pub name: String,
1010 pub value: String,
1011 }
1012