Skip to content

Commit f414237

Browse files
Add repo command to manipualte repositories in composer.json (#12388)
Fixes #9918 Co-authored-by: Jordi Boggiano <[email protected]>
1 parent 686161e commit f414237

23 files changed

+2921
-268
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
* Added repository management command `repo` to add, insert, remove and update repositories (#9918)
2+
* Changed repository structure to contain a name attribute and is stored preferably as list instead of object (#9918)
3+
14
### [2.8.12] 2025-09-19
25

36
* Fixed json schema issues with version validation (#12512)

doc/03-cli.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,33 @@ to edit extra fields as json:
892892
php composer.phar config --json extra.foo.bar '{"baz": true, "qux": []}'
893893
```
894894

895+
## repository / repo
896+
897+
The `repo` command lets you manage repositories in your `composer.json`. It is a more powerful alternative to `composer config repositories.*`.
898+
899+
### Usage
900+
901+
```shell
902+
php composer.phar repo list
903+
php composer.phar repo add foo vcs https://github.com/acme/foo
904+
php composer.phar repo add bar '{"type":"composer","url":"https://repo.example.org"}'
905+
php composer.phar repo add baz vcs https://example.org --before foo
906+
php composer.phar repo add qux vcs https://example.org --after bar
907+
php composer.phar repo remove foo
908+
php composer.phar repo set-url foo https://git.example.org/acme/foo
909+
php composer.phar repo get-url foo
910+
php composer.phar repo disable packagist
911+
php composer.phar repo enable packagist
912+
```
913+
914+
### Options
915+
916+
- **--global (-g):** to modify the global `$COMPOSER_HOME/config.json`.
917+
- **--file (-f):** to modify a specific file instead of composer.json.
918+
- **--append:** to add a repository with lower priority (by default repositories are prepended and have thus higher priority than existing ones).
919+
- **--before <name>:** to insert the new repository before an existing repository named `<name>`.
920+
- **--after <name>:** to insert the new repository after an existing repository named `<name>`. The `<name>` must match an existing repository name.
921+
895922
## create-project
896923

897924
You can use Composer to create new projects from an existing package. This is

doc/04-schema.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ By default Packagist is added last which means that custom repositories can
870870
override packages from it.
871871

872872
Using JSON object notation is also possible. However, JSON key/value pairs
873-
are to be considered unordered so consistent behaviour cannot be guaranteed.
873+
are to be considered unordered so consistent behaviour cannot be guaranteed and is deprecated.
874874

875875
```json
876876
{
@@ -883,6 +883,20 @@ are to be considered unordered so consistent behaviour cannot be guaranteed.
883883
}
884884
```
885885

886+
It will be superseded by the name property
887+
888+
```json
889+
{
890+
"repositories": [
891+
{
892+
"name": "foo",
893+
"type": "composer",
894+
"url": "http://packages.foo.com"
895+
}
896+
]
897+
}
898+
```
899+
886900
### config <span>([root-only](04-schema.md#root-package))</span>
887901

888902
A set of configuration options. It is only used for projects. See

phpstan/baseline.neon

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ parameters:
260260
count: 1
261261
path: ../src/Composer/Command/ConfigCommand.php
262262

263+
-
264+
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
265+
count: 1
266+
path: ../src/Composer/Command/RepositoryCommand.php
267+
263268
-
264269
message: "#^Only booleans are allowed in &&, Composer\\\\Package\\\\PackageInterface\\|false given on the right side\\.$#"
265270
count: 1
@@ -570,6 +575,12 @@ parameters:
570575
count: 2
571576
path: ../src/Composer/Command/SelfUpdateCommand.php
572577

578+
# one of these parameters will be non-null
579+
-
580+
message: "#^Parameter \\#3 \\$referenceName of method Composer\\\\Config\\\\JsonConfigSource\\:\\:insertRepository\\(\\) expects string, string\\|null given\\.$#"
581+
count: 1
582+
path: ../src/Composer/Command/RepositoryCommand.php
583+
573584
-
574585
message: "#^Argument of an invalid type array\\<int, array\\<string, array\\|string\\>\\>\\|string supplied for foreach, only iterables are supported\\.$#"
575586
count: 1

res/composer-schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@
971971
"type": "object",
972972
"required": ["type", "url"],
973973
"properties": {
974+
"name": { "type": "string" },
974975
"type": { "type": "string", "enum": ["composer"] },
975976
"url": { "type": "string" },
976977
"canonical": { "type": "boolean" },
@@ -998,6 +999,7 @@
998999
"type": "object",
9991000
"required": ["type", "url"],
10001001
"properties": {
1002+
"name": { "type": "string" },
10011003
"type": { "type": "string", "enum": ["vcs", "github", "git", "gitlab", "bitbucket", "git-bitbucket", "hg", "fossil", "perforce", "svn", "forgejo"] },
10021004
"url": { "type": "string" },
10031005
"canonical": { "type": "boolean" },
@@ -1031,6 +1033,7 @@
10311033
"type": "object",
10321034
"required": ["type", "url"],
10331035
"properties": {
1036+
"name": { "type": "string" },
10341037
"type": { "type": "string", "enum": ["path"] },
10351038
"url": { "type": "string" },
10361039
"canonical": { "type": "boolean" },
@@ -1062,6 +1065,7 @@
10621065
"type": "object",
10631066
"required": ["type", "url"],
10641067
"properties": {
1068+
"name": { "type": "string" },
10651069
"type": { "type": "string", "enum": ["artifact"] },
10661070
"url": { "type": "string" },
10671071
"canonical": { "type": "boolean" },
@@ -1083,6 +1087,7 @@
10831087
"type": "object",
10841088
"required": ["type", "url"],
10851089
"properties": {
1090+
"name": { "type": "string" },
10861091
"type": { "type": "string", "enum": ["pear"] },
10871092
"url": { "type": "string" },
10881093
"canonical": { "type": "boolean" },
@@ -1105,6 +1110,7 @@
11051110
"type": "object",
11061111
"required": ["type", "package"],
11071112
"properties": {
1113+
"name": { "type": "string" },
11081114
"type": { "type": "string", "enum": ["package"] },
11091115
"canonical": { "type": "boolean" },
11101116
"only": {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Composer.
5+
*
6+
* (c) Nils Adermann <[email protected]>
7+
* Jordi Boggiano <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace Composer\Command;
14+
15+
use Composer\Config;
16+
use Composer\Config\JsonConfigSource;
17+
use Composer\Json\JsonFile;
18+
use Composer\Factory;
19+
use Composer\Util\Platform;
20+
use Composer\Util\Silencer;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
24+
abstract class BaseConfigCommand extends BaseCommand
25+
{
26+
/**
27+
* @var Config
28+
*/
29+
protected $config;
30+
31+
/**
32+
* @var JsonFile
33+
*/
34+
protected $configFile;
35+
36+
/**
37+
* @var JsonConfigSource
38+
*/
39+
protected $configSource;
40+
41+
protected function initialize(InputInterface $input, OutputInterface $output): void
42+
{
43+
parent::initialize($input, $output);
44+
45+
if ($input->getOption('global') && null !== $input->getOption('file')) {
46+
throw new \RuntimeException('--file and --global can not be combined');
47+
}
48+
49+
$io = $this->getIO();
50+
$this->config = Factory::createConfig($io);
51+
52+
// When using --global flag, set baseDir to home directory for correct absolute path resolution
53+
if ($input->getOption('global')) {
54+
$this->config->setBaseDir($this->config->get('home'));
55+
}
56+
57+
$configFile = $this->getComposerConfigFile($input, $this->config);
58+
59+
// Create global composer.json if invoked using `composer global [config-cmd]`
60+
if (
61+
($configFile === 'composer.json' || $configFile === './composer.json')
62+
&& !file_exists($configFile)
63+
&& realpath(Platform::getCwd()) === realpath($this->config->get('home'))
64+
) {
65+
file_put_contents($configFile, "{\n}\n");
66+
}
67+
68+
$this->configFile = new JsonFile($configFile, null, $io);
69+
$this->configSource = new JsonConfigSource($this->configFile);
70+
71+
// Initialize the global file if it's not there, ignoring any warnings or notices
72+
if ($input->getOption('global') && !$this->configFile->exists()) {
73+
touch($this->configFile->getPath());
74+
$this->configFile->write(['config' => new \ArrayObject]);
75+
Silencer::call('chmod', $this->configFile->getPath(), 0600);
76+
}
77+
78+
if (!$this->configFile->exists()) {
79+
throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile));
80+
}
81+
}
82+
83+
/**
84+
* Get the local composer.json, global config.json, or the file passed by the user
85+
*/
86+
protected function getComposerConfigFile(InputInterface $input, Config $config): string
87+
{
88+
return $input->getOption('global')
89+
? ($config->get('home') . '/config.json')
90+
: ($input->getOption('file') ?? Factory::getComposerFile())
91+
;
92+
}
93+
94+
/**
95+
* Get the local auth.json or global auth.json, or if the user passed in a file to use,
96+
* the corresponding auth.json
97+
*/
98+
protected function getAuthConfigFile(InputInterface $input, Config $config): string
99+
{
100+
return $input->getOption('global')
101+
? ($config->get('home') . '/auth.json')
102+
: dirname($this->getComposerConfigFile($input, $config)) . '/auth.json'
103+
;
104+
}
105+
}

src/Composer/Command/ConfigCommand.php

Lines changed: 2 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* @author Joshua Estes <[email protected]>
3535
* @author Jordi Boggiano <[email protected]>
3636
*/
37-
class ConfigCommand extends BaseCommand
37+
class ConfigCommand extends BaseConfigCommand
3838
{
3939
/**
4040
* List of additional configurable package-properties
@@ -56,21 +56,6 @@ class ConfigCommand extends BaseCommand
5656
'extra',
5757
];
5858

59-
/**
60-
* @var Config
61-
*/
62-
protected $config;
63-
64-
/**
65-
* @var JsonFile
66-
*/
67-
protected $configFile;
68-
69-
/**
70-
* @var JsonConfigSource
71-
*/
72-
protected $configSource;
73-
7459
/**
7560
* @var JsonFile
7661
*/
@@ -176,52 +161,17 @@ protected function initialize(InputInterface $input, OutputInterface $output): v
176161
{
177162
parent::initialize($input, $output);
178163

179-
if ($input->getOption('global') && null !== $input->getOption('file')) {
180-
throw new \RuntimeException('--file and --global can not be combined');
181-
}
182-
183-
$io = $this->getIO();
184-
$this->config = Factory::createConfig($io);
185-
186-
// When using --global flag, set baseDir to home directory for correct absolute path resolution
187-
if ($input->getOption('global')) {
188-
$this->config->setBaseDir($this->config->get('home'));
189-
}
190-
191-
$configFile = $this->getComposerConfigFile($input, $this->config);
192-
193-
// Create global composer.json if this was invoked using `composer global config`
194-
if (
195-
($configFile === 'composer.json' || $configFile === './composer.json')
196-
&& !file_exists($configFile)
197-
&& realpath(Platform::getCwd()) === realpath($this->config->get('home'))
198-
) {
199-
file_put_contents($configFile, "{\n}\n");
200-
}
201-
202-
$this->configFile = new JsonFile($configFile, null, $io);
203-
$this->configSource = new JsonConfigSource($this->configFile);
204-
205164
$authConfigFile = $this->getAuthConfigFile($input, $this->config);
206165

207-
$this->authConfigFile = new JsonFile($authConfigFile, null, $io);
166+
$this->authConfigFile = new JsonFile($authConfigFile, null, $this->getIO());
208167
$this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
209168

210169
// Initialize the global file if it's not there, ignoring any warnings or notices
211-
if ($input->getOption('global') && !$this->configFile->exists()) {
212-
touch($this->configFile->getPath());
213-
$this->configFile->write(['config' => new \ArrayObject]);
214-
Silencer::call('chmod', $this->configFile->getPath(), 0600);
215-
}
216170
if ($input->getOption('global') && !$this->authConfigFile->exists()) {
217171
touch($this->authConfigFile->getPath());
218172
$this->authConfigFile->write(['bitbucket-oauth' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject, 'gitlab-token' => new \ArrayObject, 'http-basic' => new \ArrayObject, 'bearer' => new \ArrayObject, 'forgejo-token' => new \ArrayObject()]);
219173
Silencer::call('chmod', $this->authConfigFile->getPath(), 0600);
220174
}
221-
222-
if (!$this->configFile->exists()) {
223-
throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile));
224-
}
225175
}
226176

227177
/**
@@ -1035,29 +985,6 @@ protected function listConfiguration(array $contents, array $rawContents, Output
1035985
}
1036986
}
1037987

1038-
/**
1039-
* Get the local composer.json, global config.json, or the file passed by the user
1040-
*/
1041-
private function getComposerConfigFile(InputInterface $input, Config $config): string
1042-
{
1043-
return $input->getOption('global')
1044-
? ($config->get('home') . '/config.json')
1045-
: ($input->getOption('file') ?: Factory::getComposerFile())
1046-
;
1047-
}
1048-
1049-
/**
1050-
* Get the local auth.json or global auth.json, or if the user passed in a file to use,
1051-
* the corresponding auth.json
1052-
*/
1053-
private function getAuthConfigFile(InputInterface $input, Config $config): string
1054-
{
1055-
return $input->getOption('global')
1056-
? ($config->get('home') . '/auth.json')
1057-
: dirname($this->getComposerConfigFile($input, $config)) . '/auth.json'
1058-
;
1059-
}
1060-
1061988
/**
1062989
* Suggest setting-keys, while taking given options in account.
1063990
*/

0 commit comments

Comments
 (0)