Skip to content

Commit 0306700

Browse files
231220075genedna
andauthored
feat: Add tagger information to tags and enable code browsing by tag (#1532) (#1550)
* feat: Add tagger information to tags and enable code browsing by tag Signed-off-by: Ruizhi Huang <[email protected]> format code Signed-off-by: Ruizhi Huang <[email protected]> fix problems Signed-off-by: Ruizhi Huang <[email protected]> cargo fmt Signed-off-by: Ruizhi Huang <[email protected]> * fix: mr -> cl Signed-off-by: Ruizhi Huang <[email protected]> fix: mr -> cl Signed-off-by: Ruizhi Huang <[email protected]> * fix reviews Signed-off-by: Ruizhi Huang <[email protected]> fix errors and restore generated.ts Signed-off-by: Ruizhi Huang <[email protected]> --------- Signed-off-by: Ruizhi Huang <[email protected]> Co-authored-by: Quanyi Ma <[email protected]>
1 parent 3828ff6 commit 0306700

File tree

14 files changed

+306
-55
lines changed

14 files changed

+306
-55
lines changed

ceres/src/api_service/mod.rs

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,12 @@ pub trait ApiHandler: Send + Sync {
170170
}
171171
}
172172

173-
async fn get_tree_info(&self, path: &Path) -> Result<Vec<TreeBriefItem>, GitError> {
174-
match self.search_tree_by_path(path).await? {
173+
async fn get_tree_info(
174+
&self,
175+
path: &Path,
176+
refs: Option<&str>,
177+
) -> Result<Vec<TreeBriefItem>, GitError> {
178+
match self.search_tree_by_path_with_refs(path, refs).await? {
175179
Some(tree) => {
176180
let items = tree
177181
.tree_items
@@ -189,14 +193,61 @@ pub trait ApiHandler: Send + Sync {
189193
}
190194
}
191195

192-
async fn get_tree_commit_info(&self, path: PathBuf) -> Result<Vec<TreeCommitItem>, GitError> {
193-
let item_to_commit_map = self.item_to_commit_map(path).await?;
196+
async fn get_tree_commit_info(
197+
&self,
198+
path: PathBuf,
199+
refs: Option<&str>,
200+
) -> Result<Vec<TreeCommitItem>, GitError> {
201+
// If refs provided, resolve to commit and list items from that commit's tree at path,
202+
// attaching the same commit info to each item for consistent, minimal behavior.
203+
let maybe = refs.unwrap_or("").trim();
204+
if !maybe.is_empty() {
205+
// Resolve commit from refs (SHA or tag)
206+
let is_hex_sha1 = |s: &str| s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit());
207+
let mut commit_hash = String::new();
208+
if is_hex_sha1(maybe) {
209+
commit_hash = maybe.to_string();
210+
} else if let Ok(Some(tag)) = self.get_tag(None, maybe.to_string()).await {
211+
commit_hash = tag.object_id;
212+
}
194213

214+
if !commit_hash.is_empty() {
215+
if let Some(commit) = self.get_commit_by_hash(&commit_hash).await {
216+
// Use refs-aware tree search
217+
if let Some(tree) = self.search_tree_by_path_with_refs(&path, refs).await? {
218+
let mut items: Vec<TreeCommitItem> = tree
219+
.tree_items
220+
.into_iter()
221+
.map(|item| TreeCommitItem::from((item, Some(commit.clone()))))
222+
.collect();
223+
items.sort_by(|a, b| {
224+
a.content_type
225+
.cmp(&b.content_type)
226+
.then(a.name.cmp(&b.name))
227+
});
228+
return Ok(items);
229+
} else {
230+
// path not found under this refs
231+
return Ok(vec![]);
232+
}
233+
} else {
234+
return Err(GitError::CustomError(
235+
"Invalid refs: commit not found".to_string(),
236+
));
237+
}
238+
} else {
239+
return Err(GitError::CustomError(
240+
"Invalid refs: tag or commit not found".to_string(),
241+
));
242+
}
243+
}
244+
245+
// No refs provided: fallback to existing behavior
246+
let item_to_commit_map = self.item_to_commit_map(path).await?;
195247
let mut items: Vec<TreeCommitItem> = item_to_commit_map
196248
.into_iter()
197249
.map(TreeCommitItem::from)
198250
.collect();
199-
// sort with type and name
200251
items.sort_by(|a, b| {
201252
a.content_type
202253
.cmp(&b.content_type)
@@ -210,6 +261,15 @@ pub trait ApiHandler: Send + Sync {
210261
path: PathBuf,
211262
) -> Result<HashMap<TreeItem, Option<Commit>>, GitError>;
212263

264+
/// Refs-aware version; default fallback to refs-unaware implementation
265+
async fn item_to_commit_map_with_refs(
266+
&self,
267+
path: PathBuf,
268+
_refs: Option<&str>,
269+
) -> Result<HashMap<TreeItem, Option<Commit>>, GitError> {
270+
self.item_to_commit_map(path).await
271+
}
272+
213273
// Tag related operations shared across mono/import implementations.
214274
/// Create a tag in the repository context represented by `repo_path`.
215275
/// Returns TagInfo on success.
@@ -251,8 +311,12 @@ pub trait ApiHandler: Send + Sync {
251311
/// the dir's hash as same as old,file's hash is the content hash
252312
/// may think about change dir'hash as the content
253313
/// for now,only change the file's hash
254-
async fn get_tree_content_hash(&self, path: PathBuf) -> Result<Vec<TreeHashItem>, GitError> {
255-
match self.search_tree_by_path(&path).await? {
314+
async fn get_tree_content_hash(
315+
&self,
316+
path: PathBuf,
317+
refs: Option<&str>,
318+
) -> Result<Vec<TreeHashItem>, GitError> {
319+
match self.search_tree_by_path_with_refs(&path, refs).await? {
256320
Some(tree) => {
257321
let mut items: Vec<TreeHashItem> = tree
258322
.tree_items
@@ -277,8 +341,9 @@ pub trait ApiHandler: Send + Sync {
277341
&self,
278342
path: PathBuf,
279343
dir_name: &str,
344+
refs: Option<&str>,
280345
) -> Result<Vec<TreeHashItem>, GitError> {
281-
match self.search_tree_by_path(&path).await? {
346+
match self.search_tree_by_path_with_refs(&path, refs).await? {
282347
Some(tree) => {
283348
let items: Vec<TreeHashItem> = tree
284349
.tree_items
@@ -384,6 +449,66 @@ pub trait ApiHandler: Send + Sync {
384449
Ok(Some(search_tree))
385450
}
386451

452+
/// Get root tree for a given refs (commit SHA or tag name). If refs is None/empty, use default root.
453+
async fn get_root_tree_for_refs(&self, refs: Option<&str>) -> Result<Tree, GitError> {
454+
let maybe = refs.unwrap_or("").trim();
455+
if maybe.is_empty() {
456+
return Ok(self.get_root_tree().await);
457+
}
458+
let is_hex_sha1 = maybe.len() == 40 && maybe.chars().all(|c| c.is_ascii_hexdigit());
459+
let mut commit_hash = String::new();
460+
if is_hex_sha1 {
461+
commit_hash = maybe.to_string();
462+
} else if let Ok(Some(tag)) = self.get_tag(None, maybe.to_string()).await {
463+
commit_hash = tag.object_id;
464+
}
465+
466+
if commit_hash.is_empty() {
467+
return Err(GitError::CustomError(
468+
"Invalid refs: tag or commit not found".to_string(),
469+
));
470+
}
471+
if let Some(commit) = self.get_commit_by_hash(&commit_hash).await {
472+
Ok(self.get_tree_by_hash(&commit.tree_id.to_string()).await)
473+
} else {
474+
Err(GitError::CustomError(
475+
"Invalid refs: commit not found".to_string(),
476+
))
477+
}
478+
}
479+
480+
/// Refs-aware tree search using a resolved root from refs
481+
async fn search_tree_by_path_with_refs(
482+
&self,
483+
path: &Path,
484+
refs: Option<&str>,
485+
) -> Result<Option<Tree>, GitError> {
486+
let relative_path = self
487+
.strip_relative(path)
488+
.map_err(|e| GitError::CustomError(e.to_string()))?;
489+
let root_tree = self.get_root_tree_for_refs(refs).await?;
490+
let mut search_tree = root_tree.clone();
491+
for component in relative_path.components() {
492+
if component != Component::RootDir {
493+
let target_name = component.as_os_str().to_str().unwrap();
494+
let search_res = search_tree
495+
.tree_items
496+
.iter()
497+
.find(|x| x.name == target_name);
498+
if let Some(search_res) = search_res {
499+
if !search_res.is_tree() {
500+
return Ok(None);
501+
}
502+
let res = self.get_tree_by_hash(&search_res.id.to_string()).await;
503+
search_tree = res.clone();
504+
} else {
505+
return Ok(None);
506+
}
507+
}
508+
}
509+
Ok(Some(search_tree))
510+
}
511+
387512
/// Searches for a tree in the Git repository by its path, creating intermediate trees if necessary,
388513
/// and returns the trees involved in the update process.
389514
///

mono/src/api/router/preview_router.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ async fn get_tree_info(
154154
for part in parts {
155155
let path = part.as_ref();
156156
let handler = state.api_handler(path).await?;
157-
let tree_items = handler.get_tree_info(path).await?;
157+
let tree_items = handler
158+
.get_tree_info(path, Some(query.refs.as_str()))
159+
.await?;
158160
file_tree.insert(
159161
part,
160162
FileTreeItem {
@@ -167,7 +169,7 @@ async fn get_tree_info(
167169
let tree_items = state
168170
.api_handler(query.path.as_ref())
169171
.await?
170-
.get_tree_info(query.path.as_ref())
172+
.get_tree_info(query.path.as_ref(), Some(query.refs.as_str()))
171173
.await?;
172174
Ok(Json(CommonResult::success(Some(TreeResponse {
173175
file_tree,
@@ -194,7 +196,7 @@ async fn get_tree_commit_info(
194196
let data = state
195197
.api_handler(query.path.as_ref())
196198
.await?
197-
.get_tree_commit_info(query.path.into())
199+
.get_tree_commit_info(query.path.into(), Some(query.refs.as_str()))
198200
.await?;
199201
Ok(Json(CommonResult::success(Some(data))))
200202
}
@@ -218,7 +220,7 @@ async fn get_tree_content_hash(
218220
let data = state
219221
.api_handler(query.path.as_ref())
220222
.await?
221-
.get_tree_content_hash(query.path.into())
223+
.get_tree_content_hash(query.path.into(), Some(query.refs.as_str()))
222224
.await?;
223225
Ok(Json(CommonResult::success(Some(data))))
224226
}
@@ -250,7 +252,7 @@ async fn get_tree_dir_hash(
250252
let data = state
251253
.api_handler(parent_path.as_ref())
252254
.await?
253-
.get_tree_dir_hash(parent_path.into(), target_name)
255+
.get_tree_dir_hash(parent_path.into(), target_name, Some(query.refs.as_str()))
254256
.await?;
255257

256258
Ok(Json(CommonResult::success(Some(data))))

moon/apps/web/components/CodeView/CodeTable.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useMemo } from 'react'
66
import { FolderIcon } from '@heroicons/react/20/solid'
77
import { formatDistance, fromUnixTime } from 'date-fns'
88
import { usePathname, useRouter } from 'next/navigation'
9+
import { useSearchParams } from 'next/navigation'
910
import RTable from './Table'
1011
import { columnsType, DirectoryType } from './Table/type'
1112
import Markdown from 'react-markdown'
@@ -19,9 +20,11 @@ export interface DataType {
1920
date: number
2021
}
2122

22-
const CodeTable = ({ directory, loading, readmeContent, onCommitInfoChange}: any) => {
23+
const CodeTable = ({ directory, loading, readmeContent }: any) => {
2324
const router = useRouter()
2425
const pathname = usePathname()
26+
const searchParams = useSearchParams()
27+
const refs = searchParams?.get('refs') || undefined
2528
let real_path = pathname?.replace('/tree', '')
2629

2730
const markdownContentStyle = {
@@ -83,7 +86,7 @@ const CodeTable = ({ directory, loading, readmeContent, onCommitInfoChange}: any
8386
}
8487
pathParts?.push(encodeURIComponent(record.name))
8588

86-
newPath = `/${pathParts?.join('/')}`
89+
newPath = `/${pathParts?.join('/')}${refs ? `?refs=${encodeURIComponent(refs)}` : ''}`
8790
router.push(newPath)
8891
} else {
8992
let newPath: string
@@ -95,9 +98,8 @@ const CodeTable = ({ directory, loading, readmeContent, onCommitInfoChange}: any
9598
}
9699
pathParts?.push(encodeURIComponent(record.name))
97100

98-
newPath = `/${pathParts?.join('/')}`
101+
newPath = `/${pathParts?.join('/')}${refs ? `?refs=${encodeURIComponent(refs)}` : ''}`
99102

100-
onCommitInfoChange?.(newPath);
101103
router.push(newPath)
102104
}
103105
}

moon/apps/web/components/CodeView/Tags/MonoTagList.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useMemo, useState, useEffect } from 'react'
2-
import { Button, DotsHorizontal, Link, LinkIcon, TrashIcon, UIText } from '@gitmono/ui'
2+
import { Avatar, Button, DotsHorizontal, Link, LinkIcon, TrashIcon, UIText } from '@gitmono/ui'
33
import { DropdownMenu } from '@gitmono/ui/DropdownMenu'
44
import { buildMenuItems } from '@gitmono/ui/Menu'
55
import { useCopyToClipboard } from '@gitmono/ui/src/hooks'
66

77
import { TagResponse } from '@gitmono/types'
8+
import { MemberHovercard } from '@/components/InlinePost/MemberHovercard'
89
import { useDeleteMonoTag } from '@/hooks/useDeleteMonoTag'
10+
import { formatDistanceToNow } from 'date-fns'
911

1012
interface Props {
1113
tags: TagResponse[]
@@ -45,6 +47,37 @@ function InnerRow({ tag, onDelete }: { tag: TagResponse; onDelete?: () => void }
4547
return tag.message || `${tag.object_type} ${tag.object_id.substring(0, 8)}`
4648
}, [tag])
4749

50+
const creator = useMemo(() => {
51+
// tagger format examples: "Alice <[email protected]>", "[email protected]", "unknown"
52+
const t = (tag.tagger || '').trim()
53+
54+
if (!t) return { name: 'unknown', email: undefined as string | undefined }
55+
const m = t.match(/^(.*)\s*<([^>]+)>$/)
56+
57+
if (m) {
58+
const name = m[1].trim() || m[2].split('@')[0]
59+
60+
return { name, email: m[2] }
61+
}
62+
if (t.includes('@')) {
63+
return { name: t.split('@')[0], email: t }
64+
}
65+
return { name: t, email: undefined as string | undefined }
66+
}, [tag.tagger])
67+
68+
const username = useMemo(() => {
69+
if (creator.email) return creator.email.split('@')[0]
70+
return creator.name || ''
71+
}, [creator])
72+
73+
const createdRelative = useMemo(() => {
74+
try {
75+
return formatDistanceToNow(new Date(tag.created_at), { addSuffix: true })
76+
} catch {
77+
return ''
78+
}
79+
}, [tag.created_at])
80+
4881
const href = `/code/tags/${encodeURIComponent(tag.name)}`
4982

5083
return (
@@ -62,6 +95,15 @@ function InnerRow({ tag, onDelete }: { tag: TagResponse; onDelete?: () => void }
6295
{subtitle}
6396
</UIText>
6497
)}
98+
<MemberHovercard username={username} role='member'>
99+
<div className='mt-0.5 flex items-center gap-1.5 relative z-[1]'>
100+
<Avatar size='xs' name={creator.name} />
101+
<UIText quaternary size='text-[12px]' className='line-clamp-1'>
102+
Created by {creator.name}
103+
{createdRelative ? ` · ${createdRelative}` : ''}
104+
</UIText>
105+
</div>
106+
</MemberHovercard>
65107
</div>
66108

67109
<div className='hidden flex-none items-center gap-0.5 lg:flex'>

moon/apps/web/components/CodeView/Tags/NewMonoTagDialog.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '@gitmono/ui'
1717

1818
import { useCreateMonoTag } from '@/hooks/useCreateMonoTag'
19+
import { useGetCurrentUser } from '@/hooks/useGetCurrentUser'
1920
import { useGetLatestCommit } from '../../../hooks/useGetLatestCommit'
2021
import { useGetTreeCommitInfo } from '@/hooks/useGetTreeCommitInfo'
2122
import React from 'react'
@@ -35,6 +36,7 @@ export default function NewMonoTagDialog({ open, onOpenChange, onCreated }: Prop
3536
const [pickerQuery, setPickerQuery] = useState('')
3637

3738
const { mutateAsync, isPending } = useCreateMonoTag()
39+
const { data: currentUser } = useGetCurrentUser()
3840
const { data: latestCommit, isLoading: latestLoading } = useGetLatestCommit('/')
3941
const { data: treeCommitResp, isLoading: listLoading, isFetching: listFetching } = useGetTreeCommitInfo('/')
4042

@@ -75,7 +77,9 @@ export default function NewMonoTagDialog({ open, onOpenChange, onCreated }: Prop
7577
name: name.trim(),
7678
message: message || undefined,
7779
target: resolvedTarget,
78-
path_context: '/'
80+
path_context: '/',
81+
tagger_email: currentUser?.email || undefined,
82+
tagger_name: currentUser?.display_name || currentUser?.username || undefined
7983
})
8084

8185
if (onCreated && result?.data) {

0 commit comments

Comments
 (0)