@@ -67,6 +67,7 @@ import {
67
67
RoleOrPermission ,
68
68
WorkspaceInstanceRepoStatus ,
69
69
GetProviderRepositoriesParams ,
70
+ SuggestedRepository ,
70
71
} from "@gitpod/gitpod-protocol" ;
71
72
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol" ;
72
73
import {
@@ -1739,6 +1740,151 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
1739
1740
. map ( ( s ) => s . url ) ;
1740
1741
}
1741
1742
1743
+ public async getSuggestedRepositories ( ctx : TraceContext , organizationId : string ) : Promise < SuggestedRepository [ ] > {
1744
+ traceAPIParams ( ctx , { organizationId } ) ;
1745
+
1746
+ const user = await this . checkAndBlockUser ( "getSuggestedRepositories" ) ;
1747
+
1748
+ if ( ! uuidValidate ( organizationId ) ) {
1749
+ throw new ApplicationError ( ErrorCodes . BAD_REQUEST , "organizationId must be a valid UUID" ) ;
1750
+ }
1751
+
1752
+ const logCtx : LogContext = { userId : user . id } ;
1753
+
1754
+ type SuggestedRepositoryWithSorting = SuggestedRepository & {
1755
+ priority : number ;
1756
+ lastUse ?: string ;
1757
+ } ;
1758
+
1759
+ const fetchProjects = async ( ) : Promise < SuggestedRepositoryWithSorting [ ] > => {
1760
+ const span = TraceContext . startSpan ( "getSuggestedRepositories.fetchProjects" , ctx ) ;
1761
+ const projects = await this . projectsService . getProjects ( user . id , organizationId ) ;
1762
+
1763
+ const projectRepos = projects . map ( ( project ) => ( {
1764
+ url : project . cloneUrl . replace ( / \. g i t $ / , "" ) ,
1765
+ projectId : project . id ,
1766
+ projectName : project . name ,
1767
+ priority : 1 ,
1768
+ } ) ) ;
1769
+
1770
+ span . finish ( ) ;
1771
+
1772
+ return projectRepos ;
1773
+ } ;
1774
+
1775
+ // Load user repositories (from Git hosts directly)
1776
+ const fetchUserRepos = async ( ) : Promise < SuggestedRepositoryWithSorting [ ] > => {
1777
+ const span = TraceContext . startSpan ( "getSuggestedRepositories.fetchUserRepos" , ctx ) ;
1778
+ const authProviders = await this . getAuthProviders ( ctx ) ;
1779
+
1780
+ const providerRepos = await Promise . all (
1781
+ authProviders . map ( async ( p ) : Promise < SuggestedRepositoryWithSorting [ ] > => {
1782
+ try {
1783
+ span . setTag ( "host" , p . host ) ;
1784
+
1785
+ const hostContext = this . hostContextProvider . get ( p . host ) ;
1786
+ const services = hostContext ?. services ;
1787
+ if ( ! services ) {
1788
+ log . error ( logCtx , "Unsupported repository host: " + p . host ) ;
1789
+ return [ ] ;
1790
+ }
1791
+ const userRepos = await services . repositoryProvider . getUserRepos ( user ) ;
1792
+
1793
+ return userRepos . map ( ( r ) => ( {
1794
+ url : r . replace ( / \. g i t $ / , "" ) ,
1795
+ priority : 5 ,
1796
+ } ) ) ;
1797
+ } catch ( error ) {
1798
+ log . debug ( logCtx , "Could not get user repositories from host " + p . host , error ) ;
1799
+ }
1800
+
1801
+ return [ ] ;
1802
+ } ) ,
1803
+ ) ;
1804
+
1805
+ span . finish ( ) ;
1806
+
1807
+ return providerRepos . flat ( ) ;
1808
+ } ;
1809
+
1810
+ const fetchRecentRepos = async ( ) : Promise < SuggestedRepositoryWithSorting [ ] > => {
1811
+ const span = TraceContext . startSpan ( "getSuggestedRepositories.fetchRecentRepos" , ctx ) ;
1812
+
1813
+ const workspaces = await this . getWorkspaces ( ctx , { organizationId } ) ;
1814
+ const recentRepos : SuggestedRepositoryWithSorting [ ] = [ ] ;
1815
+
1816
+ for ( const ws of workspaces ) {
1817
+ let repoUrl ;
1818
+ if ( CommitContext . is ( ws . workspace . context ) ) {
1819
+ repoUrl = ws . workspace . context ?. repository ?. cloneUrl ?. replace ( / \. g i t $ / , "" ) ;
1820
+ }
1821
+ if ( ! repoUrl ) {
1822
+ repoUrl = ws . workspace . contextURL ;
1823
+ }
1824
+ if ( repoUrl ) {
1825
+ const lastUse = WorkspaceInfo . lastActiveISODate ( ws ) ;
1826
+
1827
+ recentRepos . push ( {
1828
+ url : repoUrl ,
1829
+ projectId : ws . workspace . projectId ,
1830
+ priority : 10 ,
1831
+ lastUse,
1832
+ } ) ;
1833
+ }
1834
+ }
1835
+
1836
+ span . finish ( ) ;
1837
+
1838
+ return recentRepos ;
1839
+ } ;
1840
+
1841
+ const repoResults = await Promise . allSettled ( [
1842
+ fetchProjects ( ) . catch ( ( e ) => log . error ( logCtx , "Could not fetch projects" , e ) ) ,
1843
+ fetchUserRepos ( ) . catch ( ( e ) => log . error ( logCtx , "Could not fetch user repositories" , e ) ) ,
1844
+ fetchRecentRepos ( ) . catch ( ( e ) => log . error ( logCtx , "Could not fetch recent repositories" , e ) ) ,
1845
+ ] ) ;
1846
+
1847
+ const sortedRepos = repoResults
1848
+ . map ( ( r ) => ( r . status === "fulfilled" ? r . value || [ ] : [ ] ) )
1849
+ . flat ( )
1850
+ . sort ( ( a , b ) => {
1851
+ // priority first
1852
+ if ( a . priority !== b . priority ) {
1853
+ return a . priority < b . priority ? 1 : - 1 ;
1854
+ }
1855
+ // Most recently used second
1856
+ if ( b . lastUse || a . lastUse ) {
1857
+ const la = a . lastUse || "" ;
1858
+ const lb = b . lastUse || "" ;
1859
+ return la < lb ? 1 : la === lb ? 0 : - 1 ;
1860
+ }
1861
+ // Otherwise, alphasort
1862
+ const ua = a . url . toLowerCase ( ) ;
1863
+ const ub = b . url . toLowerCase ( ) ;
1864
+ return ua > ub ? 1 : ua === ub ? 0 : - 1 ;
1865
+ } ) ;
1866
+
1867
+ const uniqueRepositories = new Map < string , SuggestedRepositoryWithSorting > ( ) ;
1868
+
1869
+ for ( const repo of sortedRepos ) {
1870
+ const existingRepo = uniqueRepositories . get ( repo . url ) ;
1871
+
1872
+ uniqueRepositories . set ( repo . url , {
1873
+ ...( existingRepo || { } ) ,
1874
+ ...repo ,
1875
+ } ) ;
1876
+ }
1877
+
1878
+ // Convert to return type
1879
+ return Array . from ( uniqueRepositories . values ( ) ) . map (
1880
+ ( repo ) : SuggestedRepository => ( {
1881
+ url : repo . url ,
1882
+ projectId : repo . projectId ,
1883
+ projectName : repo . projectName ,
1884
+ } ) ,
1885
+ ) ;
1886
+ }
1887
+
1742
1888
public async setWorkspaceTimeout (
1743
1889
ctx : TraceContext ,
1744
1890
workspaceId : string ,
0 commit comments