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: ⁨74f247e

⁨src/backend/git.rs⁩ - ⁨17401⁩ 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::Authenticated;
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: &Authenticated<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 assert!(matches!(raw_request.validate(public_key).await, Ok(())));
228 info!("Request was valid!");
229
230 // Check if repository already exists in the database
231 if let Ok(_repository) = self
232 .find_by_instance_username_name(
233 request.owner.instance.url.as_str(),
234 request.owner.username.as_str(),
235 request.name.as_str(),
236 )
237 .await
238 {
239 let err = GitBackendError::RepositoryAlreadyExists {
240 instance_url: request.owner.instance.url.clone(),
241 username: request.owner.instance.url.clone(),
242 name: request.name.clone(),
243 };
244 error!("{:?}", err);
245
246 return Ok(CreateRepositoryResponse::Failed);
247 }
248
249 // Insert the repository into the database
250 let _ = match sqlx::query_as!(GitRepository,
251 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5, $6) RETURNING username, instance_url, name, description, visibility as "visibility: _", default_branch"#,
252 request.owner.username, request.owner.instance.url, request.name, request.description, request.visibility as _, "master")
253 .fetch_one(&self.pg_pool.clone())
254 .await {
255 Ok(repository) => repository,
256 Err(err) => {
257 let err = GitBackendError::FailedInsertingIntoDatabase(err);
258 error!("Failed inserting into the database! {:?}", err);
259
260 return Ok(CreateRepositoryResponse::Failed);
261 }
262 };
263
264 // Create bare (server side) repository on disk
265 match git2::Repository::init_bare(PathBuf::from(format!(
266 "{}/{}/{}/{}",
267 self.repository_folder,
268 request.owner.instance.url,
269 request.owner.username,
270 request.name
271 ))) {
272 Ok(_) => {
273 debug!(
274 "Created new repository with the name {}/{}/{}",
275 request.owner.instance.url, request.owner.username, request.name
276 );
277 Ok(CreateRepositoryResponse::Created)
278 }
279 Err(err) => {
280 let err = GitBackendError::FailedCreatingRepository(err);
281 error!("Failed creating repository on disk!? {:?}", err);
282
283 // Delete repository from database
284 if let Err(err) = self
285 .delete_by_instance_username_name(
286 request.owner.instance.url.as_str(),
287 request.owner.username.as_str(),
288 request.name.as_str(),
289 )
290 .await
291 {
292 return Err(Box::new(err));
293 }
294
295 // ???
296 Ok(CreateRepositoryResponse::Failed)
297 //Err(Box::new(err))
298 }
299 }
300 }
301
302 async fn repository_info(
303 &mut self,
304 request: &RepositoryInfoRequest,
305 ) -> Result<RepositoryView, Box<dyn Error + Send>> {
306 let repository = match self
307 .find_by_instance_username_name(
308 &request.owner.instance.url,
309 &request.owner.username,
310 &request.name,
311 )
312 .await
313 {
314 Ok(repository) => repository,
315 Err(err) => return Err(Box::new(err)),
316 };
317
318 if !repository.can_user_view_repository(
319 request.owner.instance.url.as_str(),
320 Some(request.owner.username.as_str()),
321 ) {
322 return Err(Box::new(GitBackendError::RepositoryNotFound {
323 instance_url: request.owner.instance.url.clone(),
324 username: request.owner.username.clone(),
325 name: request.name.clone(),
326 }));
327 }
328
329 let git = match repository.open_git2_repository(&self.repository_folder) {
330 Ok(git) => git,
331 Err(err) => return Err(Box::new(err)),
332 };
333
334 let rev_name = match &request.rev {
335 None => {
336 if let Ok(head) = git.head() {
337 head.name().unwrap().to_string()
338 } else {
339 // Nothing in database, render empty tree.
340 return Ok(RepositoryView {
341 name: repository.name,
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 description: repository.description,
443 visibility: repository.visibility,
444 default_branch: repository.default_branch,
445 latest_commit: None,
446 tree_rev: Some(rev_name),
447 tree,
448 })
449 }
450
451 fn repository_file_inspect(
452 &mut self,
453 _request: &RepositoryFileInspectRequest,
454 ) -> Result<RepositoryFileInspectionResponse, Box<dyn Error + Send>> {
455 todo!()
456 }
457 }
458
459 impl IssuesBackend for GitBackend {
460 fn issues_count(
461 &mut self,
462 _request: &RepositoryIssuesCountRequest,
463 ) -> Result<RepositoryIssuesCountResponse, Box<dyn Error + Send>> {
464 todo!()
465 }
466
467 fn issue_labels(
468 &mut self,
469 _request: &RepositoryIssueLabelsRequest,
470 ) -> Result<RepositoryIssueLabelsResponse, Box<dyn Error + Send>> {
471 todo!()
472 }
473
474 fn issues(
475 &mut self,
476 _request: &RepositoryIssuesRequest,
477 ) -> Result<RepositoryIssuesResponse, Box<dyn Error + Send>> {
478 todo!()
479 }
480 }
481
482 async fn public_key(instance: &Instance) -> Result<String, Box<dyn Error>> {
483 let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url))
484 .await?
485 .text()
486 .await?;
487
488 Ok(key)
489 }
490