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

Showing ⁨⁨7⁩ changed files⁩ with ⁨⁨1652⁩ insertions⁩ and ⁨⁨1402⁩ deletions⁩

giterated-daemon/src/backend/git/branches.rs

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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 }