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

ambee/giterated

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

Add RepositorySummary and return in user repositories

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨8512ab4

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