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

ambee/giterated

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

Add authentication back into the operation states

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨97a26fd

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