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

ambee/giterated

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

Changes

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨0b2a26d

⁨src/backend/git.rs⁩ - ⁨16775⁩ 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::model::repository::{
9 Commit, RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility,
10 };
11 use crate::{
12 messages::repository::{
13 CreateRepositoryRequest, CreateRepositoryResponse, RepositoryFileInspectRequest,
14 RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest,
15 RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse,
16 RepositoryIssuesRequest, RepositoryIssuesResponse,
17 },
18 model::repository::RepositoryView,
19 };
20
21 use super::{IssuesBackend, RepositoryBackend};
22
23 // TODO: Handle this
24 //region database structures
25
26 /// Repository in the database
27 #[derive(Debug, sqlx::FromRow)]
28 pub struct GitRepository {
29 pub username: String,
30 pub instance_url: String,
31 pub name: String,
32 pub description: Option<String>,
33 pub visibility: RepositoryVisibility,
34 pub default_branch: String,
35 }
36
37 impl GitRepository {
38 // Separate function because "Private" will be expanded later
39 /// Checks if the user is allowed to view this repository
40 pub fn can_user_view_repository(&self, instance_url: &str, username: Option<&str>) -> bool {
41 !(matches!(self.visibility, RepositoryVisibility::Private)
42 && self.instance_url != instance_url
43 && self.username != username.map_or("", |username| username))
44 }
45
46 // This is in it's own function because I assume I'll have to add logic to this later
47 pub fn open_git2_repository(
48 &self,
49 repository_directory: &str,
50 ) -> Result<git2::Repository, GitBackendError> {
51 match git2::Repository::open(format!(
52 "{}/{}/{}/{}",
53 repository_directory, self.instance_url, self.username, self.name
54 )) {
55 Ok(repository) => Ok(repository),
56 Err(err) => {
57 let err = GitBackendError::FailedOpeningFromDisk(err);
58 error!("Couldn't open a repository, this is bad! {:?}", err);
59
60 Err(err)
61 }
62 }
63 }
64 }
65
66 //endregion
67
68 #[derive(Error, Debug)]
69 pub enum GitBackendError {
70 #[error("Failed creating repository")]
71 FailedCreatingRepository(git2::Error),
72 #[error("Failed inserting into the database")]
73 FailedInsertingIntoDatabase(sqlx::Error),
74 #[error("Failed finding repository {instance_url:?}/{username:?}/{name:?}")]
75 RepositoryNotFound {
76 instance_url: String,
77 username: String,
78 name: String,
79 },
80 #[error("Repository {instance_url:?}/{username:?}/{name:?} already exists")]
81 RepositoryAlreadyExists {
82 instance_url: String,
83 username: String,
84 name: String,
85 },
86 #[error("Repository couldn't be deleted from the disk")]
87 CouldNotDeleteFromDisk(std::io::Error),
88 #[error("Failed deleting repository from database")]
89 FailedDeletingFromDatabase(sqlx::Error),
90 #[error("Failed opening repository on disk")]
91 FailedOpeningFromDisk(git2::Error),
92 #[error("Couldn't find ref with name `{0}`")]
93 RefNotFound(String),
94 #[error("Couldn't find path in repository `{0}`")]
95 PathNotFound(String),
96 #[error("Couldn't find commit for path `{0}`")]
97 LastCommitNotFound(String),
98 }
99
100 pub struct GitBackend {
101 pub pg_pool: PgPool,
102 pub repository_folder: String,
103 }
104
105 impl GitBackend {
106 pub fn new(pg_pool: &PgPool, repository_folder: &str) -> Self {
107 Self {
108 pg_pool: pg_pool.clone(),
109 repository_folder: repository_folder.to_string(),
110 }
111 }
112
113 pub async fn find_by_instance_username_name(
114 &self,
115 instance_url: &str,
116 username: &str,
117 repository_name: &str,
118 ) -> Result<GitRepository, GitBackendError> {
119 if let Ok(repository) = sqlx::query_as!(GitRepository,
120 r#"SELECT username, instance_url, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE instance_url = $1 AND username = $2 AND name = $3"#,
121 instance_url, username, repository_name)
122 .fetch_one(&self.pg_pool.clone())
123 .await {
124 Ok(repository)
125 } else {
126 Err(GitBackendError::RepositoryNotFound {
127 instance_url: instance_url.to_string(),
128 username: username.to_string(),
129 name: repository_name.to_string(),
130 })
131 }
132 }
133
134 pub async fn delete_by_instance_username_name(
135 &self,
136 instance_url: &str,
137 username: &str,
138 repository_name: &str,
139 ) -> Result<u64, GitBackendError> {
140 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
141 "{}/{}/{}/{}",
142 self.repository_folder, instance_url, username, repository_name
143 ))) {
144 let err = GitBackendError::CouldNotDeleteFromDisk(err);
145 error!(
146 "Couldn't delete repository from disk, this is bad! {:?}",
147 err
148 );
149
150 return Err(err);
151 }
152
153 // Delete the repository from the database
154 match sqlx::query!(
155 "DELETE FROM repositories WHERE instance_url = $1 AND username = $2 AND name = $3",
156 instance_url,
157 username,
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 request: &CreateRepositoryRequest,
216 ) -> Result<CreateRepositoryResponse, Box<dyn Error + Send>> {
217 // Check if repository already exists in the database
218 if let Ok(_repository) = self
219 .find_by_instance_username_name(
220 request.owner.instance.url.as_str(),
221 request.owner.username.as_str(),
222 request.name.as_str(),
223 )
224 .await
225 {
226 let err = GitBackendError::RepositoryAlreadyExists {
227 instance_url: request.owner.instance.url.clone(),
228 username: request.owner.instance.url.clone(),
229 name: request.name.clone(),
230 };
231 error!("{:?}", err);
232
233 return Ok(CreateRepositoryResponse::Failed);
234 }
235
236 // Insert the repository into the database
237 let _ = match sqlx::query_as!(GitRepository,
238 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5, $6) RETURNING username, instance_url, name, description, visibility as "visibility: _", default_branch"#,
239 request.owner.username, request.owner.instance.url, request.name, request.description, request.visibility as _, "master")
240 .fetch_one(&self.pg_pool.clone())
241 .await {
242 Ok(repository) => repository,
243 Err(err) => {
244 let err = GitBackendError::FailedInsertingIntoDatabase(err);
245 error!("Failed inserting into the database! {:?}", err);
246
247 return Ok(CreateRepositoryResponse::Failed);
248 }
249 };
250
251 // Create bare (server side) repository on disk
252 match git2::Repository::init_bare(PathBuf::from(format!(
253 "{}/{}/{}/{}",
254 self.repository_folder,
255 request.owner.instance.url,
256 request.owner.username,
257 request.name
258 ))) {
259 Ok(_) => {
260 debug!(
261 "Created new repository with the name {}/{}/{}",
262 request.owner.instance.url, request.owner.username, request.name
263 );
264 Ok(CreateRepositoryResponse::Created)
265 }
266 Err(err) => {
267 let err = GitBackendError::FailedCreatingRepository(err);
268 error!("Failed creating repository on disk!? {:?}", err);
269
270 // Delete repository from database
271 if let Err(err) = self
272 .delete_by_instance_username_name(
273 request.owner.instance.url.as_str(),
274 request.owner.username.as_str(),
275 request.name.as_str(),
276 )
277 .await
278 {
279 return Err(Box::new(err));
280 }
281
282 // ???
283 Ok(CreateRepositoryResponse::Failed)
284 //Err(Box::new(err))
285 }
286 }
287 }
288
289 async fn repository_info(
290 &mut self,
291 request: &RepositoryInfoRequest,
292 ) -> Result<RepositoryView, Box<dyn Error + Send>> {
293 let repository = match self
294 .find_by_instance_username_name(
295 &request.owner.instance.url,
296 &request.owner.username,
297 &request.name,
298 )
299 .await
300 {
301 Ok(repository) => repository,
302 Err(err) => return Err(Box::new(err)),
303 };
304
305 if !repository.can_user_view_repository(
306 request.owner.instance.url.as_str(),
307 Some(request.owner.username.as_str()),
308 ) {
309 return Err(Box::new(GitBackendError::RepositoryNotFound {
310 instance_url: request.owner.instance.url.clone(),
311 username: request.owner.username.clone(),
312 name: request.name.clone(),
313 }));
314 }
315
316 let git = match repository.open_git2_repository(&self.repository_folder) {
317 Ok(git) => git,
318 Err(err) => return Err(Box::new(err)),
319 };
320
321 let rev_name = match &request.rev {
322 None => {
323 if let Ok(head) = git.head() {
324 head.name().unwrap().to_string()
325 } else {
326 // Nothing in database, render empty tree.
327 return Ok(RepositoryView {
328 name: repository.name,
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)),
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)),
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)),
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 description: repository.description,
430 visibility: repository.visibility,
431 default_branch: repository.default_branch,
432 latest_commit: None,
433 tree_rev: Some(rev_name),
434 tree,
435 })
436 }
437
438 fn repository_file_inspect(
439 &mut self,
440 _request: &RepositoryFileInspectRequest,
441 ) -> Result<RepositoryFileInspectionResponse, Box<dyn Error + Send>> {
442 todo!()
443 }
444 }
445
446 impl IssuesBackend for GitBackend {
447 fn issues_count(
448 &mut self,
449 _request: &RepositoryIssuesCountRequest,
450 ) -> Result<RepositoryIssuesCountResponse, Box<dyn Error + Send>> {
451 todo!()
452 }
453
454 fn issue_labels(
455 &mut self,
456 _request: &RepositoryIssueLabelsRequest,
457 ) -> Result<RepositoryIssueLabelsResponse, Box<dyn Error + Send>> {
458 todo!()
459 }
460
461 fn issues(
462 &mut self,
463 _request: &RepositoryIssuesRequest,
464 ) -> Result<RepositoryIssuesResponse, Box<dyn Error + Send>> {
465 todo!()
466 }
467 }
468