@@ -5,53 +5,74 @@ import path from 'node:path';
55import { FileUtils } from '../../../utils/file-utils.js' ;
66import Schema from './git-repository-schema.json' ;
77
8- export interface GitCloneConfig extends ResourceConfig {
8+ export interface GitRepositoryConfig extends ResourceConfig {
99 autoVerifySSH : boolean
1010 directory ?: string ,
1111 parentDirectory ?: string ,
12+ repositories ?: string [ ] ,
1213 repository : string ,
1314}
1415
15- export class GitCloneResource extends Resource < GitCloneConfig > {
16- getSettings ( ) : ResourceSettings < GitCloneConfig > {
16+ export class GitRepositoryResource extends Resource < GitRepositoryConfig > {
17+ getSettings ( ) : ResourceSettings < GitRepositoryConfig > {
1718 return {
1819 id : 'git-repository' ,
1920 operatingSystems : [ OS . Darwin , OS . Linux ] ,
2021 schema : Schema ,
2122 parameterSettings : {
23+ repositories : { type : 'array' } ,
2224 parentDirectory : { type : 'directory' } ,
2325 directory : { type : 'directory' } ,
2426 autoVerifySSH : { type : 'boolean' , default : true , setting : true } ,
2527 } ,
26- importAndDestroy :{
28+ importAndDestroy : {
2729 requiredParameters : [ 'directory' ]
2830 } ,
2931 allowMultiple : {
3032 matcher : ( desired , current ) => {
3133 const desiredPath = desired . parentDirectory
32- ? path . resolve ( desired . parentDirectory , this . extractBasename ( desired . repository ! ) ! )
34+ ? desired . repositories ?. map ( ( r ) => path . resolve ( desired . parentDirectory ! , this . extractBasename ( r ) ! ) )
3335 : path . resolve ( desired . directory ! ) ;
3436
3537 const currentPath = current . parentDirectory
36- ? path . resolve ( current . parentDirectory , this . extractBasename ( current . repository ! ) ! )
38+ ? current . repositories ?. map ( ( r ) => path . resolve ( current . parentDirectory ! , this . extractBasename ( r ) ! ) )
3739 : path . resolve ( current . directory ! ) ;
3840
3941 const isNotCaseSensitive = process . platform === 'darwin' ;
4042 if ( isNotCaseSensitive ) {
41- return desiredPath . toLowerCase ( ) === currentPath . toLowerCase ( )
43+ if ( ! Array . isArray ( desiredPath ) && ! Array . isArray ( currentPath ) ) {
44+ return desiredPath ! . toLowerCase ( ) === currentPath ! . toLowerCase ( )
45+ }
46+
47+ if ( Array . isArray ( desiredPath ) && Array . isArray ( currentPath ) ) {
48+ const currentLowered = new Set ( currentPath . map ( ( c ) => c . toLowerCase ( ) ) )
49+ return desiredPath . some ( ( d ) => currentLowered . has ( d . toLowerCase ( ) ) )
50+ }
4251 }
43-
52+
53+ if ( Array . isArray ( desiredPath ) && Array . isArray ( currentPath ) ) {
54+ return desiredPath . some ( ( d ) => currentPath . includes ( d ) )
55+ }
56+
4457 return desiredPath === currentPath ;
4558 } ,
46- findAllParameters : async ( ) => {
59+ async findAllParameters ( ) {
4760 const $ = getPty ( ) ;
4861 const { data } = await $ . spawnSafe ( 'find ~ -type d \\( -path $HOME/Library -o -path $HOME/Pictures -o -path $HOME/Utilities -o -path "$HOME/.*" \\) -prune -o -name .git -print' )
4962
50- return data
63+ const directories = data
5164 ?. split ( / \n / ) ?. filter ( Boolean )
5265 ?. map ( ( p ) => path . dirname ( p ) )
5366 ?. map ( ( directory ) => ( { directory } ) )
5467 ?? [ ] ;
68+
69+ const groupedDirectories = Object . groupBy ( directories , ( d ) => path . dirname ( d . directory ) ) ;
70+ const multipleRepositories = Object . entries ( groupedDirectories ) . filter ( ( [ _ , dirs ] ) => ( dirs ?. length ?? 0 ) > 1 )
71+ . map ( ( [ parent ] ) => ( { parentDirectory : parent } ) )
72+ const singleRepositories = Object . entries ( groupedDirectories ) . filter ( ( [ _ , dirs ] ) => ( dirs ?. length ?? 0 ) === 1 )
73+ . map ( ( [ directory ] ) => ( { directory } ) )
74+
75+ return [ ...multipleRepositories , ...singleRepositories ] ;
5576 }
5677 } ,
5778 dependencies : [
@@ -63,27 +84,47 @@ export class GitCloneResource extends Resource<GitCloneConfig> {
6384 }
6485 }
6586
66- override async refresh ( parameters : Partial < GitCloneConfig > ) : Promise < Partial < GitCloneConfig > | null > {
87+ override async refresh ( parameters : Partial < GitRepositoryConfig > ) : Promise < Partial < GitRepositoryConfig > | null > {
6788 const $ = getPty ( ) ;
6889
6990 if ( parameters . parentDirectory ) {
70- const folderName = this . extractBasename ( parameters . repository ! ) ;
71- if ( ! folderName ) {
72- throw new Error ( 'Invalid git repository or remote name. Un-able to parse' ) ;
91+ // Check if parent directory exists
92+ const parentExists = await FileUtils . checkDirExistsOrThrowIfFile ( parameters . parentDirectory ) ;
93+ if ( ! parentExists ) {
94+ return null ;
7395 }
7496
75- const fullPath = path . join ( parameters . parentDirectory , folderName ) ;
97+ // Find all git repositories in the parent directory
98+ const { data } = await $ . spawnSafe ( `find "${ parameters . parentDirectory } " -maxdepth 2 -type d -name .git` , { cwd : parameters . parentDirectory } ) ;
7699
77- const exists = await FileUtils . checkDirExistsOrThrowIfFile ( fullPath ) ;
78- if ( ! exists ) {
100+ const gitDirs = data ?. split ( / \n / ) ?. filter ( Boolean ) ?? [ ] ;
101+ if ( gitDirs . length === 0 ) {
102+ return null ;
103+ }
104+
105+ // Get repository URLs for all found git directories
106+ const repositories : string [ ] = [ ] ;
107+ for ( const gitDir of gitDirs ) {
108+ const repoPath = path . dirname ( gitDir ) ;
109+ const { data : url } = await $ . spawnSafe ( 'git config --get remote.origin.url' , { cwd : repoPath } ) ;
110+ if ( url && url . trim ( ) ) {
111+ repositories . push ( url . trim ( ) ) ;
112+ }
113+ }
114+
115+ if ( repositories . length === 0 ) {
79116 return null ;
80117 }
81118
82- const { data : url } = await $ . spawn ( 'git config --get remote.origin.url' , { cwd : fullPath } ) ;
119+ console . log ( 'Refresh' , {
120+ parentDirectory : parameters . parentDirectory ,
121+ repositories,
122+ autoVerifySSH : parameters . autoVerifySSH ,
123+ } )
83124
84125 return {
85126 parentDirectory : parameters . parentDirectory ,
86- repository : url . trim ( ) ,
127+ repositories ,
87128 autoVerifySSH : parameters . autoVerifySSH ,
88129 }
89130 }
@@ -107,29 +148,34 @@ export class GitCloneResource extends Resource<GitCloneConfig> {
107148 }
108149
109150
110- override async create ( plan : CreatePlan < GitCloneConfig > ) : Promise < void > {
151+ override async create ( plan : CreatePlan < GitRepositoryConfig > ) : Promise < void > {
111152 const $ = getPty ( ) ;
112153 const config = plan . desiredConfig ;
113154
114- if ( plan . desiredConfig . autoVerifySSH ) {
115- await this . autoVerifySSHForFirstAttempt ( config . repository )
116- }
117-
118155 if ( config . parentDirectory ) {
119156 const parentDirectory = path . resolve ( config . parentDirectory ) ;
120157 await FileUtils . createDirIfNotExists ( parentDirectory ) ;
121- await $ . spawn ( `git clone ${ config . repository } ` , { cwd : parentDirectory } ) ;
158+
159+ // Clone all repositories in the list
160+ const repositories = ( config as any ) . repositories || [ config . repository ] ;
161+ for ( const repository of repositories ) {
162+ if ( plan . desiredConfig . autoVerifySSH ) {
163+ await this . autoVerifySSHForFirstAttempt ( repository ) ;
164+ }
165+
166+ await $ . spawn ( `git clone ${ repository } ` , { cwd : parentDirectory } ) ;
167+ }
122168 } else {
123169 const directory = path . resolve ( config . directory ! ) ;
124170 await $ . spawn ( `git clone ${ config . repository } ${ directory } ` ) ;
125171 }
126172 }
127173
128- override async destroy ( plan : DestroyPlan < GitCloneConfig > ) : Promise < void > {
174+ override async destroy ( plan : DestroyPlan < GitRepositoryConfig > ) : Promise < void > {
129175 // Do nothing here. We don't want to destroy a user's repository.
130176 // TODO: change this to skip the destroy only if the user's repo has pending changes (check via git)
131- throw new Error ( `The git-clone resource is not designed to delete folders.
132- Please delete ${ plan . currentConfig . directory ?? ( plan . currentConfig . parentDirectory ! + this . extractBasename ( plan . currentConfig . repository ) ) } manually and re-apply` ) ;
177+ throw new Error ( `The git-clone resource is not designed to delete folders.
178+ Please delete ${ plan . currentConfig . directory ?? ( plan . currentConfig . repositories ?. map ( ( r ) => path . resolve ( plan . currentConfig . parentDirectory ! , this . extractBasename ( r ) ! ) ) . join ( ', ' ) ) } manually and re-apply` ) ;
133179 }
134180
135181 // Converts https://github.com/kevinwang5658/codify-homebrew-plugin.git => codify-homebrew-plugin
0 commit comments