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

ambee/giterated

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

Base protocol refactor complete

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨079d544

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