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

ambee/giterated

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

Update GitBackend to use new models

Type: Fix

emilia - ⁨2⁩ years ago

parent: tbd commit: ⁨f657cbe

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