Skip to content
Clone

feat(git): 30-day immutable cache and commits pagination, retire diff_files endpoint

ZhenYi committed
parent c79d177 commit b08d764 Browse files

20 files changed

+1080 −581

File tree 20
Modified lib/git/src/graphql/types/diff.rs View lib/git/src/graphql/types/diff.rs
@@ -1,6 +1,8 @@
11use async_graphql::SimpleObject;
22use gitbaby::diff::types::{ChangedPath, ChangedPathDiff, NumStat, ShortStat};
33
4use crate::service::diffs::ChangedPathDiffDto;
5
46use super::enums::DiffFileTypeEnum;
57
68#[derive(SimpleObject, Clone, Debug)]
@@ -13,6 +15,28 @@
1315 pub binary: bool,
1416}
1517
18impl From<&ChangedPathDiffDto> for ChangedPathDiffObj {
19 fn from(c: &ChangedPathDiffDto) -> Self {
20 let status = match c.path.status.as_str() {
21 "Added" => DiffFileTypeEnum::Add,
22 "Deleted" => DiffFileTypeEnum::Del,
23 "Modified" => DiffFileTypeEnum::Change,
24 "Renamed" => DiffFileTypeEnum::Rename,
25 "Copied" => DiffFileTypeEnum::Copy,
26 "TypeChanged" => DiffFileTypeEnum::TypeChanged,
27 _ => DiffFileTypeEnum::Change,
28 };
29 ChangedPathDiffObj {
30 path: c.path.new_path.clone(),
31 old_path: c.path.old_path.clone(),
32 status,
33 lines_added: c.lines_added as i64,
34 lines_removed: c.lines_removed as i64,
35 binary: c.binary,
36 }
37 }
38}
39
1640impl From<&ChangedPathDiff> for ChangedPathDiffObj {
1741 fn from(c: &ChangedPathDiff) -> Self {
1842 ChangedPathDiffObj {
Modified lib/git/src/handler/commits/list.rs View lib/git/src/handler/commits/list.rs
@@ -1,12 +1,11 @@
11use axum::extract::{Path, Query, State};
22use export::rest::AppResponseData;
3use gitbaby::commit::types::CommitInfo;
43use session::Session;
54
65use crate::errors::{ApiError, GitApiError};
76use crate::handler::resolve_reader;
87use crate::service::GitService;
9use crate::service::commits::ListCommitsCmd;
8use crate::service::commits::{CommitsPageDto, ListCommitsCmd};
109
1110#[utoipa::path(
1211 get,
@@ -17,12 +16,13 @@
1716 ("revision" = Option<String>, Query, description = "start revision"),
1817 ("path" = Option<String>, Query, description = "path filter"),
1918 ("max_count" = Option<u32>, Query, description = "max commits"),
19 ("skip" = Option<u32>, Query, description = "skip commits before returning (offset)"),
2020 ("since" = Option<String>, Query, description = "since date"),
2121 ("until" = Option<String>, Query, description = "until date"),
2222 ("first_parent" = bool, Query, description = "first parent only"),
2323 ),
2424 responses(
25 (status = 200, description = "list commits")
25 (status = 200, description = "list commits", body = AppResponseData<CommitsPageDto>)
2626 )
2727)]
2828pub async fn list_commits(
@@ -30,10 +30,10 @@
3030 session: Session,
3131 Path(path): Path<crate::handler::RepoPath>,
3232 Query(cmd): Query<ListCommitsCmd>,
33) -> Result<AppResponseData<Vec<CommitInfo>>, ApiError> {
34 let _ = resolve_reader(&service, &session, path.space_name.clone(), path.repo_name.clone()).await?;
33) -> Result<AppResponseData<CommitsPageDto>, ApiError> {
34 let (_, viewer) = resolve_reader(&service, &session, path.space_name.clone(), path.repo_name.clone()).await?;
3535 let out = service
36 .list_commits(path.space_name, path.repo_name, cmd)
36 .list_commits(path.space_name, path.repo_name, cmd, viewer)
3737 .await
3838 .map_err(GitApiError::from)?;
3939 Ok(AppResponseData::ok(out))
Deleted lib/git/src/handler/diffs/files.rs
@@ -1,47 +0,0 @@
1use axum::extract::{Path, Query, State};
2use export::rest::AppResponseData;
3use gitbaby::diff::types::ChangedPathDiff;
4use session::Session;
5
6use crate::errors::{ApiError, GitApiError};
7use crate::handler::resolve_reader;
8use crate::service::GitService;
9use crate::service::diffs::DiffOptionsDto;
10
11#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
12pub struct DiffBaseHeadQuery {
13 pub base: String,
14 pub head: String,
15}
16
17#[utoipa::path(
18 get,
19 path = "/repos/{space_name}/{repo_name}/diff/files-with-context",
20 tag = "diffs",
21 params(
22 crate::handler::RepoPath,
23 DiffBaseHeadQuery,
24 ),
25 responses(
26 (status = 200, description = "changed files with context")
27 )
28)]
29pub async fn diff_files(
30 State(service): State<GitService>,
31 session: Session,
32 Path(path): Path<crate::handler::RepoPath>,
33 Query(q): Query<DiffBaseHeadQuery>,
34) -> Result<AppResponseData<Vec<ChangedPathDiff>>, ApiError> {
35 let _ = resolve_reader(&service, &session, path.space_name.clone(), path.repo_name.clone()).await?;
36 let out = service
37 .diff_files(
38 path.space_name,
39 path.repo_name,
40 q.base,
41 q.head,
42 DiffOptionsDto::default(),
43 )
44 .await
45 .map_err(GitApiError::from)?;
46 Ok(AppResponseData::ok(out))
47}
No newline at end of file
Modified lib/git/src/handler/diffs/mod.rs View lib/git/src/handler/diffs/mod.rs
@@ -1,4 +1,3 @@
1pub mod files;
21pub mod files_changed_between;
32pub mod for_commit;
43pub mod range_diff;
Modified lib/git/src/handler/trees/last_updated.rs View lib/git/src/handler/trees/last_updated.rs
@@ -10,6 +10,8 @@
1010#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1111pub struct TreeLastUpdatedQuery {
1212 pub revision: String,
13 #[serde(default)]
14 pub path: Option<String>,
1315}
1416
1517#[utoipa::path(
@@ -32,7 +34,7 @@
3234) -> Result<AppResponseData<Vec<PathLastUpdated>>, ApiError> {
3335 let _ = resolve_reader(&service, &session, path.space_name.clone(), path.repo_name.clone()).await?;
3436 let out = service
35 .tree_last_updated(path.space_name, path.repo_name, q.revision)
37 .tree_last_updated(path.space_name, path.repo_name, q.revision, q.path)
3638 .await
3739 .map_err(GitApiError::from)?;
3840 Ok(AppResponseData::ok(out))
Modified lib/git/src/service/analysis.rs View lib/git/src/service/analysis.rs
@@ -1,19 +1,21 @@
11use std::collections::HashMap;
22
3use gitbaby::commit::types::{ListCommitsOptions, TreeEntryMode};
3use gitbaby::commit::types::TreeEntryMode;
4use gitbaby::error::BabyError;
45use models::git::git_languages;
56use models::repo::repos;
67use sea_orm::{
78 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, Unchanged,
89};
9use serde::Serialize;
10use serde::{Deserialize, Serialize};
1011use time::OffsetDateTime;
1112use uuid::Uuid;
1213
14use crate::service::cache::{self, IMMUTABLE_TTL};
1315use crate::service::GitService;
1416use crate::share::errors::{GitError, map_baby};
1517
16#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
18#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1719pub struct LanguageStatDto {
1820 pub language: String,
1921 pub percentage_bp: i32,
@@ -21,7 +23,7 @@
2123 pub is_primary: bool,
2224}
2325
24#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
26#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
2527pub struct ContributorStatDto {
2628 pub name: String,
2729 pub email: String,
@@ -30,7 +32,7 @@
3032 pub deletions: i64,
3133}
3234
33#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
35#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
3436pub struct CodeActivityAuthorDto {
3537 pub name: String,
3638 pub email: String,
@@ -39,7 +41,7 @@
3941 pub deletions: i64,
4042}
4143
42#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
44#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
4345pub struct CodeActivityDto {
4446 pub authors: Vec<CodeActivityAuthorDto>,
4547 pub total_commits: i64,
@@ -103,7 +105,24 @@
103105 ) -> Result<Vec<LanguageStatDto>, GitError> {
104106 let ctx = self.context(space_name, repo_name).await?;
105107 let baby = self.baby(&ctx);
106 let langs = language_stats(&baby).await?;
108 let Some(revision) =
109 resolve_analysis_revision(&baby, None, &ctx.model.default_branch).await?
110 else {
111 let db = self.db_write();
112 git_languages::Entity::delete_many()
113 .filter(git_languages::Column::RepoId.eq(ctx.model.id))
114 .exec(db)
115 .await?;
116 let am = repos::ActiveModel {
117 id: Unchanged(ctx.model.id),
118 primary_language: Set(None),
119 updated_at: Set(OffsetDateTime::now_utc()),
120 ..Default::default()
121 };
122 am.update(db).await?;
123 return Ok(Vec::new());
124 };
125 let langs = language_stats(&baby, &revision).await?;
107126 let total: i64 = langs.values().sum();
108127 let mut rows: Vec<(String, i64)> = langs.into_iter().collect();
109128 rows.sort_by_key(|(_, size)| std::cmp::Reverse(*size));
@@ -188,31 +207,44 @@
188207 repo_name: String,
189208 ) -> Result<Vec<ContributorStatDto>, GitError> {
190209 let ctx = self.context(space_name, repo_name).await?;
191 let commits = self
192 .baby(&ctx)
193 .list_commits(ListCommitsOptions {
194 revision: "HEAD".to_string(),
195 ..Default::default()
196 })
197 .await
198 .map_err(map_baby)?;
199 let mut map: HashMap<(String, String), i64> = HashMap::new();
200 for c in &commits {
201 *map.entry((c.author.name.clone(), c.author.email.clone()))
202 .or_default() += 1;
203 }
204 let mut out: Vec<ContributorStatDto> = map
205 .into_iter()
206 .map(|((name, email), commits)| ContributorStatDto {
207 name,
208 email,
209 commits,
210 additions: 0,
211 deletions: 0,
212 })
213 .collect();
214 out.sort_by_key(|b| std::cmp::Reverse(b.commits));
215 Ok(out)
210 let baby = self.baby(&ctx);
211 let Some(revision) =
212 resolve_analysis_revision(&baby, None, &ctx.model.default_branch).await?
213 else {
214 return Ok(Vec::new());
215 };
216 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
217 let key = format!("repo.{}.analysis_contributors.v2.{}", ctx.model.id, rev_oid);
218 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
219 let entries = baby
220 .commit_short_stats(&revision)
221 .await
222 .map_err(map_baby)?;
223 let mut map: HashMap<(String, String), (i64, i64, i64)> = HashMap::new();
224 for e in &entries {
225 let slot = map
226 .entry((e.author_name.clone(), e.author_email.clone()))
227 .or_default();
228 slot.0 += 1;
229 slot.1 += e.additions as i64;
230 slot.2 += e.deletions as i64;
231 }
232 let mut out: Vec<ContributorStatDto> = map
233 .into_iter()
234 .map(
235 |((name, email), (commits, additions, deletions))| ContributorStatDto {
236 name,
237 email,
238 commits,
239 additions,
240 deletions,
241 },
242 )
243 .collect();
244 out.sort_by_key(|b| std::cmp::Reverse(b.commits));
245 Ok(out)
246 })
247 .await
216248 }
217249
218250 pub async fn analysis_code_activity(
@@ -222,45 +254,93 @@
222254 branch: Option<String>,
223255 ) -> Result<CodeActivityDto, GitError> {
224256 let ctx = self.context(space_name, repo_name).await?;
225 let revision = branch.unwrap_or_else(|| "HEAD".to_string());
226 let commits = self
227 .baby(&ctx)
228 .list_commits(ListCommitsOptions {
229 revision,
230 ..Default::default()
231 })
232 .await
233 .map_err(map_baby)?;
234 let mut authors: Vec<CodeActivityAuthorDto> = Vec::new();
235 for c in &commits {
236 match authors.iter_mut().find(|a| a.email == c.author.email) {
237 Some(a) => a.commits += 1,
238 None => authors.push(CodeActivityAuthorDto {
239 name: c.author.name.clone(),
240 email: c.author.email.clone(),
241 commits: 1,
242 additions: 0,
243 deletions: 0,
244 }),
257 let baby = self.baby(&ctx);
258 let Some(revision) =
259 resolve_analysis_revision(&baby, branch.as_deref(), &ctx.model.default_branch).await?
260 else {
261 return Ok(CodeActivityDto {
262 authors: Vec::new(),
263 total_commits: 0,
264 total_additions: 0,
265 total_deletions: 0,
266 });
267 };
268 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
269 let key = format!("repo.{}.analysis_code_activity.v2.{}", ctx.model.id, rev_oid);
270 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
271 let entries = baby
272 .commit_short_stats(&revision)
273 .await
274 .map_err(map_baby)?;
275 let mut authors: Vec<CodeActivityAuthorDto> = Vec::new();
276 for e in &entries {
277 match authors.iter_mut().find(|a| a.email == e.author_email) {
278 Some(a) => {
279 a.commits += 1;
280 a.additions += e.additions as i64;
281 a.deletions += e.deletions as i64;
282 }
283 None => authors.push(CodeActivityAuthorDto {
284 name: e.author_name.clone(),
285 email: e.author_email.clone(),
286 commits: 1,
287 additions: e.additions as i64,
288 deletions: e.deletions as i64,
289 }),
290 }
245291 }
246 }
247 authors.sort_by_key(|b| std::cmp::Reverse(b.commits));
248 let total = commits.len() as i64;
249 Ok(CodeActivityDto {
250 authors,
251 total_commits: total,
252 total_additions: 0,
253 total_deletions: 0,
292 authors.sort_by_key(|b| std::cmp::Reverse(b.commits));
293 Ok(CodeActivityDto {
294 total_commits: entries.len() as i64,
295 total_additions: entries.iter().map(|e| e.additions as i64).sum(),
296 total_deletions: entries.iter().map(|e| e.deletions as i64).sum(),
297 authors,
298 })
254299 })
300 .await
301 }
302}
303
304async fn resolve_analysis_revision(
305 baby: &gitbaby::GitBaby,
306 preferred: Option<&str>,
307 default_branch: &str,
308) -> Result<Option<String>, GitError> {
309 let mut candidates: Vec<String> = Vec::new();
310 if let Some(b) = preferred
311 && !b.trim().is_empty()
312 {
313 candidates.push(b.to_string());
255314 }
315 candidates.push("HEAD".to_string());
316 let db = default_branch.trim();
317 if !db.is_empty() {
318 let head = format!("refs/heads/{db}");
319 if !candidates.contains(&head) {
320 candidates.push(head);
321 }
322 }
323 for cand in candidates {
324 match baby.rev_parse(&cand).await {
325 Ok(oid) => return Ok(Some(oid.to_string())),
326 Err(BabyError::RevisionNotFound(_)) => continue,
327 Err(e) => return Err(map_baby(e)),
328 }
329 }
330 let branches = baby
331 .list_branches(gitbaby::branch::ListBranchesOptions::default())
332 .await
333 .map_err(map_baby)?;
334 Ok(branches.into_iter().next().map(|b| b.sha.to_string()))
256335}
257336
258337pub(crate) async fn language_stats(
259338 baby: &gitbaby::GitBaby,
339 revision: &str,
260340) -> Result<HashMap<String, i64>, GitError> {
261341 let entries = baby
262342 .walk_tree(
263 "HEAD",
343 revision,
264344 std::path::Path::new(""),
265345 gitbaby::tree::types::WalkTreeOptions {
266346 recurse: true,
Modified lib/git/src/service/blame.rs View lib/git/src/service/blame.rs
@@ -1,10 +1,11 @@
11use gitbaby::blame::types::{BlameHunk, BlameOptions, Range};
2use serde::Serialize;
2use serde::{Deserialize, Serialize};
33
4use crate::service::cache::{self, IMMUTABLE_TTL};
45use crate::service::GitService;
56use crate::share::errors::{GitError, map_baby};
67
7#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
8#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
89pub struct BlameRangeDto {
910 pub starting_line: i32,
1011 pub ending_line: i32,
@@ -53,17 +54,30 @@
5354 )));
5455 }
5556 let ctx = self.context(space_name, repo_name).await?;
56 let opts = BlameOptions {
57 revision,
58 path,
59 range: match (range_start, range_end) {
60 (Some(s), Some(e)) if s <= e => Some(Range { start: s, end: e }),
61 _ => None,
62 },
63 ignore_revs_file: None,
64 bypass_ignore: false,
65 };
66 let hunks = self.baby(&ctx).blame_all(opts).await.map_err(map_baby)?;
67 Ok(hunks.iter().map(BlameRangeDto::from).collect())
57 let baby = self.baby(&ctx);
58 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
59 let key = format!(
60 "repo.{}.blame.{}:{}:{:?}:{:?}",
61 ctx.model.id,
62 rev_oid,
63 path.display(),
64 range_start,
65 range_end
66 );
67 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
68 let opts = BlameOptions {
69 revision,
70 path,
71 range: match (range_start, range_end) {
72 (Some(s), Some(e)) if s <= e => Some(Range { start: s, end: e }),
73 _ => None,
74 },
75 ignore_revs_file: None,
76 bypass_ignore: false,
77 };
78 let hunks = baby.blame_all(opts).await.map_err(map_baby)?;
79 Ok(hunks.iter().map(BlameRangeDto::from).collect())
80 })
81 .await
6882 }
6983}
Modified lib/git/src/service/blobs.rs View lib/git/src/service/blobs.rs
@@ -1,5 +1,7 @@
11use gix::ObjectId;
2use serde::{Deserialize, Serialize};
23
4use crate::service::cache::{self, IMMUTABLE_TTL};
35use crate::service::GitService;
46use crate::share::errors::{GitError, map_baby};
57
@@ -7,14 +9,14 @@
79
810pub const SNIFF_LIMIT: i64 = 8192;
911
10#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
12#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1113pub struct LfsPointerDto {
1214 pub version: String,
1315 pub oid: Option<String>,
1416 pub size: Option<i64>,
1517}
1618
17#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
19#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1820pub struct BlobMetaDto {
1921 pub oid: String,
2022 pub size: i64,
@@ -23,7 +25,7 @@
2325 pub lfs: Option<LfsPointerDto>,
2426}
2527
26#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
28#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
2729pub struct BlobContentDto {
2830 pub oid: String,
2931 pub data: Vec<u8>,
@@ -32,7 +34,7 @@
3234 pub truncated: bool,
3335}
3436
35#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
37#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
3638pub struct RawDownloadDto {
3739 pub id: String,
3840 pub size: i64,
@@ -96,15 +98,19 @@
9698 ) -> Result<BlobMetaDto, GitError> {
9799 let object_id = parse_oid(&oid)?;
98100 let ctx = self.context(space_name, repo_name).await?;
99 let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?;
100 let lfs = parse_lfs_pointer(&sniff);
101 Ok(BlobMetaDto {
102 oid,
103 size,
104 is_binary: is_binary(&sniff),
105 is_lfs_pointer: lfs.is_some(),
106 lfs,
101 let key = format!("repo.{}.blob_meta.{}", ctx.model.id, object_id);
102 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
103 let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?;
104 let lfs = parse_lfs_pointer(&sniff);
105 Ok(BlobMetaDto {
106 oid,
107 size,
108 is_binary: is_binary(&sniff),
109 is_lfs_pointer: lfs.is_some(),
110 lfs,
111 })
107112 })
113 .await
108114 }
109115
110116 pub async fn blob_content(
@@ -115,26 +121,30 @@
115121 ) -> Result<BlobContentDto, GitError> {
116122 let object_id = parse_oid(&oid)?;
117123 let ctx = self.context(space_name, repo_name).await?;
118 let baby = self.baby(&ctx);
119 let size = baby.get_blob_size(object_id).await.map_err(map_baby)?;
120124 let max_blob = self.state.config.git.max_blob_size;
121 let truncated = size > max_blob as i64;
122 let limit = if truncated {
123 Some(max_blob as i64)
124 } else {
125 None
126 };
127 let blob = baby
128 .get_blob(object_id, gitbaby::blob::types::GetBlobOptions { limit })
129 .await
130 .map_err(map_baby)?;
131 Ok(BlobContentDto {
132 oid,
133 size,
134 is_binary: is_binary(&blob.data),
135 data: blob.data,
136 truncated,
125 let key = format!("repo.{}.blob_content.{}:{}", ctx.model.id, object_id, max_blob);
126 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
127 let baby = self.baby(&ctx);
128 let size = baby.get_blob_size(object_id).await.map_err(map_baby)?;
129 let truncated = size > max_blob as i64;
130 let limit = if truncated {
131 Some(max_blob as i64)
132 } else {
133 None
134 };
135 let blob = baby
136 .get_blob(object_id, gitbaby::blob::types::GetBlobOptions { limit })
137 .await
138 .map_err(map_baby)?;
139 Ok(BlobContentDto {
140 oid,
141 size,
142 is_binary: is_binary(&blob.data),
143 data: blob.data,
144 truncated,
145 })
137146 })
147 .await
138148 }
139149
140150 pub async fn blob_raw_download(
@@ -145,12 +155,16 @@
145155 ) -> Result<RawDownloadDto, GitError> {
146156 let object_id = parse_oid(&oid)?;
147157 let ctx = self.context(space_name, repo_name).await?;
148 let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?;
149 Ok(RawDownloadDto {
150 id: oid,
151 size,
152 is_lfs_pointer: parse_lfs_pointer(&sniff).is_some(),
158 let key = format!("repo.{}.blob_raw_download.{}", ctx.model.id, object_id);
159 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
160 let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?;
161 Ok(RawDownloadDto {
162 id: oid,
163 size,
164 is_lfs_pointer: parse_lfs_pointer(&sniff).is_some(),
165 })
153166 })
167 .await
154168 }
155169
156170 pub async fn list_lfs_pointers(
Modified lib/git/src/service/cache.rs View lib/git/src/service/cache.rs
@@ -6,11 +6,12 @@
66use crate::share::errors::GitError;
77
88pub const REV_TTL: Duration = Duration::from_secs(60);
9pub const IMMUTABLE_TTL: Duration = Duration::from_secs(30 * 24 * 3600);
910
10pub(crate) fn sanitize_key(k: &str) -> String {
11pub fn sanitize_key(k: &str) -> String {
1112 let mut out = String::with_capacity(k.len());
1213 for c in k.chars() {
13 if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':') {
14 if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':' | '/') {
1415 out.push(c);
1516 } else {
1617 out.push('_');
@@ -19,6 +20,26 @@
1920 out
2021}
2122
23pub async fn cached_fetch<V, F, Fut>(
24 cache: &CacheClient,
25 key: &str,
26 ttl: Duration,
27 fetch: F,
28) -> Result<V, GitError>
29where
30 V: Serialize + DeserializeOwned,
31 F: FnOnce() -> Fut,
32 Fut: std::future::Future<Output = Result<V, GitError>>,
33{
34 let sanitized = sanitize_key(key);
35 if let Ok(Some(cached)) = cache.get::<V>(&sanitized).await {
36 return Ok(cached);
37 }
38 let val = fetch().await?;
39 let _ = cache.set(&sanitized, &val, Some(ttl)).await;
40 Ok(val)
41}
42
2243fn oid_key(kind: &str, oid: &str) -> String {
2344 sanitize_key(&format!("object.{kind}.{oid}"))
2445}
Modified lib/git/src/service/commits.rs View lib/git/src/service/commits.rs
@@ -14,6 +14,7 @@
1414use uuid::Uuid;
1515
1616use crate::context::facade::GitContext;
17use crate::service::cache::{self, IMMUTABLE_TTL};
1718use crate::service::GitService;
1819use crate::share::errors::{GitError, map_baby};
1920
@@ -30,6 +31,8 @@
3031 #[serde(default)]
3132 pub max_count: Option<u32>,
3233 #[serde(default)]
34 pub skip: Option<u32>,
35 #[serde(default)]
3336 pub since: Option<String>,
3437 #[serde(default)]
3538 pub until: Option<String>,
@@ -37,6 +40,47 @@
3740 pub first_parent: bool,
3841}
3942
43#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
44pub struct CommitDto {
45 pub id: String,
46 pub tree_id: String,
47 pub parents: Vec<String>,
48 pub author_name: String,
49 pub author_email: String,
50 pub author_time: i64,
51 pub committer_name: String,
52 pub committer_email: String,
53 pub committer_time: i64,
54 pub message: String,
55}
56
57impl From<CommitInfo> for CommitDto {
58 fn from(c: CommitInfo) -> Self {
59 Self {
60 id: c.id.to_string(),
61 tree_id: c.tree_id.to_string(),
62 parents: c.parents.iter().map(|p| p.to_string()).collect(),
63 author_name: c.author.name,
64 author_email: c.author.email,
65 author_time: c.author.time.unix_timestamp(),
66 committer_name: c.committer.name,
67 committer_email: c.committer.email,
68 committer_time: c.committer.time.unix_timestamp(),
69 message: c.message,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
75pub struct CommitsPageDto {
76 pub commits: Vec<CommitDto>,
77 pub total: u64,
78 pub skip: u32,
79 pub limit: u32,
80 pub has_more: bool,
81 pub diff_view_style: String,
82}
83
4084#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
4185#[serde(rename_all = "lowercase")]
4286pub enum CommitStatusDtoState {
@@ -121,7 +165,12 @@
121165 ) -> Result<CommitInfo, GitError> {
122166 let ctx = self.context(space_name, repo_name).await?;
123167 let baby = self.baby(&ctx);
124 baby.get_commit(&revision).await.map_err(map_baby)
168 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
169 let key = format!("repo.{}.commit.{}", ctx.model.id, rev_oid);
170 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
171 baby.get_commit(&revision).await.map_err(map_baby)
172 })
173 .await
125174 }
126175
127176 pub async fn list_commits(
@@ -129,20 +178,81 @@
129178 space_name: String,
130179 repo_name: String,
131180 cmd: ListCommitsCmd,
132 ) -> Result<Vec<CommitInfo>, GitError> {
181 viewer: Option<Uuid>,
182 ) -> Result<CommitsPageDto, GitError> {
133183 let ctx = self.context(space_name, repo_name).await?;
134184 let baby = self.baby(&ctx);
185 let limit = cmd.max_count.unwrap_or(50);
186 let skip = cmd.skip.unwrap_or(0);
187 let rev_oid = baby.rev_parse(&cmd.revision).await.map_err(map_baby)?;
188 let path_str = cmd
189 .path
190 .as_ref()
191 .map(|p| p.to_string_lossy().into_owned())
192 .unwrap_or_default();
193 let page_cache_key = format!(
194 "repo.{}.commits_page.{}:{}:{}:{}:{}:{}:{}",
195 ctx.model.id,
196 rev_oid,
197 skip,
198 limit,
199 path_str,
200 cmd.since.as_deref().unwrap_or(""),
201 cmd.until.as_deref().unwrap_or(""),
202 cmd.first_parent,
203 );
135204 let opts = ListCommitsOptions {
136205 revision: cmd.revision,
137206 path: cmd.path,
138 max_count: cmd.max_count,
207 max_count: Some(limit),
208 skip: Some(skip),
139209 since: cmd.since,
140210 until: cmd.until,
141211 first_parent: cmd.first_parent,
142212 };
143 baby.list_commits(opts).await.map_err(map_baby)
213 let (commits, total): (Vec<CommitDto>, u64) = cache::cached_fetch(
214 &ctx.cache,
215 &page_cache_key,
216 IMMUTABLE_TTL,
217 || async {
218 let total = baby.count_commits(opts.clone()).await.map_err(map_baby)?;
219 let commits = baby
220 .list_commits(opts)
221 .await
222 .map_err(map_baby)?
223 .into_iter()
224 .map(CommitDto::from)
225 .collect::<Vec<_>>();
226 Ok((commits, total))
227 },
228 )
229 .await?;
230 let has_more = (skip as u64 + commits.len() as u64) < total;
231 let diff_view_style = self.user_diff_view_style(viewer).await;
232 Ok(CommitsPageDto {
233 commits,
234 total,
235 skip,
236 limit,
237 has_more,
238 diff_view_style,
239 })
144240 }
145241
242 pub async fn user_diff_view_style(&self, viewer: Option<Uuid>) -> String {
243 if let Some(uid) = viewer
244 && let Ok(Some(row)) =
245 models::users::user_preferences::Entity::find_by_id(uid)
246 .one(self.db_read())
247 .await
248 && let Some(style) = row.diff_view_style
249 && !style.is_empty()
250 {
251 return style;
252 }
253 "unified".to_string()
254 }
255
146256 pub async fn commit_stats(
147257 &self,
148258 space_name: String,
@@ -152,9 +262,18 @@
152262 ) -> Result<CommitStats, GitError> {
153263 let ctx = self.context(space_name, repo_name).await?;
154264 let baby = self.baby(&ctx);
155 baby.commit_stats(&revision, path.as_deref())
156 .await
157 .map_err(map_baby)
265 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
266 let path_str = path
267 .as_ref()
268 .map(|p| p.to_string_lossy().into_owned())
269 .unwrap_or_default();
270 let key = format!("repo.{}.commit_stats.{}.{}", ctx.model.id, rev_oid, path_str);
271 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
272 baby.commit_stats(&revision, path.as_deref())
273 .await
274 .map_err(map_baby)
275 })
276 .await
158277 }
159278
160279 pub async fn get_tree_entries(
Modified lib/git/src/service/conflicts.rs View lib/git/src/service/conflicts.rs
@@ -1,17 +1,18 @@
11use gitbaby::conflict::types::{ConflictFile, ListConflictOptions};
2use serde::Serialize;
2use serde::{Deserialize, Serialize};
33
4use crate::service::cache::{self, IMMUTABLE_TTL};
45use crate::service::GitService;
56use crate::share::errors::{GitError, map_baby};
67
7#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
8#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
89pub struct MergeStatusDto {
910 pub mergeable: bool,
1011 pub conflicts: Vec<String>,
1112 pub fast_forwardable: bool,
1213}
1314
14#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
15#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1516pub struct ConflictDetailDto {
1617 pub path: String,
1718 pub content: Option<String>,
@@ -38,41 +39,44 @@
3839 head: String,
3940 ) -> Result<MergeStatusDto, GitError> {
4041 let ctx = self.context(space_name, repo_name).await?;
41 let baby = self.baby(&ctx);
42 let fast_forwardable = baby.is_ancestor(&base, &head).await.map_err(map_baby)?;
43
4442 let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?;
45 let files = baby
46 .list_conflict_files(
47 &our_oid,
48 &their_oid,
49 &ListConflictOptions {
50 allow_tree_conflicts: false,
51 skip_content: true,
52 },
53 )
54 .await
55 .map_err(map_baby)?;
56 let conflicts: Vec<String> = files
57 .iter()
58 .filter_map(|f| {
59 f.header
60 .our_path
61 .clone()
62 .map(|p| p.to_string_lossy().into_owned())
63 .or_else(|| {
64 f.header
65 .their_path
66 .clone()
67 .map(|p| p.to_string_lossy().into_owned())
68 })
43 let key = format!("repo.{}.conflict_check.{}.{}", ctx.model.id, our_oid, their_oid);
44 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
45 let baby = self.baby(&ctx);
46 let fast_forwardable = baby.is_ancestor(&base, &head).await.map_err(map_baby)?;
47 let files = baby
48 .list_conflict_files(
49 &our_oid,
50 &their_oid,
51 &ListConflictOptions {
52 allow_tree_conflicts: false,
53 skip_content: true,
54 },
55 )
56 .await
57 .map_err(map_baby)?;
58 let conflicts: Vec<String> = files
59 .iter()
60 .filter_map(|f| {
61 f.header
62 .our_path
63 .clone()
64 .map(|p| p.to_string_lossy().into_owned())
65 .or_else(|| {
66 f.header
67 .their_path
68 .clone()
69 .map(|p| p.to_string_lossy().into_owned())
70 })
71 })
72 .collect();
73 Ok(MergeStatusDto {
74 mergeable: conflicts.is_empty(),
75 fast_forwardable,
76 conflicts,
6977 })
70 .collect();
71 Ok(MergeStatusDto {
72 mergeable: conflicts.is_empty(),
73 fast_forwardable,
74 conflicts,
7578 })
79 .await
7680 }
7781
7882 pub async fn conflict_conflicted_files(
@@ -83,21 +87,26 @@
8387 head: String,
8488 ) -> Result<Vec<String>, GitError> {
8589 let ctx = self.context(space_name, repo_name).await?;
86 let baby = self.baby(&ctx);
8790 let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?;
88 let files = baby
89 .list_conflict_files(
90 &our_oid,
91 &their_oid,
92 &ListConflictOptions {
93 allow_tree_conflicts: false,
94 skip_content: false,
95 },
96 )
97 .await
98 .map_err(map_baby)?;
99 Ok(conflict_paths(&files))
91 let key = format!("repo.{}.conflict_files.{}.{}", ctx.model.id, our_oid, their_oid);
92 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
93 let baby = self.baby(&ctx);
94 let files = baby
95 .list_conflict_files(
96 &our_oid,
97 &their_oid,
98 &ListConflictOptions {
99 allow_tree_conflicts: false,
100 skip_content: false,
101 },
102 )
103 .await
104 .map_err(map_baby)?;
105 Ok(conflict_paths(&files))
106 })
107 .await
100108 }
109
101110 pub async fn conflict_detail(
102111 &self,
103112 space_name: String,
@@ -107,31 +116,38 @@
107116 path: String,
108117 ) -> Result<ConflictDetailDto, GitError> {
109118 let ctx = self.context(space_name, repo_name).await?;
110 let baby = self.baby(&ctx);
111119 let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?;
112 let files = baby
113 .list_conflict_files(
114 &our_oid,
115 &their_oid,
116 &ListConflictOptions {
117 allow_tree_conflicts: false,
118 skip_content: false,
119 },
120 )
121 .await
122 .map_err(map_baby)?;
123 let wanted = std::path::Path::new(&path);
124 let file = files.into_iter().find(|f| {
125 f.header.our_path.as_deref() == Some(wanted)
126 || f.header.their_path.as_deref() == Some(wanted)
127 });
128 match file {
129 Some(f) => Ok(ConflictDetailDto {
130 path,
131 content: f.content,
132 }),
133 None => Err(GitError::NotFound),
134 }
120 let key = format!(
121 "repo.{}.conflict_detail.{}.{}.{}",
122 ctx.model.id, our_oid, their_oid, path
123 );
124 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
125 let baby = self.baby(&ctx);
126 let files = baby
127 .list_conflict_files(
128 &our_oid,
129 &their_oid,
130 &ListConflictOptions {
131 allow_tree_conflicts: false,
132 skip_content: false,
133 },
134 )
135 .await
136 .map_err(map_baby)?;
137 let wanted = std::path::Path::new(&path);
138 let file = files.into_iter().find(|f| {
139 f.header.our_path.as_deref() == Some(wanted)
140 || f.header.their_path.as_deref() == Some(wanted)
141 });
142 match file {
143 Some(f) => Ok(ConflictDetailDto {
144 path,
145 content: f.content,
146 }),
147 None => Err(GitError::NotFound),
148 }
149 })
150 .await
135151 }
136152
137153 pub async fn conflict_check_threeway(
@@ -142,25 +158,37 @@
142158 theirs: String,
143159 ancestor: String,
144160 ) -> Result<MergeStatusDto, GitError> {
145 let _ = &ancestor;
146161 let ctx = self.context(space_name, repo_name).await?;
147 let baby = self.baby(&ctx);
148162 let (our_oid, their_oid) = self.resolve_pair(&ctx, &ours, &theirs).await?;
149 let merged = baby
150 .merge_tree(&our_oid, &their_oid, Some(&ancestor), false)
163 let ancestor_oid = self
164 .baby(&ctx)
165 .rev_parse(&ancestor)
151166 .await
152 .map_err(map_baby)?;
153 let fast_forwardable = baby.is_ancestor(&ours, &theirs).await.map_err(map_baby)?;
154 let conflicts: Vec<String> = merged
155 .conflict_files
156 .iter()
157 .map(|p| p.to_string_lossy().into_owned())
158 .collect();
159 Ok(MergeStatusDto {
160 mergeable: !merged.has_conflicts,
161 conflicts,
162 fast_forwardable,
167 .map_err(map_baby)?
168 .to_string();
169 let key = format!(
170 "repo.{}.conflict_threeway.{}.{}.{}",
171 ctx.model.id, our_oid, their_oid, ancestor_oid
172 );
173 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
174 let baby = self.baby(&ctx);
175 let merged = baby
176 .merge_tree(&our_oid, &their_oid, Some(&ancestor_oid), false)
177 .await
178 .map_err(map_baby)?;
179 let fast_forwardable = baby.is_ancestor(&ours, &theirs).await.map_err(map_baby)?;
180 let conflicts: Vec<String> = merged
181 .conflict_files
182 .iter()
183 .map(|p| p.to_string_lossy().into_owned())
184 .collect();
185 Ok(MergeStatusDto {
186 mergeable: !merged.has_conflicts,
187 conflicts,
188 fast_forwardable,
189 })
163190 })
191 .await
164192 }
165193}
166194
Modified lib/git/src/service/diffs.rs View lib/git/src/service/diffs.rs
@@ -5,6 +5,7 @@
55use gix::ObjectId;
66use serde::{Deserialize, Serialize};
77
8use crate::service::cache::{self, IMMUTABLE_TTL};
89use crate::service::GitService;
910use crate::share::errors::{GitError, map_baby};
1011
@@ -13,7 +14,7 @@
1314 .map_err(|_| GitError::InvalidArgument(format!("invalid object id `{hex}`")))
1415}
1516
16#[derive(Debug, Clone, Deserialize, Default)]
17#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1718pub struct DiffOptionsDto {
1819 #[serde(default)]
1920 pub paths: Option<Vec<std::path::PathBuf>>,
@@ -31,7 +32,54 @@
3132 }
3233}
3334
34#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
35#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
36pub struct ChangedPathDto {
37 pub old_path: Option<String>,
38 pub new_path: String,
39 pub old_mode: u32,
40 pub new_mode: u32,
41 pub old_oid: Option<String>,
42 pub new_oid: Option<String>,
43 pub status: String,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
47pub struct ChangedPathDiffDto {
48 pub path: ChangedPathDto,
49 pub lines_added: u64,
50 pub lines_removed: u64,
51 pub binary: bool,
52 pub patch: Option<String>,
53}
54
55impl From<ChangedPathDiff> for ChangedPathDiffDto {
56 fn from(c: ChangedPathDiff) -> Self {
57 let patch = c.patch.and_then(|p| {
58 if c.binary {
59 None
60 } else {
61 Some(String::from_utf8_lossy(&p).into_owned())
62 }
63 });
64 Self {
65 path: ChangedPathDto {
66 old_path: c.path.old_path.map(|p| p.to_string_lossy().into_owned()),
67 new_path: c.path.new_path.to_string_lossy().into_owned(),
68 old_mode: c.path.old_mode,
69 new_mode: c.path.new_mode,
70 old_oid: c.path.old_oid.map(|o| o.to_string()),
71 new_oid: c.path.new_oid.map(|o| o.to_string()),
72 status: format!("{:?}", c.path.status),
73 },
74 lines_added: c.lines_added,
75 lines_removed: c.lines_removed,
76 binary: c.binary,
77 patch,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
3583pub struct DiffTreeRecordDto {
3684 pub status: String,
3785 pub score: u8,
@@ -51,8 +99,19 @@
5199 head: &str,
52100 ) -> Result<DiffRequest, GitError> {
53101 let baby = self.baby(ctx);
54 let left = baby.rev_parse(base).await.map_err(map_baby)?;
55 let right = baby.rev_parse(head).await.map_err(map_baby)?;
102 let empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
103 let left = if base.trim() == empty_tree {
104 ObjectId::from_hex(empty_tree.as_bytes())
105 .map_err(|_| GitError::InvalidArgument("empty tree oid".into()))?
106 } else {
107 baby.rev_parse(base).await.map_err(map_baby)?
108 };
109 let right = if head.trim() == empty_tree {
110 ObjectId::from_hex(empty_tree.as_bytes())
111 .map_err(|_| GitError::InvalidArgument("empty tree oid".into()))?
112 } else {
113 baby.rev_parse(head).await.map_err(map_baby)?
114 };
56115 Ok(DiffRequest { left, right })
57116 }
58117
@@ -65,7 +124,11 @@
65124 ) -> Result<String, GitError> {
66125 let ctx = self.context(space_name, repo_name).await?;
67126 let req = self.diff_request(&ctx, &base, &head).await?;
68 self.baby(&ctx).raw_diff(&req, None).await.map_err(map_baby)
127 let key = format!("repo.{}.diff_raw.{}.{}", ctx.model.id, req.left, req.right);
128 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
129 self.baby(&ctx).raw_diff(&req, None).await.map_err(map_baby)
130 })
131 .await
69132 }
70133
71134 pub async fn diff_files_changed_between(
@@ -79,19 +142,23 @@
79142 let baby = self.baby(&ctx);
80143 let left = baby.rev_parse(&base).await.map_err(map_baby)?;
81144 let right = baby.rev_parse(&head).await.map_err(map_baby)?;
82 let paths = baby
83 .find_changed_paths(
84 vec![ChangedPathRequest::TreePair(left, right)],
85 None,
86 None,
87 false,
88 )
89 .await
90 .map_err(map_baby)?;
91 Ok(paths
92 .into_iter()
93 .map(|p| p.new_path.to_string_lossy().into_owned())
94 .collect())
145 let key = format!("repo.{}.diff_changed_paths.{}.{}", ctx.model.id, left, right);
146 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
147 let paths = baby
148 .find_changed_paths(
149 vec![ChangedPathRequest::TreePair(left, right)],
150 None,
151 None,
152 false,
153 )
154 .await
155 .map_err(map_baby)?;
156 Ok(paths
157 .into_iter()
158 .map(|p| p.new_path.to_string_lossy().into_owned())
159 .collect())
160 })
161 .await
95162 }
96163
97164 pub async fn diff_short_stat(
@@ -103,10 +170,14 @@
103170 ) -> Result<ShortStat, GitError> {
104171 let ctx = self.context(space_name, repo_name).await?;
105172 let req = self.diff_request(&ctx, &base, &head).await?;
106 self.baby(&ctx)
107 .shortstat(&req, &DiffOptions::default())
108 .await
109 .map_err(map_baby)
173 let key = format!("repo.{}.diff_short_stat.{}.{}", ctx.model.id, req.left, req.right);
174 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
175 self.baby(&ctx)
176 .shortstat(&req, &DiffOptions::default())
177 .await
178 .map_err(map_baby)
179 })
180 .await
110181 }
111182
112183 pub async fn diff_stats(
@@ -119,10 +190,17 @@
119190 ) -> Result<Vec<NumStat>, GitError> {
120191 let ctx = self.context(space_name, repo_name).await?;
121192 let req = self.diff_request(&ctx, &base, &head).await?;
122 self.baby(&ctx)
123 .diff_stats(&req, &opts.into())
124 .await
125 .map_err(map_baby)
193 let key = format!(
194 "repo.{}.diff_stats.{}.{}:{:?}",
195 ctx.model.id, req.left, req.right, opts.paths
196 );
197 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
198 self.baby(&ctx)
199 .diff_stats(&req, &opts.into())
200 .await
201 .map_err(map_baby)
202 })
203 .await
126204 }
127205
128206 pub async fn diff_files(
@@ -132,13 +210,57 @@
132210 base: String,
133211 head: String,
134212 opts: DiffOptionsDto,
135 ) -> Result<Vec<ChangedPathDiff>, GitError> {
213 ) -> Result<Vec<ChangedPathDiffDto>, GitError> {
136214 let ctx = self.context(space_name, repo_name).await?;
137215 let req = self.diff_request(&ctx, &base, &head).await?;
138 self.baby(&ctx)
139 .commit_diff(&req, &opts.into(), None)
140 .await
141 .map_err(map_baby)
216 let key = format!(
217 "repo.{}.diff_files_dto.{}.{}:{:?}",
218 ctx.model.id, req.left, req.right, opts.paths
219 );
220 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
221 let list = self
222 .baby(&ctx)
223 .commit_diff(&req, &opts.into(), None)
224 .await
225 .map_err(map_baby)?
226 .into_iter()
227 .map(ChangedPathDiffDto::from)
228 .collect::<Vec<_>>();
229 Ok(list)
230 })
231 .await
232 }
233
234 pub async fn commit_diff_for_html(
235 &self,
236 ctx: &std::sync::Arc<crate::context::facade::GitContext>,
237 commit: &str,
238 ) -> Result<(String, Vec<ChangedPathDiffDto>), GitError> {
239 const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
240 let baby = self.baby(ctx);
241 let head_oid = baby.rev_parse(commit).await.map_err(map_baby)?;
242 let info = baby.get_commit(&head_oid.to_string()).await.map_err(map_baby)?;
243 let base = info
244 .parents
245 .first()
246 .map(|p| p.to_string())
247 .unwrap_or_else(|| EMPTY_TREE.to_string());
248 let req = self
249 .diff_request(ctx, &base, &head_oid.to_string())
250 .await?;
251 let key = format!("repo.{}.diff_files_dto.{}.{:?}", ctx.model.id, req.left, None::<Vec<std::path::PathBuf>>);
252 let files = cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
253 let list = baby
254 .commit_diff(&req, &DiffOptions::default(), None)
255 .await
256 .map_err(map_baby)?
257 .into_iter()
258 .map(ChangedPathDiffDto::from)
259 .collect::<Vec<_>>();
260 Ok(list)
261 })
262 .await?;
263 Ok((base, files))
142264 }
143265
144266 pub async fn diff_for_commit(
@@ -149,10 +271,14 @@
149271 ) -> Result<Vec<ChangedPath>, GitError> {
150272 let ctx = self.context(space_name, repo_name).await?;
151273 let oid = parse_oid(&commit)?;
152 self.baby(&ctx)
153 .find_changed_paths(vec![ChangedPathRequest::Commit(oid)], None, None, false)
154 .await
155 .map_err(map_baby)
274 let key = format!("repo.{}.diff_for_commit.{}", ctx.model.id, oid);
275 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
276 self.baby(&ctx)
277 .find_changed_paths(vec![ChangedPathRequest::Commit(oid)], None, None, false)
278 .await
279 .map_err(map_baby)
280 })
281 .await
156282 }
157283
158284 pub async fn diff_tree(
@@ -171,33 +297,40 @@
171297 } else {
172298 baby.rev_parse(&base).await.map_err(map_baby)?.to_string()
173299 };
174 let right = baby.rev_parse(&head).await.map_err(map_baby)?;
175 let rows = baby
176 .diff_trees(
177 &left,
178 &right.to_string(),
179 std::path::Path::new(""),
180 gitbaby::tree::types::DiffTreesOptions {
181 recurse: true,
182 detect_renames: true,
183 ..Default::default()
184 },
185 )
186 .await
187 .map_err(map_baby)?;
188 Ok(rows
189 .into_iter()
190 .map(|d| DiffTreeRecordDto {
191 status: format!("{:?}", d.status).to_lowercase(),
192 score: 0,
193 head_path: d.new_path.map(|p| p.to_string_lossy().into_owned()),
194 base_path: d.old_path.map(|p| p.to_string_lossy().into_owned()),
195 head_mode: d.new_mode.map(|m| m as u32),
196 base_mode: d.old_mode.map(|m| m as u32),
197 head_blob_id: d.new_oid.map(|o| o.to_string()),
198 base_blob_id: d.old_oid.map(|o| o.to_string()),
199 })
200 .collect())
300 let right = baby.rev_parse(&head).await.map_err(map_baby)?.to_string();
301 let key = format!(
302 "repo.{}.diff_tree.{}.{}:{}",
303 ctx.model.id, left, right, use_merge_base
304 );
305 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
306 let rows = baby
307 .diff_trees(
308 &left,
309 &right,
310 std::path::Path::new(""),
311 gitbaby::tree::types::DiffTreesOptions {
312 recurse: true,
313 detect_renames: true,
314 ..Default::default()
315 },
316 )
317 .await
318 .map_err(map_baby)?;
319 Ok(rows
320 .into_iter()
321 .map(|d| DiffTreeRecordDto {
322 status: format!("{:?}", d.status).to_lowercase(),
323 score: 0,
324 head_path: d.new_path.map(|p| p.to_string_lossy().into_owned()),
325 base_path: d.old_path.map(|p| p.to_string_lossy().into_owned()),
326 head_mode: d.new_mode.map(|m| m as u32),
327 base_mode: d.old_mode.map(|m| m as u32),
328 head_blob_id: d.new_oid.map(|o| o.to_string()),
329 base_blob_id: d.old_oid.map(|o| o.to_string()),
330 })
331 .collect())
332 })
333 .await
201334 }
202335
203336 pub async fn range_diff(
Modified lib/git/src/service/logs.rs View lib/git/src/service/logs.rs
@@ -1,16 +1,18 @@
11use gitbaby::commit::types::{CommitInfo, ListCommitsOptions};
2use serde::{Deserialize, Serialize};
23
4use crate::service::cache::{self, IMMUTABLE_TTL};
35use crate::service::GitService;
46use crate::share::errors::{GitError, map_baby};
57
6#[derive(Debug, Clone, serde::Serialize)]
8#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct LogListDto {
810 pub commits: Vec<CommitInfo>,
911 pub cursor: Option<String>,
1012 pub has_more: bool,
1113}
1214
13#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
15#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
1416pub struct LogOptions {
1517 #[serde(default)]
1618 pub revision: Option<String>,
@@ -20,7 +22,7 @@
2022 pub skip: Option<u64>,
2123}
2224
23#[derive(Debug, Clone, serde::Serialize)]
25#[derive(Debug, Clone, Serialize, Deserialize)]
2426pub struct GraphNode {
2527 pub commit: CommitInfo,
2628 pub lanes: Vec<usize>,
@@ -44,34 +46,41 @@
4446 let revision = opts.revision.clone().unwrap_or_else(|| "HEAD".to_string());
4547 let ctx = self.context(space_name, repo_name).await?;
4648 let baby = self.baby(&ctx);
49 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
4750
48 let max = limit + skip + 1;
49 let max_count = if max <= u32::MAX as u64 {
50 Some(max as u32)
51 } else {
52 None
53 };
54 let commits = baby
55 .list_commits(ListCommitsOptions {
56 revision: revision.clone(),
57 max_count,
58 ..Default::default()
51 let key = format!(
52 "repo.{}.log_list.{}:{}:{}",
53 ctx.model.id, rev_oid, skip, limit
54 );
55 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
56 let max = limit + skip + 1;
57 let max_count = if max <= u32::MAX as u64 {
58 Some(max as u32)
59 } else {
60 None
61 };
62 let commits = baby
63 .list_commits(ListCommitsOptions {
64 revision: rev_oid.to_string(),
65 max_count,
66 ..Default::default()
67 })
68 .await
69 .map_err(map_baby)?;
70 let has_more = commits.len() > limit as usize + skip as usize;
71 let mut page = commits;
72 if page.len() > limit as usize + skip as usize {
73 page.truncate(limit as usize + skip as usize);
74 }
75 let page = page.into_iter().skip(skip as usize).collect::<Vec<_>>();
76 let cursor = page.last().map(|c| c.id.to_string());
77 Ok(LogListDto {
78 commits: page,
79 cursor: if has_more { cursor } else { None },
80 has_more,
5981 })
60 .await
61 .map_err(map_baby)?;
62 let has_more = commits.len() > limit as usize + skip as usize;
63 let mut page = commits;
64 if page.len() > limit as usize + skip as usize {
65 page.truncate(limit as usize + skip as usize);
66 }
67 let page = page.into_iter().skip(skip as usize).collect::<Vec<_>>();
68 let cursor = page.last().map(|c| c.id.to_string());
69 let _ = revision;
70 Ok(LogListDto {
71 commits: page,
72 cursor: if has_more { cursor } else { None },
73 has_more,
7482 })
83 .await
7584 }
7685
7786 pub async fn log_commits_between(
@@ -85,19 +94,29 @@
8594 let limit = limit.clamp(1, MAX_LOG_LIMIT);
8695 let ctx = self.context(space_name, repo_name).await?;
8796 let baby = self.baby(&ctx);
88 let commits = baby
89 .list_commits(ListCommitsOptions {
90 revision: format!("{after}..{before}"),
91 max_count: Some(limit as u32),
92 ..Default::default()
97 let after_oid = baby.rev_parse(&after).await.map_err(map_baby)?;
98 let before_oid = baby.rev_parse(&before).await.map_err(map_baby)?;
99
100 let key = format!(
101 "repo.{}.log_between.{}.{}:{}",
102 ctx.model.id, after_oid, before_oid, limit
103 );
104 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
105 let commits = baby
106 .list_commits(ListCommitsOptions {
107 revision: format!("{after_oid}..{before_oid}"),
108 max_count: Some(limit as u32),
109 ..Default::default()
110 })
111 .await
112 .map_err(map_baby)?;
113 Ok(LogListDto {
114 commits,
115 cursor: None,
116 has_more: false,
93117 })
94 .await
95 .map_err(map_baby)?;
96 Ok(LogListDto {
97 commits,
98 cursor: None,
99 has_more: false,
100118 })
119 .await
101120 }
102121
103122 pub async fn log_last_commit(
@@ -108,7 +127,12 @@
108127 ) -> Result<CommitInfo, GitError> {
109128 let ctx = self.context(space_name, repo_name).await?;
110129 let baby = self.baby(&ctx);
111 baby.get_commit(&branch).await.map_err(map_baby)
130 let rev_oid = baby.rev_parse(&branch).await.map_err(map_baby)?;
131 let key = format!("repo.{}.commit.{}", ctx.model.id, rev_oid);
132 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
133 baby.get_commit(&branch).await.map_err(map_baby)
134 })
135 .await
112136 }
113137
114138 /// Commit graph; `lanes` are placeholders (matching the reference impl).
Modified lib/git/src/service/patches.rs View lib/git/src/service/patches.rs
@@ -1,23 +1,25 @@
1use serde::Serialize;
1use serde::{Deserialize, Serialize};
22use time::OffsetDateTime;
33
4use crate::service::cache::{self, IMMUTABLE_TTL};
5use crate::service::gitcmd::run_git_soft;
46use crate::service::GitService;
5use crate::service::gitcmd::run_git_soft;
67use crate::share::errors::{GitError, map_baby};
7#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
8
9#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
810pub struct PatchDto {
911 pub content: String,
1012 pub size: i64,
1113}
1214
13#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
15#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1416pub struct ParsedPatchDto {
1517 pub files: Vec<String>,
1618 pub additions: i64,
1719 pub deletions: i64,
1820}
1921
20#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
22#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
2123pub struct AppliedPatchDto {
2224 pub applied: bool,
2325 pub conflicts: Vec<String>,
@@ -32,22 +34,28 @@
3234 head: String,
3335 ) -> Result<PatchDto, GitError> {
3436 let ctx = self.context(space_name, repo_name).await?;
35 let body = self
36 .baby(&ctx)
37 .raw_diff(
38 &gitbaby::diff::types::DiffRequest {
39 left: self.baby(&ctx).rev_parse(&base).await.map_err(map_baby)?,
40 right: self.baby(&ctx).rev_parse(&head).await.map_err(map_baby)?,
41 },
42 None,
43 )
44 .await
45 .map_err(map_baby)?;
46 let size = body.len() as i64;
47 Ok(PatchDto {
48 content: body,
49 size,
37 let baby = self.baby(&ctx);
38 let left = baby.rev_parse(&base).await.map_err(map_baby)?;
39 let right = baby.rev_parse(&head).await.map_err(map_baby)?;
40 let key = format!("repo.{}.patch_generate.{}.{}", ctx.model.id, left, right);
41 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
42 let body = baby
43 .raw_diff(
44 &gitbaby::diff::types::DiffRequest {
45 left,
46 right,
47 },
48 None,
49 )
50 .await
51 .map_err(map_baby)?;
52 let size = body.len() as i64;
53 Ok(PatchDto {
54 content: body,
55 size,
56 })
5057 })
58 .await
5159 }
5260
5361 pub async fn patch_parse(&self, content: String) -> Result<ParsedPatchDto, GitError> {
Modified lib/git/src/service/path_log.rs View lib/git/src/service/path_log.rs
@@ -1,12 +1,13 @@
11use std::path::{Path, PathBuf};
22
33use gitbaby::commit::types::ListCommitsOptions;
4use serde::Serialize;
4use serde::{Deserialize, Serialize};
55
6use crate::service::cache::{self, IMMUTABLE_TTL};
67use crate::service::GitService;
78use crate::share::errors::{GitError, map_baby};
89
9#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
10#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1011pub struct PathLogEntryDto {
1112 pub path: String,
1213 pub short_sha: String,
@@ -89,44 +90,48 @@
8990 .map_err(map_baby)?
9091 .to_string();
9192
92 let entries = baby
93 .get_tree_entries(&oid, &target_dir)
94 .await
95 .map_err(map_baby)?;
96
97 let mut out = Vec::with_capacity(entries.len());
98 for entry in &entries {
99 if entry.name.starts_with('.') {
100 continue;
101 }
102 let entry_path = match target_dir.to_str() {
103 Some("") | Some(".") => entry.name.clone(),
104 Some(d) if d.ends_with('/') => format!("{d}{}", entry.name),
105 Some(d) => format!("{d}/{}", entry.name),
106 None => continue,
107 };
108 let commits = baby
109 .list_commits(ListCommitsOptions {
110 revision: oid.clone(),
111 path: Some(PathBuf::from(&entry_path)),
112 max_count: Some(1),
113 ..Default::default()
114 })
93 let key = format!("repo.{}.path_log.{}.{}", ctx.model.id, oid, target_dir.display());
94 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
95 let entries = baby
96 .get_tree_entries(&oid, &target_dir)
11597 .await
11698 .map_err(map_baby)?;
117 let Some(commit) = commits.into_iter().next() else {
118 continue;
119 };
120 let hex = commit.id.to_string();
121 let short_sha = hex.get(..7).unwrap_or("").to_string();
122 out.push(PathLogEntryDto {
123 path: entry_path,
124 short_sha,
125 date: iso_date(&commit.committer.time),
126 author: commit.author.name.clone(),
127 subject: first_line(&commit.message),
128 });
129 }
130 Ok(out)
99
100 let mut out = Vec::with_capacity(entries.len());
101 for entry in &entries {
102 if entry.name.starts_with('.') {
103 continue;
104 }
105 let entry_path = match target_dir.to_str() {
106 Some("") | Some(".") => entry.name.clone(),
107 Some(d) if d.ends_with('/') => format!("{d}{}", entry.name),
108 Some(d) => format!("{d}/{}", entry.name),
109 None => continue,
110 };
111 let commits = baby
112 .list_commits(ListCommitsOptions {
113 revision: oid.clone(),
114 path: Some(PathBuf::from(&entry_path)),
115 max_count: Some(1),
116 ..Default::default()
117 })
118 .await
119 .map_err(map_baby)?;
120 let Some(commit) = commits.into_iter().next() else {
121 continue;
122 };
123 let hex = commit.id.to_string();
124 let short_sha = hex.get(..7).unwrap_or("").to_string();
125 out.push(PathLogEntryDto {
126 path: entry_path,
127 short_sha,
128 date: iso_date(&commit.committer.time),
129 author: commit.author.name.clone(),
130 subject: first_line(&commit.message),
131 });
132 }
133 Ok(out)
134 })
135 .await
131136 }
132137}
Modified lib/git/src/service/repos.rs View lib/git/src/service/repos.rs
@@ -64,7 +64,11 @@
6464 name: m.name,
6565 description: m.description,
6666 website: m.website,
67 default_branch: m.default_branch,
67 default_branch: m
68 .default_branch
69 .strip_prefix("refs/heads/")
70 .unwrap_or(&m.default_branch)
71 .to_string(),
6872 nums_watch: m.nums_watch,
6973 nums_star: m.nums_star,
7074 nums_pr: m.nums_pr,
@@ -297,13 +301,21 @@
297301 .await;
298302
299303 if let Err(err) = init_result {
300 let _ = tokio::fs::remove_dir_all(&repo_dir).await;
301 let _ = repos::Entity::delete_by_id(repo.id)
304 if let Err(e) = tokio::fs::remove_dir_all(&repo_dir).await {
305 tracing::warn!(error = %e, "failed to clean up repo dir after init failure");
306 }
307 if let Err(e) = repos::Entity::delete_by_id(repo.id)
302308 .exec(self.db_write())
303 .await;
304 let _ = dirs::Entity::delete_by_id(dir.id)
309 .await
310 {
311 tracing::warn!(error = %e, "failed to clean up repo row after init failure");
312 }
313 if let Err(e) = dirs::Entity::delete_by_id(dir.id)
305314 .exec(self.db_write())
306 .await;
315 .await
316 {
317 tracing::warn!(error = %e, "failed to clean up dir row after init failure");
318 }
307319 return Err(err);
308320 }
309321
Modified lib/git/src/service/signatures.rs View lib/git/src/service/signatures.rs
@@ -1,10 +1,11 @@
1use serde::Serialize;
1use serde::{Deserialize, Serialize};
22
3use crate::service::cache::{self, IMMUTABLE_TTL};
4use crate::service::gitcmd::run_git_soft;
35use crate::service::GitService;
4use crate::service::gitcmd::run_git_soft;
56use crate::share::errors::{GitError, map_baby};
67
7#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
8#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
89pub struct VerifyResultDto {
910 pub signed: bool,
1011 pub valid: bool,
@@ -29,49 +30,53 @@
2930 )));
3031 }
3132 let ctx = self.context(space_name, repo_name).await?;
32 let timeout = self.state.config.git.git_command_timeout_secs;
33 let out = run_git_soft(
34 "signatures::verify_commit",
35 &ctx.repo_dir(),
36 vec![
37 "verify-commit".to_string(),
38 "--raw".to_string(),
39 sha_trim.to_string(),
40 ],
41 &[],
42 timeout,
43 )
44 .await?;
45 let stdout = String::from_utf8_lossy(&out.stdout);
46 let mut valid = false;
47 let mut signer: Option<String> = None;
48 let mut key_id: Option<String> = None;
49 for line in stdout.lines() {
50 if line.starts_with("good ") || line.starts_with("valid ") {
51 valid = true;
52 }
53 if let Some(idx) = line.find("signer \"") {
54 let rest = &line[idx + 8..];
55 if let Some(end) = rest.find('"') {
56 signer = Some(rest[..end].to_string());
33 let key = format!("repo.{}.sig_verify_commit.{}", ctx.model.id, sha_trim);
34 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
35 let timeout = self.state.config.git.git_command_timeout_secs;
36 let out = run_git_soft(
37 "signatures::verify_commit",
38 &ctx.repo_dir(),
39 vec![
40 "verify-commit".to_string(),
41 "--raw".to_string(),
42 sha_trim.to_string(),
43 ],
44 &[],
45 timeout,
46 )
47 .await?;
48 let stdout = String::from_utf8_lossy(&out.stdout);
49 let mut valid = false;
50 let mut signer: Option<String> = None;
51 let mut key_id: Option<String> = None;
52 for line in stdout.lines() {
53 if line.starts_with("good ") || line.starts_with("valid ") {
54 valid = true;
5755 }
56 if let Some(idx) = line.find("signer \"") {
57 let rest = &line[idx + 8..];
58 if let Some(end) = rest.find('"') {
59 signer = Some(rest[..end].to_string());
60 }
61 }
62 if let Some(idx) = line.find("key ") {
63 key_id = Some(line[idx + 4..].trim().to_string());
64 }
5865 }
59 if let Some(idx) = line.find("key ") {
60 key_id = Some(line[idx + 4..].trim().to_string());
61 }
62 }
63 let error_message = if !out.status.success() {
64 Some(String::from_utf8_lossy(&out.stderr).trim().to_string())
65 } else {
66 None
67 };
68 Ok(VerifyResultDto {
69 signed: valid || key_id.is_some() || signer.is_some(),
70 valid,
71 key_id,
72 signer,
73 error_message,
66 let error_message = if !out.status.success() {
67 Some(String::from_utf8_lossy(&out.stderr).trim().to_string())
68 } else {
69 None
70 };
71 Ok(VerifyResultDto {
72 signed: valid || key_id.is_some() || signer.is_some(),
73 valid,
74 key_id,
75 signer,
76 error_message,
77 })
7478 })
79 .await
7580 }
7681
7782 pub async fn signature_verify_tag(
@@ -81,17 +86,21 @@
8186 name: String,
8287 ) -> Result<VerifyResultDto, GitError> {
8388 let ctx = self.context(space_name, repo_name).await?;
84 let res = self
85 .baby(&ctx)
86 .verify_tag_signature(&name)
87 .await
88 .map_err(map_baby)?;
89 Ok(VerifyResultDto {
90 signed: res.valid || res.signer.is_some(),
91 valid: res.valid,
92 key_id: res.fingerprint,
93 signer: res.signer,
94 error_message: res.error_message,
89 let key = format!("repo.{}.sig_verify_tag.{}", ctx.model.id, name);
90 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
91 let res = self
92 .baby(&ctx)
93 .verify_tag_signature(&name)
94 .await
95 .map_err(map_baby)?;
96 Ok(VerifyResultDto {
97 signed: res.valid || res.signer.is_some(),
98 valid: res.valid,
99 key_id: res.fingerprint,
100 signer: res.signer,
101 error_message: res.error_message,
102 })
95103 })
104 .await
96105 }
97106}
Modified lib/git/src/service/submodules.rs View lib/git/src/service/submodules.rs
@@ -1,12 +1,13 @@
11use std::collections::HashMap;
22
33use gitbaby::commit::types::TreeEntryMode;
4use serde::Serialize;
4use serde::{Deserialize, Serialize};
55
6use crate::service::cache::{self, IMMUTABLE_TTL};
67use crate::service::GitService;
78use crate::share::errors::{GitError, map_baby};
89
9#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
10#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1011pub struct SubmoduleDto {
1112 pub path: String,
1213 pub url: String,
@@ -23,31 +24,36 @@
2324 ) -> Result<Vec<SubmoduleDto>, GitError> {
2425 let ctx = self.context(space_name, repo_name).await?;
2526 let baby = self.baby(&ctx);
26 let entries = baby
27 .get_tree_entries(&revision, std::path::Path::new(""))
28 .await
29 .map_err(map_baby)?;
27 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
28 let key = format!("repo.{}.submodules.{}", ctx.model.id, rev_oid);
29 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
30 let entries = baby
31 .get_tree_entries(&revision, std::path::Path::new(""))
32 .await
33 .map_err(map_baby)?;
3034
31 let mut meta: HashMap<String, (String, String)> = HashMap::new();
32 if let Ok(submodules) = baby.list_submodules(&revision).await {
33 for s in submodules {
34 meta.insert(s.path.clone(), (s.url.clone(), s.branch.clone()));
35 let mut meta: HashMap<String, (String, String)> = HashMap::new();
36 if let Ok(submodules) = baby.list_submodules(&revision).await {
37 for s in submodules {
38 meta.insert(s.path.clone(), (s.url.clone(), s.branch.clone()));
39 }
3540 }
36 }
3741
38 let mut out = Vec::new();
39 for e in entries {
40 if e.mode == TreeEntryMode::Submodule {
41 let (url, branch) = meta.get(&e.name).cloned().unwrap_or_default();
42 out.push(SubmoduleDto {
43 path: e.name.clone(),
44 url,
45 branch: Some(branch).filter(|b| !b.is_empty()),
46 commit_sha: Some(e.oid.to_string()),
47 });
42 let mut out = Vec::new();
43 for e in entries {
44 if e.mode == TreeEntryMode::Submodule {
45 let (url, branch) = meta.get(&e.name).cloned().unwrap_or_default();
46 out.push(SubmoduleDto {
47 path: e.name.clone(),
48 url,
49 branch: Some(branch).filter(|b| !b.is_empty()),
50 commit_sha: Some(e.oid.to_string()),
51 });
52 }
4853 }
49 }
50 Ok(out)
54 Ok(out)
55 })
56 .await
5157 }
5258
5359 pub async fn submodule_resolve(
@@ -59,14 +65,19 @@
5965 ) -> Result<String, GitError> {
6066 let ctx = self.context(space_name, repo_name).await?;
6167 let baby = self.baby(&ctx);
62 let entries = baby
63 .get_tree_entries(&revision, std::path::Path::new(""))
64 .await
65 .map_err(map_baby)?;
66 entries
67 .into_iter()
68 .find(|e| e.name == path && e.mode == TreeEntryMode::Submodule)
69 .map(|e| e.oid.to_string())
70 .ok_or(GitError::NotFound)
68 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
69 let key = format!("repo.{}.submodule_resolve.{}.{}", ctx.model.id, rev_oid, path);
70 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
71 let entries = baby
72 .get_tree_entries(&revision, std::path::Path::new(""))
73 .await
74 .map_err(map_baby)?;
75 entries
76 .into_iter()
77 .find(|e| e.name == path && e.mode == TreeEntryMode::Submodule)
78 .map(|e| e.oid.to_string())
79 .ok_or(GitError::NotFound)
80 })
81 .await
7182 }
7283}
Modified lib/git/src/service/tags.rs View lib/git/src/service/tags.rs
@@ -11,13 +11,14 @@
1111use time::OffsetDateTime;
1212
1313use crate::context::facade::GitContext;
14use crate::service::cache::{self, IMMUTABLE_TTL};
1415use crate::service::GitService;
1516use crate::share::errors::{GitError, map_baby};
1617use crate::share::policy::load_tag_policy;
1718
1819const CALLER: &str = "gitdata::git:tags";
1920
20#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
21#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
2122pub struct TagDto {
2223 pub name: String,
2324 pub tag_type: TagType,
@@ -38,7 +39,7 @@
3839 pub annotated: bool,
3940}
4041
41#[derive(Debug, Clone)]
42#[derive(Debug, Clone, Serialize, Deserialize)]
4243struct RawTagMeta {
4344 target_sha: String,
4445 is_annotated: bool,
@@ -111,29 +112,33 @@
111112}
112113
113114async fn read_tag_meta(ctx: &GitContext, name: &str) -> Result<RawTagMeta, GitError> {
114 let dir = ctx.repo_dir();
115 let (ok, stdout, stderr) = run_git(&dir, &["show-ref", "--tags", "--", name]).await?;
116 if !ok {
117 let s = stderr.trim();
118 if s.contains("not found") || stdout.is_empty() {
119 return Err(GitError::NotFound);
115 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, name);
116 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
117 let dir = ctx.repo_dir();
118 let (ok, stdout, stderr) = run_git(&dir, &["show-ref", "--tags", "--", name]).await?;
119 if !ok {
120 let s = stderr.trim();
121 if s.contains("not found") || stdout.is_empty() {
122 return Err(GitError::NotFound);
123 }
124 return Err(GitError::Internal(format!("show-ref {name} failed: {s}")));
125 }
126 let oid = stdout
127 .split_whitespace()
128 .next()
129 .ok_or(GitError::NotFound)?
130 .to_string();
131 let (ok, stdout, stderr) = run_git(&dir, &["cat-file", "-p", &oid]).await?;
132 if !ok {
133 return Err(GitError::Internal(format!(
134 "cat-file {oid} failed: {}",
135 stderr.trim()
136 )));
120137 }
121 return Err(GitError::Internal(format!("show-ref {name} failed: {s}")));
122 }
123 let oid = stdout
124 .split_whitespace()
125 .next()
126 .ok_or(GitError::NotFound)?
127 .to_string();
128 let (ok, stdout, stderr) = run_git(&dir, &["cat-file", "-p", &oid]).await?;
129 if !ok {
130 return Err(GitError::Internal(format!(
131 "cat-file {oid} failed: {}",
132 stderr.trim()
133 )));
134 }
135 parse_cat_file(&stdout, &oid)
136 .ok_or_else(|| GitError::Internal(format!("parse tag object {oid} for `{name}` failed")))
138 parse_cat_file(&stdout, &oid)
139 .ok_or_else(|| GitError::Internal(format!("parse tag object {oid} for `{name}` failed")))
140 })
141 .await
137142}
138143
139144impl GitService {
@@ -172,6 +177,9 @@
172177 baby.create_lightweight_tag(opts).await.map_err(map_baby)?;
173178 }
174179
180 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, cmd.name);
181 let _ = ctx.cache_remove(&key).await;
182
175183 let meta = read_tag_meta(&ctx, &cmd.name).await?;
176184 let row = git_tags::ActiveModel {
177185 id: Set(uuid::Uuid::now_v7()),
@@ -299,6 +307,8 @@
299307 }
300308 let baby = self.baby(&ctx);
301309 baby.delete_tag(&name).await.map_err(map_baby)?;
310 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, name);
311 let _ = ctx.cache_remove(&key).await;
302312 git_tags::Entity::delete_many()
303313 .filter(git_tags::Column::RepoId.eq(ctx.model.id))
304314 .filter(git_tags::Column::Name.eq(&name))
Modified lib/git/src/service/trees.rs View lib/git/src/service/trees.rs
@@ -2,12 +2,14 @@
22
33use gitbaby::commit::types::{TreeEntry, TreeEntryMode};
44use gitbaby::tree::types::WalkTreeOptions;
5use serde::{Deserialize, Serialize};
56
7use crate::service::cache::{self, IMMUTABLE_TTL};
68use crate::service::GitService;
79use crate::share::errors::{GitError, map_baby};
810use crate::share::validate::validate_revision;
911
10#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
12#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
1113pub struct PathLastUpdated {
1214 pub path: String,
1315 pub object_type: i8,
@@ -34,9 +36,14 @@
3436 validate_revision(&revision)?;
3537 let ctx = self.context(space_name, repo_name).await?;
3638 let baby = self.baby(&ctx);
37 baby.get_tree_entries(&revision, &path)
38 .await
39 .map_err(map_baby)
39 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
40 let key = format!("repo.{}.tree_read.{}.{}", ctx.model.id, rev_oid, path.display());
41 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
42 baby.get_tree_entries(&revision, &path)
43 .await
44 .map_err(map_baby)
45 })
46 .await
4047 }
4148
4249 pub async fn tree_resolve_path(
@@ -49,15 +56,16 @@
4956 validate_revision(&revision)?;
5057 let ctx = self.context(space_name, repo_name).await?;
5158 let baby = self.baby(&ctx);
52 let entries = baby
53 .get_tree_entries(&revision, std::path::Path::new(""))
54 .await
55 .map_err(map_baby)?;
5659 let name = path.trim().trim_start_matches("./").to_string();
57 entries
58 .into_iter()
59 .find(|e| e.name == name)
60 .ok_or(GitError::NotFound)
60 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
61 let key = format!("repo.{}.tree_resolve.{}.{}", ctx.model.id, rev_oid, name);
62 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
63 baby.tree_entry_at(&revision, std::path::Path::new(&name))
64 .await
65 .map_err(map_baby)?
66 .ok_or(GitError::NotFound)
67 })
68 .await
6169 }
6270
6371 pub async fn tree_walk(
@@ -71,16 +79,24 @@
7179 validate_revision(&revision)?;
7280 let ctx = self.context(space_name, repo_name).await?;
7381 let baby = self.baby(&ctx);
74 baby.walk_tree(
75 &revision,
76 std::path::Path::new(""),
77 WalkTreeOptions {
78 recurse,
79 show_trees,
80 },
81 )
82 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
83 let key = format!(
84 "repo.{}.tree_walk.{}:{}:{}",
85 ctx.model.id, rev_oid, recurse, show_trees
86 );
87 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
88 baby.walk_tree(
89 &revision,
90 std::path::Path::new(""),
91 WalkTreeOptions {
92 recurse,
93 show_trees,
94 },
95 )
96 .await
97 .map_err(map_baby)
98 })
8299 .await
83 .map_err(map_baby)
84100 }
85101
86102 pub async fn tree_follow(
@@ -121,10 +137,10 @@
121137 target_path = parent.join(target_path);
122138 }
123139 let target_entry = baby
124 .get_tree_entries(&revision, &target_path)
140 .tree_entry_at(&revision, &target_path)
125141 .await
126142 .ok()
127 .and_then(|entries| entries.into_iter().next())
143 .flatten()
128144 .filter(|_| !target.is_empty());
129145 Ok((entry, Some(target), target_entry))
130146 }
@@ -134,23 +150,30 @@
134150 space_name: String,
135151 repo_name: String,
136152 revision: String,
153 path: Option<String>,
137154 ) -> Result<Vec<PathLastUpdated>, GitError> {
138155 validate_revision(&revision)?;
139156 let ctx = self.context(space_name, repo_name).await?;
140157 let baby = self.baby(&ctx);
141 let rows = baby
142 .tree_entries_with_latest(&revision, None)
143 .await
144 .map_err(map_baby)?;
145 Ok(rows
146 .into_iter()
147 .map(|row| PathLastUpdated {
148 path: entry_full_path(&row.entry),
149 object_type: mode_to_i8(row.entry.mode),
150 last_updated: row.latest.author.time.unix_timestamp(),
151 commit_id: row.latest.id.to_string(),
152 })
153 .collect())
158 let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?;
159 let path_str = path.as_deref().unwrap_or("");
160 let key = format!("repo.{}.tree_last_updated.{}.{}", ctx.model.id, rev_oid, path_str);
161 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
162 let rows = baby
163 .tree_entries_with_latest(&revision, path.as_deref())
164 .await
165 .map_err(map_baby)?;
166 Ok(rows
167 .into_iter()
168 .map(|row| PathLastUpdated {
169 path: entry_full_path(&row.entry),
170 object_type: mode_to_i8(row.entry.mode),
171 last_updated: row.latest.author.time.unix_timestamp(),
172 commit_id: row.latest.id.to_string(),
173 })
174 .collect())
175 })
176 .await
154177 }
155178
156179 pub async fn tree_entries_with_latest_html(
@@ -160,9 +183,14 @@
160183 ) -> Result<Vec<gitbaby::tree::types::TreeEntryWithLatest>, GitError> {
161184 validate_revision(revision)?;
162185 let baby = self.baby(ctx);
163 baby.tree_entries_with_latest(revision, None)
164 .await
165 .map_err(map_baby)
186 let rev_oid = baby.rev_parse(revision).await.map_err(map_baby)?;
187 let key = format!("repo.{}.tree_entries_latest_html.{}", ctx.model.id, rev_oid);
188 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
189 baby.tree_entries_with_latest(revision, None)
190 .await
191 .map_err(map_baby)
192 })
193 .await
166194 }
167195
168196 pub async fn tree_with_commits_html(
@@ -176,10 +204,15 @@
176204 return Err(GitError::InvalidArgument("invalid path".into()));
177205 }
178206 let baby = self.baby(ctx);
179 let p = if path.is_empty() { None } else { Some(path) };
180 baby.tree_entries_with_latest(revision, p)
181 .await
182 .map_err(map_baby)
207 let rev_oid = baby.rev_parse(revision).await.map_err(map_baby)?;
208 let key = format!("repo.{}.tree_dir_entries_html.{}.{}", ctx.model.id, rev_oid, path);
209 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
210 let p = if path.is_empty() { None } else { Some(path) };
211 baby.tree_dir_entries_with_latest(revision, p)
212 .await
213 .map_err(map_baby)
214 })
215 .await
183216 }
184217}
185218