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

ambee/giterated

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

Add users table

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨9f36e3f

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