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

ambee/giterated

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

Fix handling stack

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨c53b026

⁨giterated-daemon/src/backend/git.rs⁩ - ⁨24694⁩ bytes
Raw
1 use anyhow::Error;
2 use async_trait::async_trait;
3 use futures_util::StreamExt;
4
5 use giterated_models::instance::{Instance, RepositoryCreateRequest};
6
7 use giterated_models::repository::{
8 Commit, DefaultBranch, Description, IssueLabel, LatestCommit, Repository,
9 RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest,
10 RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryObjectType,
11 RepositoryTreeEntry, RepositoryVisibility, Visibility,
12 };
13 use giterated_models::settings::{AnySetting, Setting};
14 use giterated_models::user::{User, UserParseError};
15 use giterated_models::value::{AnyValue, GiteratedObjectValue};
16 use serde_json::Value;
17 use sqlx::PgPool;
18 use std::{
19 path::{Path, PathBuf},
20 sync::Arc,
21 };
22 use thiserror::Error;
23 use tokio::sync::Mutex;
24
25 use super::{IssuesBackend, MetadataBackend, RepositoryBackend};
26
27 // TODO: Handle this
28 //region database structures
29
30 /// Repository in the database
31 #[derive(Debug, sqlx::FromRow)]
32 pub struct GitRepository {
33 #[sqlx(try_from = "String")]
34 pub owner_user: User,
35 pub name: String,
36 pub description: Option<String>,
37 pub visibility: RepositoryVisibility,
38 pub default_branch: String,
39 }
40
41 impl GitRepository {
42 // Separate function because "Private" will be expanded later
43 /// Checks if the user is allowed to view this repository
44 pub fn can_user_view_repository(&self, user: Option<&User>) -> bool {
45 !matches!(self.visibility, RepositoryVisibility::Private)
46 || (matches!(self.visibility, RepositoryVisibility::Private)
47 && Some(&self.owner_user) == user)
48 }
49
50 // This is in it's own function because I assume I'll have to add logic to this later
51 pub fn open_git2_repository(
52 &self,
53 repository_directory: &str,
54 ) -> Result<git2::Repository, GitBackendError> {
55 match git2::Repository::open(format!(
56 "{}/{}/{}/{}",
57 repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name
58 )) {
59 Ok(repository) => Ok(repository),
60 Err(err) => {
61 let err = GitBackendError::FailedOpeningFromDisk(err);
62 error!("Couldn't open a repository, this is bad! {:?}", err);
63
64 Err(err)
65 }
66 }
67 }
68 }
69
70 //endregion
71
72 #[derive(Error, Debug)]
73 pub enum GitBackendError {
74 #[error("Failed creating repository")]
75 FailedCreatingRepository(git2::Error),
76 #[error("Failed inserting into the database")]
77 FailedInsertingIntoDatabase(sqlx::Error),
78 #[error("Failed finding repository {owner_user:?}/{name:?}")]
79 RepositoryNotFound { owner_user: String, name: String },
80 #[error("Repository {owner_user:?}/{name:?} already exists")]
81 RepositoryAlreadyExists { owner_user: String, name: String },
82 #[error("Repository couldn't be deleted from the disk")]
83 CouldNotDeleteFromDisk(std::io::Error),
84 #[error("Failed deleting repository from database")]
85 FailedDeletingFromDatabase(sqlx::Error),
86 #[error("Failed opening repository on disk")]
87 FailedOpeningFromDisk(git2::Error),
88 #[error("Couldn't find ref with name `{0}`")]
89 RefNotFound(String),
90 #[error("Couldn't find path in repository `{0}`")]
91 PathNotFound(String),
92 #[error("Couldn't find commit for path `{0}`")]
93 LastCommitNotFound(String),
94 }
95
96 pub struct GitBackend {
97 pub pg_pool: PgPool,
98 pub repository_folder: String,
99 pub instance: Instance,
100 pub settings_provider: Arc<Mutex<dyn MetadataBackend + Send>>,
101 }
102
103 impl GitBackend {
104 pub fn new(
105 pg_pool: &PgPool,
106 repository_folder: &str,
107 instance: impl ToOwned<Owned = Instance>,
108 settings_provider: Arc<Mutex<dyn MetadataBackend + Send>>,
109 ) -> Self {
110 Self {
111 pg_pool: pg_pool.clone(),
112 repository_folder: repository_folder.to_string(),
113 instance: instance.to_owned(),
114 settings_provider,
115 }
116 }
117
118 pub async fn find_by_owner_user_name(
119 &self,
120 user: &User,
121 repository_name: &str,
122 ) -> Result<GitRepository, GitBackendError> {
123 if let Ok(repository) = sqlx::query_as!(GitRepository,
124 r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#,
125 user.to_string(), repository_name)
126 .fetch_one(&self.pg_pool.clone())
127 .await {
128 Ok(repository)
129 } else {
130 Err(GitBackendError::RepositoryNotFound {
131 owner_user: user.to_string(),
132 name: repository_name.to_string(),
133 })
134 }
135 }
136
137 pub async fn delete_by_owner_user_name(
138 &self,
139 user: &User,
140 repository_name: &str,
141 ) -> Result<u64, GitBackendError> {
142 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
143 "{}/{}/{}/{}",
144 self.repository_folder, user.instance.url, user.username, repository_name
145 ))) {
146 let err = GitBackendError::CouldNotDeleteFromDisk(err);
147 error!(
148 "Couldn't delete repository from disk, this is bad! {:?}",
149 err
150 );
151
152 return Err(err);
153 }
154
155 // Delete the repository from the database
156 match sqlx::query!(
157 "DELETE FROM repositories WHERE owner_user = $1 AND name = $2",
158 user.to_string(),
159 repository_name
160 )
161 .execute(&self.pg_pool.clone())
162 .await
163 {
164 Ok(deleted) => Ok(deleted.rows_affected()),
165 Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)),
166 }
167 }
168
169 // TODO: Find where this fits
170 // TODO: Cache this and general repository tree and invalidate select files on push
171 // TODO: Find better and faster technique for this
172 pub fn get_last_commit_of_file(
173 path: &str,
174 git: &git2::Repository,
175 start_commit: &git2::Commit,
176 ) -> anyhow::Result<Commit> {
177 let mut revwalk = git.revwalk()?;
178 revwalk.set_sorting(git2::Sort::TIME)?;
179 revwalk.push(start_commit.id())?;
180
181 for oid in revwalk {
182 let oid = oid?;
183 let commit = git.find_commit(oid)?;
184
185 // Merge commits have 2 or more parents
186 // Commits with 0 parents are handled different because we can't diff against them
187 if commit.parent_count() == 0 {
188 return Ok(commit.into());
189 } else if commit.parent_count() == 1 {
190 let tree = commit.tree()?;
191 let last_tree = commit.parent(0)?.tree()?;
192
193 // Get the diff between the current tree and the last one
194 let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?;
195
196 for dd in diff.deltas() {
197 // Get the path of the current file we're diffing against
198 let current_path = dd.new_file().path().unwrap();
199
200 // Path or directory
201 if current_path.eq(Path::new(&path)) || current_path.starts_with(path) {
202 return Ok(commit.into());
203 }
204 }
205 }
206 }
207
208 Err(GitBackendError::LastCommitNotFound(path.to_string()))?
209 }
210 }
211
212 #[async_trait]
213 impl RepositoryBackend for GitBackend {
214 async fn exists(&mut self, repository: &Repository) -> Result<bool, Error> {
215 if let Ok(_repository) = self
216 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
217 .await
218 {
219 Ok(true)
220 } else {
221 Ok(false)
222 }
223 }
224
225 async fn create_repository(
226 &mut self,
227 _user: &User,
228 request: &RepositoryCreateRequest,
229 ) -> Result<Repository, GitBackendError> {
230 // Check if repository already exists in the database
231 if let Ok(repository) = self
232 .find_by_owner_user_name(&request.owner, &request.name)
233 .await
234 {
235 let err = GitBackendError::RepositoryAlreadyExists {
236 owner_user: repository.owner_user.to_string(),
237 name: repository.name,
238 };
239 error!("{:?}", err);
240
241 return Err(err);
242 }
243
244 // Insert the repository into the database
245 let _ = match sqlx::query_as!(GitRepository,
246 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
247 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
248 .fetch_one(&self.pg_pool.clone())
249 .await {
250 Ok(repository) => repository,
251 Err(err) => {
252 let err = GitBackendError::FailedInsertingIntoDatabase(err);
253 error!("Failed inserting into the database! {:?}", err);
254
255 return Err(err);
256 }
257 };
258
259 // Create bare (server side) repository on disk
260 match git2::Repository::init_bare(PathBuf::from(format!(
261 "{}/{}/{}/{}",
262 self.repository_folder,
263 request.owner.instance.url,
264 request.owner.username,
265 request.name
266 ))) {
267 Ok(_) => {
268 debug!(
269 "Created new repository with the name {}/{}/{}",
270 request.owner.instance.url, request.owner.username, request.name
271 );
272 Ok(Repository {
273 owner: request.owner.clone(),
274 name: request.name.clone(),
275 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
276 })
277 }
278 Err(err) => {
279 let err = GitBackendError::FailedCreatingRepository(err);
280 error!("Failed creating repository on disk!? {:?}", err);
281
282 // Delete repository from database
283 self.delete_by_owner_user_name(&request.owner, request.name.as_str())
284 .await?;
285
286 // ???
287 Err(err)
288 }
289 }
290 }
291
292 async fn get_value(
293 &mut self,
294 repository: &Repository,
295 name: &str,
296 ) -> Result<AnyValue<Repository>, Error> {
297 Ok(unsafe {
298 if name == Description::value_name() {
299 AnyValue::from_raw(self.get_setting(repository, Description::name()).await?.0)
300 } else if name == Visibility::value_name() {
301 AnyValue::from_raw(self.get_setting(repository, Visibility::name()).await?.0)
302 } else if name == DefaultBranch::value_name() {
303 AnyValue::from_raw(self.get_setting(repository, DefaultBranch::name()).await?.0)
304 } else if name == LatestCommit::value_name() {
305 AnyValue::from_raw(serde_json::to_value(LatestCommit(None)).unwrap())
306 } else {
307 return Err(UserParseError.into());
308 }
309 })
310 }
311
312 async fn get_setting(
313 &mut self,
314 repository: &Repository,
315 name: &str,
316 ) -> Result<AnySetting, Error> {
317 let mut provider = self.settings_provider.lock().await;
318
319 Ok(provider.repository_get(repository, name).await?)
320 }
321
322 async fn write_setting(
323 &mut self,
324 repository: &Repository,
325 name: &str,
326 setting: &Value,
327 ) -> Result<(), Error> {
328 let mut provider = self.settings_provider.lock().await;
329
330 provider
331 .repository_write(repository, name, AnySetting(setting.clone()))
332 .await
333 }
334
335 // async fn repository_info(
336 // &mut self,
337 // requester: Option<&User>,
338 // request: &RepositoryInfoRequest,
339 // ) -> Result<RepositoryView, Error> {
340 // let repository = match self
341 // .find_by_owner_user_name(
342 // // &request.owner.instance.url,
343 // &request.repository.owner,
344 // &request.repository.name,
345 // )
346 // .await
347 // {
348 // Ok(repository) => repository,
349 // Err(err) => return Err(Box::new(err).into()),
350 // };
351
352 // if let Some(requester) = requester {
353 // if !repository.can_user_view_repository(Some(&requester)) {
354 // return Err(Box::new(GitBackendError::RepositoryNotFound {
355 // owner_user: request.repository.owner.to_string(),
356 // name: request.repository.name.clone(),
357 // })
358 // .into());
359 // }
360 // } else if matches!(repository.visibility, RepositoryVisibility::Private) {
361 // info!("Unauthenticated");
362 // // Unauthenticated users can never view private repositories
363
364 // return Err(Box::new(GitBackendError::RepositoryNotFound {
365 // owner_user: request.repository.owner.to_string(),
366 // name: request.repository.name.clone(),
367 // })
368 // .into());
369 // }
370
371 // let git = match repository.open_git2_repository(&self.repository_folder) {
372 // Ok(git) => git,
373 // Err(err) => return Err(Box::new(err).into()),
374 // };
375
376 // let rev_name = match &request.rev {
377 // None => {
378 // if let Ok(head) = git.head() {
379 // head.name().unwrap().to_string()
380 // } else {
381 // // Nothing in database, render empty tree.
382 // return Ok(RepositoryView {
383 // name: repository.name,
384 // owner: request.repository.owner.clone(),
385 // description: repository.description,
386 // visibility: repository.visibility,
387 // default_branch: repository.default_branch,
388 // latest_commit: None,
389 // tree_rev: None,
390 // tree: vec![],
391 // });
392 // }
393 // }
394 // Some(rev_name) => {
395 // // Find the reference, otherwise return GitBackendError
396 // match git
397 // .find_reference(format!("refs/heads/{}", rev_name).as_str())
398 // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
399 // {
400 // Ok(reference) => reference.name().unwrap().to_string(),
401 // Err(err) => return Err(Box::new(err).into()),
402 // }
403 // }
404 // };
405
406 // // Get the git object as a commit
407 // let rev = match git
408 // .revparse_single(rev_name.as_str())
409 // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
410 // {
411 // Ok(rev) => rev,
412 // Err(err) => return Err(Box::new(err).into()),
413 // };
414 // let commit = rev.as_commit().unwrap();
415
416 // // this is stupid
417 // let mut current_path = rev_name.replace("refs/heads/", "");
418
419 // // Get the commit tree
420 // let git_tree = if let Some(path) = &request.path {
421 // // Add it to our full path string
422 // current_path.push_str(format!("/{}", path).as_str());
423 // // Get the specified path, return an error if it wasn't found.
424 // let entry = match commit
425 // .tree()
426 // .unwrap()
427 // .get_path(&PathBuf::from(path))
428 // .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
429 // {
430 // Ok(entry) => entry,
431 // Err(err) => return Err(Box::new(err).into()),
432 // };
433 // // Turn the entry into a git tree
434 // entry.to_object(&git).unwrap().as_tree().unwrap().clone()
435 // } else {
436 // commit.tree().unwrap()
437 // };
438
439 // // Iterate over the git tree and collect it into our own tree types
440 // let mut tree = git_tree
441 // .iter()
442 // .map(|entry| {
443 // let object_type = match entry.kind().unwrap() {
444 // ObjectType::Tree => RepositoryObjectType::Tree,
445 // ObjectType::Blob => RepositoryObjectType::Blob,
446 // _ => unreachable!(),
447 // };
448 // let mut tree_entry =
449 // RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode());
450
451 // if request.extra_metadata {
452 // // Get the file size if It's a blob
453 // let object = entry.to_object(&git).unwrap();
454 // if let Some(blob) = object.as_blob() {
455 // tree_entry.size = Some(blob.size());
456 // }
457
458 // // Could possibly be done better
459 // let path = if let Some(path) = current_path.split_once('/') {
460 // format!("{}/{}", path.1, entry.name().unwrap())
461 // } else {
462 // entry.name().unwrap().to_string()
463 // };
464
465 // // Get the last commit made to the entry
466 // if let Ok(last_commit) =
467 // GitBackend::get_last_commit_of_file(&path, &git, commit)
468 // {
469 // tree_entry.last_commit = Some(last_commit);
470 // }
471 // }
472
473 // tree_entry
474 // })
475 // .collect::<Vec<RepositoryTreeEntry>>();
476
477 // // Sort the tree alphabetically and with tree first
478 // tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
479 // tree.sort_unstable_by_key(|entry| {
480 // std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
481 // });
482
483 // Ok(RepositoryView {
484 // name: repository.name,
485 // owner: request.repository.owner.clone(),
486 // description: repository.description,
487 // visibility: repository.visibility,
488 // default_branch: repository.default_branch,
489 // latest_commit: Some(Commit::from(commit.clone())),
490 // tree_rev: Some(rev_name),
491 // tree,
492 // })
493 // }
494
495 async fn repository_file_inspect(
496 &mut self,
497 requester: Option<&User>,
498 repository: &Repository,
499 request: &RepositoryFileInspectRequest,
500 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
501 let repository = match self
502 .find_by_owner_user_name(
503 // &request.owner.instance.url,
504 &repository.owner,
505 &repository.name,
506 )
507 .await
508 {
509 Ok(repository) => repository,
510 Err(err) => return Err(Box::new(err).into()),
511 };
512
513 if let Some(requester) = requester {
514 if !repository.can_user_view_repository(Some(requester)) {
515 return Err(Box::new(GitBackendError::RepositoryNotFound {
516 owner_user: repository.owner_user.to_string(),
517 name: repository.name.clone(),
518 })
519 .into());
520 }
521 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
522 info!("Unauthenticated");
523 // Unauthenticated users can never view private repositories
524
525 return Err(Box::new(GitBackendError::RepositoryNotFound {
526 owner_user: repository.owner_user.to_string(),
527 name: repository.name.clone(),
528 })
529 .into());
530 }
531
532 let git = match repository.open_git2_repository(&self.repository_folder) {
533 Ok(git) => git,
534 Err(err) => return Err(Box::new(err).into()),
535 };
536
537 let rev_name = match &request.rev {
538 None => {
539 if let Ok(head) = git.head() {
540 head.name().unwrap().to_string()
541 } else {
542 // Nothing in database, render empty tree.
543 return Ok(vec![]);
544 }
545 }
546 Some(rev_name) => {
547 // Find the reference, otherwise return GitBackendError
548 match git
549 .find_reference(format!("refs/heads/{}", rev_name).as_str())
550 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
551 {
552 Ok(reference) => reference.name().unwrap().to_string(),
553 Err(err) => return Err(Box::new(err).into()),
554 }
555 }
556 };
557
558 // Get the git object as a commit
559 let rev = match git
560 .revparse_single(rev_name.as_str())
561 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
562 {
563 Ok(rev) => rev,
564 Err(err) => return Err(Box::new(err).into()),
565 };
566 let commit = rev.as_commit().unwrap();
567
568 // this is stupid
569 let mut current_path = rev_name.replace("refs/heads/", "");
570
571 // Get the commit tree
572 let git_tree = if let Some(path) = &request.path {
573 // Add it to our full path string
574 current_path.push_str(format!("/{}", path).as_str());
575 // Get the specified path, return an error if it wasn't found.
576 let entry = match commit
577 .tree()
578 .unwrap()
579 .get_path(&PathBuf::from(path))
580 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
581 {
582 Ok(entry) => entry,
583 Err(err) => return Err(Box::new(err).into()),
584 };
585 // Turn the entry into a git tree
586 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
587 } else {
588 commit.tree().unwrap()
589 };
590
591 // Iterate over the git tree and collect it into our own tree types
592 let mut tree = git_tree
593 .iter()
594 .map(|entry| {
595 let object_type = match entry.kind().unwrap() {
596 git2::ObjectType::Tree => RepositoryObjectType::Tree,
597 git2::ObjectType::Blob => RepositoryObjectType::Blob,
598 _ => unreachable!(),
599 };
600 let mut tree_entry =
601 RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode());
602
603 if request.extra_metadata {
604 // Get the file size if It's a blob
605 let object = entry.to_object(&git).unwrap();
606 if let Some(blob) = object.as_blob() {
607 tree_entry.size = Some(blob.size());
608 }
609
610 // Could possibly be done better
611 let path = if let Some(path) = current_path.split_once('/') {
612 format!("{}/{}", path.1, entry.name().unwrap())
613 } else {
614 entry.name().unwrap().to_string()
615 };
616
617 // Get the last commit made to the entry
618 if let Ok(last_commit) =
619 GitBackend::get_last_commit_of_file(&path, &git, commit)
620 {
621 tree_entry.last_commit = Some(last_commit);
622 }
623 }
624
625 tree_entry
626 })
627 .collect::<Vec<RepositoryTreeEntry>>();
628
629 // Sort the tree alphabetically and with tree first
630 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
631 tree.sort_unstable_by_key(|entry| {
632 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
633 });
634
635 Ok(tree)
636 }
637 }
638
639 impl IssuesBackend for GitBackend {
640 fn issues_count(
641 &mut self,
642 _requester: Option<&User>,
643 _request: &RepositoryIssuesCountRequest,
644 ) -> Result<u64, Error> {
645 todo!()
646 }
647
648 fn issue_labels(
649 &mut self,
650 _requester: Option<&User>,
651 _request: &RepositoryIssueLabelsRequest,
652 ) -> Result<Vec<IssueLabel>, Error> {
653 todo!()
654 }
655
656 fn issues(
657 &mut self,
658 _requester: Option<&User>,
659 _request: &RepositoryIssuesRequest,
660 ) -> Result<Vec<RepositoryIssue>, Error> {
661 todo!()
662 }
663 }
664
665 #[derive(Debug, sqlx::FromRow)]
666 struct RepositoryMetadata {
667 pub repository: String,
668 pub name: String,
669 pub value: String,
670 }
671