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

ambee/giterated

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

fixes

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨b91129f

⁨src/backend/git.rs⁩ - ⁨17688⁩ 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 request: &RepositoryInfoRequest,
313 ) -> Result<RepositoryView, Box<dyn Error + Send>> {
314 let repository = match self
315 .find_by_instance_username_name(
316 // &request.owner.instance.url,
317 // &request.owner.username,
318 &request.repository.name,
319 )
320 .await
321 {
322 Ok(repository) => repository,
323 Err(err) => return Err(Box::new(err)),
324 };
325
326 // if !repository.can_user_view_repository(
327 // request.owner.instance.url.as_str(),
328 // Some(request.owner.username.as_str()),
329 // ) {
330 // return Err(Box::new(GitBackendError::RepositoryNotFound {
331 // instance_url: request.owner.instance.url.clone(),
332 // username: request.owner.username.clone(),
333 // name: request.name.clone(),
334 // }));
335 // }
336
337 let git = match repository.open_git2_repository(&self.repository_folder) {
338 Ok(git) => git,
339 Err(err) => return Err(Box::new(err)),
340 };
341
342 let rev_name = match &request.rev {
343 None => {
344 if let Ok(head) = git.head() {
345 head.name().unwrap().to_string()
346 } else {
347 // Nothing in database, render empty tree.
348 return Ok(RepositoryView {
349 name: repository.name,
350 description: repository.description,
351 visibility: repository.visibility,
352 default_branch: repository.default_branch,
353 latest_commit: None,
354 tree_rev: None,
355 tree: vec![],
356 });
357 }
358 }
359 Some(rev_name) => {
360 // Find the reference, otherwise return GitBackendError
361 match git
362 .find_reference(format!("refs/heads/{}", rev_name).as_str())
363 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
364 {
365 Ok(reference) => reference.name().unwrap().to_string(),
366 Err(err) => return Err(Box::new(err)),
367 }
368 }
369 };
370
371 // Get the git object as a commit
372 let rev = match git
373 .revparse_single(rev_name.as_str())
374 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
375 {
376 Ok(rev) => rev,
377 Err(err) => return Err(Box::new(err)),
378 };
379 let commit = rev.as_commit().unwrap();
380
381 // this is stupid
382 let mut current_path = rev_name.replace("refs/heads/", "");
383
384 // Get the commit tree
385 let git_tree = if let Some(path) = &request.path {
386 // Add it to our full path string
387 current_path.push_str(format!("/{}", path).as_str());
388 // Get the specified path, return an error if it wasn't found.
389 let entry = match commit
390 .tree()
391 .unwrap()
392 .get_path(&PathBuf::from(path))
393 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
394 {
395 Ok(entry) => entry,
396 Err(err) => return Err(Box::new(err)),
397 };
398 // Turn the entry into a git tree
399 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
400 } else {
401 commit.tree().unwrap()
402 };
403
404 // Iterate over the git tree and collect it into our own tree types
405 let mut tree = git_tree
406 .iter()
407 .map(|entry| {
408 let object_type = match entry.kind().unwrap() {
409 ObjectType::Tree => RepositoryObjectType::Tree,
410 ObjectType::Blob => RepositoryObjectType::Blob,
411 _ => unreachable!(),
412 };
413 let mut tree_entry =
414 RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode());
415
416 // if request.extra_metadata {
417 // // Get the file size if It's a blob
418 // let object = entry.to_object(&git).unwrap();
419 // if let Some(blob) = object.as_blob() {
420 // tree_entry.size = Some(blob.size());
421 // }
422
423 // // Could possibly be done better
424 // let path = if let Some(path) = current_path.split_once('/') {
425 // format!("{}/{}", path.1, entry.name().unwrap())
426 // } else {
427 // entry.name().unwrap().to_string()
428 // };
429
430 // // Get the last commit made to the entry
431 // if let Ok(last_commit) =
432 // GitBackend::get_last_commit_of_file(&path, &git, commit)
433 // {
434 // tree_entry.last_commit = Some(last_commit);
435 // }
436 // }
437
438 tree_entry
439 })
440 .collect::<Vec<RepositoryTreeEntry>>();
441
442 // Sort the tree alphabetically and with tree first
443 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
444 tree.sort_unstable_by_key(|entry| {
445 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
446 });
447
448 Ok(RepositoryView {
449 name: repository.name,
450 description: repository.description,
451 visibility: repository.visibility,
452 default_branch: repository.default_branch,
453 latest_commit: None,
454 tree_rev: Some(rev_name),
455 tree,
456 })
457 }
458
459 fn repository_file_inspect(
460 &mut self,
461 _request: &RepositoryFileInspectRequest,
462 ) -> Result<RepositoryFileInspectionResponse, Box<dyn Error + Send>> {
463 todo!()
464 }
465 }
466
467 impl IssuesBackend for GitBackend {
468 fn issues_count(
469 &mut self,
470 _request: &RepositoryIssuesCountRequest,
471 ) -> Result<RepositoryIssuesCountResponse, Box<dyn Error + Send>> {
472 todo!()
473 }
474
475 fn issue_labels(
476 &mut self,
477 _request: &RepositoryIssueLabelsRequest,
478 ) -> Result<RepositoryIssueLabelsResponse, Box<dyn Error + Send>> {
479 todo!()
480 }
481
482 fn issues(
483 &mut self,
484 _request: &RepositoryIssuesRequest,
485 ) -> Result<RepositoryIssuesResponse, Box<dyn Error + Send>> {
486 todo!()
487 }
488 }
489
490 async fn public_key(instance: &Instance) -> Result<String, Box<dyn Error>> {
491 let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url))
492 .await?
493 .text()
494 .await?;
495
496 Ok(key)
497 }
498