22
33namespace CodedMonkey \Dirigent \Composer ;
44
5+ use CodedMonkey \Dirigent \Doctrine \Entity \CredentialsType ;
56use CodedMonkey \Dirigent \Doctrine \Entity \Package ;
67use CodedMonkey \Dirigent \Doctrine \Entity \Registry ;
78use Composer \Config ;
89use Composer \Factory ;
910use Composer \IO \IOInterface ;
1011use Composer \IO \NullIO ;
12+ use Composer \Pcre \Preg ;
1113use Composer \Repository \ComposerRepository ;
1214use Composer \Repository \VcsRepository ;
1315use Composer \Util \HttpDownloader ;
16+ use Composer \Util \Url ;
17+ use Symfony \Component \DependencyInjection \Attribute \Autowire ;
18+ use Symfony \Component \Filesystem \Filesystem ;
1419
15- class ComposerClient
20+ // use Composer\Util\Filesystem as ComposerFilesystem;
21+ // use Composer\Util\Git as GitUtility;
22+ // use Composer\Util\ProcessExecutor;
23+ // use Symfony\Component\Process\Process;
24+
25+ readonly class ComposerClient
1626{
27+ private Filesystem $ filesystem ;
28+
29+ public function __construct (
30+ #[Autowire(param: 'dirigent.storage.path ' )]
31+ private string $ storagePath ,
32+ ) {
33+ $ this ->filesystem = new Filesystem ();
34+ }
35+
1736 public function createComposerRepository (Package |Registry $ registry , ?IOInterface $ io = null , ?Config $ config = null ): ComposerRepository
1837 {
1938 $ registry = $ registry instanceof Package ? $ registry ->getMirrorRegistry () : $ registry ;
@@ -30,16 +49,24 @@ public function createComposerRepository(Package|Registry $registry, ?IOInterfac
3049
3150 public function createVcsRepository (Package $ package , ?IOInterface $ io = null , ?Config $ config = null ): VcsRepository
3251 {
33- $ repoUrl = $ package ->getRepositoryUrl ();
52+ $ repositoryUrl = $ package ->getRepositoryUrl ();
53+ $ repositoryCredentials = $ package ->getRepositoryCredentials ();
3454
35- $ config ??= ConfigFactory::createForVcsRepository ($ repoUrl , $ package -> getRepositoryCredentials () );
55+ $ config ??= ConfigFactory::createForVcsRepository ($ repositoryUrl , $ repositoryCredentials );
3656 if (!$ io ) {
3757 $ io = new NullIO ();
3858 $ io ->loadConfiguration ($ config );
3959 }
4060 $ httpDownloader = $ this ->createHttpDownloader ($ io , $ config );
4161
42- return new VcsRepository (['url ' => $ repoUrl ], $ io , $ config , $ httpDownloader );
62+ $ cacheRepositoryName = Preg::replace ('{[^a-z0-9.]}i ' , '- ' , Url::sanitize ($ repositoryUrl ));
63+ $ cachePath = $ config ->get ('cache-vcs-dir ' ) . '/ ' . $ cacheRepositoryName . '/ ' ;
64+
65+ if (CredentialsType::SshKey === $ repositoryCredentials ->getType () && !$ this ->filesystem ->exists ($ cachePath )) {
66+ $ this ->cloneAuthenticatedVcsRepository ($ package , $ cachePath );
67+ }
68+
69+ return new VcsRepository (['url ' => $ repositoryUrl ], $ io , $ config , $ httpDownloader );
4370 }
4471
4572 public function createHttpDownloader (?IOInterface $ io = null , ?Config $ config = null ): HttpDownloader
@@ -60,4 +87,80 @@ public static function getHttpDownloaderOptions(): array
6087
6188 return $ options ;
6289 }
90+
91+ private function cloneAuthenticatedVcsRepository (Package $ package , string $ cachePath ): void
92+ {
93+ $ repositoryUrl = $ package ->getRepositoryUrl ();
94+ $ repositoryCredentials = $ package ->getRepositoryCredentials ();
95+
96+ $ cacheRepositoryName = Preg::replace ('{[^a-z0-9.]}i ' , '- ' , Url::sanitize ($ repositoryUrl ));
97+ $ keyPath = "$ this ->storagePath /keys/ $ cacheRepositoryName " ;
98+
99+ // todo delete key file after every use
100+ $ this ->filesystem ->mkdir (dirname ($ keyPath ));
101+ $ this ->filesystem ->dumpFile ($ keyPath , str_replace ("\r" , '' , $ repositoryCredentials ->getKey () . PHP_EOL ));
102+ $ this ->filesystem ->chmod ($ keyPath , 0400 );
103+
104+ $ nullStream = fopen ('/dev/null ' , 'c ' );
105+ $ descriptorSpec = [
106+ ['pipe ' , 'r ' ],
107+ $ nullStream ,
108+ $ nullStream ,
109+ ];
110+
111+ $ gitConfig = sprintf ('core.sshcommand="ssh -i %s" ' , $ keyPath );
112+ // todo improve error handling
113+ proc_open ("git clone -c $ gitConfig --mirror -- $ repositoryUrl $ cachePath " , $ descriptorSpec , $ pipes );
114+
115+ /*
116+ // Concepts to clone with libraries
117+
118+ $gitUtility = new GitUtility(
119+ $io,
120+ $config,
121+ $process = new ProcessExecutor($io),
122+ new ComposerFilesystem($process),
123+ );
124+
125+ // The following commands give the following output:
126+ //
127+ // ssh -i <key-file>: No such file or directory
128+ // fatal: Could not read from remote repository.
129+ //
130+ // This is caused by Symfony Process enclosing each argument with ' which messes with
131+ // git using the proper ssh command (I guess).
132+
133+ $gitUtility->runCommands([
134+ ['git', 'clone', '-c', $gitConfig, '--mirror', '--', '%url%', $cachePath],
135+ ], $repositoryUrl, $cachePath, true, $lol);
136+
137+ $process = new Process(['git', 'clone', '-c', $gitConfig, '--mirror', $repositoryUrl, $cachePath]);
138+ $process->mustRun();
139+
140+ // The following code is an attempt to clone by initializing an empty
141+ // repository and adding the ssh command and remote afterward. Unfortunately
142+ // this doesn't mirror all branches like `git clone --mirror` does, which
143+ // Composer doesn't handle correctly.
144+
145+ $mirrorCachePath = $config->get('cache-vcs-dir') . '/' . $cacheRepositoryName . '~mirror/';
146+ $this->filesystem->mkdir($mirrorCachePath);
147+
148+ $gitUtility->runCommands([
149+ ['git', 'init'],
150+ ['git', 'config', 'core.sshCommand', "ssh -i $keyPath"],
151+ ['git', 'remote', 'add', 'origin', '--', '%url%'],
152+ ['git', 'remote', 'update', '--prune', 'origin'],
153+ ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
154+ ['git', 'gc', '--auto'],
155+ ], $repositoryUrl, $mirrorCachePath);
156+
157+ $this->filesystem->rename("$mirrorCachePath/.git", $cachePath);
158+
159+ $gitUtility->runCommands([
160+ ['git', 'config', 'core.bare', 'true'],
161+ ], $repositoryUrl, $cachePath);
162+
163+ $this->filesystem->remove($mirrorCachePath);
164+ */
165+ }
63166}
0 commit comments