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

ambee/giterated

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

Add docs

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨51aad53

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