Move towards having GitBackend split into files
parent: tbd commit: e55da0e
Showing 7 changed files with 1652 insertions and 1402 deletions
giterated-daemon/src/backend/git/branches.rs
@@ -0,0 +1,203 @@ | ||
1 | use anyhow::Error; | |
2 | use git2::BranchType; | |
3 | use giterated_models::{ | |
4 | object::Object, | |
5 | repository::{ | |
6 | BranchStaleAfter, DefaultBranch, Repository, RepositoryBranch, RepositoryBranchFilter, | |
7 | RepositoryBranchRequest, RepositoryBranchesRequest, | |
8 | }, | |
9 | }; | |
10 | use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; | |
11 | ||
12 | use super::{GitBackend, GitBackendError}; | |
13 | ||
14 | impl GitBackend { | |
15 | /// .0: List of branches filtering by passed requirements. | |
16 | /// .1: Total amount of branches after being filtered | |
17 | pub async fn handle_repository_get_branches( | |
18 | &mut self, | |
19 | requester: &Option<AuthenticatedUser>, | |
20 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
21 | OperationState(operation_state): OperationState<StackOperationState>, | |
22 | request: &RepositoryBranchesRequest, | |
23 | ) -> Result<(Vec<RepositoryBranch>, usize), Error> { | |
24 | let repository = repository_object.object(); | |
25 | let git = self | |
26 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
27 | .await?; | |
28 | ||
29 | let default_branch_name = repository_object | |
30 | .get::<DefaultBranch>(&operation_state) | |
31 | .await?; | |
32 | let default_branch = git | |
33 | .find_branch(&default_branch_name.0, BranchType::Local) | |
34 | .map_err(|_| GitBackendError::DefaultNotFound)?; | |
35 | ||
36 | // Get the stale(after X seconds) setting | |
37 | let stale_after = repository_object | |
38 | .get::<BranchStaleAfter>(&operation_state) | |
39 | .await | |
40 | .unwrap_or_default() | |
41 | .0; | |
42 | ||
43 | // Could be done better with the RepositoryBranchFilter::None check done beforehand. | |
44 | let mut filtered_branches = git | |
45 | .branches(None)? | |
46 | .filter_map(|branch| { | |
47 | let branch = branch.ok()?.0; | |
48 | ||
49 | let Some(name) = branch.name().ok().flatten() else { | |
50 | return None; | |
51 | }; | |
52 | ||
53 | // TODO: Non UTF-8? | |
54 | let Some(commit) = GitBackend::get_last_commit_in_rev( | |
55 | &git, | |
56 | branch.get().name().unwrap(), | |
57 | &default_branch_name, | |
58 | ) | |
59 | .ok() else { | |
60 | return None; | |
61 | }; | |
62 | ||
63 | let stale = chrono::Utc::now() | |
64 | .naive_utc() | |
65 | .signed_duration_since(commit.time) | |
66 | .num_seconds() | |
67 | > stale_after.into(); | |
68 | ||
69 | // Filter based on if the branch is stale or not | |
70 | if request.filter != RepositoryBranchFilter::None { | |
71 | #[allow(clippy::if_same_then_else)] | |
72 | if stale && request.filter == RepositoryBranchFilter::Active { | |
73 | return None; | |
74 | } else if !stale && request.filter == RepositoryBranchFilter::Stale { | |
75 | return None; | |
76 | } | |
77 | } | |
78 | ||
79 | Some((name.to_string(), branch, stale, commit)) | |
80 | }) | |
81 | .collect::<Vec<_>>(); | |
82 | ||
83 | // Get the total amount of filtered branches | |
84 | let branch_count = filtered_branches.len(); | |
85 | ||
86 | if let Some(search) = &request.search { | |
87 | // TODO: Caching | |
88 | // Search by sorting using a simple fuzzy search algorithm | |
89 | filtered_branches.sort_by(|(n1, _, _, _), (n2, _, _, _)| { | |
90 | strsim::damerau_levenshtein(search, n1) | |
91 | .cmp(&strsim::damerau_levenshtein(search, n2)) | |
92 | }); | |
93 | } else { | |
94 | // Sort the branches by commit date | |
95 | filtered_branches.sort_by(|(_, _, _, c1), (_, _, _, c2)| c2.time.cmp(&c1.time)); | |
96 | } | |
97 | ||
98 | // Go to the requested position | |
99 | let mut filtered_branches = filtered_branches.iter().skip(request.range.0); | |
100 | ||
101 | let mut branches = vec![]; | |
102 | ||
103 | // Iterate through the filtered branches using the passed range | |
104 | for _ in request.range.0..request.range.1 { | |
105 | let Some((name, branch, stale, commit)) = filtered_branches.next() else { | |
106 | break; | |
107 | }; | |
108 | ||
109 | // Get how many commits are ahead of and behind of the head | |
110 | let ahead_behind_default = | |
111 | if default_branch.get().target().is_some() && branch.get().target().is_some() { | |
112 | git.graph_ahead_behind( | |
113 | branch.get().target().unwrap(), | |
114 | default_branch.get().target().unwrap(), | |
115 | ) | |
116 | .ok() | |
117 | } else { | |
118 | None | |
119 | }; | |
120 | ||
121 | branches.push(RepositoryBranch { | |
122 | name: name.to_string(), | |
123 | stale: *stale, | |
124 | last_commit: Some(commit.clone()), | |
125 | ahead_behind_default, | |
126 | }) | |
127 | } | |
128 | ||
129 | Ok((branches, branch_count)) | |
130 | } | |
131 | ||
132 | pub async fn handle_repository_get_branch( | |
133 | &mut self, | |
134 | requester: &Option<AuthenticatedUser>, | |
135 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
136 | OperationState(operation_state): OperationState<StackOperationState>, | |
137 | request: &RepositoryBranchRequest, | |
138 | ) -> Result<RepositoryBranch, Error> { | |
139 | let repository = repository_object.object(); | |
140 | let git = self | |
141 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
142 | .await?; | |
143 | ||
144 | // TODO: Don't duplicate search when the default branch and the requested one are the same | |
145 | // Get the default branch to compare against | |
146 | let default_branch_name = repository_object | |
147 | .get::<DefaultBranch>(&operation_state) | |
148 | .await?; | |
149 | let default_branch = git | |
150 | .find_branch(&default_branch_name.0, BranchType::Local) | |
151 | .map_err(|_| GitBackendError::DefaultNotFound)?; | |
152 | ||
153 | // Find the requested branch | |
154 | let branch = git | |
155 | .find_branch(&request.name, BranchType::Local) | |
156 | .map_err(|_| GitBackendError::BranchNotFound(request.name.clone()))?; | |
157 | ||
158 | // Get the stale(after X seconds) setting | |
159 | let stale_after = repository_object | |
160 | .get::<BranchStaleAfter>(&operation_state) | |
161 | .await | |
162 | .unwrap_or_default() | |
163 | .0; | |
164 | ||
165 | // TODO: Non UTF-8? | |
166 | let last_commit = GitBackend::get_last_commit_in_rev( | |
167 | &git, | |
168 | branch.get().name().unwrap(), | |
169 | &default_branch_name, | |
170 | ) | |
171 | .ok(); | |
172 | ||
173 | let stale = if let Some(ref last_commit) = last_commit { | |
174 | chrono::Utc::now() | |
175 | .naive_utc() | |
176 | .signed_duration_since(last_commit.time) | |
177 | .num_seconds() | |
178 | > stale_after.into() | |
179 | } else { | |
180 | // TODO: Make sure it's acceptable to return false here | |
181 | false | |
182 | }; | |
183 | ||
184 | // Get how many commits are ahead of and behind of the head | |
185 | let ahead_behind_default = | |
186 | if default_branch.get().target().is_some() && branch.get().target().is_some() { | |
187 | git.graph_ahead_behind( | |
188 | branch.get().target().unwrap(), | |
189 | default_branch.get().target().unwrap(), | |
190 | ) | |
191 | .ok() | |
192 | } else { | |
193 | None | |
194 | }; | |
195 | ||
196 | Ok(RepositoryBranch { | |
197 | name: request.name.clone(), | |
198 | stale, | |
199 | last_commit, | |
200 | ahead_behind_default, | |
201 | }) | |
202 | } | |
203 | } |
giterated-daemon/src/backend/git/commit.rs
@@ -0,0 +1,123 @@ | ||
1 | use anyhow::Error; | |
2 | use giterated_models::{ | |
3 | object::Object, | |
4 | repository::{ | |
5 | Commit, DefaultBranch, Repository, RepositoryCommitBeforeRequest, | |
6 | RepositoryCommitFromIdRequest, | |
7 | }, | |
8 | }; | |
9 | use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; | |
10 | ||
11 | use super::{GitBackend, GitBackendError}; | |
12 | ||
13 | impl GitBackend { | |
14 | /// Gets the total amount of commits using revwalk | |
15 | pub fn get_total_commit_count( | |
16 | git: &git2::Repository, | |
17 | start_commit: &git2::Commit, | |
18 | ) -> anyhow::Result<usize> { | |
19 | // TODO: There must be a better way | |
20 | let mut revwalk = git.revwalk()?; | |
21 | revwalk.set_sorting(git2::Sort::TIME)?; | |
22 | revwalk.push(start_commit.id())?; | |
23 | ||
24 | Ok(revwalk.count()) | |
25 | } | |
26 | ||
27 | /// Gets the last commit in a rev | |
28 | pub fn get_last_commit_in_rev( | |
29 | git: &git2::Repository, | |
30 | rev: &str, | |
31 | default_branch: &DefaultBranch, | |
32 | ) -> anyhow::Result<Commit> { | |
33 | let oid = Self::get_oid_from_reference(git, Some(rev), default_branch)?; | |
34 | ||
35 | // Walk through the repository commit graph starting at our rev | |
36 | let mut revwalk = git.revwalk()?; | |
37 | revwalk.set_sorting(git2::Sort::TIME)?; | |
38 | revwalk.push(oid)?; | |
39 | ||
40 | if let Some(Ok(commit_oid)) = revwalk.next() { | |
41 | if let Ok(commit) = git | |
42 | .find_commit(commit_oid) | |
43 | .map_err(|_| GitBackendError::CommitNotFound(commit_oid.to_string())) | |
44 | { | |
45 | return Ok(Commit::from(commit)); | |
46 | } | |
47 | } | |
48 | ||
49 | Err(GitBackendError::RefNotFound(oid.to_string()).into()) | |
50 | } | |
51 | ||
52 | pub async fn handle_repository_commit_from_id( | |
53 | &mut self, | |
54 | requester: &Option<AuthenticatedUser>, | |
55 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
56 | OperationState(_operation_state): OperationState<StackOperationState>, | |
57 | request: &RepositoryCommitFromIdRequest, | |
58 | ) -> Result<Commit, Error> { | |
59 | let repository = repository_object.object(); | |
60 | let git = self | |
61 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
62 | .await?; | |
63 | ||
64 | // Parse the passed object ids | |
65 | let oid = git2::Oid::from_str(request.0.as_str()) | |
66 | .map_err(|_| GitBackendError::InvalidObjectId(request.0.clone()))?; | |
67 | ||
68 | // Get the commit from the oid | |
69 | let commit = git | |
70 | .find_commit(oid) | |
71 | .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?; | |
72 | ||
73 | Ok(Commit::from(commit)) | |
74 | } | |
75 | ||
76 | pub async fn handle_repository_commit_before( | |
77 | &mut self, | |
78 | requester: &Option<AuthenticatedUser>, | |
79 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
80 | OperationState(_operation_state): OperationState<StackOperationState>, | |
81 | request: &RepositoryCommitBeforeRequest, | |
82 | ) -> Result<Commit, Error> { | |
83 | let repository = repository_object.object(); | |
84 | let git = self | |
85 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
86 | .await?; | |
87 | ||
88 | // Parse the passed object id | |
89 | let oid = match git2::Oid::from_str(request.0.as_str()) { | |
90 | Ok(oid) => oid, | |
91 | Err(_) => { | |
92 | return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()) | |
93 | } | |
94 | }; | |
95 | ||
96 | // Find the commit using the parsed oid | |
97 | let commit = match git.find_commit(oid) { | |
98 | Ok(commit) => commit, | |
99 | Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()), | |
100 | }; | |
101 | ||
102 | // Get the first parent it has | |
103 | let parent = commit.parent(0); | |
104 | if let Ok(parent) = parent { | |
105 | return Ok(Commit::from(parent)); | |
106 | } else { | |
107 | // TODO: See if can be done better | |
108 | // Walk through the repository commit graph starting at our current commit | |
109 | let mut revwalk = git.revwalk()?; | |
110 | revwalk.set_sorting(git2::Sort::TIME)?; | |
111 | revwalk.push(commit.id())?; | |
112 | ||
113 | if let Some(Ok(before_commit_oid)) = revwalk.next() { | |
114 | // Find the commit using the parsed oid | |
115 | if let Ok(before_commit) = git.find_commit(before_commit_oid) { | |
116 | return Ok(Commit::from(before_commit)); | |
117 | } | |
118 | } | |
119 | ||
120 | Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into()) | |
121 | } | |
122 | } | |
123 | } |
giterated-daemon/src/backend/git/diff.rs
@@ -0,0 +1,202 @@ | ||
1 | use anyhow::Error; | |
2 | use giterated_models::{ | |
3 | object::Object, | |
4 | repository::{ | |
5 | Commit, Repository, RepositoryChunkLine, RepositoryDiff, RepositoryDiffFile, | |
6 | RepositoryDiffFileChunk, RepositoryDiffFileInfo, RepositoryDiffFileStatus, | |
7 | RepositoryDiffPatchRequest, RepositoryDiffRequest, | |
8 | }, | |
9 | }; | |
10 | use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; | |
11 | ||
12 | use super::{GitBackend, GitBackendError}; | |
13 | ||
14 | impl GitBackend { | |
15 | pub async fn handle_repository_diff( | |
16 | &mut self, | |
17 | requester: &Option<AuthenticatedUser>, | |
18 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
19 | OperationState(_operation_state): OperationState<StackOperationState>, | |
20 | request: &RepositoryDiffRequest, | |
21 | ) -> Result<RepositoryDiff, Error> { | |
22 | let repository = repository_object.object(); | |
23 | let git = self | |
24 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
25 | .await?; | |
26 | ||
27 | // Parse the passed object ids | |
28 | let oid_old = git2::Oid::from_str(request.old_id.as_str()) | |
29 | .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; | |
30 | let oid_new = git2::Oid::from_str(request.new_id.as_str()) | |
31 | .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; | |
32 | ||
33 | // Get the ids associates commits | |
34 | let commit_old = git | |
35 | .find_commit(oid_old) | |
36 | .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; | |
37 | let commit_new = git | |
38 | .find_commit(oid_new) | |
39 | .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; | |
40 | ||
41 | // Get the commit trees | |
42 | let tree_old = commit_old | |
43 | .tree() | |
44 | .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; | |
45 | let tree_new = commit_new | |
46 | .tree() | |
47 | .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; | |
48 | ||
49 | // Diff the two trees against each other | |
50 | let diff = git | |
51 | .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) | |
52 | .map_err(|_| { | |
53 | GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) | |
54 | })?; | |
55 | ||
56 | // Should be safe to unwrap? | |
57 | let stats = diff.stats().unwrap(); | |
58 | let mut files: Vec<RepositoryDiffFile> = vec![]; | |
59 | ||
60 | diff.deltas().enumerate().for_each(|(i, delta)| { | |
61 | // Parse the old file info from the delta | |
62 | let old_file_info = match delta.old_file().exists() { | |
63 | true => Some(RepositoryDiffFileInfo { | |
64 | id: delta.old_file().id().to_string(), | |
65 | path: delta | |
66 | .old_file() | |
67 | .path() | |
68 | .unwrap() | |
69 | .to_str() | |
70 | .unwrap() | |
71 | .to_string(), | |
72 | size: delta.old_file().size(), | |
73 | binary: delta.old_file().is_binary(), | |
74 | }), | |
75 | false => None, | |
76 | }; | |
77 | // Parse the new file info from the delta | |
78 | let new_file_info = match delta.new_file().exists() { | |
79 | true => Some(RepositoryDiffFileInfo { | |
80 | id: delta.new_file().id().to_string(), | |
81 | path: delta | |
82 | .new_file() | |
83 | .path() | |
84 | .unwrap() | |
85 | .to_str() | |
86 | .unwrap() | |
87 | .to_string(), | |
88 | size: delta.new_file().size(), | |
89 | binary: delta.new_file().is_binary(), | |
90 | }), | |
91 | false => None, | |
92 | }; | |
93 | ||
94 | let mut chunks: Vec<RepositoryDiffFileChunk> = vec![]; | |
95 | if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() { | |
96 | for chunk_num in 0..patch.num_hunks() { | |
97 | if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) { | |
98 | let mut lines: Vec<RepositoryChunkLine> = vec![]; | |
99 | ||
100 | for line_num in 0..chunk_num_lines { | |
101 | if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) { | |
102 | if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) { | |
103 | lines.push(RepositoryChunkLine { | |
104 | change_type: line.origin_value().into(), | |
105 | content: line_utf8, | |
106 | old_line_num: line.old_lineno(), | |
107 | new_line_num: line.new_lineno(), | |
108 | }); | |
109 | } | |
110 | ||
111 | continue; | |
112 | } | |
113 | } | |
114 | ||
115 | chunks.push(RepositoryDiffFileChunk { | |
116 | header: String::from_utf8(chunk.header().to_vec()).ok(), | |
117 | old_start: chunk.old_start(), | |
118 | old_lines: chunk.old_lines(), | |
119 | new_start: chunk.new_start(), | |
120 | new_lines: chunk.new_lines(), | |
121 | lines, | |
122 | }); | |
123 | } | |
124 | } | |
125 | }; | |
126 | ||
127 | let file = RepositoryDiffFile { | |
128 | status: RepositoryDiffFileStatus::from(delta.status()), | |
129 | old_file_info, | |
130 | new_file_info, | |
131 | chunks, | |
132 | }; | |
133 | ||
134 | files.push(file); | |
135 | }); | |
136 | ||
137 | Ok(RepositoryDiff { | |
138 | new_commit: Commit::from(commit_new), | |
139 | files_changed: stats.files_changed(), | |
140 | insertions: stats.insertions(), | |
141 | deletions: stats.deletions(), | |
142 | files, | |
143 | }) | |
144 | } | |
145 | ||
146 | pub async fn handle_repository_diff_patch( | |
147 | &mut self, | |
148 | requester: &Option<AuthenticatedUser>, | |
149 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
150 | OperationState(_operation_state): OperationState<StackOperationState>, | |
151 | request: &RepositoryDiffPatchRequest, | |
152 | ) -> Result<String, Error> { | |
153 | let repository = repository_object.object(); | |
154 | let git = self | |
155 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
156 | .await?; | |
157 | ||
158 | // Parse the passed object ids | |
159 | let oid_old = git2::Oid::from_str(request.old_id.as_str()) | |
160 | .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; | |
161 | let oid_new = git2::Oid::from_str(request.new_id.as_str()) | |
162 | .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; | |
163 | ||
164 | // Get the ids associates commits | |
165 | let commit_old = git | |
166 | .find_commit(oid_old) | |
167 | .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; | |
168 | let commit_new = git | |
169 | .find_commit(oid_new) | |
170 | .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; | |
171 | ||
172 | // Get the commit trees | |
173 | let tree_old = commit_old | |
174 | .tree() | |
175 | .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; | |
176 | let tree_new = commit_new | |
177 | .tree() | |
178 | .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; | |
179 | ||
180 | // Diff the two trees against each other | |
181 | let diff = git | |
182 | .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) | |
183 | .map_err(|_| { | |
184 | GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) | |
185 | })?; | |
186 | ||
187 | // Print the entire patch | |
188 | let mut patch = String::new(); | |
189 | ||
190 | diff.print(git2::DiffFormat::Patch, |_, _, line| { | |
191 | match line.origin() { | |
192 | '+' | '-' | ' ' => patch.push(line.origin()), | |
193 | _ => {} | |
194 | } | |
195 | patch.push_str(std::str::from_utf8(line.content()).unwrap()); | |
196 | true | |
197 | }) | |
198 | .unwrap(); | |
199 | ||
200 | Ok(patch) | |
201 | } | |
202 | } |
giterated-daemon/src/backend/git/file.rs
@@ -0,0 +1,288 @@ | ||
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 | } |
giterated-daemon/src/backend/git/mod.rs
@@ -0,0 +1,701 @@ | ||
1 | use anyhow::Error; | |
2 | use async_trait::async_trait; | |
3 | ||
4 | use git2::BranchType; | |
5 | use giterated_models::instance::{Instance, RepositoryCreateRequest}; | |
6 | ||
7 | use giterated_models::object::Object; | |
8 | use giterated_models::repository::{ | |
9 | AccessList, Commit, DefaultBranch, Description, IssueLabel, Repository, RepositoryBranch, | |
10 | RepositoryBranchRequest, RepositoryBranchesRequest, RepositoryCommitBeforeRequest, | |
11 | RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffPatchRequest, | |
12 | RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, | |
13 | RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, | |
14 | RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, | |
15 | RepositoryLastCommitOfFileRequest, RepositoryStatistics, RepositoryStatisticsRequest, | |
16 | RepositoryTag, RepositoryTagsRequest, RepositoryTreeEntry, RepositoryVisibility, Visibility, | |
17 | }; | |
18 | ||
19 | use giterated_models::user::User; | |
20 | ||
21 | use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; | |
22 | ||
23 | use sqlx::PgPool; | |
24 | use std::ops::Deref; | |
25 | use std::{path::PathBuf, sync::Arc}; | |
26 | use thiserror::Error; | |
27 | use tokio::sync::OnceCell; | |
28 | ||
29 | pub mod branches; | |
30 | pub mod commit; | |
31 | pub mod diff; | |
32 | pub mod file; | |
33 | pub mod tags; | |
34 | ||
35 | use super::{IssuesBackend, RepositoryBackend}; | |
36 | ||
37 | //region database structures | |
38 | ||
39 | /// Repository in the database | |
40 | #[derive(Debug, sqlx::FromRow)] | |
41 | pub struct GitRepository { | |
42 | #[sqlx(try_from = "String")] | |
43 | pub owner_user: User, | |
44 | pub name: String, | |
45 | pub description: Option<String>, | |
46 | pub visibility: RepositoryVisibility, | |
47 | pub default_branch: String, | |
48 | } | |
49 | ||
50 | impl GitRepository { | |
51 | // Separate function because "Private" will be expanded later | |
52 | /// Checks if the user is allowed to view this repository | |
53 | pub async fn can_user_view_repository( | |
54 | &self, | |
55 | our_instance: &Instance, | |
56 | user: &Option<AuthenticatedUser>, | |
57 | stack: &GiteratedStack, | |
58 | ) -> bool { | |
59 | if matches!(self.visibility, RepositoryVisibility::Public) { | |
60 | return true; | |
61 | } | |
62 | ||
63 | // User must exist for any further checks to pass | |
64 | let user = match user { | |
65 | Some(user) => user, | |
66 | None => return false, | |
67 | }; | |
68 | ||
69 | if *user.deref() == self.owner_user { | |
70 | // owner can always view | |
71 | return true; | |
72 | } | |
73 | ||
74 | if matches!(self.visibility, RepositoryVisibility::Private) { | |
75 | // Check if the user can view\ | |
76 | let access_list = stack | |
77 | .new_get_setting::<_, AccessList>(&Repository { | |
78 | owner: self.owner_user.clone(), | |
79 | name: self.name.clone(), | |
80 | instance: our_instance.clone(), | |
81 | }) | |
82 | .await | |
83 | .unwrap(); | |
84 | ||
85 | access_list | |
86 | .0 | |
87 | .iter() | |
88 | .any(|access_list_user| access_list_user == user.deref()) | |
89 | } else { | |
90 | false | |
91 | } | |
92 | } | |
93 | ||
94 | // This is in it's own function because I assume I'll have to add logic to this later | |
95 | pub fn open_git2_repository( | |
96 | &self, | |
97 | repository_directory: &str, | |
98 | ) -> Result<git2::Repository, GitBackendError> { | |
99 | match git2::Repository::open(format!( | |
100 | "{}/{}/{}/{}", | |
101 | repository_directory, self.owner_user.instance, self.owner_user.username, self.name | |
102 | )) { | |
103 | Ok(repository) => Ok(repository), | |
104 | Err(err) => { | |
105 | let err = GitBackendError::FailedOpeningFromDisk(err); | |
106 | error!("Couldn't open a repository, this is bad! {:?}", err); | |
107 | ||
108 | Err(err) | |
109 | } | |
110 | } | |
111 | } | |
112 | } | |
113 | ||
114 | //endregion | |
115 | ||
116 | #[derive(Error, Debug)] | |
117 | pub enum GitBackendError { | |
118 | #[error("Failed creating repository")] | |
119 | FailedCreatingRepository(git2::Error), | |
120 | #[error("Failed inserting into the database")] | |
121 | FailedInsertingIntoDatabase(sqlx::Error), | |
122 | #[error("Failed finding repository {owner_user:?}/{name:?}")] | |
123 | RepositoryNotFound { owner_user: String, name: String }, | |
124 | #[error("Repository {owner_user:?}/{name:?} already exists")] | |
125 | RepositoryAlreadyExists { owner_user: String, name: String }, | |
126 | #[error("Repository couldn't be deleted from the disk")] | |
127 | CouldNotDeleteFromDisk(std::io::Error), | |
128 | #[error("Failed deleting repository from database")] | |
129 | FailedDeletingFromDatabase(sqlx::Error), | |
130 | #[error("Failed opening repository on disk")] | |
131 | FailedOpeningFromDisk(git2::Error), | |
132 | #[error("Couldn't find ref with name `{0}`")] | |
133 | RefNotFound(String), | |
134 | #[error("Couldn't find repository head")] | |
135 | HeadNotFound, | |
136 | #[error("Couldn't find default repository branch")] | |
137 | DefaultNotFound, | |
138 | #[error("Couldn't find path in repository `{0}`")] | |
139 | PathNotFound(String), | |
140 | #[error("Couldn't find branch with name `{0}`")] | |
141 | BranchNotFound(String), | |
142 | #[error("Couldn't find commit for path `{0}`")] | |
143 | LastCommitNotFound(String), | |
144 | #[error("Object ID `{0}` is invalid")] | |
145 | InvalidObjectId(String), | |
146 | #[error("Blob with ID `{0}` not found")] | |
147 | BlobNotFound(String), | |
148 | #[error("Tree with ID `{0}` not found")] | |
149 | TreeNotFound(String), | |
150 | #[error("Commit with ID `{0}` not found")] | |
151 | CommitNotFound(String), | |
152 | #[error("Parent for commit with ID `{0}` not found")] | |
153 | CommitParentNotFound(String), | |
154 | #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")] | |
155 | FailedDiffing(String, String), | |
156 | } | |
157 | ||
158 | pub struct GitBackend { | |
159 | pg_pool: PgPool, | |
160 | repository_folder: String, | |
161 | instance: Instance, | |
162 | stack: Arc<OnceCell<GiteratedStack>>, | |
163 | } | |
164 | ||
165 | impl GitBackend { | |
166 | pub fn new( | |
167 | pg_pool: &PgPool, | |
168 | repository_folder: &str, | |
169 | instance: impl ToOwned<Owned = Instance>, | |
170 | stack: Arc<OnceCell<GiteratedStack>>, | |
171 | ) -> Self { | |
172 | let instance = instance.to_owned(); | |
173 | ||
174 | Self { | |
175 | pg_pool: pg_pool.clone(), | |
176 | // We make sure there's no end slash | |
177 | repository_folder: repository_folder.trim_end_matches(&['/', '\\']).to_string(), | |
178 | instance, | |
179 | stack, | |
180 | } | |
181 | } | |
182 | ||
183 | pub async fn find_by_owner_user_name( | |
184 | &self, | |
185 | user: &User, | |
186 | repository_name: &str, | |
187 | ) -> Result<GitRepository, GitBackendError> { | |
188 | // TODO: Patch for use with new GetValue system | |
189 | if let Ok(repository) = sqlx::query_as!(GitRepository, | |
190 | r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, | |
191 | user.to_string(), repository_name) | |
192 | .fetch_one(&self.pg_pool.clone()) | |
193 | .await { | |
194 | Ok(repository) | |
195 | } else { | |
196 | Err(GitBackendError::RepositoryNotFound { | |
197 | owner_user: user.to_string(), | |
198 | name: repository_name.to_string(), | |
199 | }) | |
200 | } | |
201 | } | |
202 | ||
203 | pub async fn delete_by_owner_user_name( | |
204 | &self, | |
205 | user: &User, | |
206 | repository_name: &str, | |
207 | ) -> Result<u64, GitBackendError> { | |
208 | if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( | |
209 | "{}/{}/{}/{}", | |
210 | self.repository_folder, user.instance, user.username, repository_name | |
211 | ))) { | |
212 | let err = GitBackendError::CouldNotDeleteFromDisk(err); | |
213 | error!( | |
214 | "Couldn't delete repository from disk, this is bad! {:?}", | |
215 | err | |
216 | ); | |
217 | ||
218 | return Err(err); | |
219 | } | |
220 | ||
221 | // Delete the repository from the database | |
222 | self.delete_from_database(user, repository_name).await | |
223 | } | |
224 | ||
225 | /// Deletes the repository from the database | |
226 | pub async fn delete_from_database( | |
227 | &self, | |
228 | user: &User, | |
229 | repository_name: &str, | |
230 | ) -> Result<u64, GitBackendError> { | |
231 | match sqlx::query!( | |
232 | "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", | |
233 | user.to_string(), | |
234 | repository_name | |
235 | ) | |
236 | .execute(&self.pg_pool.clone()) | |
237 | .await | |
238 | { | |
239 | Ok(deleted) => Ok(deleted.rows_affected()), | |
240 | Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), | |
241 | } | |
242 | } | |
243 | ||
244 | pub async fn open_repository_and_check_permissions( | |
245 | &self, | |
246 | owner: &User, | |
247 | name: &str, | |
248 | requester: &Option<AuthenticatedUser>, | |
249 | ) -> Result<git2::Repository, GitBackendError> { | |
250 | let repository = match self | |
251 | .find_by_owner_user_name( | |
252 | // &request.owner.instance.url, | |
253 | owner, name, | |
254 | ) | |
255 | .await | |
256 | { | |
257 | Ok(repository) => repository, | |
258 | Err(err) => return Err(err), | |
259 | }; | |
260 | ||
261 | if let Some(requester) = requester { | |
262 | if !repository | |
263 | .can_user_view_repository( | |
264 | &self.instance, | |
265 | &Some(requester.clone()), | |
266 | self.stack.get().unwrap(), | |
267 | ) | |
268 | .await | |
269 | { | |
270 | return Err(GitBackendError::RepositoryNotFound { | |
271 | owner_user: repository.owner_user.to_string(), | |
272 | name: repository.name.clone(), | |
273 | }); | |
274 | } | |
275 | } else if matches!(repository.visibility, RepositoryVisibility::Private) { | |
276 | // Unauthenticated users can never view private repositories | |
277 | ||
278 | return Err(GitBackendError::RepositoryNotFound { | |
279 | owner_user: repository.owner_user.to_string(), | |
280 | name: repository.name.clone(), | |
281 | }); | |
282 | } | |
283 | ||
284 | match repository.open_git2_repository(&self.repository_folder) { | |
285 | Ok(git) => Ok(git), | |
286 | Err(err) => Err(err), | |
287 | } | |
288 | } | |
289 | ||
290 | /// Attempts to get the oid in this order: | |
291 | /// 1. Full refname (refname_to_id) | |
292 | /// 2. Short branch name (find_branch) | |
293 | /// 3. Other (revparse_single) | |
294 | pub fn get_oid_from_reference( | |
295 | git: &git2::Repository, | |
296 | rev: Option<&str>, | |
297 | default_branch: &DefaultBranch, | |
298 | ) -> Result<git2::Oid, GitBackendError> { | |
299 | // If the rev is None try and get the default branch instead | |
300 | let rev = rev.unwrap_or(default_branch.0.as_str()); | |
301 | ||
302 | // TODO: This is far from ideal or speedy and would love for a better way to check this in the same order, but I can't find proper methods to do any of this. | |
303 | trace!("Attempting to get ref with name {}", rev); | |
304 | ||
305 | // Try getting it as a refname (refs/heads/name) | |
306 | if let Ok(oid) = git.refname_to_id(rev) { | |
307 | Ok(oid) | |
308 | // Try finding it as a short branch name | |
309 | } else if let Ok(branch) = git.find_branch(rev, BranchType::Local) { | |
310 | // SHOULD be safe to unwrap | |
311 | Ok(branch.get().target().unwrap()) | |
312 | // As last resort, try revparsing (will catch short oid and tags) | |
313 | } else if let Ok(object) = git.revparse_single(rev) { | |
314 | Ok(match object.kind() { | |
315 | Some(git2::ObjectType::Tag) => { | |
316 | if let Ok(commit) = object.peel_to_commit() { | |
317 | commit.id() | |
318 | } else { | |
319 | object.id() | |
320 | } | |
321 | } | |
322 | _ => object.id(), | |
323 | }) | |
324 | } else { | |
325 | Err(GitBackendError::RefNotFound(rev.to_string())) | |
326 | } | |
327 | } | |
328 | } | |
329 | ||
330 | #[async_trait(?Send)] | |
331 | impl RepositoryBackend for GitBackend { | |
332 | async fn create_repository( | |
333 | &mut self, | |
334 | _user: &AuthenticatedUser, | |
335 | request: &RepositoryCreateRequest, | |
336 | ) -> Result<Repository, GitBackendError> { | |
337 | // Check if repository already exists in the database | |
338 | if let Ok(repository) = self | |
339 | .find_by_owner_user_name(&request.owner, &request.name) | |
340 | .await | |
341 | { | |
342 | let err = GitBackendError::RepositoryAlreadyExists { | |
343 | owner_user: repository.owner_user.to_string(), | |
344 | name: repository.name, | |
345 | }; | |
346 | error!("{:?}", err); | |
347 | ||
348 | return Err(err); | |
349 | } | |
350 | ||
351 | // Insert the repository into the database | |
352 | let _ = match sqlx::query_as!(GitRepository, | |
353 | r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#, | |
354 | request.owner.to_string(), request.name, request.description, request.visibility as _, "master") | |
355 | .fetch_one(&self.pg_pool.clone()) | |
356 | .await { | |
357 | Ok(repository) => repository, | |
358 | Err(err) => { | |
359 | let err = GitBackendError::FailedInsertingIntoDatabase(err); | |
360 | error!("Failed inserting into the database! {:?}", err); | |
361 | ||
362 | return Err(err); | |
363 | } | |
364 | }; | |
365 | ||
366 | // Create bare (server side) repository on disk | |
367 | match git2::Repository::init_bare(PathBuf::from(format!( | |
368 | "{}/{}/{}/{}", | |
369 | self.repository_folder, request.owner.instance, request.owner.username, request.name | |
370 | ))) { | |
371 | Ok(_) => { | |
372 | debug!( | |
373 | "Created new repository with the name {}/{}/{}", | |
374 | request.owner.instance, request.owner.username, request.name | |
375 | ); | |
376 | ||
377 | let stack = self.stack.get().unwrap(); | |
378 | ||
379 | let repository = Repository { | |
380 | owner: request.owner.clone(), | |
381 | name: request.name.clone(), | |
382 | instance: request.instance.as_ref().unwrap_or(&self.instance).clone(), | |
383 | }; | |
384 | ||
385 | stack | |
386 | .write_setting( | |
387 | &repository, | |
388 | Description(request.description.clone().unwrap_or_default()), | |
389 | ) | |
390 | .await | |
391 | .unwrap(); | |
392 | ||
393 | stack | |
394 | .write_setting(&repository, Visibility(request.visibility.clone())) | |
395 | .await | |
396 | .unwrap(); | |
397 | ||
398 | stack | |
399 | .write_setting(&repository, DefaultBranch(request.default_branch.clone())) | |
400 | .await | |
401 | .unwrap(); | |
402 | ||
403 | Ok(repository) | |
404 | } | |
405 | Err(err) => { | |
406 | let err = GitBackendError::FailedCreatingRepository(err); | |
407 | error!("Failed creating repository on disk {:?}", err); | |
408 | ||
409 | // Delete repository from database | |
410 | self.delete_from_database(&request.owner, request.name.as_str()) | |
411 | .await?; | |
412 | ||
413 | // ??? | |
414 | Err(err) | |
415 | } | |
416 | } | |
417 | } | |
418 | ||
419 | /// If the OID can't be found because there's no repository head, this will return an empty `Vec`. | |
420 | async fn repository_file_inspect( | |
421 | &mut self, | |
422 | requester: &Option<AuthenticatedUser>, | |
423 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
424 | OperationState(operation_state): OperationState<StackOperationState>, | |
425 | request: &RepositoryFileInspectRequest, | |
426 | ) -> Result<Vec<RepositoryTreeEntry>, Error> { | |
427 | self.handle_repository_file_inspect( | |
428 | requester, | |
429 | repository_object, | |
430 | OperationState(operation_state), | |
431 | request, | |
432 | ) | |
433 | .await | |
434 | } | |
435 | ||
436 | async fn repository_file_from_id( | |
437 | &mut self, | |
438 | requester: &Option<AuthenticatedUser>, | |
439 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
440 | OperationState(operation_state): OperationState<StackOperationState>, | |
441 | request: &RepositoryFileFromIdRequest, | |
442 | ) -> Result<RepositoryFile, Error> { | |
443 | self.handle_repository_file_from_id( | |
444 | requester, | |
445 | repository_object, | |
446 | OperationState(operation_state), | |
447 | request, | |
448 | ) | |
449 | .await | |
450 | } | |
451 | ||
452 | async fn repository_file_from_path( | |
453 | &mut self, | |
454 | requester: &Option<AuthenticatedUser>, | |
455 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
456 | OperationState(operation_state): OperationState<StackOperationState>, | |
457 | request: &RepositoryFileFromPathRequest, | |
458 | ) -> Result<(RepositoryFile, String), Error> { | |
459 | self.handle_repository_file_from_path( | |
460 | requester, | |
461 | repository_object, | |
462 | OperationState(operation_state), | |
463 | request, | |
464 | ) | |
465 | .await | |
466 | } | |
467 | ||
468 | async fn repository_commit_from_id( | |
469 | &mut self, | |
470 | requester: &Option<AuthenticatedUser>, | |
471 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
472 | OperationState(operation_state): OperationState<StackOperationState>, | |
473 | request: &RepositoryCommitFromIdRequest, | |
474 | ) -> Result<Commit, Error> { | |
475 | self.handle_repository_commit_from_id( | |
476 | requester, | |
477 | repository_object, | |
478 | OperationState(operation_state), | |
479 | request, | |
480 | ) | |
481 | .await | |
482 | } | |
483 | ||
484 | async fn repository_last_commit_of_file( | |
485 | &mut self, | |
486 | requester: &Option<AuthenticatedUser>, | |
487 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
488 | OperationState(operation_state): OperationState<StackOperationState>, | |
489 | request: &RepositoryLastCommitOfFileRequest, | |
490 | ) -> Result<Commit, Error> { | |
491 | self.repository_last_commit_of_file( | |
492 | requester, | |
493 | repository_object, | |
494 | OperationState(operation_state), | |
495 | request, | |
496 | ) | |
497 | .await | |
498 | } | |
499 | ||
500 | async fn repository_diff( | |
501 | &mut self, | |
502 | requester: &Option<AuthenticatedUser>, | |
503 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
504 | OperationState(operation_state): OperationState<StackOperationState>, | |
505 | request: &RepositoryDiffRequest, | |
506 | ) -> Result<RepositoryDiff, Error> { | |
507 | self.handle_repository_diff( | |
508 | requester, | |
509 | repository_object, | |
510 | OperationState(operation_state), | |
511 | request, | |
512 | ) | |
513 | .await | |
514 | } | |
515 | ||
516 | async fn repository_diff_patch( | |
517 | &mut self, | |
518 | requester: &Option<AuthenticatedUser>, | |
519 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
520 | OperationState(operation_state): OperationState<StackOperationState>, | |
521 | request: &RepositoryDiffPatchRequest, | |
522 | ) -> Result<String, Error> { | |
523 | self.handle_repository_diff_patch( | |
524 | requester, | |
525 | repository_object, | |
526 | OperationState(operation_state), | |
527 | request, | |
528 | ) | |
529 | .await | |
530 | } | |
531 | ||
532 | async fn repository_commit_before( | |
533 | &mut self, | |
534 | requester: &Option<AuthenticatedUser>, | |
535 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
536 | OperationState(operation_state): OperationState<StackOperationState>, | |
537 | request: &RepositoryCommitBeforeRequest, | |
538 | ) -> Result<Commit, Error> { | |
539 | self.handle_repository_commit_before( | |
540 | requester, | |
541 | repository_object, | |
542 | OperationState(operation_state), | |
543 | request, | |
544 | ) | |
545 | .await | |
546 | } | |
547 | ||
548 | // TODO: See where this would need to go in terms of being split up into a different file | |
549 | /// Returns zero for all statistics if an OID wasn't found | |
550 | async fn repository_get_statistics( | |
551 | &mut self, | |
552 | requester: &Option<AuthenticatedUser>, | |
553 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
554 | OperationState(operation_state): OperationState<StackOperationState>, | |
555 | request: &RepositoryStatisticsRequest, | |
556 | ) -> Result<RepositoryStatistics, Error> { | |
557 | let repository = repository_object.object(); | |
558 | let git = self | |
559 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
560 | .await?; | |
561 | ||
562 | let default_branch = repository_object | |
563 | .get::<DefaultBranch>(&operation_state) | |
564 | .await?; | |
565 | let tree_id = | |
566 | match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) { | |
567 | Ok(oid) => oid, | |
568 | Err(_) => return Ok(RepositoryStatistics::default()), | |
569 | }; | |
570 | ||
571 | // Count the amount of branches and tags | |
572 | let mut branches = 0; | |
573 | let mut tags = 0; | |
574 | if let Ok(references) = git.references() { | |
575 | for reference in references.flatten() { | |
576 | if reference.is_branch() { | |
577 | branches += 1; | |
578 | } else if reference.is_tag() { | |
579 | tags += 1; | |
580 | } | |
581 | } | |
582 | } | |
583 | ||
584 | // Attempt to get the commit from the oid | |
585 | let commits = if let Ok(commit) = git.find_commit(tree_id) { | |
586 | // Get the total commit count if we found the tree oid commit | |
587 | GitBackend::get_total_commit_count(&git, &commit)? | |
588 | } else { | |
589 | 0 | |
590 | }; | |
591 | ||
592 | Ok(RepositoryStatistics { | |
593 | commits, | |
594 | branches, | |
595 | tags, | |
596 | }) | |
597 | } | |
598 | ||
599 | /// .0: List of branches filtering by passed requirements. | |
600 | /// .1: Total amount of branches after being filtered | |
601 | async fn repository_get_branches( | |
602 | &mut self, | |
603 | requester: &Option<AuthenticatedUser>, | |
604 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
605 | OperationState(operation_state): OperationState<StackOperationState>, | |
606 | request: &RepositoryBranchesRequest, | |
607 | ) -> Result<(Vec<RepositoryBranch>, usize), Error> { | |
608 | self.handle_repository_get_branches( | |
609 | requester, | |
610 | repository_object, | |
611 | OperationState(operation_state), | |
612 | request, | |
613 | ) | |
614 | .await | |
615 | } | |
616 | ||
617 | async fn repository_get_branch( | |
618 | &mut self, | |
619 | requester: &Option<AuthenticatedUser>, | |
620 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
621 | OperationState(operation_state): OperationState<StackOperationState>, | |
622 | request: &RepositoryBranchRequest, | |
623 | ) -> Result<RepositoryBranch, Error> { | |
624 | self.handle_repository_get_branch( | |
625 | requester, | |
626 | repository_object, | |
627 | OperationState(operation_state), | |
628 | request, | |
629 | ) | |
630 | .await | |
631 | } | |
632 | ||
633 | /// .0: List of tags in passed range | |
634 | /// .1: Total amount of tags | |
635 | async fn repository_get_tags( | |
636 | &mut self, | |
637 | requester: &Option<AuthenticatedUser>, | |
638 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
639 | OperationState(operation_state): OperationState<StackOperationState>, | |
640 | request: &RepositoryTagsRequest, | |
641 | ) -> Result<(Vec<RepositoryTag>, usize), Error> { | |
642 | self.handle_repository_get_tags( | |
643 | requester, | |
644 | repository_object, | |
645 | OperationState(operation_state), | |
646 | request, | |
647 | ) | |
648 | .await | |
649 | } | |
650 | ||
651 | async fn exists( | |
652 | &mut self, | |
653 | requester: &Option<AuthenticatedUser>, | |
654 | repository: &Repository, | |
655 | ) -> Result<bool, Error> { | |
656 | if let Ok(repository) = self | |
657 | .find_by_owner_user_name(&repository.owner.clone(), &repository.name) | |
658 | .await | |
659 | { | |
660 | Ok(repository | |
661 | .can_user_view_repository(&self.instance, requester, self.stack.get().unwrap()) | |
662 | .await) | |
663 | } else { | |
664 | Ok(false) | |
665 | } | |
666 | } | |
667 | } | |
668 | ||
669 | impl IssuesBackend for GitBackend { | |
670 | fn issues_count( | |
671 | &mut self, | |
672 | _requester: &Option<AuthenticatedUser>, | |
673 | _request: &RepositoryIssuesCountRequest, | |
674 | ) -> Result<u64, Error> { | |
675 | todo!() | |
676 | } | |
677 | ||
678 | fn issue_labels( | |
679 | &mut self, | |
680 | _requester: &Option<AuthenticatedUser>, | |
681 | _request: &RepositoryIssueLabelsRequest, | |
682 | ) -> Result<Vec<IssueLabel>, Error> { | |
683 | todo!() | |
684 | } | |
685 | ||
686 | fn issues( | |
687 | &mut self, | |
688 | _requester: &Option<AuthenticatedUser>, | |
689 | _request: &RepositoryIssuesRequest, | |
690 | ) -> Result<Vec<RepositoryIssue>, Error> { | |
691 | todo!() | |
692 | } | |
693 | } | |
694 | ||
695 | #[allow(unused)] | |
696 | #[derive(Debug, sqlx::FromRow)] | |
697 | struct RepositoryMetadata { | |
698 | pub repository: String, | |
699 | pub name: String, | |
700 | pub value: String, | |
701 | } |
giterated-daemon/src/backend/git/tags.rs
@@ -0,0 +1,135 @@ | ||
1 | use anyhow::Error; | |
2 | use giterated_models::{ | |
3 | object::Object, | |
4 | repository::{Commit, CommitSignature, Repository, RepositoryTag, RepositoryTagsRequest}, | |
5 | }; | |
6 | use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; | |
7 | ||
8 | use super::GitBackend; | |
9 | ||
10 | impl GitBackend { | |
11 | /// .0: List of tags in passed range | |
12 | /// .1: Total amount of tags | |
13 | pub async fn handle_repository_get_tags( | |
14 | &mut self, | |
15 | requester: &Option<AuthenticatedUser>, | |
16 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
17 | OperationState(_operation_state): OperationState<StackOperationState>, | |
18 | request: &RepositoryTagsRequest, | |
19 | ) -> Result<(Vec<RepositoryTag>, usize), Error> { | |
20 | let repository = repository_object.object(); | |
21 | let git = self | |
22 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
23 | .await?; | |
24 | ||
25 | let mut tags = vec![]; | |
26 | ||
27 | // Iterate over each tag | |
28 | let _ = git.tag_foreach(|id, name| { | |
29 | // Get the name in utf8 | |
30 | let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1); | |
31 | ||
32 | // Find the tag so we can get the messages attached if any | |
33 | if let Ok(tag) = git.find_tag(id) { | |
34 | // Get the tag message and split it into a summary and body | |
35 | let (summary, body) = if let Some(message) = tag.message() { | |
36 | // Iterate over the lines | |
37 | let mut lines = message | |
38 | .lines() | |
39 | .map(|line| { | |
40 | // Trim the whitespace for every line | |
41 | let mut whitespace_removed = String::with_capacity(line.len()); | |
42 | ||
43 | line.split_whitespace().for_each(|word| { | |
44 | if !whitespace_removed.is_empty() { | |
45 | whitespace_removed.push(' '); | |
46 | } | |
47 | ||
48 | whitespace_removed.push_str(word); | |
49 | }); | |
50 | ||
51 | whitespace_removed | |
52 | }) | |
53 | .collect::<Vec<String>>(); | |
54 | ||
55 | let summary = Some(lines.remove(0)); | |
56 | let body = if lines.is_empty() { | |
57 | None | |
58 | } else { | |
59 | Some(lines.join("\n")) | |
60 | }; | |
61 | ||
62 | (summary, body) | |
63 | } else { | |
64 | (None, None) | |
65 | }; | |
66 | ||
67 | // Get the commit the tag is (possibly) pointing to | |
68 | let commit = tag | |
69 | .peel() | |
70 | .map(|obj| obj.into_commit().ok()) | |
71 | .ok() | |
72 | .flatten() | |
73 | .map(|c| Commit::from(c)); | |
74 | // Get the author of the tag | |
75 | let author: Option<CommitSignature> = tag.tagger().map(|s| s.into()); | |
76 | // Get the time the tag or pointed commit was created | |
77 | let time = if let Some(ref author) = author { | |
78 | Some(author.time) | |
79 | } else { | |
80 | // Get possible commit time if the tag has no author time | |
81 | commit.as_ref().map(|c| c.time.clone()) | |
82 | }; | |
83 | ||
84 | tags.push(RepositoryTag { | |
85 | id: id.to_string(), | |
86 | name: name.to_string(), | |
87 | summary, | |
88 | body, | |
89 | author, | |
90 | time, | |
91 | commit, | |
92 | }); | |
93 | } else { | |
94 | // Lightweight commit, we try and find the commit it's pointing to | |
95 | let commit = git.find_commit(id).ok().map(|c| Commit::from(c)); | |
96 | ||
97 | tags.push(RepositoryTag { | |
98 | id: id.to_string(), | |
99 | name: name.to_string(), | |
100 | summary: None, | |
101 | body: None, | |
102 | author: None, | |
103 | time: commit.as_ref().map(|c| c.time.clone()), | |
104 | commit, | |
105 | }); | |
106 | }; | |
107 | ||
108 | true | |
109 | }); | |
110 | ||
111 | // Get the total amount of tags | |
112 | let tag_count = tags.len(); | |
113 | ||
114 | if let Some(search) = &request.search { | |
115 | // TODO: Caching | |
116 | // Search by sorting using a simple fuzzy search algorithm | |
117 | tags.sort_by(|n1, n2| { | |
118 | strsim::damerau_levenshtein(search, &n1.name) | |
119 | .cmp(&strsim::damerau_levenshtein(search, &n2.name)) | |
120 | }); | |
121 | } else { | |
122 | // Sort the tags using their creation or pointer date | |
123 | tags.sort_by(|t1, t2| t2.time.cmp(&t1.time)); | |
124 | } | |
125 | ||
126 | // Get the requested range of tags | |
127 | let tags = tags | |
128 | .into_iter() | |
129 | .skip(request.range.0) | |
130 | .take(request.range.1.saturating_sub(request.range.0)) | |
131 | .collect::<Vec<RepositoryTag>>(); | |
132 | ||
133 | Ok((tags, tag_count)) | |
134 | } | |
135 | } |