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

ambee/giterated

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

Begin new protocol refactor

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨26651b1

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