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

ambee/giterated

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

Move towards having GitBackend split into files

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨e55da0e

⁨giterated-daemon/src/backend/git/file.rs⁩ - ⁨11481⁩ bytes
Raw
1 use std::path::{Path, PathBuf};
2
3 use anyhow::Error;
4 use giterated_models::{
5 object::Object,
6 repository::{
7 Commit, DefaultBranch, Repository, RepositoryFile, RepositoryFileFromIdRequest,
8 RepositoryFileFromPathRequest, RepositoryFileInspectRequest,
9 RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryTreeEntry,
10 },
11 };
12 use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState};
13
14 use crate::backend::git::GitBackendError;
15
16 use super::GitBackend;
17
18 impl GitBackend {
19 // TODO: Cache this and general repository tree and invalidate select files on push
20 // TODO: Find better and faster technique for this
21 /// Gets the last commit made to a file by revwalking starting at the passed commit and sorting by time
22 pub fn get_last_commit_of_file(
23 path: &str,
24 git: &git2::Repository,
25 start_commit: &git2::Commit,
26 ) -> anyhow::Result<Commit> {
27 trace!("Getting last commit for file: {}", path);
28
29 let mut revwalk = git.revwalk()?;
30 revwalk.set_sorting(git2::Sort::TIME)?;
31 revwalk.push(start_commit.id())?;
32
33 for oid in revwalk {
34 let oid = oid?;
35 let commit = git.find_commit(oid)?;
36
37 // Merge commits have 2 or more parents
38 // Commits with 0 parents are handled different because we can't diff against them
39 if commit.parent_count() == 0 {
40 return Ok(commit.into());
41 } else if commit.parent_count() == 1 {
42 let tree = commit.tree()?;
43 let last_tree = commit.parent(0)?.tree()?;
44
45 // Get the diff between the current tree and the last one
46 let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?;
47
48 for dd in diff.deltas() {
49 // Get the path of the current file we're diffing against
50 let current_path = dd.new_file().path().unwrap();
51
52 // Path or directory
53 if current_path.eq(Path::new(&path)) || current_path.starts_with(path) {
54 return Ok(commit.into());
55 }
56 }
57 }
58 }
59
60 Err(GitBackendError::LastCommitNotFound(path.to_string()))?
61 }
62
63 /// If the OID can't be found because there's no repository head, this will return an empty `Vec`.
64 pub async fn handle_repository_file_inspect(
65 &mut self,
66 requester: &Option<AuthenticatedUser>,
67 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
68 OperationState(operation_state): OperationState<StackOperationState>,
69 request: &RepositoryFileInspectRequest,
70 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
71 let repository = repository_object.object();
72 let git = self
73 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
74 .await?;
75
76 let default_branch = repository_object
77 .get::<DefaultBranch>(&operation_state)
78 .await?;
79 // Try and find the tree_id/branch
80 let tree_id =
81 match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) {
82 Ok(oid) => oid,
83 Err(GitBackendError::HeadNotFound) => return Ok(vec![]),
84 Err(err) => return Err(err.into()),
85 };
86
87 // Get the commit from the oid
88 let commit = match git.find_commit(tree_id) {
89 Ok(commit) => commit,
90 // If the commit isn't found, it's generally safe to assume the tree is empty.
91 Err(_) => return Ok(vec![]),
92 };
93
94 // this is stupid
95 let rev = request.rev.clone().unwrap_or_else(|| "master".to_string());
96 let mut current_path = rev.clone();
97
98 // Get the commit tree
99 let git_tree = if let Some(path) = &request.path {
100 // Add it to our full path string
101 current_path.push_str(format!("/{}", path).as_str());
102 // Get the specified path, return an error if it wasn't found.
103 let entry = match commit
104 .tree()
105 .unwrap()
106 .get_path(&PathBuf::from(path))
107 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
108 {
109 Ok(entry) => entry,
110 Err(err) => return Err(Box::new(err).into()),
111 };
112 // Turn the entry into a git tree
113 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
114 } else {
115 commit.tree().unwrap()
116 };
117
118 // Iterate over the git tree and collect it into our own tree types
119 let mut tree = git_tree
120 .iter()
121 .map(|entry| {
122 let object_type = match entry.kind().unwrap() {
123 git2::ObjectType::Tree => RepositoryObjectType::Tree,
124 git2::ObjectType::Blob => RepositoryObjectType::Blob,
125 _ => unreachable!(),
126 };
127 let mut tree_entry = RepositoryTreeEntry::new(
128 entry.id().to_string().as_str(),
129 entry.name().unwrap(),
130 object_type,
131 entry.filemode(),
132 );
133
134 if request.extra_metadata {
135 // Get the file size if It's a blob
136 let object = entry.to_object(&git).unwrap();
137 if let Some(blob) = object.as_blob() {
138 tree_entry.size = Some(blob.size());
139 }
140
141 // Get the path to the folder the file is in by removing the rev from current_path
142 let mut path = current_path.replace(&rev, "");
143 if path.starts_with('/') {
144 path.remove(0);
145 }
146
147 // Format it as the path + file name
148 let full_path = if path.is_empty() {
149 entry.name().unwrap().to_string()
150 } else {
151 format!("{}/{}", path, entry.name().unwrap())
152 };
153
154 // Get the last commit made to the entry
155 if let Ok(last_commit) =
156 GitBackend::get_last_commit_of_file(&full_path, &git, &commit)
157 {
158 tree_entry.last_commit = Some(last_commit);
159 }
160 }
161
162 tree_entry
163 })
164 .collect::<Vec<RepositoryTreeEntry>>();
165
166 // Sort the tree alphabetically and with tree first
167 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
168 tree.sort_unstable_by_key(|entry| {
169 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
170 });
171
172 Ok(tree)
173 }
174
175 pub async fn handle_repository_file_from_id(
176 &mut self,
177 requester: &Option<AuthenticatedUser>,
178 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
179 OperationState(_operation_state): OperationState<StackOperationState>,
180 request: &RepositoryFileFromIdRequest,
181 ) -> Result<RepositoryFile, Error> {
182 let repository = repository_object.object();
183 let git = self
184 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
185 .await?;
186
187 // Parse the passed object id
188 let oid = match git2::Oid::from_str(request.0.as_str()) {
189 Ok(oid) => oid,
190 Err(_) => {
191 return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into())
192 }
193 };
194
195 // Find the file and turn it into our own struct
196 let file = match git.find_blob(oid) {
197 Ok(blob) => RepositoryFile {
198 id: blob.id().to_string(),
199 content: blob.content().to_vec(),
200 binary: blob.is_binary(),
201 size: blob.size(),
202 },
203 Err(_) => return Err(Box::new(GitBackendError::BlobNotFound(oid.to_string())).into()),
204 };
205
206 Ok(file)
207 }
208
209 pub async fn handle_repository_file_from_path(
210 &mut self,
211 requester: &Option<AuthenticatedUser>,
212 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
213 OperationState(operation_state): OperationState<StackOperationState>,
214 request: &RepositoryFileFromPathRequest,
215 ) -> Result<(RepositoryFile, String), Error> {
216 let repository = repository_object.object();
217 let git = self
218 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
219 .await?;
220
221 let default_branch = repository_object
222 .get::<DefaultBranch>(&operation_state)
223 .await?;
224 let tree_id = Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch)?;
225
226 // unwrap might be dangerous?
227 // Get the commit from the oid
228 let commit = git.find_commit(tree_id).unwrap();
229
230 // this is stupid
231 let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string());
232
233 // Add it to our full path string
234 current_path.push_str(format!("/{}", request.path).as_str());
235 // Get the specified path, return an error if it wasn't found.
236 let entry = match commit
237 .tree()
238 .unwrap()
239 .get_path(&PathBuf::from(request.path.clone()))
240 .map_err(|_| GitBackendError::PathNotFound(request.path.to_string()))
241 {
242 Ok(entry) => entry,
243 Err(err) => return Err(Box::new(err).into()),
244 };
245
246 // Find the file and turn it into our own struct
247 let file = match git.find_blob(entry.id()) {
248 Ok(blob) => RepositoryFile {
249 id: blob.id().to_string(),
250 content: blob.content().to_vec(),
251 binary: blob.is_binary(),
252 size: blob.size(),
253 },
254 Err(_) => {
255 return Err(Box::new(GitBackendError::BlobNotFound(entry.id().to_string())).into())
256 }
257 };
258
259 Ok((file, commit.id().to_string()))
260 }
261
262 pub async fn handle_repository_last_commit_of_file(
263 &mut self,
264 requester: &Option<AuthenticatedUser>,
265 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
266 OperationState(_operation_state): OperationState<StackOperationState>,
267 request: &RepositoryLastCommitOfFileRequest,
268 ) -> Result<Commit, Error> {
269 let repository = repository_object.object();
270 let git = self
271 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
272 .await?;
273
274 // Parse the passed object ids
275 let oid = git2::Oid::from_str(&request.start_commit)
276 .map_err(|_| GitBackendError::InvalidObjectId(request.start_commit.clone()))?;
277
278 // Get the commit from the oid
279 let commit = git
280 .find_commit(oid)
281 .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?;
282
283 // Find the last commit of the file
284 let commit = GitBackend::get_last_commit_of_file(request.path.as_str(), &git, &commit)?;
285
286 Ok(commit)
287 }
288 }
289