frontend/app/(main)/[owner]/[repo]/[...filePath]/ui/file-commits.tsx
frontend/app/(main)/[owner]/[repo]/[...filePath]/ui/folder-viewer.tsx
frontend/app/(main)/[owner]/[repo]/commits/[sha]/ui/commit-header.tsx
frontend/app/(main)/[owner]/[repo]/commits/page.tsx
frontend/app/(main)/[owner]/[repo]/ui/sidebar/repo-sidebar-commits.tsx
53 )); 54 let repo_service = Arc :: new ( RepositoryServiceImpl :: new ( 55 git_client . clone (), 56 org_repo . clone (), 57 repo_repo . clone (), .. 58 )); 59 let question_service = Arc :: new ( QuestionServiceImpl :: new ( 60 question_repo . clone (), 61 repo_repo . clone (), 53 )); 54 let repo_service = Arc :: new ( RepositoryServiceImpl :: new ( 55 git_client . clone (), 56 org_repo . clone (), 57 repo_repo . clone (), 58 user_repo . clone ( ) , 59 )); 60 let question_service = Arc :: new ( QuestionServiceImpl 8 use chrono ::{ DateTime , Utc }; 9 use serde :: Serialize ; 10 use uuid :: Uuid ; 11 12 use gitdot_core :: dto ::{ RepositoryCommitResponse , RepositoryResponse }; 13 14 pub use create_repository :: CreateRepositoryServerRequest ; 15 pub use get_repository_commits ::{ GetRepositoryCommitsQuery , GetRepositoryCommitsServerResponse }; 16 pub use get_repository_file ::{ GetRepositoryFileQuery , GetRepositoryFileServerResponse }; 8 use chrono ::{ DateTime , Utc }; 9 use serde :: Serialize ; 10 use uuid :: Uuid ; 11 12 use gitdot_core :: dto ::{ CommitAuthorResponse , RepositoryCommitResponse , RepositoryResponse }; 13 14 pub use create_repository :: CreateRepositoryServerRequest ; 15 pub use get_repository_commits ::{ GetRepositoryCommitsQuery , GetRepositoryCommitsServerResponse
41 42 #[derive( Debug , Clone , PartialEq , Eq , Serialize )] 43 pub struct RepositoryCommitServerResponse { 44 pub sha : String , 45 pub message : String , 46 pub author : String , .. .. ..
51 fn from ( commit : RepositoryCommitResponse ) -> Self { 52 Self { 53 sha : commit .sha, 54 message : commit .message, 55 author : commit .author , 56 date : commit .date , 57 } 58 } 59 57 } 58 59 impl From <& git2 :: Commit <' _ >> for RepositoryCommitResponse { 60 fn from ( commit : & git2 :: Commit ) -> Self { 61 let author = commit . author (); 62 Self { 63 sha : commit . id (). to_string (), 64 message : commit . message (). unwrap_or ( "" ). to_string (), 65 author : author . name ( ) . unwrap_or ( " Unknown " ) . to_string ( ) , 66 date : DateTime :: from_timestamp ( author . when (). seconds (), 0 ). unwrap_or_default (), 67 } 68 } 69 } 70 64 } 65 66 impl From <& git2 :: Commit <' _ >> for RepositoryCommitResponse { 67 fn from ( commit : & git2 :: Commit ) -> Self { 68 let git_author = commit . author (); .. 69 Self { 70 sha : commit . id (). to_string (), 8 pub trait UserRepository : Send + Sync + Clone + ' static { 9 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error >; 10 11 async fn get_by_id (& self , id : Uuid ) -> Result < Option < User >, Error >; 12 } .. 13 14 #[derive( Debug , Clone )] 15 pub struct UserRepositoryImpl { 16 pool : PgPool , 8 pub trait UserRepository : Send + Sync + Clone + ' static { 9 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error >; 10 11 async fn get_by_id (& self , id : Uuid ) -> Result < Option < User >, Error >; 12 13 async
46 Ok ( user ) 47 } 48 } 49 .. .. .. .. .. .. .. .. .. .. .. .. .. 1 use async_trait ::async_trait; 2 3 use crate :: client ::{ Git2Client , GitClient }; 4 use crate :: dto ::{ 5 CreateRepositoryRequest , GetRepositoryCommitsRequest , GetRepositoryFileCommitsRequest , 6 GetRepositoryFileRequest , GetRepositoryPreviewRequest , GetRepositoryTreeRequest , 7 RepositoryCommitsResponse , RepositoryFileResponse , RepositoryPreviewResponse , 8 RepositoryResponse , RepositoryTreeResponse , 9 }; 10 use crate :: error :: RepositoryError ; 11 use crate :: model :: RepositoryOwnerType ; 12 use crate :: repository ::{ 13 OrganizationRepository , OrganizationRepositoryImpl , RepositoryRepository , 14 RepositoryRepositoryImpl , 15 }; 16 17 #[async_trait] 18 pub trait RepositoryService : Send + Sync + ' static { 3 use async_trait ::async_trait; 4 5 use crate :: client ::{ Git2Client , GitClient }; 6 use crate :: dto ::{ 7 CommitAuthorResponse , CreateRepositoryRequest , GetRepositoryCommitsRequest , 8 GetRepositoryFileCommitsRequest , GetRepositoryFileRequest , GetRepositoryPreviewRequest , 9 GetRepositoryTreeRequest , RepositoryCommitResponse , RepositoryCommitsResponse , 10 RepositoryFileResponse
47 ) -> Result < RepositoryPreviewResponse , RepositoryError >; 48 } 49 50 #[derive( Debug , Clone )] 51 pub struct RepositoryServiceImpl < G , O , R > 52 where 53 G : GitClient , 54 O : OrganizationRepository , 55 R
65 org_repo : OrganizationRepositoryImpl , 66 repo_repo : RepositoryRepositoryImpl , 67 ) -> Self { 68 Self { 69 git_client : git_client , 70 org_repo : org_repo , 71 repo_repo : repo_repo , .. ..
165 async fn get_repository_commits ( 166 & self , 167 request : GetRepositoryCommitsRequest , 168 ) -> Result < RepositoryCommitsResponse , RepositoryError > { 169 self .git_client 170 . get_repo_commits ( 171 & request .owner_name, 172 & request .name, 173 &
181 async fn get_repository_file_commits ( 182 & self , 183 request : GetRepositoryFileCommitsRequest , 184 ) -> Result < RepositoryCommitsResponse , RepositoryError > { 185 self .git_client 186 . get_repo_file_commits ( 187 & request .owner_name, 188 & request .name, 189 &
173 & request .ref_name, 174 request .page, 175 request .per_page, 176 ) 177 . await 178 . map_err ( | e | e . into ( ) ) 179 } 180 181 async fn
190 & request .path, 191 request .page, 192 request .per_page, 193 ) 194 . await 195 . map_err ( | e | e . into ( ) ) 196 } 197 198 async fn get_repository_preview 11 href , 12 }: { 13 commit : { sha : string ; message : string ; author : string ; date : string } ; 14 isSelected : boolean ; 15 href : string ; .. .. 16 }) { 17 return ( 18 // TODO: if we move this to be a client-side component and fetches we can safe on things 19 // 1: making it a single call to fetch all files (getRepositoryFiles) 11 href , 12 }: { 13 commit : { 14 sha : string ; 15 message : string ; 16 author : { id ? : string ; name : string ; email : string } ; 17 date : string ; 18 }
29 <div className = " flex flex-col w-full justify-start items-start min-w-0 " > 30 <div className = " text-sm truncate mb-0.5 w-full " > { commit . message } </div> 31 32 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 33 <span className = " truncate min-w-0 " > { commit . author } </span> 34 58 </span> 59 </span> 60 <span className = " truncate " > { entry . commit . message } </span> 61 <span className = " text-primary/60 ml-4 whitespace-nowrap " > 62 { entry . commit . author } • { timeAgo ( new Date ( entry . commit . date )) } 63 </span> 64 </ Link > 65 ); 66 } 58 </span> 59 </span> 60 <span className = " truncate " > { entry . commit . message } </span> 61 <span className = " text-primary/60 ml-4 whitespace-nowrap " > 62 { entry . commit . author . name } • { timeAgo ( new Date ( entry . commit . date )) } 63 </span> 64 31 return ( 32 <div className = " shrink-0 border-border border-b p-2 " > 33 <div className = " mb-4 " > 34 <div className = " flex items-center gap-1 text-xs text-muted-foreground mb-1 " > 35 <span> { commit . author } </span> 36 <span> • </span> 37 <span> { formatDateTime ( new Date ( commit . date )) } </span> 38 </div> 39 <div className = " text-sm text-primary " > { commit . message } </div> 31 return ( 32 <div className = " shrink-0 border-border border-b p-2 " > 33 <div className = " mb-4 " > 34 <div className = " flex items-center gap-1 text-xs text-muted-foreground mb-1 " > 35 <span> { commit . author . name } </span> 36 <span> • </span> 37 <span> { formatDateTime ( new Date ( commit . 36 <div className = " text-sm truncate mb-0.5 w-full " > 37 { commit . message } 38 </div> 39 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 40 <span className = " truncate min-w-0 " > { commit . author } </span> 41 <span className = " shrink-0 " > • </span> 42 <span className = " shrink-0 " > 43 { formatTime ( new Date ( commit . date )) } 44 </span> 36 <div className = " text-sm truncate mb-0.5 w-full " > 37 { commit . message } 38 </div> 39 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 40 <span className = " truncate min-w-0 " > { commit . author . name } </span> 41 <span className = " shrink-0 " > • </span> 42 47 <div className = " text-sm truncate mb-0.5 w-full " > 48 { commit . message } 49 </div> 50 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 51 <span className = " truncate min-w-0 " > { commit . author } </span> 52 <span className = " shrink-0 " > • </span> 53 <span className = " shrink-0 " > 54 { formatTime ( new Date ( commit . date )) } 55 </span> 47 <div className = " text-sm truncate mb-0.5 w-full " > 48 { commit . message } 49 </div> 50 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 51 <span className = " truncate min-w-0 " > { commit . author . name } </span> 52 <span className = " shrink-0 " > • </span> 53 32 > ; 33 34 export const RepositoryCommitSchema = z . object ( { 35 sha : z . string (), 36 message : z . string (), 37 author : z . string ( ) , .. .. .. .. .. .. .. 39 } ); 40 41 export type RepositoryCommit = z . infer < typeof RepositoryCommitSchema > ; 42 32 > ; 33 34 export const CommitAuthorSchema = z . object ( { 35 id : z . uuid ( ) . optional ( ) , 36 name : z . string ( ) , .. 34 ::
new
(
61 question_repo . clone (),
62 repo_repo . clone (),
};
16 pub use get_repository_file ::{ GetRepositoryFileQuery , GetRepositoryFileServerResponse };
..
..
..
..
..
..
..
..
..
..
..
..
..
..
49
50 impl From < RepositoryCommitResponse > for RepositoryCommitServerResponse {
51 fn from ( commit : RepositoryCommitResponse ) -> Self {
52 Self {
41 42 #[derive( Debug , Clone , PartialEq , Eq , Serialize )] 43 pub struct RepositoryCommitServerResponse { 44 pub sha : String , 45 pub message : String , .. 47 pub author : CommitAuthorServerResponse , 50 # [ derive ( Debug , Clone , PartialEq , Eq , Serialize ) ] 51 pub struct CommitAuthorServerResponse { 52 # [ serde ( skip_serializing_if = " Option::is_none " ) ] 53 pub id : Option < Uuid > , 54 pub name : String , 55 pub email : String , 56 } 58 impl From < CommitAuthorResponse > for CommitAuthorServerResponse { 59 fn from ( author : CommitAuthorResponse ) -> Self { 60 Self { 61 id : author . id , 62 name : author . name , 63 email : author . email , 64 } 65 } 66 } 67 68 impl From < RepositoryCommitResponse > for RepositoryCommitServerResponse { 69 fn from ( commit : RepositoryCommitResponse ) -> Self { 70 Self { }
60
69 fn from ( commit : RepositoryCommitResponse ) -> Self { 70 Self { 71 sha : commit .sha, 72 message : commit .message, 73 date : commit .date , 74 author : commit . author .into ( ) ,75 } 76 } 77 } 78 71
message
:
commit
.
message
().
unwrap_or
(
""
).
to_string
(),
72 date : DateTime :: from_timestamp ( git_author . when (). seconds (), 0 ). unwrap_or_default (),
73 author : CommitAuthorResponse {
74 id : None ,
75 name : git_author . name ( ) . unwrap_or ( " Unknown " ) . to_string ( ) ,
76 email : git_author . email ( ) . unwrap_or ( " " ) . to_string ( ) ,
fn
get_by_emails
( &
self
,
emails
: & [
String
] ) ->
Result
<
Vec
<
User
> ,
Error
> ;
14 }
15
16 #[derive( Debug , Clone )]
17 pub struct UserRepositoryImpl {
..
46 . await ?; 47 48 Ok ( user ) 49 } 51 async fn get_by_emails ( & self , emails : & [ String ] ) -> Result < Vec < User > , Error > { 52 if emails . is_empty ( ) { 53 return Ok ( Vec :: new ( ) ) ; 54 } 55 56 let users = sqlx :: query_as :: < _ , User > ( 57 " SELECT id, email, name, created_at FROM users WHERE email = ANY($1) " , 58 ) 59 . bind ( emails ) 60 . fetch_all ( & self . pool ) 61 . await ? ; 62 63 Ok ( users ) 64 } ,
RepositoryPreviewResponse
,
RepositoryResponse
,
RepositoryTreeResponse
,
11 };
12 use crate :: error :: RepositoryError ;
13 use crate :: model :: RepositoryOwnerType ;
14 use crate :: repository ::{
15 OrganizationRepository , OrganizationRepositoryImpl , RepositoryRepository ,
16 RepositoryRepositoryImpl , UserRepository , UserRepositoryImpl ,
17 };
18
19 #[async_trait]
20 pub trait RepositoryService : Send + Sync + ' static {
:
RepositoryRepository
,
49 ) -> Result < RepositoryPreviewResponse , RepositoryError >; 50 } 51 52 #[derive( Debug , Clone )] 53 pub struct RepositoryServiceImpl < G , O , R , U > 54 where 55 G : GitClient , 56 O : OrganizationRepository , 57 R : RepositoryRepository , ..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
72 }
73 }
74 }
75
76 #[async_trait]
77 impl < G , O , R > RepositoryService for RepositoryServiceImpl < G , O , R >
78 where
79 G : GitClient ,
80 O : OrganizationRepository ,
81 R : RepositoryRepository ,
77 repo_repo : RepositoryRepositoryImpl , 78 user_repo : UserRepositoryImpl , 79 ) -> Self { 80 Self { 81 git_client , 82 org_repo , 83 repo_repo , 84 user_repo , 85 } 86 } 87 } 88 89 impl < G , O , R , U > RepositoryServiceImpl < G , O , R , U > 90 where 91 G : GitClient , 92 O : OrganizationRepository , 93 R : RepositoryRepository , 94 U : UserRepository , 95 { 96 async fn enrich_commits_with_users ( 97 & self , 98 commits : & mut [ RepositoryCommitResponse ] , 99 ) -> Result < ( ) , RepositoryError > { 100 let emails : Vec < _ > = commits 101 . iter ( ) 102 . map ( | c | c . author . email . clone ( ) ) 103 . filter ( | e | ! e . is_empty ( ) ) 104 . collect :: < HashSet < _ > > ( ) 105 . into_iter ( ) 106 . collect ( ) ; 107 108 if emails . is_empty ( ) { 109 return Ok ( ( ) ) ; 110 } 111 112 let users = self . user_repo . get_by_emails ( & emails ) . await ? ; 113 let email_to_user : HashMap < _ , _ > = users . iter ( ) . map ( | u | ( u . email . as_str ( ) , u ) ) . collect ( ) ; 114 for commit in commits { 115 if let Some ( user ) = email_to_user . get ( commit . author . email . as_str ( ) ) { 116 commit . author = CommitAuthorResponse { 117 id : Some ( user . id ) , 118 name : user . name . clone ( ) , 119 email : commit . author . email . clone ( ) , 120 } ; 121 } 122 } 123 124 Ok ( ( ) ) 125 } 126 } 127 128 #[async_trait] 129 impl < G , O , R , U > RepositoryService for RepositoryServiceImpl < G , O , R , U > 130 where 131 G : GitClient , 132 O : OrganizationRepository , 133 R : RepositoryRepository , request
.ref_name,
228 async fn get_repository_commits ( 229 & self , 230 request : GetRepositoryCommitsRequest , 231 ) -> Result < RepositoryCommitsResponse , RepositoryError > { 232 let mut response = self 233 .git_client 234 . get_repo_commits ( 235 & request .owner_name, 236 & request .name, request
.ref_name,
190 & request .path,
191 request .page,
192 request .per_page,
193 )
194 . await
195 . map_err ( | e | e . into ( ) )
196 }
197
198 async fn get_repository_preview (
199 & self ,
200 request : GetRepositoryPreviewRequest ,
144 self .git_client
..
145 . get_repo_tree (& request .owner_name, & request .name, & request .ref_name)
146 . await
147 . map_err ( | e | e . into ( ) )
148 }
149
150 async fn get_repository_file (
151 & self ,
249 async fn get_repository_file_commits ( 250 & self , 251 request : GetRepositoryFileCommitsRequest , 252 ) -> Result < RepositoryCommitsResponse , RepositoryError > { 253 let mut response = self .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 197 let mut response = self 198 .git_client 199 . get_repo_tree (& request .owner_name, & request .name, & request .ref_name) 200 . await ? ; 201 202 let mut commits : Vec < RepositoryCommitResponse > = 203 response . entries . iter ( ) . map ( | e | e . commit . clone ( ) ) . collect ( ) ; 204 self . enrich_commits_with_users ( & mut commits ) . await ? ; 205 get_repository_file_commits
(
182 & self ,
237 & request .ref_name, 238 request .page, 239 request .per_page, 240 ) 241 . await ? ; 242 243 self . enrich_commits_with_users ( & mut response . commits ) 244 . await ? ; 245 246 Ok ( response ) (
199 & self ,
259 & request .path, 260 request .page, 261 request .per_page, 262 ) 263 . await ? ; 264 265 self . enrich_commits_with_users ( & mut response . commits ) 266 . await ? ; 267 268 Ok ( response ) ;
19 isSelected : boolean ;
20 href : string ;
21 }) {
<span
className
=
"
shrink-0
"
>
•
</span>
35 <span className = " shrink-0 " > { commit . sha . substring ( 0 , 7 ) } </span>
36 <span className = " ml-auto shrink-0 " >
37 { timeAgo ( new Date ( commit . date )) }
34 <div className = " flex flex-col w-full justify-start items-start min-w-0 " > 35 <div className = " text-sm truncate mb-0.5 w-full " > { commit . message } </div> 36 37 <div className = " text-xs text-muted-foreground flex items-center gap-1 w-full min-w-0 " > 38 <span className = " truncate min-w-0 " > { commit . author . name } </span> 39 <span className = " shrink-0 " > • </span> 40 <span className = " shrink-0 " > { commit . sha . substring ( 0 , 7 ) } </span> 41 <span className = " ml-auto shrink-0 " > 42 { timeAgo ( new Date ( commit . date )) } </
Link
>
65 );
66 }
date
))
}
</span>
38 </div>
39 <div className = " text-sm text-primary " > { commit . message } </div>
<span
className
=
"
shrink-0
"
>
43 { formatTime ( new Date ( commit . date )) }
44 </span>
<span
className
=
"
shrink-0
"
>
54 { formatTime ( new Date ( commit . date )) }
55 </span>
export
const
CommitAuthorSchema
=
z
.
object
(
{
35 id : z . uuid ( ) . optional ( ) ,
36 name : z . string ( ) ,
37 email : z . string ( ) ,
38 } ) ;
40 export type CommitAuthor = z . infer < typeof CommitAuthorSchema > ;
46 author : CommitAuthorSchema ,
47 } );
48
49 export type RepositoryCommit = z . infer < typeof RepositoryCommitSchema > ;
50