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

ambee/giterated

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

Add token extension

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨86d028f

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