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

ambee/giterated

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

Add more info to chunk lines

erremilia - ⁨2⁩ years ago

parent: tbd commit: ⁨7ae7f1f

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