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

ambee/giterated

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

Return last commit in repository_info

Type: Feature

emilia - ⁨2⁩ years ago

parent: tbd commit: ⁨f18ea18

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